前端框架

Vue

手写mini版的MVVM框架

实现效果:2s后修改姓名和年龄这两个值,页面响应式更新渲染
前端知识体系总结(框架+浏览器篇) - 图1
实现流程
1)定义observe函数,利用Object.defineProperty把data中的属性变成响应式的,同时给每一个属性添加一个dep对象(用来存储对应的watcher观察者)
2)定义compile 函数,模板编译,遍历 DOM,遇到 mustache(双大括号{{}})形式的文本,则替换成 data.key对应的值,同时将该dom节点添加到对应key值的dep对象中
3)当data的数据变化时,调用dep对象的update方法,更新所有观察者中的dom节点

  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>vue的MVVM简单实现</title></head>
  6. <body>
  7. <div id="app">
  8. <p>姓名: <span>{{name}}</span></p>
  9. <p>年龄: <span>{{age}}</span></p>
  10. </div>
  11. <script>
  12. window.onload = function () {
  13. // new一个vue实例
  14. let vue = new Vue(
  15. {
  16. el: '#app',
  17. data: {
  18. name: '加载中', age: '18'
  19. }
  20. }
  21. )
  22. // 2s后更新页面的信息
  23. setTimeout(() => {
  24. // 修改vue中$data的name和age属性
  25. vue.$data.name = '小明';
  26. vue.$data.age = 20;
  27. }, 2000)
  28. }
  29. class Vue {
  30. constructor(options) {
  31. this.options = options
  32. this.$data = options.data
  33. this.observe(options.data)
  34. this.compile(document.querySelector(options.el))
  35. }
  36. // 监听data中属性的变化
  37. observe(data) {
  38. Object.keys(data).forEach(key => {
  39. // 给data中的每一个属性添加一个dep对象(该对象用来存储对应的watcher观察者)
  40. let observer = new Dep()
  41. // 利用闭包 获取和设置属性的时候,操作的都是value
  42. let value = data[key]
  43. Object.defineProperty(data, key, {
  44. get() {
  45. // 观察者对象添加对应的dom节点
  46. Dep.target && observer.add(Dep.target)
  47. return value
  48. },
  49. set(newValue) {
  50. value = newValue
  51. // 属性值变化时,更新观察者中所有节点
  52. observer.update(newValue)
  53. }
  54. })
  55. })
  56. }
  57. compile(dom) {
  58. dom.childNodes.forEach(child => {
  59. // nodeType 为3时为文本节点,并且该节点的内容包含`mustache`(双大括号{{}})
  60. if(child.nodeType === 3 && /\{\{(.*)\}\}/.test(child.textContent)) {
  61. // RegExp.$1是正则表达式匹配的第一个字符串,这里对应的就是data中的key值
  62. let key = RegExp.$1.trim()
  63. // 将该节点添加到对应的观察者对象中,在下面的的this.options.data[key]中触发对应的get方法
  64. Dep.target = child
  65. // 将{{key}} 替换成data中对应的值
  66. child.textContent = child.textContent.replace(`{{${key}}}`, this.options.data[key])
  67. Dep.target = null
  68. }
  69. // 递归遍历子节点
  70. if(child.childNodes.length) {
  71. this.compile(child)
  72. }
  73. })
  74. }
  75. }
  76. // dep对象存储所有的观察者
  77. class Dep {
  78. constructor() {
  79. this.watcherList = []
  80. }
  81. // 添加watcher
  82. add(node) {
  83. this.watcherList.push(node)
  84. }
  85. // 更新watcher
  86. update(value) {
  87. this.watcherList.forEach(node => {
  88. node.textContent= value
  89. })
  90. }
  91. }
  92. </script>
  93. </body>
  94. </html>

50行代码的MVVM,感受闭包的艺术

手写 v-model 数据双向绑定

前端知识体系总结(框架+浏览器篇) - 图2
和前文mini版MVVM框架的区别
1)实现v-model指令,input值改变后,页面对应的数据也会变化,实现了数据的双向绑定
2)给input元素绑定input事件,当输入值变化会触发对应属性的dep.update方法,通知对应的观察者发生变化
3)增加了数据代理,通过this.info.person.name就可以直接修 $data对应的值,实现了this对this.$data的代理
4)数据劫持,对data增加了递归和设置新值的劫持,让data中每一层数据都是响应式的,如info.person.name

  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport"
  6. content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  7. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  8. <title>vue双向绑定的简单实现</title>
  9. </head>
  10. <body>
  11. <div id="app">
  12. <div>年龄: <span>{{info.person.name}}</span></div>
  13. <p>{{job}}</p>
  14. <input v-model="job" placeholder="请输入工作" type="text">
  15. </div>
  16. <script>
  17. window.onload = function () {
  18. // new一个vue对象
  19. let vm = new Vue({
  20. // el为需要挂载的dom节点
  21. el: '#app',
  22. data: {
  23. info: {
  24. person: {
  25. name: '加载中'
  26. }
  27. },
  28. job: '程序猿'
  29. }
  30. })
  31. setTimeout(() => {
  32. vm.info.person.name = '小明'
  33. }, 2000)
  34. }
  35. class Vue {
  36. constructor(options) {
  37. this.$data = options.data
  38. this.$el = document.querySelector(options.el)
  39. observe(options.data)
  40. this.proxy(this.$data, this)
  41. this.compile(this.$el, this)
  42. }
  43. // 模板编译
  44. compile (dom, vm) {
  45. Array.from(dom.childNodes).forEach(child => {
  46. // 元素节点,匹配v-model,如input textArea元素等
  47. if (child.nodeType == 1) {
  48. Array.from(child.attributes).forEach(attr => {
  49. // 判断元素是否设置 v-model 属性
  50. if (attr.name.includes('v-model')) {
  51. Dep.target = child
  52. child.value = vm.$data[attr.value]
  53. Dep.target = null
  54. // 重点:给input原定绑定原生的input事件
  55. child.addEventListener('input', (e) => {
  56. // 当input输入内容发生变化时,动态设置vm.$data[attr.value]的值
  57. vm.$data[attr.value] = e.target.value
  58. })
  59. }
  60. })
  61. }
  62. if (child.nodeType === 3 && /\{\{(.*)\}\}/.test(child.textContent)) {
  63. let key = RegExp.$1.trim()
  64. let keyList = key.split('.')
  65. let value = vm.$data
  66. Dep.target = child
  67. // 循环遍历,找到info.person.name对应的name值
  68. keyList.forEach(item => {
  69. value = value[item]
  70. })
  71. Dep.target = null
  72. child.textContent = child.textContent.replace(`{{${key}}}`, value)
  73. }
  74. if (child.childNodes.length > 0) {
  75. // 递归模板编译
  76. this.compile(child, vm)
  77. }
  78. })
  79. }
  80. // this代理 this.$data
  81. // vm.info.person.name 相当于 vm.$data.info.person.name
  82. proxy ($data, vm) {
  83. Object.keys($data).forEach(key => {
  84. Object.defineProperty(vm, key, {
  85. set (newValue) {
  86. $data[key] = newValue
  87. },
  88. get () {
  89. return $data[key]
  90. }
  91. })
  92. })
  93. }
  94. }
  95. function observe (data) {
  96. if (data && typeof data == 'object') {
  97. return new Observe(data)
  98. }
  99. }
  100. // 递归进行数据劫持,使data中的每一层都是响应式的
  101. function Observe(data) {
  102. Object.keys(data).forEach(key => {
  103. let value = data[key]
  104. let dep = new Dep()
  105. // 递归
  106. observe(value)
  107. Object.defineProperty(data, key, {
  108. get () {
  109. Dep.target && dep.add(Dep.target)
  110. return value
  111. },
  112. set (newValue) {
  113. value = newValue
  114. // 如果新设置的值是一个对象,该对象也要变成响应式的
  115. observe(newValue)
  116. dep.update(newValue)
  117. }
  118. })
  119. })
  120. }
  121. class Dep {
  122. constructor() {
  123. this.subs = []
  124. }
  125. add (target) {
  126. this.subs.push(target)
  127. }
  128. update (newValue) {
  129. this.subs.forEach(node => {
  130. if (node.tagName == 'INPUT' || node.tagName == 'TEXTATEA') {
  131. node.value = newValue
  132. } else {
  133. node.textContent = newValue
  134. }
  135. })
  136. }
  137. }
  138. </script>
  139. </body>
  140. </html>

手写v-model的github源码地址

使用proxy实现数据监听

vue3底层通过Proxy实现了数据监听,替代了vue2中的Object.defineProperty

  1. function observe(target) {
  2. return new Proxy(target, {
  3. get(target, key, receiver) {
  4. let result = Reflect.get(target, key);
  5. // 递归获取对象多层嵌套的情况,如pro.info.type(递归监听,保证每一层返回都是proxy对象)
  6. return isObject(result);
  7. },
  8. set(target, key, value, receiver) {
  9. if (key !== 'length') {
  10. // 解决对数组修改,重复更新视图的问题
  11. console.log('更新视图');
  12. }
  13. return Reflect.set(target, key, value, receiver);
  14. }
  15. });
  16. }
  17. function isObject(target) {
  18. if (typeof target === 'object' && target !== null) {
  19. return observe(target);
  20. } else {
  21. return target;
  22. }
  23. }
  24. let target = { name: '测试', info: { type: '1' } };
  25. let pro = observe(target);
  26. pro.info.type = 2; // 更新视图

vue 异步更新原理

Vue的数据频繁变化,但为什么dom只会更新一次?
1)Vue数据发生变化之后,不会立即更新dom,而是异步更新的
2)侦听到数据变化,Vue 将开启一个队列,并缓存在同一事件循环中发生的所有数据变更
3)如果同一个 watcher 被多次触发,只会被推入到队列中一次,可以避免重复修改相同的dom,这种去除重复数据,对于避免不必要的计算和 DOM 操作是非常重要的
4)同步任务执行完毕,开始执行异步 watcher 队列的任务,一次性更新 DOM

nextTick为什么要优先使用微任务实现?

1)vue nextTick的源码实现,异步优先级判断,总结就是Promise > MutationObserver > setImmediate > setTimeout
2)优先使用Promise,因为根据 event loop 与浏览器更新渲染时机,宏任务 → 微任务 → 渲染更新,使用微任务,本次event loop轮询就可以获取到更新的dom
3)如果使用宏任务,要到下一次event loop中,才能获取到更新的dom

computed 和 watch的区别

1)计算属性本质上是 computed watcher,而watch本质上是 user watcher(用户自己定义的watcher)
2)computed有缓存的功能,通过dirty控制
3)wacher设置deep:true,实现深度监听的功能
4)computed可以监听多个值的变化
computed原理
1)初始化计算属性时,遍历computed对象,给其中每一个计算属性分别生成唯一computed watcher,并将该watcher中的dirty设置为true
初始化时,计算属性并不会立即计算(vue做的优化之一),只有当获取的计算属性值才会进行对应计算
2)初始化计算属性时,将Dep.target设置成当前的computed watcher,将computed watcher添加到所依赖data值对应的dep中(依赖收集的过程),然后计算computed对应的值,后将dirty改成false
3)当所依赖data中的值发生变化时,调用set方法触发dep的notify方法,将computed watcher中的dirty设置为true
4)下次获取计算属性值时,若dirty为true, 重新计算属性的值
5)dirty是控制缓存的关键,当所依赖的data发生变化,dirty设置为true,再次被获取时,就会重新计算
watch原理
1)遍历watch对象, 给其中每一个watch属性,生成对应的user watcher
2)调用watcher中的get方法,将Dep.target设置成当前的user watcher,并将user watcher添加到监听data值对应的dep中(依赖收集的过程)
3)当所监听data中的值发生变化时,会调用set方法触发dep的notify方法,执行watcher中定义的方法
4)设置成deep:true的情况,递归遍历所监听的对象,将user watcher添加到对象中每一层key值的dep对象中,这样无论当对象的中哪一层发生变化,wacher都能监听到。通过对象的递归遍历,实现了深度监听功能
Vue.js的computed和watch是如何工作的?
手写Vue2.0源码(十)-计算属性原理
珠峰:史上最全最专业的Vue.js面试题训练营

虚拟dom

什么是虚拟dom?
Virtual DOM是JS模拟真实DOM节点,这个对象就是更加轻量级的对DOM的描述

为什么现在主流的框架都使用虚拟dom?

1)前端性能优化的一个秘诀就是尽可能少地操作DOM,频繁变动DOM会造成浏览器的回流或者重绘
2)使用虚拟dom,当数据变化,页面需要更新时,通过diff算法,对新旧虚拟dom节点进行对比,比较两棵树的差异,生成差异对象,一次性对DOM进行批量更新操作,进而有效提高了性能
3)虚拟 DOM 本质上是 js 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便的跨平台操作,例如服务器渲染、weex 开发等等

虚拟dom与真实dom的相互转化
  1. // 将真实dom转化为虚拟dom
  2. function dom2Json(dom) {
  3. if (!dom.tagName) return;
  4. let obj = {};
  5. obj.tag = dom.tagName;
  6. obj.children = [];
  7. dom.childNodes.forEach(item => {
  8. // 去掉空的节点
  9. dom2Json(item) && obj.children.push(dom2Json(item));
  10. });
  11. return obj;
  12. }
  13. // 虚拟dom包含三个参数 type, props, children
  14. class Element {
  15. constructor(type, props, children) {
  16. this.type = type
  17. this.props = props
  18. this.children = children
  19. }
  20. }
  21. // 将虚拟dom渲染成真实的dom
  22. function render(domObj) {
  23. let el = document.createElement(domObj.type)
  24. Object.keys(domObj.props).forEach(key => { // 设置属性
  25. let value = domObj.props[key]
  26. switch (key) {
  27. case('value'):
  28. if (el.tagName == 'INPUT' || el.tagName == 'TEXTAREA') {
  29. el.value = value
  30. } else {
  31. el.setAttribute(key, value)
  32. }
  33. break;
  34. case('style'):
  35. el.style.cssText = value
  36. break;
  37. default:
  38. el.setAttribute(key, value)
  39. }
  40. })
  41. domObj.children.forEach(child => {
  42. child = child instanceof Element ? render(child) : document.createTextNode(child)
  43. })
  44. return el
  45. }

让虚拟DOM和DOM-diff不再成为你的绊脚石
虚拟 DOM 到底是什么?
详解vue的diff算法

vue-router原理

1)创建的页面路由会与该页面形成一个路由表(key value形式,key为该路由,value为对应的页面)
2)vue-router原理是监听 URL 的变化,然后匹配路由规则,会用新路由的页面替换到老的页面 ,无需刷新
3)目前单页面使用的路由有两种实现方式: hash 模式、history 模式
5)hash模式(路由中带#号),通过hashchange事件来监听路由的变化
window.addEventListener(‘hashchange’, ()=>{})
6)history 模式,利用了pushState() 和replaceState() 方法,实现往history中添加新的浏览记录、或替换对应的浏览记录
通过popstate事件来监听路由的变化,window.addEventListener(‘popstate’, ()=>{})
前端路由简介以及vue-router实现原理
Vue Router原理

vue3与vue2的区别

1)vue3性能比Vue2.x快1.2~2倍
2)使用proxy取代Object.defineproperty,解决了vue2中新增属性监听不到的问题,同时proxy也支持数组,不需要像vue2那样对数组的方法做拦截处理
3)diff方法优化
vue3新增了静态标记(patchflag),虚拟节点对比时,就只会对比这些带有静态标记的节点
4)静态提升
vue3对于不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用即可。vue2无论元素是否参与更新,每次都会重新创建然后再渲染
5)事件侦听器缓存
默认情况下onClick会被视为动态绑定,所以每次都会追踪它的变化,但是因为是同一个函数,所以不用追踪变化,直接缓存起来复用即可
6)按需引入,通过treeSharking 体积比vue2.x更小
7)组合API(类似react hooks),可以将data与对应的逻辑写到一起,更容易理解
8)提供了很灵活的api 比如toRef、shallowRef等等,可以灵活控制数据变化是否需要更新ui渲染
9)更好的Ts支持
VUE3对比VUE2的优势及新特性原理

从输入URL到页面加载发生了什么?

1)浏览器查找当前URL是否存在缓存,并比较缓存是否过期。(先判断HTTP请求浏览器是否已缓存)
有缓存
如为强制缓存,通过Expires或Cache-Control:max-age判断该缓存是否过期,未过期,直接使用该资源;Expires和max-age,如果两者同时存在,则被Cache-Control的max-age覆盖。
如为协商缓存,请求头部带上相关信息如if-none-match(Etag)与if-modified-since(last-modified),验证缓存是否有效,若有效则返回状态码为304,若无效则重新返回资源,状态码为200
2)DNS解析URL对应的IP(DNS解析流程见下文)
3)根据IP建立TCP连接(三次握手)(握手过程见下文)
4)HTTP发起请求
5)服务器处理请求,浏览器接收HTTP响应
6)渲染页面,构建DOM树
①HTML 解析,生成DOM树
②根据 CSS 解析生成 CSS 树
③结合 DOM 树和 CSS 规则树,生成渲染树
④根据渲染树计算每一个节点的信息(layout布局)
⑤根据计算好的信息绘制页面
如果遇到 script 标签,则判断是否含有 defer 或者 async 属性,如果有,异步去下载该资源;如果没有设置,暂停dom的解析,去加载script的资源,然后执行该js代码(script标签加载和执行会阻塞页面的渲染
7)关闭TCP连接(四次挥手)(挥手过程见下文)
从输入url到页面加载完成发生了什么详解
在浏览器输入 URL 回车之后发生了什么(超详细版)

彻底弄懂cors跨域请求

cors是解决跨域问题的常见解决方法,关键是服务器要设置Access-Control-Allow-Origin,控制哪些域名可以共享资源
origin是cors的重要标识,只要是非同源或者POST请求都会带上Origin字段,接口返回后服务器也可以将Access-Control-Allow-Origin设置为请求的Origin,解决cors如何指定多个域名的问题
cors将请求分为简单请求和非简单请求
简单请求
1)只支持HEAD,get、post请求方式;
2)没有自定义的请求头;
3)Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。如果浏览器发现这个接口回应的头信息没有包含Access-Control-Allow-Origin字段的话就会报跨域错误
非简单请求的跨域处理
非简单请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(options),用来判断当前网页所在的域名是否在服务器的许可名单之中
如果在许可名单中,就会发正式请求;如果不在,就会报跨越错误
注:新版chrome浏览器看不到OPTIONS预检请求,可以网上查找对应的查看方法
跨域资源共享 CORS 详解

TCP的三次握手和四次挥手

三次握手的过程:
1)第一次握手:客户端向服务端发送连接请求报文,请求发送后,客户端便进入 SYN-SENT 状态
2)第二次握手:服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,发送完成后便进入 SYN-RECEIVED 状态
3)第三次握手:当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED(已建立的) 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功
为什么需要三次握手?
三次握手之所以是三次,是保证client和server均让对方知道自己的接收和发送能力没问题而保证的最小次数。两次不安全,四次浪费资源
四次挥手的过程
当服务端收到客户端关闭报文时,并不会立即关闭,先回复一个报文,告诉客户端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送连接释放请求,因此不能一起发送。故需要四步挥手
举例:
Browser:先告诉服务器 “我数据都发完了,你可以关闭连接了。”
Server:回复浏览器 “关闭的请求我收到了,我先看看我这边还有没有数据没传完。”
Server:确认过以后,再次回复浏览器 “我这边数据传输完成了,你可以关闭连接了。”
Browser:告诉服务器 “好的,那我关闭了。不用回复了。”
客户端又等了2MSL,确认确实没有再收到请求了,才会真的关闭TCP连接。
为什么需要四次挥手?
1)TCP 使用四次挥手的原因,是因为 TCP 的连接是全双工的,所以需要双方分别释放掉对方的连接
2)单独一方的连接释放,只代 表不能再向对方发送数据,连接处于的是半释放的状态
3)最后一次挥手中,客户端会等待一段时间再关闭的原因,是为了防止客户端发送给服务器的确认报文段丢失或者出错,从而导致服务器端不能正常关闭
什么是2MSL?
MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”
四次挥手后,为什么客户端最后还要等待2MSL?
1)保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,如果服务端没有收到,服务端会重发一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器
2)防止“已经失效的连接请求报文段”出现在本连接中
客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的所产生的所有报文都从网络中消失。这样新的连接中不会出现旧连接的请求报文
TCP的三次握手和四次挥手
TCP的三次握手和四次挥手及常见面试题
什么是2MSL

WebSocket

WebSocket是HTML5提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议,WebSocket没有跨域的限制
相比于接口轮询,需要不断的建立 http 连接,严重浪费了服务器端和客户端的资源
WebSocket基于TCP传输协议,并复用HTTP的握手通道。浏览器和服务器只需要建立一次http连接,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
缺点
websocket 不稳定,要建立心跳检测机制,如果断开,自动连接
手摸手教你使用WebSocket[其实WebSocket也不难]
socket 及 websocket的握手过程

TCP和UDP的区别

相同点: UDP协议和TCP协议都是传输层协议
不同点:
1)TCP 面向有连接; UDP:面向无连接
2)TCP 要提供可靠的、面向连接的传输服务。TCP在建立通信前,必须建立一个TCP连接,之后才能传输数据。TCP建立一个连接需要3次握手,断开连接需要4次挥手,并且提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端
3)UDP不可靠性,只是把应用程序传给IP层的数据报发送出去,但是不能保证它们能到达目的地
4)应用场景
TCP效率要求相对低,但对准确性要求相对高的场景。如常见的接口调用、文件传输、远程登录等
UDP效率要求相对高,对准确性要求相对低的场景。如在线视频、网络语音电话等
面试题:UDP&TCP的区别
TCP和UDP的区别及应用场景

keep-alive 持久连接

keep-alive 又叫持久连接,它通过重用一个 TCP 连接来发送/接收多个 HTTP请求,来减少创建/关闭多个 TCP 连接的开销,启用Keep-Alive模式性能更高
在 HTTP1.1 协议中默认开启,可以在请求头上看到Connection: keep-alive 开启的标识
在HTTP1.0 中非KeepAlive模式时,每次请求都要新建一个TCP请求,请求结束后,要关闭 TCP 连接。效率很低
注意:持久连接采用阻塞模式,下次请求必须等到上次响应返回后才能发起,如果上次的请求还没返回响应内容,下次请求就只能等着(就是常说的线头阻塞)
HTTP keep-alive 二三事

http1、2、3的区别

http1、2的区别:
1)二进制传输,HTTP/2 采用二进制格式传输数据,而非HTTP/1.x 里纯文本形式的报文 ,二进制协议解析起来更高效
2)Header 压缩
HTTP/1.x的请求和响应头部带有大量信息,而且每次请求都要重复发送。HTTP2在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再每次请求和响应发送
3)多路复用
就是在一个 TCP 连接中可以发送多个请求,可以避免 HTTP 旧版本中的线头阻塞问题(下次请求必须等到上次响应返回后才能发起)
这样某个请求任务耗时严重,不会影响到其它连接的正常执行,极大的提高传输性能
在 HTTP/2 中,有两个非常重要的概念,分别是帧(frame)和流(stream)。 帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流(即请求),通过重新排序还原请求
4)服务端推送: 这里的服务端推送,是指把客户端所需要的css/js/img资源伴随着index.html,一起发送到客户端,省去了客户端重复请求的步骤
Http3.0的区别
http 协议是应用层协议,都是建立在传输层之上的。2.0 和 1.0 都是基于 TCP 的,而 Http3.0 则是建立在 UDP 的基础上
http3.0 新特性
1)多路复用,彻底解决TCP中队头阻塞的问题
2)集成了TLS加密功能
3)向前纠错机制
http1、2、3总结:
1)HTTP/1.1有两个主要的缺点:安全不足和性能不高
2)HTTP/2完全兼容HTTP/1,是“更安全的HTTP、更快的HTTPS”,头部压缩、多路复用等技术可以充分利用带宽,降低延迟,从而大幅度提高上网体验
3)QUIC 基于 UDP 实现,是 HTTP/3 中的底层支撑协议,该协议基于 UDP,又取了 TCP 中的精华,实现了即快又可靠的协议
解密HTTP/2与HTTP/3 的新特性
HTTP/3 新特性

HTTPS 握手过程

https采用非对称加密+对称加密,非对称加密来传递密钥;对称加密来加密内容
1)客户端使用https的url访问web服务器,要求与服务器建立ssl连接
2)服务器收到客户端请求后, 会将网站的证书(包含公钥)传送一份给客户端
3)客户端收到网站证书后会检查证书的颁发机构以及过期时间, 如果没有问题就随机产生一个秘钥
4)客户端利用公钥将会话秘钥加密, 并传送给服务端
5)服务端利用自己的私钥解密出会话秘钥,之后服务器与客户端使用秘钥加密传输
加密速度对比
对称加密解密的速度比较快,适合数据比较长时的使用
非对称加密和解密花费的时间长、速度相对较慢,只适合对少量数据的使用
(非对称加密:有公钥私钥,公钥加密,私钥解密;对称加密:同一个秘钥进行加密或解密)
一个故事讲完https

介绍下中间人攻击

中间人攻击过程如下:
1)客户端向服务器发送建立连接的请求
2)服务器向客户端发送公钥
3)攻击者截获公钥,保留在自己手上
4)然后攻击者自己生成一个【伪造的】公钥,发给客户端
5)客户端收到伪造的公钥后,生成加密的秘钥值发给服务器
6)攻击者获得加密秘钥,用自己的私钥解密获得秘钥
7)同时生成假的加密秘钥,发给服务器
8)服务器用私钥解密获得假秘钥
9)服务器用假秘钥加密传输信息
防范方法:
服务端在发送浏览器的公钥中加入CA证书,浏览器可以验证CA证书的有效性
介绍下 HTTPS 中间人攻击

DNS解析过程

DNS 解析过程:将域名解析成 IP 地址
DNS叫做域名系统,是域名和对应ip地址的分布式数据库。有了它,就可以用域名来访问对应的服务器
过程:
1)在浏览器中输入后url后,会优先在浏览器dns缓存中查找,如果有缓存,则直接响应用户的请求
2)如果没有要访问的域名,就继续在操作系统的dns缓存中查找,如果也没有,最后通过本地的dns服务器查到对应的ip地址
3)DNS服务器完整的查询过程
本地DNS服务器向根域名服务器发送请求,根域名服务器会返回一个所查询域的顶级域名服务器地址
本地DNS服务器向顶级域名服务器发送请求,接受请求的服务器查询自己的缓存,如果有记录,就返回查询结果,如果没有就返回相关的下一级的权威域名服务器的地址
本地DNS服务器向权威域名服务器发送请求,权威域名服务器返回对应的结果
本地DNS服务器将返回结果保存在缓存中,便于下次使用
本地DNS服务器将返回结果返回给浏览器
DNS预解析
DNS Prefetch 是一种DNS 预解析技术,当你浏览网页时,浏览器会在对网页中的域名进行解析缓存,这样当页面中需要加载该域名的资源时就无需解析,减少用户等待时间,提高用户体验

DNS完整的查询过程
dns-prefetch对网站速度能提升有多少?

XSS(跨站脚本攻击)

XSS攻击介绍: 攻击者通过在页面注入恶意脚本,使之在用户的浏览器上运行
攻击案例:

  1. <div><script>alert('XSS')</script></div>
  2. <a href="javascript:alert('XSS')">123456</a>
  3. <a onclick='alert("xss攻击")'>链接</a>

XSS 攻击的几种方式
1)常见于带有用户提交数据的网站功能,如填写基本信息、论坛发帖、商品评论等;在可输入内容的地方提交如之类的代码
XSS 的恶意代码存在数据库里,浏览器接收到响应后解析执行,混在其中的恶意代码也被执行
2)用户点击http://xxx/search?keyword=">,前端直接从url中将keyword后的参数取出来,并显示到页面上,但是没有做转义,就造成了XSS攻击。
XSS攻击的防范
1)前端尽量对用户输入内容长度控制、输入内容限制(比如电话号码、邮箱、包括特殊字符的限制)
2)服务器对前端提交的内容做好必要的转义,避免将恶意代码存储到数据库中,造成存储性xss攻击
3)前端对服务器返回的数据做好必要的转义,保证显示到页面的内容正常
vue中如何防止XSS攻击
1)vue中使用{{}}模板渲染数据或通过v-bind给元素绑定属性时,都已将内容转义,防止xss攻击

  1. // 案例
  2. <h1>{{string}}</h1>
  3. string = '<script>alert("hi")</script>'`
  4. //被转义成为如下 &lt;script&gt;alert(&quot;hi&quot;)&lt;/script&gt;

2)尽量避免使用v-html,如果必须使用,可以使用vue-xss插件对文本内容进行转义,该插件可以同时去掉上面绑定的事件

  1. // 案例
  2. `<div v-html="$xss(xss)"></div>`
  3. // p标签正常显示,但上面绑定的事件已被去掉
  4. xss= "<p onclick='console.log(document.cookie)'>123</p>"

前端安全系列(一):如何防止XSS攻击?

csrf 跨站请求伪造

csrf的攻击原理:
诱导受害者进入钓鱼网站,在钓鱼网站中利用你在被攻击网站已登录的凭证(凭证存在cookie中),冒充用户发送恶意请求,这些请求因为携带有用户的登录信息,会被服务器当做正常的请求处理,从而使你的个人隐私泄露或财产损失
csrf的攻击过程:
1)受害者登录A站点,并保留了登录凭证(Cookie)
2)攻击者诱导受害者访问了站点B
3)站点B向站点A发送了一个请求,浏览器会默认携带站点A的Cookie信息
4)站点A接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者发送的请求
5)站点A以受害者的名义执行了站点B的请求,攻击完成,攻击者在受害者不知情的情况下,冒充受害者完成了攻击
csrf的攻击的必要条件:
1)用户已登录过某网站,并且没有退出,登录信息存储在cookie中(发送请求时,浏览器会自动在请求头上带上要请求域名的cookie)
2)在不登出A的情况下,访问危险网站B
CSRF如何防御
1)根据攻击的原理可以看出,csrf通常是跨域请求(从钓鱼网站B发送请求网站A的请求),请求头上的Referer或origin字段可以判断请求的来源,如果服务器判断请求的域名不在白名单中,就拒绝对应的请求
2)添加token验证
CSRF攻击之所以能够成功,是因为用户验证信息都存在cookie中,攻击者可以完全伪造用户的请求。从请求头或请求参数中添加用户的token用来验证用户,如果请求没有或token不对,就拒绝对应的请求
3)验证码
对于转账或支付的环节,强制用户必须与应用进行交互,才能完成最终请求
前端安全系列(二):如何防止CSRF攻击?
WEB安全之-CSRF(跨站请求伪造)

jsonp安全防范

jsonp是以callback的形式,返回服务端的数据 如http://www.qq.com/getUserInfo?callback=action
1)白名单验证
通过请求头上的Referer或origin字段可以判断请求的来源,如果服务器判断请求的域名不在白名单中,就拒绝对应的请求
2)对返回的内容进行验证或转义
根据jsonp的原理,当拿到callback的参数后,会直接当js代码执行,如果callback后面的参数是script标签,就会变成xss攻击了,所以要对返回的内容进行转义并限制长度,防范类似的攻击
例如http://youdomain.com?callback=
前端也需要了解的 JSONP 安全

浏览器如何验证ca证书的有效性

浏览器读取证书中的证书所有者、有效期等信息进行校验
1)校验证书的网站域名是否与证书颁发的域名一致
2)校验证书是否在有效期内
3)浏览器查找操作系统中已内置的受信任的证书发布机构,与服务器发来的证书中的颁发者做比对,用于校验证书是否为合法机构颁发
HTTPS 握手过程中,客户端如何验证证书的合法性

浏览器原理

js的单线程

js是单线程,只是说js的执行是单线程的,但js的宿主环境,无论是 Node 还是浏览器都是多线程的
以Chrome浏览器中为例,当你打开一个页面,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。 当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。
JS为什么设计成单线程?
如果有多个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时这两个节点会有很大冲突,为了避免这个冲突,所以决定了它只能是单线程

线程与进程

一句话:进程可以包含多个线程
进程是 CPU 资源分配的最小单位;线程是 CPU 调度的最小单位
浏览器进程包括:
1)浏览器主进程(Browser进程)
主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
2)浏览器渲染进程(Renderer进程)
浏览器渲染进程:即通常所说的浏览器内核
核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下
3)GPU 进程
GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制
4)第三方插件进程
主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,每种类型的插件对应一个进程, 以保证插件进程崩溃不会对浏览器和页面造成影响。
浏览器渲染进程(Renderer进程)包含5种线程:
1)GUI渲染线程
主要负责页面的渲染,解析 HTML、CSS,构建 DOM 树,布局和绘制等
2)JS引擎线程
该线程主要负责处理 JavaScript 脚本,执行代码。该线程与 GUI 渲染线程互斥,当 JS 引擎线程执行 JavaScript 脚本时间过长,将导致页面渲染的阻塞。
3)事件触发线程
主要负责将准备好的事件交给 JS 引擎线程执行。比如 setTimeout 定时器计数结束, ajax 等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待 JS 引擎线程的执行
4)定时器触发线程
负责执行异步定时器一类的函数的线程,如: setTimeout,setInterval
5)异步http请求线程
负责执行异步请求一类的函数的线程,如: Promise,axios,ajax 等
浏览器的线程和进程
浏览器相关原理(面试题)详细总结一
浏览器与Node的事件循环(Event Loop)有何区别?

浏览器页面渲染机制

浏览器有GUI渲染线程与JS引擎线程,这两个线程是互斥的关系
JavaScript的加载、解析与执行会阻塞DOM的构建。也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建
但是如果遇到带有async和defer的script标签,就会异步请求这些资源,不会阻塞页面渲染
浏览器渲染过程分为:构建DOM -> 构建CSSOM -> 构建渲染树 -> layout布局 -> 绘制

script标签 async defer的区别

直接使用script会阻塞DOM渲染,在脚本加载&执行的过程中,会阻塞后续的DOM渲染
使用async和defer,这两个属性使得script都不会阻塞DOM的渲染
async和defer的区别
async是无顺序的加载,而defer是有顺序的加载
1)执行顺序的区别
async的执行,并不会按照script在页面中的顺序来执行,而是谁先加载完谁执行
defer的执行,则会按照引入的顺序执行,即便是后面的script资源先返回
2)对window.onload的影响
使用defer的script标签,会在window.onload 事件之前被执行
使用async的script标签,对window.onload 事件没有影响,window.onload可以在之前或之后执行
使用场景的区别
1)defer可以用来控制js文件的加载顺序
比如jq 和 Bootstrap,因为Bootstrap中的js插件依赖于jqery,所以必须先引入jQuery,再引入Bootstrap js文件
2)如果你的脚本并不关心页面中的DOM元素(文档是否解析完毕),并且也不会产生其他脚本需要的数据,使用async, 如统计、埋点等功能
浅谈script标签中的async和defer

DOM事件流

DOM事件流:事件流简单来说就是事件执行顺序
DOM同时支持两种事件模型:捕获型事件流和冒泡型事件流
DOM2事件流的三个阶段:
1)事件捕获阶段
2)处于目标阶段
3)事件冒泡阶段
DOM事件捕获的具体流程:
window➡️document➡️html➡️body➡️目标元素;
事件冒泡:就是这个顺序反过来
运用:事件委托,利用事件冒泡原理
事件委托:当一组元素要添加相同的事件时,可以在父元素上绑定一个事件,利用事件冒泡原理,达到父元素代理子元素事件,点击子元素,通过e.target || e.srcElement 可以获取点击的具体子元素
时间委托的优点
可以减少事件的注册,节省内存,也可以实现当新增对象时无需再次对其绑定事件
addEventListener的第三个参数
第三个参数默认是false,表示在事件冒泡阶段调用;当该值为true表示在事件捕获阶段调用。
验证整个事件流执行顺序(先捕获再冒泡)

  1. // 鼠标点击子元素后,打印顺序为
  2. // 父捕获
  3. // 子捕获
  4. // 子冒泡
  5. // 父冒泡
  6. <html>
  7. <div class="parent">
  8. <div class="child">子元素</div>
  9. </div>
  10. <script>
  11. let parentDom = document.querySelector('.parent');
  12. parentDom.addEventListener('click', function () {console.log('父捕获'); }, true)
  13. parentDom.addEventListener('click', function () {console.log('父冒泡');}, false)
  14. let childDom = document.querySelector('.child')
  15. childDom.addEventListener('click', function () {console.log('子捕获');}, true)
  16. childDom.addEventListener('click', function () {console.log('子冒泡');}, false)
  17. </script>
  18. </html>

浏览器空闲时间

页面是一帧一帧绘制出来的,一般情况下,设备的屏幕刷新率为1s 60次,而当FPS小于60时,会出现一定程度的卡顿现象
下面来看完整的一帧中,具体做了哪些事情:
1)首先需要处理输入事件,能够让用户得到最早的反馈
2)接下来是处理定时器,需要检查定时器是否到时间,并执行对应的回调
3)接下来处理 Begin Frame(开始帧),即每一帧的事件,包括 window.resize、scroll、media query change 等
4)接下来执行请求动画帧 requestAnimationFrame(rAF),即在每次绘制之前,会执行 rAF 回调
5)紧接着进行 Layout 操作,包括计算布局和更新布局,即这个元素的样式是怎样的,它应该在页面如何展示
6)接着进行 Paint 操作,得到树中每个节点的尺寸与位置等信息,浏览器针对每个元素进行内容填充
7)到这时以上的六个阶段都已经完成了,接下来处于空闲阶段(Idle Peroid)
requestIdleCallback
在空闲阶段(Idle Peroid)时,可以执行 requestIdleCallback 里注册的任务
requestIdleCallback接收两个参数:
window.requestIdleCallback(callback, { timeout: 1000 })
1)第一个参数是一个函数,该函数的入参,可以获取当前帧的剩余时间,以及该任务是否超时

  1. window.requestIdleCallback(deadline => {
  2. // 返回当前帧还剩多少时间供用户使用
  3. deadline.timeRamining;
  4. // 返回 callback 任务是否超时
  5. deadline.didTimeout;
  6. });

2)第二个参数,传入timeout参数自定义超时时间,如果到了超时时间,浏览器必须立即执行
例子:打印此帧的剩余时间

  1. // 该函数的执行时间超过1s
  2. function calc() {
  3. let start = performance.now();
  4. let sum = 0;
  5. for (let i = 0; i < 10000; i++) {
  6. for (let i = 0; i < 10000; i++) {
  7. sum += Math.random();
  8. }
  9. }
  10. let end = performance.now();
  11. let totolTime = end - start;
  12. // 得到该函数的计算用时
  13. console.log(totolTime, "totolTime");
  14. }
  15. let tasks = [
  16. () => {
  17. calc();
  18. console.log(1);
  19. },
  20. () => {
  21. calc();
  22. console.log(2);
  23. },
  24. () => {
  25. console.log(3);
  26. }
  27. ];
  28. let work = deadline => {
  29. console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`);
  30. // 如果此帧剩余时间大于0或者已经到了定义的超时时间(上文定义了timeout时间为1000,到达时间时必须强制执行),且当时存在任务,则直接执行这个任务
  31. // 如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器
  32. while (
  33. (deadline.timeRemaining() > 0 || deadline.didTimeout) &&
  34. tasks.length > 0
  35. ) {
  36. let fn = tasks.shift();
  37. fn();
  38. }
  39. // 如果还有未完成的任务,继续调用requestIdleCallback申请下一个时间片
  40. if (tasks.length > 0) {
  41. window.requestIdleCallback(work, { timeout: 500 });
  42. }
  43. };
  44. window.requestIdleCallback(work, { timeout: 500 });

执行结果:分3帧进行计算
前端知识体系总结(框架+浏览器篇) - 图3
走进React Fiber的世界

浏览器缓存

分为协商缓存和强制缓存

协商缓存的流程

1)第一次请求
1、客户端发送GET请求,去请求文件;
2、服务器处理请求,返回文件内容和一堆Header,包括Etag,状态码200
2)第二次请求
1、客户端发起 HTTP GET 请求一个文件,注意这个时候客户端请求头上,会带上if-none-match值为Etag和if-modified-since值为last-modified
2、服务器优先判断Etag和计算出来的Etag匹配,若匹配status状态为304,客户端继续使用本地缓存

Etag

Etag是服务器文件的唯一标识,当文件内容变化时Etag值也会发生变化
Etag主要为了解决 Last-Modified 无法解决的一些问题。一些文件也许会周期性的更改,但是它的内容并不改变(仅仅改变的修改时间),此时希望重用缓存,而不是重新请求

Etag比last-modified哪个优先级更高?

当ETag和Last-Modified同时存在时,服务器优先检查ETag

强缓存

强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的
当同时存在Expires 和 Cache-Control:max-age 时 哪个优先级高?
Cache-Control:max-age优先级高,Cache-Control:max-age表示缓存内容在xxx秒后失效;Expires表示服务端返回的到期时间
Expires缺点:返回的是服务端时间,与客户端时间相比,可能会出现时间不一致
Etag详解
为什么Etag比last-modified优先级更高?

Cache-Control: no-cache 和no-store的区别

Cache-Control: no-cache:这个很容易让人产生误解,使人误以为是响应不被缓存
实际上Cache-Control: no-cache是会被缓存的,只不过浏览器每次都会向服务器发起请求,来验证当前缓存的有效性
Cache-Control: no-store:这个才是响应不被缓存的意思

垃圾回收机制

GC 垃圾回收策略

1)标记清除
分为 标记 和 清除 两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁
在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
然后从各个根对象开始遍历,把不是垃圾的节点改成1,清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间。最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
2)引用计数
一个对象,如果没有其他对象引用到它,这个对象就是零引用,将被垃圾回收机制回收
它的策略是跟踪记录每个变量值被使用的次数
一个对象被其他对象引用时,这个对象的引用次数就为 1,如果同一个值又被赋给另一个变量,那么引用数加 1,如果该变量的值被其他的值覆盖了,则引用次数减 1
当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存

分代式垃圾回收机制

V8采用了一种代回收的策略,将内存分为两个生代:新生代和老生代
新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象

内存回收的例子

假设代码中有一个对象 jerry ,这个对象从创建到被销毁,刚好走完了整个生命周期,通常会是这样一个过程
1)这个对象被分配到了新生代;随着程序的运行,新生代塞满了,GC 开始清理 新生代里的死对象,jerry 因为还处于活跃状态,所以没被清理出去;
2)GC清理了两遍新生代,发现 jerry 依然还活跃着,就把 jerry 移动到了老生代
3)随着程序的运行,老生代也塞满了,GC 开始清理老生代,这时候发现 jerry 已经没有被引用了,就把 jerry 给清理出去了。

新老生代垃圾回收方式

新老生代采用不同的垃圾回收算法来提高效率,对象最开始都会先被分配到新生代(如果新生代内存空间不够,直接分配到老生代),新生代中的对象会在满足某些条件后,被移动到老生代,这个过程也叫晋升
新生代的垃圾回收方式
将内存空间一分为二,分为From空间(使用状态), To空间(闲置状态)
当新生代内存不足时,会将From空间中存活的对象复制到到To空间,然后将From空间清空,交换From空间和To空间(将原来的From空间变为To空间),继续下一轮
老生代的垃圾回收方式
V8在老生代中主要采用了Mark-Sweep和Mark-Compact相结合的方式
Mark-Sweep遍历堆内存中的所有对象,并标记活着的对象,在随后的清除阶段,只清除没有被标记的对象
Mark-Sweep最大的问题就是,在进行清除回收以后,内存空间会出现不连续的状态,会造成内存碎片化
Mark-Compact用来解决内存碎片的问题,将将存活对象向内存一侧移动,清空内存的另一侧,这样空闲的内存都是连续的
分代内存
64位系统,新生代内存大小为32MB,老生代内存为1.4G;32位系统,新生代内存大小为16MB,老生代内存为0.7G
V8 内存浅析
「硬核JS」你真的了解垃圾回收机制吗

作者:海阔_天空
链接:https://juejin.cn/post/7146996646394462239
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。