JS 基础相关
前端数据类型?
基本类型:string、number、boolean、undefined、null、symbol、bigint
引用类型:object、array、function
区别
- 基本数据类型是存储在栈中的简单数据段
- 引用数据类型是存储在堆内存中的数据,在栈中存储了他的引用地址。
数据类型检测的方法
- typeof: 数组、对象、null 都会被判断为 object
- instanceof: 只能正确判断引用类型,实际上就是判断在其原型链中能否找到该类型的原型
- constructor: 判断数据的类型,对象实例通过 constructor 对象访问它的构造函数,如果改变了原构造函数就不能用来判断类型了
- Object.prototype.toString.call(): 最准确的方法,可以判断所有类型
let const var 的区别
let/const
- ES6 的方法
- 不存在变量提升
- 有块作用域
- 必须先声明再使用
- 不能重复声明
- let 定义变量,const 定义常量
var
- 存在变量提升
- 没有块作用域
注:let 严格来说也是存在变量提升的,但是由于暂时性死区的关系,并不能被访问。 通过 let 与 const 声明定义了作用域的变量到正在运行的执行上下文(context)的词法环境(LexicalEnvironment),变量在实例化的时候通过词法环境(LexicalEnvironment)完成创建但由于还为进行词法绑定,还不能被访问。如果词法绑定未在初始化时为赋值的,则在词法绑定(LexicalBinding)时,为变量分配 undefined 的值。如果绑定应该没有完成初始化的值会抛出 ReferenceError 错误。
new 的过程
- 创建一个对象
- 在这个新的对象内部设置[[prototype]]属性,指向构造函数的 prototype 属性
- 将构造函数内部的 this 指向这个新对象,为这个对象添加属性和方法
- 如果构造函数返回非空对象,则返回该对象;否则返回刚创建的对象。
箭头函数与普通函数的区别
- 书写上不一样
- 箭头函数没有自己的 this,指向在定义时确定了
- 箭头函数继承的 this 指向不会改变
- 尖头函数不能用作构造函数使用
- 箭头函数没有 arguments
- 箭头函数不能用作 Generator 函数,不能使用 yeild 关键字
this 指向
this 指向除了尖头函数有 4 种情况:
- 默认绑定:独立函数调用时,使用默认绑定
- 隐式绑定:函数作为对象的方法调用
- 显示绑定:call\bind\apply
- new 绑定:构造函数调用
优先级: 默认绑定 < 隐式绑定 < 显示绑定 < new 绑定
注:创建一个函数的 间接引用,这种情况使用默认绑定规则
(obj2.foo = obj1.foo)
call/bind/apply 的区别
- call 函数
- 传入的参数数量不固定,第一个参数时函数体内的 this 指向,后面的参数是函数的参数
- call()方法调用一个对象的一个方法,以另一个对象替换当前对象
- 立即执行函数
- apply 函数
- 传入的参数是一个数组
- apply()方法调用一个对象的一个方法,以另一个对象替换当前对象
- 立即执行函数
- bind 函数
- bind()方法会创建一个新函数,称为绑定函数
- 当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind()方法的第二个参数加上绑定函数运行时传入的参数按顺序作为原函数的参数
为什么函数是一等公民
在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。
函数柯里化
是一种将多个参数的函数转换为单个参数函数的技术。
为什么要柯里化
- 函数复用:非常容易地为函数的一部分参数进行复用
- 延迟执行:使函数的执行被延迟到最后一个参数被传递进来的时候再进行
- 简单化函数:把多参数函数转换成单参数函数
- 函数组合:使不同的函数更加容易组合起来使用
高阶函数:
高阶函数是一个接收函数作为参数或者将函数作为返回输出的函数
常见的高阶函数: Array.map()\reduce()\filter()\sort()\forEach()\find()等等...
前端存储方式
- cookie
- 设置过期时间
- 指定那些主机
- 大小限制 4k
- 请求时会在请求头中携带
- 后端生成
- localStorage
- 没有过期时间
- 大小限制 5M
- 只能存储字符串
- 同源策略
- sessionStorage
- 会话级别的存储
- 关闭浏览器就会被清除
- 大小限制 5M
- 只能存储字符串
- 同源策略
- indexDB
- 异步 API
- 大小限制 无限制
- 可以存储二进制数据
- 同源策略
for...in 和 for...of 的区别
- for...of:是 ES6 新增的遍历方法,允许便利一个含有 iterator 接口的数据结构,并返回各项值。不会便利原型链
- for...in:遍历对象的可枚举属性,包括原型链上的属性。 对数组来说 for...of 只返回数组的元素,而 for...in 会返回数组所有可枚举的属性。
map 和 weakMap/Set 和 weakSet 的区别
- map 和 set 是强引用,会阻止垃圾回收机制回收对象;weakMap 和 weakSet 是弱引用,不会阻止垃圾回收机制回收对象
- map 的 键可以是任意类型,weakMap 键只能是对象类型
什么是 promise
Promise 是为了解决回调地狱而产生的,是一个对象,用来传递异步操作的消息。Promise 有 3 种状态:pending、fulfilled、rejected。状态一旦改变就不能再变。
Promise 构造函数接收一个函数作为参数,该函数的两个参数分别是 resolve 和 reject,分别表示成功和失败的回调函数。
缺点
- 无法取消 Promise,一旦新建就立即执行无法中途取消。
- 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
- 当处于 pending 状态时,无法得知目前进展到哪一个阶段。
方法
- .then():对应 resolve 成功的处理
- .catch():对应 reject 失败的处理
- .all():可以完成并行任务,将多个 Promise 实例数组包装成一个新的 Promise 实例,返回一个 Promise 实例
- .race():将多个 Promise 实例包装成一个新的 Promise 实例,只要有一个实例率先改变状态,新的 Promise 实例就跟着改变状态
- .allSettled():等到所有的 Promise 都有结果,无论成功失败,才会有最终的状态
async/await
其实是 generator 的语法糖,为优化 then 的链式调用而生。通过 async 关键字声明一个异步函数, await 用于等待一个异步方法执行完成,并且会阻塞执行。 async 函数返回的是一个 Promise 对象,如果在函数中 return 一个变量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。如果没有返回值,返回 Promise.resolve(undefined)。 优势:
- 代码更加清晰
- 错误处理更加方便
- 适用于链式调用
用过哪些设计模式
- 单例模式: 保证类只有一个实例,并提供一个访问它的全局访问点
- 工厂模式:用来创建对象,根据不同的参数返回不同的对象类型
- 策略模式:定义一系列算法,把他们一个一个封装起来,使他们可以互相替换
- 装饰器模式:再不改变对象原型的基础上,对其进行包装扩展。
- 观察者模式:定义了对象间一种一对多关系,当目标对象状态发生改变时,所有依赖它对对象都会得到通知。
- 发布订阅模式:基于一个主题/事件通道,接收通知的对象通过自定义事件订阅主题,被激活事件的对象通过发布主题事件的方式通知各个订阅对象。
JS 高级相关
jS 执行原理
v8 引擎原理
JS 代码大致会经过以下步骤: 词法分析 -> 语法解析 -> 代码生成
首先,对于我们电脑的 CPU 来说,他只能认识 0 和 1 这种语言,JavaScript 引擎的作用就是将我们写的 JavaScript 代码转换成机器能够识别的二进制代码。
JavaScript 引擎的工作流程大致如下: text
- 首先就是词法分析(扫描器 Scanner) 词法分析会将代码分解成有意义的代码块(Token)
- 然后会进行语法解析(解析器 Parser)
语法解析会将上述生成的代码块变成一个抽象语法树(AST)
注:此处有一个优化,就是延迟解析,将不必要的函数进行预解析,只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行。预解析不会生成 AST,只验证函数语法是否有效、解析函数声明、确定函数作用域。
- 解释器转换(解释器 Ignition) 将 AST 转换成字节码的模块,同时收集 TurboFan 优化需要的信息,比如函数参数的类型,如果函数只执行一次,那么 Ignition 会执行解释称 ByteCode
- 编译器编译(编译器 TurboFan)
将上述的字节码编译成 CPU 可以直接执行的机器码
注:v8 引擎中编译器和解释器是互相协作的,如果一个函数被多次调用了,就会被标记成一个热点函数,那么就会经过 TurboFan 转换成优化的机器码,提高代码的执行性能。同时,机器码也会被还原成 ByteCode,因为如果后续执行函数的过程中,类型发生了变化,之前优化的机器码就不能正确处理运算,就会逆向的转换成字节码。
JS 执行过程
执行流程
- 堆内存中初始化全局对象(GO——Global Object):全局对象在所有的作用域都能被访问,里面包含了 Date、Array、String、Number、setTimeout 等等,其中还有一个 window 属性指向自己
- 执行上下文栈(ECS:Execution Context Stack)按照后进先出原则执行全局的代码块(会构建一个 GEC 放到 ECS 中执行,Global Execution COntext 即全局执行上下文),执行的过程中包含两部分内容:
- 在代码执行前,在 parser 转成 AST 的过程中,会将全局定义的变量、函数等加入到 Global Object 中,但是并不会赋值(这个过程就是变量提升)
- 在代码执行中,对变量赋值或者执行其他的函数
- 函数执行,根据函数创建一个函数执行上下文(FEC,Function Execution Context)压入到栈顶
- FEC 中包含三部分内容:
- 在解析函数成 AST 树结构时,会创建一个 Activation Object(AO)对象
- 作用域链:由 VO(在函数中就是 AO 对象)和父级 VO 组成,查找时会一层层查找
- this 绑定的值
补充知识:最新的 ECMA 规范中,对一些词汇进行修改,每一个执行上下文会关联到一个变量环境(Variable Environment VE)中,在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。
- FEC 中包含三部分内容:
上下文和作用域的关系:代码执行环境就是上下文,而作用域是在编写代码时就确定了的,不会改变的,上下文中有作用域和作用域链,作用域链就是访问一个变量时,会从当前作用域(VE)开始查找,如果没有找到就会一层层向上查找,直到找到(GO)为止。
作用域
作用域就是变量起作用的范围和区域,目的是隔离变量,保证不同作用域下同名变量不会冲突。
作用域实现机制: 就是上述的 js 执行过程中。
js 查询变量的 2 种方式:(可参考《你不知道的 JavaScript 第一册》)
- LHS 查询:当变量出现在赋值操作左侧时,变量赋值操作,强调写入内存。
- RHS 查询:当变量出现在赋值操作右侧或没有赋值操作时,变量查找操作,强调从内存中读取。
词法作用域:
也称静态作用域,在代码书写时就确定了。
动态作用域:(了解)
在代码执行时确定作用域。
eval 和 with 可以在代码运行的时候改变词法作用域,但是不推荐使用。
作用域链就不说了,看上面的 JS 执行过程,其实很全。
作用域类型:(了解)
- Global 作用域(全局作用域)
- Local 作用域(本地作用域,函数作用域)
- Block 作用域(块级作用域)
- Script 作用域(let、const 声明的全局变量会保存在 Script 作用域,这些变量可以直接访问,但却不能通过 window.xx 访问)node 环境下没有 Script 作用域
- 模块作用域:node 环境下有个 Local 作用域,这个作用域还有 module、exports、require 等变量
- Catch Block 作用域 (Catch 语句也会生成一个特殊的作用域)
- With Block 作用域
- Closure 作用域(闭包作用域)
- Eval 作用域 (eval 的代码里声明的变量都在这个作用域里)
闭包
定义:
计算机科学中:在支持头等函数(函数可以作为第一公民)的编程语言中,实现词法绑定的技术;实现上是一个结构体,存储了一个函数和一个关联的环境。 JavaScript 中:一个函数和对其周围状态(lexical environment,词法环境)的引用绑定在一起,这样的组合就是闭包,闭包可以让你在哪层函数中访问到其外层的函数的作用域。创建一个函数时,闭包就会在函数创建的同时被创建出来。 总结:一个普通的函数如果可以访问外层作用域的自由变量,这个函数就是一个闭包。
闭包的作用:
- 将变量保存在内存中不被垃圾回收机制清理
- 变量私有化,避免造成污染
垃圾回收机制(会比较长)
回答流程:什么是垃圾回收机制?-> 如何进行的?-> v8 的垃圾回收机制?(优化)-> 如何避免内存泄漏(扩展,略)
什么是垃圾回收机制?
引用数据类型存储在堆内存中,栈中存储的变量是对引用数据类型的引用地址。当我们一个变量a
首先指向了对象{a:1}
,然后又指向了另一个对象{b:2}
,那么第一个对象如果没有被任何变量引用的时候(也就是不可达),就会被清理掉,这个过程就是垃圾回收机制。
如何进行的——垃圾回收策略
2 个常见算法策略:
- 标记清除法
- 引用计数法
标记清除法
JavaScript 引擎 里这种算法是最常用的,分成标记
和清除
两个阶段。大致过程如下:
- 垃圾收集器运行的时候给内存中所有的变量打上标记,比如:0
- 从根对象开始遍历,把不是垃圾的节点变成 1
- 清理所有标记为 0 的垃圾,销毁并回收内存
- 把所有内存对象标记修改为 0,等待下次垃圾回收
优化: 上述方法会导致内存碎片,因为被清理的对象所在的内存空间不是连续的。因此有一个标记整理算法,它会在标记结束的时候将活着的对象向内存一端移动,清理掉边界的内存。
引用计数法
把对象是否不再需要定义为对象有没有被其他对象引用,如果没有引用指向该对象,就会被垃圾回收机制回收。 大致过程如下:
- 声明一个变量,这个变量引用一个对象,那这个对象的引用次数+1
- 如果这个变量指向了另一个对象,那么前一个对象的引用次数-1
- 当引用次数为 0 的时候,就会被垃圾回收机制回收
存在的问题:
- 需要 1 个计数器
- 存在循环引用的问题
v8 的垃圾回收机制?(优化)
分代式垃圾回收
v8 的垃圾回收策略将堆内存分为新生代和老生代。 新生代式存活时间较短的对象,一般只有 1-8M 的容量,老生代是存活时间长或者容量大的对象。2 者采用不同的垃圾回收器。
- 新生代
- 将堆内存一分为二,一个是使用区,一个是空闲区,新加入的对象会被存储到使用区,当使用区满了,就会触发垃圾回收机制。
- 垃圾回收机制会对使用区中的活动对象做标记,标记完成之后将使用区的活动对象复制进空闲区并进行排序,然后将非活动对象清理掉。
- 之后会把原来的使用区变成空闲区,空闲区变成使用区,这样就完成了一次垃圾回收。
- 当一个对象经过多次复制还存活,那么就会被移动到老生代中。(如果复制一个对象到空闲区时,空闲区空间占用超过 25%就会直接晋升到老生代)
- 老生代 就是标记清除算法
采用分代式的原因:
- 新生代中的对象存活时间短,老生代中的对象存活时间长,分开处理可以提高效率
优化
并行回收
因为 jS 是单线程的,垃圾回收时会阻塞 JS 脚本执行,需要等垃圾回收完毕再回复脚本执行,这种叫全停顿 全停顿可能会造成页面卡顿,因此就有了并行回收,在回收的时候会开启多个线程来负责新生代中的垃圾清理操作,这些线程同时将对象空间中的数据移动到空闲区域,这个过程中由于数据地址会发生改变,所以还需要同步更新引用这些对象的指针。
增量标记和懒性清理
上述并行回收虽然有优化,但他还是全停顿的,新生代还算 OK,但是老生代数据比较大,全停顿时间会比较长,因此就有了增量标记和懒性清理。
增量标记:将标记过程分成几个小步骤,每执行完一个小步骤就让 JS 脚本执行一会,这样就可以减少全停顿的时间。但是如何恢复?如果引用关系更改了又怎么办?
因此就有了三色标记法(暂停和恢复),将对象分成 3 种颜色,白色、灰色、黑色,白色表示未访问,灰色表示访问过但是还没有访问其子节点,黑色表示访问过并且访问了其子节点。下次恢复的时候就找灰色的节点继续访问。
同时通过写屏障的方式,如果对象的引用关系发生了变化,就会将对象的颜色变成灰色,这样就可以保证对象的引用关系不会发生错误。
懒性清理(真正清理释放内存):在老生代中,清理的时候不会立即清理,而是只是标记,而是等到内存不够的时候再清理,这样可以减少清理的次数,提高效率。
并发回收
主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作,辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不会被挂起。但是这需要考虑主线程在执行时,堆中对象引用关系可能发生变化,需要额外实现一些读写锁机制来控制。
原型和原型链
每个构造函数(constructor)都有一个原型对象(prototype),原型对象中都有一个指向构造函数的指针(constructor),构造函数的实例对象(instance)都有一个指向原型对象的指针(__proto:[[prototype]])。
有这么一个规则,如果引用实例对象的某个熟悉,首先会在这个实例对象内部寻找该属性,如果找不到,就会在实例对象的原型对象中寻找,如果还找不到,就会在原型对象的原型对象中寻找,直到找到 Object.prototype 为止。这个就是原型链。
下面这张图非常重要!!!!!!核心!!!!!!
创建对象的方式(参考:juejin.cn/post/719932…
-
工厂模式 通过一个普通的函数,内部通过 new Object()创建一个对象,依次给创建的对象添加新的属性后 return 该对象。
function factory() { const obj = new Object(); obj.a = 1; obj.say = function () { console.log("我是一个工厂函数"); }; return obj; }
存在的问题:
- constructor 始终指向了 Object
- 工厂函数内的方法始终都一样,但是会重复声明多次。
-
构造函数模式 通过创建一个构造函数来实现,首字母大写!!!通过 new 来创建实例
function ConstructFn(name, age) { this.name = name; this.age = age; this.say = function () { console.log(this.name, "这是构造函数创建的对象"); }; } const constructObj = new ConstructFn("test", 1);
优点: 能够通过 constructor 和 instanceof 来识别出实例的类型 缺点:多个实例的方法都是实现一样的效果却存储多次
-
原型模式 创建一个构造函数,构造函数函数体内不进行操作,在外部给构造函数的原型增加属性和方法。
function PrototypeFn() {} PrototypeFn.prototype.name = "hanmeimei"; PrototypeFn.prototype.say = function () { console.log("这是原型模式创建的对象"); }; PrototypeFn.prototype.arr = [1, 2]; const obj1 = new PrototypeFn();
优点:每个实例都共享原型上的方法和属性 缺点:没法创建自己的属性和方法;多个实例共享同个引用地址,一个实例改变引用的值会影响其他实例。
-
构造函数和原型组合模式 结合构造函数模式和原型模式,方法使用原型模式来创建,属性使用构造函数模式来创建
function ConstructAndPrototypeFn(name) { this.name = name; this.friends = ["lilei"]; ConstructAndPrototypeFn.prototype.say = function () { console.log("这是组合模式创建的"); }; }
-
动态原型模式 在上述组合模式的基础上,给原型添加方法时增加一层判断,如果已经存在某个方法或者属性则不进行添加。
function dynamicFn(name) { this.name = name; if (typeof this.say !== "function") { dynamicFn.prototype.say = function () { alert(this.name); }; } }
优点:检查某个应该存在的方法是否有效,来决定是否需要初始化原型
-
寄生构造函数模式 这种模式和工厂模式比较像,返回的对象与构造函数或者与构造函数的原型属性之间没有关系,与构造函数内部的 new 的构造函数有关系。
function ParasitismConstructorFn(name) { var o = new Object(); o.name = name; o.say = function () { alert(this.name); }; return o; }
优点:可以在特殊的情况下用来为对象创建构造函数 缺点:不能用 instanceof 来判断类型
-
稳妥构造函数模式 和寄生构造函数有点类似,但是不会将实参赋值给构造函数内创建的实例,而是通过构造函数内的方法去访问。 优点:安全,那么好像成为了私有变量,只能通过构造函数内的方法去进行访问。 缺点:不能区分实例的类型
function Person(name) { var o = new Object(); o.say = function () { console.log(name); }; return o; }
继承的方式(参考:juejin.cn/post/720327…
继承原理:依赖于原型链来实现。
-
原型链继承 原型链继承的本质就是复制,会把子构造函数的原型对象重写为父类的构造函数的原型。
function FatherFun(name, age) { this.name = name; this.age = age; } FatherFun.prototype.getType = () => { console.log("我是父构造函数"); }; FatherFun.prototype.wealth = ["money", "house"]; function SonFun(name, age) { this.name = name; this.age = age; } SonFun.prototype = FatherFun.prototype;
存在的问题:
- 无法给父构造函数传递参数
- 子类和父类使用同一个原型对象
- 无法通过 constructor 正确判断子类类型
-
借用构造函数方式 在子类的构造函数内部通过 call 或者 apply 调用父类的构造函数,并将 this 指向为新创建的对象。
function FatherFun(name, age) { this.name = name; this.age = age; this.wealth = ["money", "house"]; this.work = function () { console.log("FatherFun构造函数内部方法"); }; } FatherFun.prototype.sayHi = function () { console.log("这是Father的原型里的sayHi"); }; function SonFun(name, age) { FatherFun.call(this, name, age); //继承了 Father,且向父类型传递参数 }
缺点:
- 方法都在构造函数中定义
- 超类原型对象定义得到方法对子类的实例不可见,实现不了方法的共享
-
组合继承 结合上述的两种继承方式,使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。
function FatherFun(name, age) { this.name = name; this.age = age; this.wealth = ["money", "house"]; } FatherFun.prototype.getSay = function () { console.log("父构造函数原型的getSay方法"); }; function SonFun(sex, nickname, name, age) { this.sex = sex; this.nickname = nickname; FatherFun.call(this, name, age); } SonFun.prototype = new FatherFun();
缺点:
- 子构造函数的原型是父构造函数的一个实例
- 会调用 2 次父构造函数
-
原型式继承 原型式继承本质是一个浅拷贝,在函数内部先创建一个临时的构造函数,然后将传入函数的对象作为这个函数的原型,最后返回这个函数的实例。
function object(o) { function F() {} F.prototype = o; return new F(); }
-
寄生式继承 寄生式继承的实现方式就是在原型式继承的基础上,通过某种方式来增加对象。
function object(o) { function F() {} F.prototype = o; return new F(); } function createAnother(original) { var clone = object(original); //通过调用object函数创建一个新对象 clone.sayHi = function () { //以某种方式来增强这个对象 alert("hi"); }; return clone; //返回这个对象 }
-
寄生式组合继承 可以理解为寄生式继承和组合式继承的结合版本
-
首先一个寄生继承的核心函数
-
在这个函数内部来修改子类的原型
-
这个函数有 2 个参数,分别是子构造函数和父构造函数
-
定义一个临时构造函数 F,将这个构造函数的原型指向父构造函数的原型
-
将子构造函数的原型变成临时构造函数 F 的实例。
-
注意记得将修改过后的子构造函数的原型的 constructor 指向自己。
- 子构造函数的内部需要使用 call 去调用父构造函数,将 this 指向新创建的对象
function inheritPrototype(subType, superType) { function F() {} //F()的原型指向的是superType F.prototype = superType.prototype; //subType的原型指向的是F() subType.prototype = new F(); // 重新将构造函数指向自己,修正构造函数 subType.prototype.constructor = subType; } // 设置父类 function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; SuperType.prototype.sayName = function () { console.log(this.name); }; } // 设置子类 function SubType(name, age) { //构造函数式继承--子类构造函数中执行父类构造函数 SuperType.call(this, name); this.age = age; }
-
事件循环
此处其实有 2 个版本,一个版本是老的,一个版本是新的,存在一些细微差别。
JS 引擎是单线程的,但是游览器是多线程的,JS 引擎通过事件循环机制来实现异步操作。 JS 的任务可分为同步任务和异步任务,也就是阻塞和非阻塞,同步任务会在主线程上执行,异步任务会在异步任务队列中等待执行。
事件循环机制: 当我们执行代码的时候,碰到了同步任务,会把同步任务放进执行栈中按照后进先出的原则执行代码,当碰到异步任务的时候,会将异步任务放进工作线程中挂起,工作线程会把到期的异步任务放进异步任务队列中,等待主线程执行完毕后,会去异步任务队列中查找是否有到期的异步任务,如果有就会放进执行栈中执行,直到异步任务队列为空。
宏任务和微任务
在 JS 中异步任务又可以分为宏任务和微任务,
宏任务是由宿主环境发起的,比如 setTimeout、setInterval、I/O、UI 渲染等,微任务是由 JS 自身发起的,比如 Promise、process.nextTick、MutationObserver 等。
宏任务会按照任务的时间节点顺序,也就是谁先到期谁先执行,每一个宏任务内部可以注册当前任务的微任务队列,微任务会在当前宏任务执行完毕后立即执行(在下一次宏任务执行前)。
补充 1: setTimeout(0)并不是延时 0ms,而是 4ms 左右。
补充 2:requestAnimationFrame 平均 16ms 一次,具体还需要根据游览器的刷新频率来决定。
补充 3:现在游览器随着性能的提高,已经不再把异步任务作为宏任务了,而是分为多个任务队列,不同的异步任务会放到不同的任务队列中,这样可以提高性能。具体可查看官方文档:html.spec.whatwg.org/multipage/w…