这篇文章源自我历时8个月,整理的前端知识体系与大厂面试笔记,不知不觉,已经超过了10万字
这些笔记帮助我从一个菜鸟,一步步蜕变为高级开发、前端“砖家”,并助力我拿到一些大厂的offer
同时,我将遇到过的面试题也都整理了进去,面试的公司包含:阿里、头条、美团、京东、网易、小米、叮咚买菜、喜马拉雅、货拉拉等
每次面试前,我都会花一周的时间去复习一遍这些知识点 ( 亲测保熟 ) 💕
文章面对的群体👨👩👧👧 :
1) 1到3年的初中级前端工程师
2) 备战大厂的朋友
前言
编程如逆水行舟,不进则退
入行这几年,焦虑与迷茫常伴左右,总会遇到各种瓶颈,不知道如何继续深入下去
这篇文章结合自己的学习与面试经历,整理出来一些学习路线,希望对小伙伴们有所启发
前端知识体系繁杂,文章中总结的知识点难免有所纰漏,希望大家多多指正,一起交流学习😊😘
前端知识体系分为4篇 基础知识篇、算法篇 、 工程化篇 、 前端框架和浏览器原理篇 分享给大家
后面还有几万字关于 实战项目总结与收获 的笔记,正在整理中,会陆续发出来
前端知识体系导图
图片太大,就不展示了,点击可查看大图
下面,我们一起开始吧,升职加薪,YYDS!💪💪💪
JS 基础
执行上下文和执行栈
什么是执行上下文?
Javascript 代码都是在执行上下文中运行的
执行上下文: 指当前执行环境中的变量、函数声明、作用域链、this等信息
执行上下文生命周期
1)创建阶段
生成变量对象、建立作用域链、确定this的指向
2)执行阶段
变量赋值、函数的引用、执行其他代码
变量对象
变量对象是与执行上下文相关的数据作用域,存储了上下文中定义的变量和函数声明
变量对象是一个抽象的概念,在全局执行上下文中,变量对象就是全局对象。 在顶层js代码中,this指向全局对象,全局变量会作为该对象的属性来被查询。在浏览器中,window就是全局对象
执行栈
是一种先进后出的数据结构,用来存储代码运行的所有执行上下文
1)当 JS 引擎第一次遇到js脚本时,会创建一个全局的执行上下文并且压入当前执行栈
2)每当JS 引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部
3)当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文
4)一旦所有代码执行完毕,JS 引擎从当前栈中移除全局执行上下文
执行栈示例
var a = 1; // 1. 全局上下文环境
function bar (x) {
console.log('bar')
var b = 2;
fn(x + b); // 3. fn上下文环境
}
function fn (c) {
console.log(c);
}
bar(3); // 2. bar上下文环境
全局、函数、Eval执行上下文
执行上下文分为全局、函数、Eval执行上下文
1)全局执行上下文(浏览器环境下,为全局的 window 对象)
2)函数执行上下文,每当一个函数被调用时, 都会为该函数创建一个新的上下文
3)Eval 函数执行上下文,如eval(“1 + 2”)
对于每个执行上下文,都有三个重要属性:变量对象、作用域链(Scope chain)、this
执行上下文的特点:
1)单线程,只在主线程上运行;
2)同步执行,从上向下按顺序执行;
3)全局上下文只有一个,也就是window对象;
4)函数每调用一次就会产生一个新的执行上下文环境。
理解 JavaScript 中的执行上下文和执行栈
理解JavaScript的执行上下文
JavaScript进阶-执行上下文
作用域
作用域:可访问变量的集合
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突
作用域类型
全局作用域、函数作用域、ES6中新增了块级作用域
函数作用域
是指声明在函数内部的变量,函数的作用域在函数定义的时候就决定了
块作用域
1)块作用域由{ }包括,if和for语句里面的{ }也属于块作用域
2)在块级作用域中,可通过let和const声明变量,该变量在指定块的作用域外无法被访问
var、let、const的区别
1)var定义的变量,没有块的概念,可以跨块访问, 可以变量提升
2)let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明
3)const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明
let和const声明的变量只在块级作用域内有效,示例
function func() {
if (true) {
let i = 3;
}
console.log(i); // 报错 "i is not defined"
}
func();
作用域链
当查找变量的时候,首先会先从当前上下文的变量对象(作用域)中查找,如果没有找到,就会从父级的执行上下文的变量对象中查找,如果还没有找到,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
JavaScript深入之作用域链
js块级作用域和let,const,var区别
this
this的5种绑定方式
1)默认绑定(非严格模式下this指向全局对象,严格模式下函数内的this指向undefined)
2)隐式绑定(当函数引用有上下文对象时, 如 obj.foo()的调用方式, foo内的this指向obj)
3)显示绑定(通过call或者apply方法直接指定this的绑定对象, 如foo.call(obj))
4)new构造函数绑定,this指向新生成的对象
5)箭头函数,this指向的是定义该函数时,外层环境中的this,箭头函数的this在定义时就决定了,不能改变
this 题目1
"use strict";
var a = 10; // var定义的a变量挂载到window对象上
function foo () {
console.log('this1', this) // undefined
console.log(window.a) // 10
console.log(this.a) // 报错,Uncaught TypeError: Cannot read properties of undefined (reading 'a')
}
console.log('this2', this) // window
foo();
注意:开启了严格模式,只是使得函数内的this指向undefined,它并不会改变全局中this的指向。因此this1中打印的是undefined,而this2还是window对象。
this 题目2
let a = 10
const b = 20
function foo () {
console.log(this.a) // undefined
console.log(this.b) // undefined
}
foo();
console.log(window.a) // undefined
如果把 var 改成了 let 或 const,变量是不会被绑定到window上的,所以此时会打印出三个undefined
this 题目3
var a = 1
function foo () {
var a = 2
console.log(this) // window
console.log(this.a) // 1
}
foo()
foo()函数内的this指向的是window,因为是window调用的foo,打印出的this.a是window下的a
this 题目4
var obj2 = {
a: 2,
foo1: function () {
console.log(this.a) // 2
},
foo2: function () {
setTimeout(function () {
console.log(this) // window
console.log(this.a) // 3
}, 0)
}
}
var a = 3
obj2.foo1()
obj2.foo2()
对于setTimeout中的函数,这里存在隐式绑定的this丢失,也就是当我们将函数作为参数传递时,会被隐式赋值,回调函数丢失this绑定,因此这时候setTimeout中函数内的this是指向window
this 题目5
var obj = {
name: 'obj',
foo1: () => {
console.log(this.name) // window
},
foo2: function () {
console.log(this.name) // obj
return () => {
console.log(this.name) // obj
}
}
}
var name = 'window'
obj.foo1()
obj.foo2()()
这道题非常经典,它证明了箭头函数内的this是由外层作用域决定的
题目5解析:
1)对于obj.foo1()函数的调用,它的外层作用域是window,对象obj当然不属于作用域了(作用域只有全局作用域、函数作用域、块级作用域),所以会打印出window
2)obj.foo2()(),首先会执行obj.foo2(),这不是个箭头函数,所以它里面的this是调用它的obj对象,因此第二个打印为obj,而返回的匿名函数是一个箭头函数,它的this由外层作用域决定,那也就是它的this会和foo2函数里的this一样,第三个打印也是obj
再来40道this面试题酸爽继续(1.2w字用手整理)
call apply bind
三者的区别
1)三者都可以显式绑定函数的this指向
2)三者第一个参数都是this要指向的对象,若该参数为undefined或null,this则默认指向全局window
3)传参不同:apply是数组、call是参数列表,而bind可以分为多次传入,实现参数的合并
4)call、apply是立即执行,bind是返回绑定this之后的函数,如果这个新的函数作为构造函数被调用,那么this不再指向传入给bind的第一个参数,而是指向新生成的对象
手写call apply bind
// 手写call
Function.prototype.Call = function(context, ...args) {
// context为undefined或null时,则this默认指向全局window
if (!context || context === null) {
context = window;
}
// 利用Symbol创建一个唯一的key值,防止新增加的属性与obj中的属性名重复
let fn = Symbol();
// this指向调用call的函数
context[fn] = this;
// 隐式绑定this,如执行obj.foo(), foo内的this指向obj
let res = context[fn](...args);
// 执行完以后,删除新增加的属性
delete context[fn];
return res;
};
// apply与call相似,只有第二个参数是一个数组,
Function.prototype.Apply = function(context, args) {
if (!context || context === null) {
context = window;
}
let fn = Symbol();
context[fn] = this;
let res = context[fn](...args);
delete context[fn];
return res;
};
// bind要考虑返回的函数,作为构造函数被调用的情况
Function.prototype.Bind = function(context, ...args) {
if (!context || context === null) {
context = window;
}
let fn = this;
let f = Symbol();
const result = function(...args1) {
if (this instanceof fn) {
// result如果作为构造函数被调用,this指向的是new出来的对象
// this instanceof fn,判断new出来的对象是否为fn的实例
this[f] = fn;
let res = this[f](...args1, ...args);
delete this[f];
return res;
} else {
// bind返回的函数作为普通函数被调用时
context[f] = fn;
let res = context[f](...args1, ...args);
delete context[f];
return res;
}
};
// 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
// 实现继承的方式: 使用Object.create
result.prototype = Object.create(fn.prototype);
return result;
};
闭包
闭包:就是函数引用了外部作用域的变量
闭包常见的两种情况:
一是函数作为返回值; 另一个是函数作为参数传递
闭包的作用:
可以让局部变量的值始终保持在内存中;对内部变量进行保护,使外部访问不到
最常见的案例:函数节流和防抖
闭包的垃圾回收:
副作用:不合理的使用闭包,会造成内存泄露(就是该内存空间使用完毕之后未被回收)
闭包中引用的变量直到闭包被销毁时才会被垃圾回收
闭包的示例
// 原始题目
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 1s后打印出5个5
}, 1000);
}
// ⬅️利用闭包,将上述题目改成1s后,打印0,1,2,3,4
// 方法一:
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, 1000);
})(i);
}
// 方法二:
// 利用setTimeout的第三个参数,第三个参数将作为setTimeout第一个参数的参数
for (var i = 0; i < 5; i++) {
setTimeout(function fn(i) {
console.log(i);
}, 1000, i); // 第三个参数i,将作为fn的参数
}
// ⬅️将上述题目改成每间隔1s后,依次打印0,1,2,3,4
for (var i = 0; i < 5; i++) {
setTimeout(function fn(i) {
console.log(i);
}, 1000 * i, i);
}
发现 JavaScript 中闭包的强大威力
破解前端面试(80% 应聘者不及格系列):从闭包说起
原型/原型链
原型的作用
原型被定义为给其它对象提供共享属性的对象,函数的实例可以共享原型上的属性和方法
原型链
它的作用就是当你在访问一个对象上属性的时候,如果该对象内部不存在这个属性,那么就会去它proto属性所指向的对象(原型对象)上查找。如果原型对象依旧不存在这个属性,那么就会去其原型的proto属性所指向的原型对象上去查找。以此类推,直到找到nul,而这个查找的线路,也就构成了我们常说的原型链
原型链和作用域的区别: 原型链是查找对象上的属性,作用域链是查找当前上下文中的变量
proto、prototype、constructor属性介绍
1)js中对象分为两种,普通对象和函数对象
2)proto和constructor是对象独有的。prototype属性是函数独有的,它的作用是包含可以给特定类型的所有实例提供共享的属性和方法;但是在 JS 中,函数也是对象,所以函数也拥有proto和 constructor属性
3)constructor属性是对象所独有的,它是一个对象指向一个函数,这个函数就是该对象的构造函数
构造函数.prototype.constructor === 该构造函数本身
4)一个对象的proto指向其构造函数的prototype
函数创建的对象.proto === 该函数.prototype
5)特殊的Object、Function
console.log(Function.prototype === Function.__proto__); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
instanceof
instanceof 的基本用法,它可以判断一个对象的原型链上是否包含该构造函数的原型,经常用来判断对象是否为该构造函数的实例
特殊示例
console.log(Object instanceof Object); //true
console.log(Function instanceof Function); //true
console.log(Function instanceof Object); //true
console.log(function() {} instanceof Function); //true
手写instanceof方法
function instanceOf(obj, fn) {
let proto = obj.__proto__;
if (proto) {
if (proto === fn.prototype) {
return true;
} else {
return instanceOf(proto, fn);
}
} else {
return false;
}
}
// 测试
function Dog() {}
let dog = new Dog();
console.log(instanceOf(dog, Dog), instanceOf(dog, Object)); // true true
new 关键字
new一个对象,到底发生什么?
1)创建一个对象,该对象的原型指向构造函数的原型
2)调用该构造函数,构造函数的this指向新生成的对象
3)判断构造函数是否有返回值,如果有返回值且返回值是一个对象或一个方法,则返回该值;否则返回新生成的对象
构造函数有返回值的案例
function Dog(name) {
this.name = name;
return { test: 1 };
}
let obj = new Dog("ming");
console.log(obj); // {test:1}
手写new
function selfNew(fn, ...args) {
// 创建一个instance对象,该对象的原型是fn.prototype
let instance = Object.create(fn.prototype);
// 调用构造函数,使用apply,将this指向新生成的对象
let res = fn.apply(instance, args);
// 如果fn函数有返回值,并且返回值是一个对象或方法,则返回该对象,否则返回新生成的instance对象
return typeof res === "object" || typeof res === "function" ? res : instance;
}
继承
多种继承方式
1)原型链继承,缺点:引用类型的属性被所有实例共享
2)借用构造函数(经典继承)
3)原型式继承
4)寄生式继承
5)组合继承
6)寄生组合式继承
寄生组合式继承的优势
优势:借用父类的构造函数,在不需要生成父类实例的情况下,继承了父类原型上的属性和方法
手写寄生组合式继承
// 精简版
class Child {
constructor() {
// 调用父类的构造函数
Parent.call(this);
// 利用Object.create生成一个对象,新生成对象的原型是父类的原型,并将该对象作为子类构造函数的原型,继承了父类原型上的属性和方法
Child.prototype = Object.create(Parent.prototype);
// 原型对象的constructor指向子类的构造函数
Child.prototype.constructor = Child;
}
}
// 通用版
function Parent(name) {
this.name = name;
}
Parent.prototype.getName = function() {
console.log(this.name);
};
function Child(name, age) {
// 调用父类的构造函数
Parent.call(this, name);
this.age = age;
}
function createObj(o) {
// 目的是为了继承父类原型上的属性和方法,在不需要实例化父类构造函数的情况下,避免生成父类的实例,如new Parent()
function F() {}
F.prototype = o;
// 创建一个空对象,该对象原型指向父类的原型对象
return new F();
}
// 等同于 Child.prototype = Object.create(Parent.prototype)
Child.prototype = createObj(Parent.prototype);
Child.prototype.constructor = Child;
let child = new Child("tom", 12);
child.getName(); // tom
一文吃透所有JS原型相关知识点
最详尽的 JS 原型与原型链终极详解
Class 类
1) Class 类可以看作是构造函数的语法糖
class Point {}
console.log(typeof Point); // "function"
console.log(Point === Point.prototype.constructor); // true
2) Class 类中定义的方法,都是定义在该构造函数的原型上
class Point {
constructor() {}
toString() {}
}
// 等同于
Point.prototype = { constructor() {}, toString() {} };
3)使用static关键字,作为静态方法(静态方法,只能通过类调用,实例不能调用)
class Foo {
static classMethod() {
return "hello";
}
}
Foo.classMethod(); // 'hello'
4)实例属性的简写写法
class Foo {
bar = "hello";
baz = "world";
}
// 等同于
class Foo {
constructor() {
this.bar = "hello";
this.baz = "world";
}
}
5)extends 关键字,底层也是利用的寄生组合式继承
class Parent {
constructor(age) {
this.age = age;
}
getName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(age);
this.name = name;
}
}
let child = new Child("li", 16);
child.getName(); // li
手写Class类
ES6的 Class 内部是基于寄生组合式继承,它是目前最理想的继承方式
ES6的 Class 允许子类继承父类的静态方法和静态属性
// Child 为子类的构造函数, Parent为父类的构造函数
function selfClass(Child, Parent) {
// Object.create 第二个参数,给生成的对象定义属性和属性描述符/访问器描述符
Child.prototype = Object.create(Parent.prototype, {
// 子类继承父类原型上的属性和方法
constructor: {
enumerable: false,
configurable: false,
writable: true,
value: Child
}
});
// 继承父类的静态属性和静态方法
Object.setPrototypeOf(Child, Parent);
}
// 测试
function Child() {
this.name = 123;
}
function Parent() {}
// 设置父类的静态方法getInfo
Parent.getInfo = function() {
console.log("info");
};
Parent.prototype.getName = function() {
console.log(this.name);
};
selfClass(Child, Parent);
Child.getInfo(); // info
let tom = new Child();
tom.getName(); // 123
Promise
Promise的底层原理:callback回调函数 + 发布订阅模式
链式调用
1)promise的回调只能被捕获一次
2)在then函数加上return,后面的then函数才能继续捕获到
链式调用示例
// 只有第一个then函数能捕获到结果,第二个then打印undefined
let pro = new Promise((resolve, reject) => resolve(1));
pro.then(res => {
console.log(res);
})
.then(res => {
console.log(res);
});
手写promise
class Promise {
constructor(fn) {
// resolve时的回调函数列表
this.resolveTask = [];
// reject时的回调函数列表
this.rejectTask = [];
// state记录当前状态,共有pending、fulfilled、rejected 3种状态
this.state = "pending";
let resolve = value => {
// state状态只能改变一次,resolve和reject只会触发一种
if (this.state !== "pending") return;
this.state = "fulfilled";
this.data = value;
// 模拟异步,保证resolveTask事件先注册成功,要考虑在Promise里面写同步代码的情况
setTimeout(() => {
this.resolveTask.forEach(cb => cb(value));
});
};
let reject = err => {
if (this.state !== "pending") return;
this.state = "rejected";
this.error = err;
// 保证rejectTask事件注册成功
setTimeout(() => {
this.rejectTask.forEach(cb => cb(err));
});
};
// 关键代码,执行fn函数
try {
fn(resolve, reject);
} catch (error) {
reject(error);
}
}
then(resolveCallback, rejectCallback) {
// 解决链式调用的情况,继续返回Promise
return new Promise((resolve, reject) => {
// 将then传入的回调函数,注册到resolveTask中
this.resolveTask.push(() => {
// 重点:判断resolveCallback事件的返回值
// 假如用户注册的resolveCallback事件又返回一个Promise,将resolve和reject传进去,这样就实现控制了链式调用的顺序
const res = resolveCallback(this.data);
if (res instanceof Promise) {
res.then(resolve, reject);
} else {
// 假如返回值为普通值,resolve传递出去
resolve(res);
}
});
this.rejectTask.push(() => {
// 同理:判断rejectCallback事件的返回值
// 假如返回值为普通值,reject传递出去
const res = rejectCallback(this.error);
if (res instanceof Promise) {
res.then(resolve, reject);
} else {
reject(res);
}
});
});
}
}
// 测试
// 打印结果:依次打印1、2
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 500);
}).then(
res => {
console.log(res);
return new Promise(resolve => {
setTimeout(() => {
resolve(2);
}, 1000);
});
}
).then(data => {
console.log(data);
});
手写race、all
race:返回promises列表中第一个执行完的结果
all:返回promises列表中全部执行完的结果
class Promise {
// race静态方法,返回promises列表中第一个执行完的结果
static race(promises) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
// Promise.resolve包一下,防止promises[i]不是Promise类型
Promise.resolve(promises[i])
.then(res => {
resolve(res);
})
.catch(err => {
reject(err);
});
}
});
}
// all静态方法, 返回promises列表中全部执行完的结果
static all(promises) {
let result = [];
let index = 0;
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i])
.then(res => {
// 输出结果的顺序和promises的顺序一致
result[i] = res;
index++;
if (index === promises.length) {
resolve(result);
}
})
.catch(err => {
reject(err);
});
}
});
}
}
async、await
作用:用同步方式,执行异步操作
总结
1)async函数是generator(迭代函数)的语法糖
2)async函数返回的是一个Promise对象,有无值看有无return值
3)await关键字只能放在async函数内部,await关键字的作用 就是获取Promise中返回的resolve或者reject的值
4)async、await要结合try/catch使用,防止意外的错误
generator
1)generator函数跟普通函数在写法上的区别就是,多了一个星号
2)只有在generator函数中才能使用yield,相当于generator函数执行的中途暂停点
3)generator函数是不会自动执行的,每一次调用它的next方法,会停留在下一个yield的位置
*async、await示例
const getData = () => new Promise(resolve => setTimeout(() => resolve("data"), 1000));
async function test() {
const data = await getData();
console.log("data: ", data);
const data2 = await getData();
console.log("data2: ", data2);
return "success";
}
test().then(res => console.log(res))
20分钟就能搞定的async/await原理
手写async await的最简实现
async/await 一定要加 try/catch吗?
深拷贝
深拷贝的方式
1)JSON.parse(JSON.stringify())
缺点: 无法拷贝 函数、正则、时间格式、原型上的属性和方法等
2)递归实现深拷贝
手写深拷贝
解决 循环引用 和 多个属性引用同一个对象(重复拷贝)的情况
1)循环拷贝:对象的属性引用自己
let target = {name: 'target'};
target.target = target
2)重复拷贝:对象的属性引用同一个对象
let obj = {};
let target = {a: obj, b: obj};
手写深拷贝代码
// 使用hash 存储已拷贝过的对象,避免循环拷贝和重复拷贝
function deepClone(target, hash = new WeakMap()) {
if (!isObject(target)) return target;
if (hash.get(target)) return hash.get(target);
// 兼容数组和对象
let newObj = Array.isArray(target) ? [] : {};
// 关键代码,解决对象的属性循环引用 和 多个属性引用同一个对象的问题,避免重复拷贝
hash.set(target, newObj);
for (let key in target) {
if (target.hasOwnProperty(key)) {
if (isObject(target[key])) {
newObj[key] = deepClone(target[key], hash); // 递归拷贝
} else {
newObj[key] = target[key];
}
}
}
return newObj;
}
function isObject(target) {
return typeof target === "object" && target !== null;
}
// 示例
let info = { item: 1 };
let obj = {
key1: info,
key2: info,
list: [1, 2]
};
// 循环引用深拷贝示例
obj.key3 = obj;
let val = deepClone(obj);
console.log(val);
使用WeakMap的好处是,WeakMap存储的key必须是对象,并且key都是弱引用,便于垃圾回收
JSON.parse(JSON.stringify()) 实现对对象的深拷贝
如何实现一个深拷贝
事件轮询机制 Event Loop
JS 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。所有任务都需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着
所有任务可以分成两种,一种是宏任务,另一种是微任务
宏任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务
微任务指的是,不进入主线程、而进入”微任务列表”的任务
当前宏任务执行完后,会判断微任务列表中是否有任务。如果有,会把该微任务放到主线程中并执行,如果没有,就继续执行下一个宏任务
宏任务 微任务
1)宏任务(Macrotasks)
script全部代码(注意同步代码也属于宏任务)、setTimeout、setInterval、setImmediate等
2)微任务(Microtasks)
Promise、MutationObserver
事件轮询机制执行过程
1)代码执行过程中,宏任务和微任务放在不同的任务队列中
2)当某个宏任务执行完后,会查看微任务队列是否有任务。如果有,执行微任务队列中的所有微任务(注意这里是执行所有的微任务)
3)微任务执行完成后,会读取宏任务队列中排在最前的第一个宏任务(注意宏任务是一个个取),执行该宏任务,如果执行过程中,遇到微任务,依次加入微任务队列
4)宏任务执行完成后,再次读取微任务队列里的任务,依次类推。
event loop 与 浏览器更新渲染时机
1) 浏览器更新渲染会在event loop中的 宏任务 和 微任务 完成后进行,即宏任务 → 微任务 → 渲染更新(先宏任务 再微任务,然后再渲染更新)
2)宏任务队列中,如果有大量任务等待执行时,将dom的变动作为微任务,能更快的将变化呈现给用户,这样就可以在这一次的事件轮询中更新dom
event loop与 vue nextTick
vue nextTick为什么要优先使用微任务实现?
1) vue nextTick的源码实现,优先级判断,总结就是Promise > MutationObserver > setImmediate > setTimeout
2)这里优先使用Promise,因为根据event loop与浏览器更新渲染时机,使用微任务,本次event loop轮询就可以获取到更新的dom
3)如果使用宏任务,要到下一次event loop中,才能获取到更新的dom
Node中的process.nextTick
有很多文章把Node的process.nextTick和微任务混为一谈,但其实并不是同一个东西
process.nextTick 是 Node.js 自身定义实现的一种机制,有自己的 nextTickQueue
process.nextTick执行顺序早于微任务
定时器
setTimeout/setInterval
setTimeout固定时长后执行
setInterval间隔固定时间重复执行
setTimeout、setInterval最短时长为4ms
定时器不准的原因
setTimeout/setInterval的执行时间并不是确定的
setTimeout/setInterval是宏任务,根据事件轮询机制,其他任务会阻塞或延迟js任务的执行
考虑极端情况,假如定时器里面的代码需要进行大量的计算,或者是DOM操作,代码执行时间超过定时器的时间,会出现定时器不准的情况
setTimeout/setInterval 动画卡顿
不同设备的屏幕刷新频率可能不同, setTimeout/setInterval只能设置固定的时间间隔,这个时间和屏幕刷新间隔可能不同
setTimeout/setInterval通过设置一个间隔时间,来不断改变图像实现动画效果,在不同设备上可能会出现卡顿、抖动等现象
requestAnimationFrame
requestAnimationFrame 是浏览器专门为动画提供的API
requestAnimationFrame刷新频率与显示器的刷新频率保持一致,使用该api可以避免使用setTimeout/setInterval造成动画卡顿的情况
requestAnimationFrame:告诉浏览器在下次重绘之前执行传入的回调函数(通常是操纵dom,更新动画的函数)
setTimeout、setInterval、requestAnimationFrame 三者的区别
1)引擎层面
setTimeout属于 JS引擎 ,存在事件轮询
requestAnimationFrame 属于 GUI引擎
JS引擎与GUI引擎是互斥的,也就是说 GUI引擎在渲染时会阻塞JS引擎的计算
这样设计的原因,如果在GUI渲染的时候,JS同时又改变了dom,那么就会造成页面渲染不同步
2)性能层面
当页面被隐藏或最小化时,定时器 setTimeout仍会在后台执行动画任务
当页面处于未激活的状态下,该页面的屏幕刷新任务会被系统暂停,requestAnimationFrame也会停止
setTimeout模拟实现setInterval
// 使用闭包实现
function mySetInterval(fn, t) {
let timer = null;
function interval() {
fn();
timer = setTimeout(interval, t);
}
interval();
return {
// cancel用来清除定时器
cancel() {
clearTimeout(timer);
}
};
}
setInterval模拟实现setTimeout
function mySetTimeout(fn, time) {
let timer = setInterval(() => {
clearInterval(timer);
fn();
}, time);
}
// 使用
mySetTimeout(() => {
console.log(1);
}, 2000);
作者:海阔_天空
链接:https://juejin.cn/post/7146973901166215176
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。