01 - 事件循环

1.1 - 如何理解事件循环 ?

是浏览器核心原理之一,计时器、Promise、ajax、node...都与之相关

事件循环又称为消息循环(W3C 的规范称为 event loop,谷歌浏览器的实现为 message loop),是浏览器渲染主线程的工作方式(浏览器是多进程多线程的应用程序,渲染进程中存在一个渲染主线程)。 在谷歌浏览器的代码实现中,开启了一个不会结束的 for 循环(for ( ; ; ) ),每次循环从消息队列中取出一个任务执行,其他线程只需要将任务加到队列末尾即可。 过去消息队列简单的分为宏队列和微队列(JS分为同步任务与异步任务,异步任务中有宏任务和和微任务),现在随着浏览器复杂度的增高,宏任务已经被一种通过将任务分类型的方式所替代。W3C 的官方解释为,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务也可以在同一队列,队列之间有不同的优先级,优先级决定渲染主线程取任务的先后。具体的优先级是由浏览器实现的,但浏览器中必须设置一个微队列,而微队列拥有最高的优先级,必须优先队列,例如在 JS 中 Promise .then 的函数将会进入微队列中。另外,在谷歌浏览器中交互队列的优先级是高于延时队列的。 事件循环是 JS 异步的实现方式,而单线程又是异步产生的原因。异步可以保证渲染主线程不被阻塞,防止页面卡死的现象出现。

1.1.1 - 什么是进程 ? 程序运行在内存之中所专属的内存空间,可以简单的理解为一个进程,例如每一个运行的.exe,进程是系统资源分配的基本单位。每个应用至少有一个进程,进程之间是相互独立的,进程之间的通信需要双方同意。 1.1.2 - 什么是线程 ? 一个进程至少有一个线程,多个线程之间可以共享同一个进程的资源,例如共享进程的堆栈,同时线程又拥有自己的程序计数器、本地方法栈等,所以线程是系统任务调度和执行的基本单位。 多进程可以理解为系统中运行多个程序,多线程可以理解为一个程序中的运行的多个任务。 1.1.3 - 浏览器有哪些进程和线程 ? 浏览器是一个多进程且多线程的应用程序,比如浏览器进程、网络进程、渲染进程...可以减少连环崩溃的几率。渲染进程启动后,会开启一个渲染主线程,主线程负责执行 HTML、CSS、JS代码。默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响。 1.1.4 - 渲染主线程是如何工作的 ? 渲染主线程负责解析、计算、布局、处理、执行......主线程要解决大量的调度任务。为了应对这些任务,渲染主线程创建了消息/事件队列(message queue)让任务排队处理,正在处理的任务可能产生子任务,子任务也要进入队列排队处理,另外其他线程产生的任务也会安排进队列中排队,存在多个队列。 最开始的时候,渲染主线程会进入一个无限循环。每一次循环会检查消息队列中是否有任务存在,如果有就取出第一个任务执行,执行后进入下一次循环,否则进入休眠状态。其他线程,包括其他进程的线程可以随时向消息队列添加任务,新任务会加到消息队列的末尾,添加时如果主线程是休眠状态则会将其唤醒继续循环拿任务,整个过程就称为事件(消息)循环。 1.1.5 - 什么是异步 ? 代码执行过程中无法立即执行的任务,例如计时完成后需要执行的任务、网络通信完成后需要执行的任务、用户操作后需要执行的任务(事件监听)...完全同步,会造成阻塞,导致程序无法继续出现卡死现象。 1.1.6 - JS为什么会阻塞渲染 ? 有些代码段的执行消耗时间过久或出现短暂死循环(如需要等待特定时间后符合退出循环的条件),导致渲染任务及后续的任务被短暂阻塞(卡顿) 1.1.7 - 任务有优先级吗 ? 任务没有优先级,但存放任务的队列存在优先级。

image.png

1.2 - 如何理解 JS 的异步 ?

JS 是一门单线程的语言,因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。渲染主线程承担着大量的任务,例如解析代码、渲染页面、执行 JS 等都在其中,这其中是事件循环在推动着线程的运行。如果采用同步的方式,极有可能导致主线程阻塞(可能是一个计时任务,或者监听的点击事件),从而导致消息队列中很多其他任务无法得到执行,浪费了宝贵的时间,页面可能无法更新造成用户界面卡死的状态。 浏览器采用异步的方式来避免这一问题,具体的做法是当某任务发生时,例如计时器、网络请求、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行下一任务的代码,当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾等待主线程的调度,在这种异步模式下,可以保证主线程不会出现阻塞,从而保证了单线程的流畅运行。

1.3 - JS 中的计时器能够做到精确计时吗 ?

不行。

  1. 浏览器方面,受事件循环的影响,计时器的回调函数只能在主线程空闲并且优先级到达时运行,所以带来了一定的偏差;
  2. W3C 规范方面,浏览器在实现计时器时如果嵌套的层级超过了5层,则会设置4毫秒的最少时间,这样计时小于4毫秒的设定将会带来偏差;
  3. 操作系统方面,JS的计时器最终调用的是操作系统的接口,操作系统的计时方法本身可能就带有一定量的偏差
  4. 硬件方面,计算机硬件中没有原子钟做不到绝对精确的时间。

02 - 浏览器渲染原理

渲染(render):网络请求到的 HTML 字符串通过渲染计算得到页面

2.1 - 浏览器是如何渲染页面的 ?

当浏览器的网络线程收到 HTML 文档后,会产生一个渲染任务,会将该任务传递给渲染主线程的消息队列。在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。 整个渲染流程分为多个阶段:解析 HTML、样式计算、布局、分层、绘制、分块、光栅化、画,每一个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。

  • 第一阶段,解析 HTML

解析的过程中遇到 CSS 就会解析 CSS,遇到 JS 默认会执行 JS,将会生成 DOM 树和 CSSOM 树,树中的每个节点就是对象。为了提升解析的效率,浏览器在开始解析之前会启动一个预解析线程,率先下载 HTML 中的外部 CSS 文件和外部的 JS 文件。 如果主线程解析到 link 位置,此时外部 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML,因为下载解析 CSS 的工作是在预解析线程中进行的,这也是 CSS 不会阻塞 HTML 解析的原因。 一般情况下如果解析到 script 位置,会停止解析 HTML,转而等待 JS 文件下载好,待执行完毕之后继续解析 HTML ,这是因为代码执行过程中可能会修改当前的 DOM 树,所以 DOM 树的生成需要暂停,这就是 JS 会阻塞 HTML 解析的原因。

默认情况下如上所诉, JS 的解析是同步的,在设置 defer 或 async 属性之后,JS 的解析变为异步执行,此时解析到 JS 代码之后,不会停下来等待它的下载和执行,而是继续解析。其中 defer 下载完毕后不会立即执行而是在文档解析完毕 DOM 树已经构建完成之后才会执行,适用于代码依赖 DOM 时使用,而 async 在下载完毕之后,需要停下当前的解析工作转而执行下载完毕的 JS,适用于代码不依赖 DOM 时使用。

这一步完成之后,会得到 DOM 树和 CSSOM 树。浏览器的默认是样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。

  • 第二阶段,样式计算

主线程会遍历解析时得到的 DOM 树,依次为树中的每个节点得出其计算属性或者说是最终的样式。这个过程中很多的预设值会变成绝对值,比如 black 会变成 rgb(0,0,0) ,相对单位的 em 会变成 px... 这一步完成之后,会得到一颗带有样式的 DOM 树。

  • 第三阶段,布局

内容必须放置在行盒中(块盒中的内容会追加一个匿名行盒),行盒和块盒不能相邻(相邻会追加匿名块盒等)

布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息,例如节点的宽高、相对包含块的位置,生成一颗布局树。大部分时候,DOM 树的布局树并非一一对应,例如 display: none 的节点没有任何几何信息,因此不会生成到布局树,又比如使用了伪元素选择器,虽然 DOM 树中不存在这些伪元素节点,但是他们拥有几何信息,所以会生成到布局树中,还有匿名行盒、匿名块盒等都会导致 DOM 树和布局树(layout)无法一一对应。

  • 第四阶段,分层

主线程会使用一套复杂的策略对整个布局树进行分层。分层的好处在于,将来某一个层改变之后,仅会对该层进行后续处理,从而提升效率。 滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过 will-change 属性更大程度的影响分层结果。

  • 第五阶段,绘制

主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。 完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余的工作由合成线程完成。

  • 第六阶段,分块

合成线程首先对每个图层进行分块,将其划分为更多的小区域,它会从线程池中拿取多个线程来完成分块工作。

  • 第七阶段,光栅化

合成线程会将块信息交给 GPU 进程,以极高的速度完成光栅化。GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块,光栅化的结果就是一块一块的位图。

  • 第八阶段,画

合成线程拿到每个层、每个块的位图之后生成一个个指引(quad)信息。指引会标识出每个位图应该画在屏幕的哪个位置,以及考虑到旋转、缩放等变形。 变形发生在合成线程与渲染主线程无关,这就是 transform 效率高的本质原因。合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用提交给 GPU 硬件,完成最终的屏幕成像。

2.2 - 什么是 reflow ?

reflow(重排版) 的本质是重新计算布局(layout)树。当进行了影响布局树的操作后,需要重新计算布局树。为了避免连续多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成之后再进行统一计算。所以改动属性造成的 reflow 是异步完成的,正因如此,当 JS 获取布局属性时,就可能造成无法获取到最新的布局信息,浏览器在权衡下最终决定当获取属性时应立即 reflow

2.3 - 什么是 repaint ?

repaint (重绘制)的本质就是重新根据分层信息计算了绘制指令。当改动了可见样式后,就需要重新计算会引发 repaint。由于元素的布局信息也属于可见样式,所以 reflow 一定会引发 repaint

2.4 - 为什么 transform 的效率高 ?

因为 transform 既不影响布局也不影响绘制指令,它影响的只是渲染流程的最后一个 draw 阶段,由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌也不会影响 transform 的变化。

03 - 区分 property 和 attribute

  • attribute(特性),是HTML 标签上的特性,例如 type、value、id、class等以及自定义的特性,其值只能为字符串。使用 setAttribute、getAttribute、removeAttribute 来设置获取和移除特性。
  • property(属性),是 JS 获取到的 DOM对象上的属性,其中包含了 HTML 上自带的特性,不包括自定义的。属性是可以赋值任何类型值的。
  • property 和 attributes 都是 properties 的子集,attribute 是 attributes的子集。
  • 通过 HTML 添加的都是 attribute 特性,property 需要使用 JS 进行属性设置。关于 value,property 是同步当前输入设置的 value 值,attribute 为初始化时的值,初始时二者相等。更改 attribute 需要使用 setAttribute,更改 attribute 的 value 值会影响到 properrty 反之不影响。大部分情况二者是相互影响的。

04 - Vue 响应式原理

**Vue2:**普通的 JS 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。 image.png

由于 Object.defineProperty 的限制,Vue 不能检测数组和对象的变化。

对于对象:Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名。 Vue 不能检测以下数组的变动:当利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue;当你修改数组的长度时,例如:vm.items.length = newLength。解决办法:Vue.set(vm.items, indexOfItem, newValue)vm.items.splice(indexOfItem, 1, newValue)vm.$set(vm.items, indexOfItem, newValue)vm.items.splice(newLength) Vue 在更新 DOM 时是异步执行的,只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。 Vue3: Proxy...

05 - Vue 虚拟 DOM

return createElement('h1', this.blogTitle) createElement 返回的不是一个实际的 DOM 元素,它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是对由 Vue 组件树建立起来的整个 VNode 树的称呼。

SaaS 软件即服务 ERP 企业资源计划 CRM 客户关系管理 OA 办公自动化 HR 人力资源 进销存系统 财务系统 jwt B端 C端 O2O 双向数据绑定 hooks 与 class pwa indexdb websql localStorage sessionStorage 为什么渲染进程不适合用多个线程来处理 登录状态如何保持 原型与原型链(图) 浏览器缓存 cooking与token ES6新增了哪些新特性:箭头函数、类(语法糖)、对象字面量扩展语法、模板字符串、解构赋值、let const、Iterators + For..Of、Generators、Unicode、模块化(CommonJS\AMD)、Map + Set + WeakMap + WeakSet、Proxy、Symbols、Promises 闭包问题,闭包和let的关系 栈和队列,两个栈实现队列效果 TCP的三次握手和四次挥手的过程 两个人同时登录一个账号 小程序中的双向绑定和vue中双向绑定的区别 call、apply、bind 算法题:数组去重,查找链表的第N个和倒数第N个值 笛卡尔积 diff算法 计时器相关 斐波那契数列,动态规划 git 排除某些文件的上传 包含块 行盒、块盒;盒模型等 flex grid 浮动 页面静态化处理 HTML5 history 模式或 hash 模式 mvvm 如何分层 每层的作用,mvc 如何分层 每层的作用 埋点 如何封装组件,如何注册组件 git

附录 - 常用框架-库-工具的作用:

  • Browserify:用于 CommonJS 模块化规范(同步执行),此工具用于浏览器端程序编译打包

module.exports require(...)

  • RequireJS:用于 AMD 模块化规范(异步执行),此工具用于浏览器端 js 文件的模块化,配置各模块

define(['module1', 'module2' ...], function(module1, module2) {... return {...})(function() {requirejs.config({baseUrl:..., paths: {模块名与路径的映射关系}}), requirejs(['modules'], function() {})})<script data-main="js/app.js" src="js/require.js"></script>

  • **Babel:**JS编译器,将ES6+语法编译为ES5+语法,结合 **Browserify **可以实现 ES6 模块化规范

export ... 可以分别暴露也可以统一暴露 import ... from ...分别暴露和统一暴露(常规暴露)时需要使用对象进行接收(解构赋值),可以使用 as 起别名,使用export defalut默认暴露可以暴露任意数据类型同时可以使用任意变量接收