这篇文章源自我历时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) // undefinedconsole.log(window.a) // 10console.log(this.a) // 报错,Uncaught TypeError: Cannot read properties of undefined (reading 'a')}console.log('this2', this) // windowfoo();
注意:开启了严格模式,只是使得函数内的this指向undefined,它并不会改变全局中this的指向。因此this1中打印的是undefined,而this2还是window对象。
this 题目2
let a = 10const b = 20function foo () {console.log(this.a) // undefinedconsole.log(this.b) // undefined}foo();console.log(window.a) // undefined
如果把 var 改成了 let 或 const,变量是不会被绑定到window上的,所以此时会打印出三个undefined
this 题目3
var a = 1function foo () {var a = 2console.log(this) // windowconsole.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) // windowconsole.log(this.a) // 3}, 0)}}var a = 3obj2.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) // objreturn () => {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
// 手写callFunction.prototype.Call = function(context, ...args) {// context为undefined或null时,则this默认指向全局windowif (!context || context === null) {context = window;}// 利用Symbol创建一个唯一的key值,防止新增加的属性与obj中的属性名重复let fn = Symbol();// this指向调用call的函数context[fn] = this;// 隐式绑定this,如执行obj.foo(), foo内的this指向objlet 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.createresult.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,4for (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__); // trueconsole.log(Object.__proto__ === Function.prototype); // trueconsole.log(Function.prototype.__proto__ === Object.prototype); // trueconsole.log(Object.prototype.__proto__ === null); // true
instanceof
instanceof 的基本用法,它可以判断一个对象的原型链上是否包含该构造函数的原型,经常用来判断对象是否为该构造函数的实例
特殊示例
console.log(Object instanceof Object); //trueconsole.log(Function instanceof Function); //trueconsole.log(Function instanceof Object); //trueconsole.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.prototypelet 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() {}// 设置父类的静态方法getInfoParent.getInfo = function() {console.log("info");};Parent.prototype.getName = function() {console.log(this.name);};selfClass(Child, Parent);Child.getInfo(); // infolet tom = new Child();tom.getName(); // 123
Promise
Promise的底层原理:callback回调函数 + 发布订阅模式
链式调用
1)promise的回调只能被捕获一次
2)在then函数加上return,后面的then函数才能继续捕获到
链式调用示例
// 只有第一个then函数能捕获到结果,第二个then打印undefinedlet 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) {// 解决链式调用的情况,继续返回Promisereturn 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、2new 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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
