本文由 dellyoung 独家受权发布,若是以为文章有帮助,欢迎点击阅读原文给做者点个赞~前端
「 本文共 8606 字,预计阅读全文须要 28 分钟 」”
本文将从万物初始讲起JS世界的运转规则,也就是事件循环,在这个过程当中你就能明白为何须要这些规则。有了规则JS世界才能稳稳的运转起来,因此这些规则很是重要,可是你真的了解它们了吗?面试
阅读本文前能够思考下面几个问题:编程
第一节:深刻理解JS的深拷贝
第二节:深刻理解JS的原型和原型链
第三节:深刻理解JS的事件循环json
本文基于chromium内核讲解”
刚开始让万物运转是件挺容易的事情,毕竟刚开始嘛,也没什么复琐事,好比有以下一系列任务:api
function MainThread() { let a = 1 + 2; let b = 3 / 4; console.log(a + b) }
JS世界拿到这个任务一看很简单啊:首先建一条流水线(一个单线程),而后依次处理这三个任务,最后执行完后撤掉流水线(线程退出)就好了。数组
如今我们的事件循环系统很容易就能处理这几个任务了,能够得出:promise
单线程解决了处理任务的问题:若是有一些肯定好的任务,可使用一个单线程来按照顺序处理这些任务。
可是有一些问题:浏览器
要想解决上面的问题,就须要引入循环机制,让线程持续运转,再来任务就能执行啦前端框架
转换成代码就像这样微信
function MainThread() { while(true){ ······ } }
如今的JS的事件循环系统就能持续运转起来啦:
循环机制解决了不能循环执行的问题:引入了循环机制,经过一个 while 循环语句,线程会一直循环执行
不过又有其余问题出现了:
交给主线程的这些任务,确定得按必定顺序执行,而且还要得主线程空闲才能作这些任务,因此就须要先将这些任务按顺序存起来,等着主线程有空后一个个执行。
可是如何按顺序存储这些任务呢?
很容易想到用队列,由于这种状况符合队列“先进先出”的特色,也就是说 要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。
有了队列以后,主线程就能够从消息队列中读取一个任务,而后执行该任务,主线程就这样一直循环往下执行,所以只要消息队列中有任务,主线程就会去执行。
咱们要注意的是:
其实渲染进程会有一个IO线程:IO线程负责和其它进程IPC通讯,接收其余进程传进来的消息,如图所示:
我们如今知道页面主线程是如何接收外部任务了:
到如今,其实已经完成chromium内核基本的事件循环系统了:
如今已经知道:页面线程全部执行的任务都来自于任务队列。任务队列是“先进先出”的,也就是说放入队列中的任务,须要等待前面的任务被执行完,才会被执行。
这就致使两个问题了:
处理高优先级的任务-微任务
以监听dom变化为例,若是dom变化则触发任务回调,可是若是将这个任务回调放到队列尾部,等到轮到它出队列,可能已通过去一段时间了,影响了监听的实时性。而且若是变化很频繁的话,往队列中插入了这么多的任务,必然也下降了效率。
因此须要一种既能兼顾实时性,又能兼顾效率的方法。
解决方案V8引擎已经给出了:在每一个任务内部,开辟一个属于该任务的队列,把须要兼顾实时性和效率的任务,先放到这个任务内部的队列中等待执行,等到当前任务快执行完准备退出前,执行该任务内部的队列。我们把放入到这个特殊队列中的任务称为微任务。
这样既不会影响当前的任务又不会下降多少实时性。
如图所示以任务1放为例:
能够总结一下:
V8引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束。也就是说在执行微任务过程当中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。
咱们来看看微任务怎么产生?在现代浏览器里面,产生微任务只有两种方式。
第二种方式是使用 Promise,当调用 Promise.resolve()或者 Promise.reject() 的时候,也会产生微任务。
而常见的宏任务又有哪些呢?
消息通道:MessageChannel
而且咱们要知道:
若是页面上有动画,当有一个JavaScript任务运行时间较长的时候(好比大于16.7ms),主线程没法交给排版引擎 Blink来工做,动画也就没法渲染来,形成卡顿的效果。这固然是很是糟糕的用户体验。想要避免这种问题,就须要用到回调来解决。
到如今已经知道了,JS世界是由事件循环和任务队列来驱动的。
setTimeout你们都很熟悉,它是一个定时器,用来指定某个函数在多少毫秒后执行。那浏览器是怎么实现setTimeout的呢?
要搞清楚浏览器是怎么实现setTimeout就先要弄明白两个问题:
回调任务的信息包含:回调函数、当前发起时间、延迟执行时间
具体我画了个图:
setTimeout到时间后怎么触发
当主线程执行完任务队列中的一个任务以后,主线程会对延迟任务队列中的任务,经过当前发起时间和延迟执行时间计算出已经到期的任务,而后依次的执行这些到期的任务,等到期的任务所有执行完后,主线程就进入到下一次循环中。具体呢我也画了个图:
ps:为了讲清楚,画配图真的好累哦,点个赞吧!”
到这就清楚setTimeout是如何实现的了:
promise很是重要,新加入的原生api和前端框架都大量使用了promise,promise已然成为前端的“水”和“电”。
promise解决了什么问题呢?promise解决的是异步编码风格的问题。
我们来看,之前咱们的异步代码长这样:
let fs = require('fs'); fs.readFile('./dellyoung.json',function(err,data){ fs.readFile(data,function(err,data){ fs.readFile(data,function(err,data){ console.log(data) }) }) })
层层嵌套,环环相扣,想拿到回调结果已经够费劲了,若是还想进行错误处理。。。那简直太难受了。
而promise出现后,这些问题迎刃而解:
let fs = require('fs'); function getFile(url){ return new Promise((resolve,reject)=>{ fs.readFile(url,function(error,data){ if(error){ reject(error) } resolve(data) }) }) } getFile('./dellyoung.json').then(data=>{ return getFile(data) }).then(data=>{ return getFile(data) }).then(data=>{ console.log(data) }).catch(err=>{ // 统一错误处理 console.log(err) })
简直好用了太多。
能够发现,使用promise解决了异步回调的嵌套调用和错误处理的问题。
你们已经知道promise很是重要了,可是如何彻底学会promise呢?手撕一遍promise天然就贯通啦,我们开始撕,在过程当中抽丝剥茧。
咱们如今想写一个promise,可是谁来告诉怎样才算一个合格的promise,不用担忧,业界是经过一个规则指标来实现promise的,这就是Promise / A+,还有一篇翻译可供参考【翻译】Promises / A+规范。
接下来就开始逐步实现吧!
先从一个最简单的promise实现开始
先实现promise的地基:初始化用的构造函数
class ObjPromise { constructor(executor) { // promise状态 this.status = 'pending'; // resolve回调成功,resolve方法里的参数值 this.successVal = null; // reject回调成功,reject方法里的参数值 this.failVal = null; // 定义resolve函数 const resolve = (successVal) => { if (this.status !== 'pending') { return; } this.status = 'resolve'; this.successVal = successVal; }; // 定义reject const reject = (failVal) => { if (this.status !== 'pending') { return; } this.status = 'reject'; this.failVal = failVal; }; try { // 将resolve函数给使用者 executor(resolve, reject) } catch (e) { // 执行抛出异常时 reject(e) } } }
我们先写一个constructor用来初始化promise。
接下来分析一下:
then方法做用:拿到promise中的resolve或者reject的值。
1.基础版then方法
在class里面放上以下then方法:
then(onResolved, onRejected) { switch (this.status) { case "resolve": onResolved(this.successVal); break; case "reject": onRejected(this.failVal); break; } }
来分析一下:
new Promise((resolve,reject)=>{ resolve(1); }).then((resp)=>{ console.log(resp); // 1 }).then(()=>{ ... })
2.使then方法支持链式调用
其实支持链式核心就是then方法要返回一个新的promise,我们来改造一下实现支持链式调用。
then(onResolved, onRejected) { // 要返回一个promise对象 let resPromise; switch (this.status) { case "resolve": resPromise = new ObjPromise((resolve, reject) => { try{ // 传入的第一个函数 onResolved(this.successVal); resolve(); }catch (e) { reject(e); } }); break; case "reject": resPromise = new ObjPromise((resolve, reject) => { try{ // 传入的第二个函数 onRejected(this.failVal); resolve(); }catch (e) { reject(e); } }); break; } return resPromise; }
再分析一下:
可是你没有发现一个问题,我then方法内的第一个参数,也就是onResolved()函数,函数内部的返回值应该是要可以传递给下面接着进行链式调用的then方法的,以下所示:
new Promise((resolve,reject)=>{ resolve(1); }).then((resp)=>{ console.log(resp); // 1 return 2; // <<< 关注这行 }).then((resp)=>{ console.log(resp); // 2 接受到了参数2 })
这该如何实现呢?
其实很简单:
then(onResolved, onRejected) { // 定义这个变量保存要返回的promise对象 let resPromise; switch (this.status) { case "resolve": resPromise = new ObjPromise((resolve, reject) => { try{ // 传入的第一个函数 let data = onResolved(this.successVal); resolve(data); }catch (e) { reject(e); } }); break; case "reject": resPromise = new ObjPromise((resolve, reject) => { try{ // 传入的第二个函数 let data = onRejected(this.failVal); resolve(data); }catch (e) { reject(e); } }); break; } return resPromise; }
很简单:
再看看这段常见的代码:
new Promise((resolve,reject)=>{ resolve(1); }).then((resp)=>{ console.log(resp); // 1 return 2; }).then((resp)=>{ console.log(resp); // 2 })
能够看到,then方法的参数能够只传一个,继续来改造:
then(onResolved, onRejected) { const isFunction = (fn) => { return Object.prototype.toString.call(fn) === "[object Function]" }; onResolved = isFunction(onResolved) ? onResolved : (e) => e; onRejected = isFunction(onRejected) ? onRejected : err => { throw err }; ······ }
分析一下:
class ObjPromise { constructor(executor) { // promise状态 this.status = 'pending'; // resolve回调成功,resolve方法里的参数值 this.successVal = null; // reject回调成功,reject方法里的参数值 this.failVal = null; // 定义resolve函数 const resolve = (successVal) => { if (this.status !== 'pending') { return; } this.status = 'resolve'; this.successVal = successVal; }; // 定义reject const reject = (failVal) => { if (this.status !== 'pending') { return; } this.status = 'reject'; this.failVal = failVal; }; try { // 将resolve函数给使用者 executor(resolve, reject) } catch (e) { // 执行抛出异常时 reject(e) } } then(onResolved, onRejected) { const isFunction = (fn) => { return Object.prototype.toString.call(fn) === "[object Function]" }; onResolved = isFunction(onResolved) ? onResolved : (e) => e; onRejected = isFunction(onRejected) ? onRejected : err => { throw err }; // 定义这个变量保存要返回的promise对象 let resPromise; switch (this.status) { case "resolve": resPromise = new ObjPromise((resolve, reject) => { try{ // 传入的第一个函数 let data = onResolved(this.successVal); resolve(data); }catch (e) { reject(e); } }); break; case "reject": resPromise = new ObjPromise((resolve, reject) => { try{ // 传入的第二个函数 let data = onRejected(this.failVal); resolve(data); }catch (e) { reject(e); } }); break; } return resPromise; } }
你能够在控制台运行下面这个测试代码:
new ObjPromise((resolve,reject)=>{ resolve(1); }).then((resp)=>{ console.log(resp); // 1 return 2; }).then((resp)=>{ console.log(resp); // 2 })
控制台会依次打印出 1 2。
5.then返回值处理
到如今同步promise代码已经没问题啦,可是还不够,由于Promise/A+规定:then方法能够返回任何值,固然包括Promise对象,而若是是Promise对象,咱们就须要将他拆解,直到它不是一个Promise对象,取其中的值。
由于status状态为'resolve'和'reject'时都须要进行这样的处理,因此咱们就能够把处理过程封装成一个函数,代码以下:
then(onResolved, onRejected) { ··· let resPromise; switch (this.status) { case "resolve": resPromise = new ObjPromise((resolve, reject) => { try { // 传入的第一个函数 let data = onResolved(this.successVal); this.resolvePromise(data, resolve, reject); } catch (e) { reject(e); } }); break; case "reject": resPromise = new ObjPromise((resolve, reject) => { try { // 传入的第二个函数 let data = onRejected(this.failVal); this.resolvePromise(data, resolve, reject); } catch (e) { reject(e); } }); break; } return resPromise; } // data为返回值 // newResolve为新的promise的resolve方法 // newReject为新的promise的reject方法 resolvePromise(data, newResolve, newReject) { // 判断是不是promise,不是直接resolve就行 if(!(data instanceof ObjPromise)){ return newResolve(data) } try { let then = data.then; const resolveFunction = (newData) => { this.resolvePromise(newData, newResolve, newReject); }; const rejectFunction = (err) => { newReject(err); }; then.call(data, resolveFunction, rejectFunction) } catch (e) { // 错误处理 newReject(e); } }
分析一下:
如今又有问题了:
若是新的promise出现循环引用的话就永远也递归不到头了
看看执行下面这个代码:
let testPromise = new ObjPromise((resolve, reject) => { resolve(1); }) let testPromiseB = testPromise.then((resp) => { console.log(resp); // 1 return testPromiseB; })
会报错栈溢出。
解决这个问题的方法就是:经过给resolvePromise()方法传递当前新的promise对象,判断当前新的promise对象和函数执行返回值不一样就能够了
class ObjPromise { constructor(executor) { // promise状态 this.status = 'pending'; // resolve回调成功,resolve方法里的参数值 this.successVal = null; // reject回调成功,reject方法里的参数值 this.failVal = null; // 定义resolve函数 const resolve = (successVal) => { if (this.status !== 'pending') { return; } this.status = 'resolve'; this.successVal = successVal; }; // 定义reject const reject = (failVal) => { if (this.status !== 'pending') { return; } this.status = 'reject'; this.failVal = failVal; }; try { // 将resolve函数给使用者 executor(resolve, reject) } catch (e) { // 执行抛出异常时 reject(e) } } resolvePromise(resPromise, data, newResolve, newReject) { if (resPromise === data) { return newReject(new TypeError('循环引用')) } if (!(data instanceof ObjPromise)) { return newResolve(data) } try { let then = data.then; const resolveFunction = (newData) => { this.resolvePromise(resPromise, newData, newResolve, newReject); }; const rejectFunction = (err) => { newReject(err); }; then.call(data, resolveFunction, rejectFunction) } catch (e) { // 错误处理 newReject(e); } } then(onResolved, onRejected) { const isFunction = (fn) => { return Object.prototype.toString.call(fn) === "[object Function]" }; onResolved = isFunction(onResolved) ? onResolved : (e) => e; onRejected = isFunction(onRejected) ? onRejected : err => { throw err }; // 定义这个变量保存要返回的promise对象 let resPromise; switch (this.status) { case "resolve": resPromise = new ObjPromise((resolve, reject) => { try { // 传入的第一个函数 let data = onResolved(this.successVal); this.resolvePromise(resPromise, data, resolve, reject); } catch (e) { reject(e); } }); break; case "reject": resPromise = new ObjPromise((resolve, reject) => { try { // 传入的第二个函数 let data = onRejected(this.failVal); this.resolvePromise(resPromise, data, resolve, reject); } catch (e) { reject(e); } }); break; } return resPromise; } }
能够在控制台中调用以下代码试试啦:
new ObjPromise((resolve, reject) => { resolve(1); }).then((resp) => { console.log(resp); // 1 return 2 }).then((resp) => { console.log(resp); // 2 return new ObjPromise((resolve, reject) => { resolve(3) }) }).then((resp) => { console.log(resp); // 3 });
控制台会一次打印出 1 2 3
如今我们实现了同步版的promise,可是不少状况下,promise的resolve或reject是被异步调用的,异步调用的话,执行到then()方法时,当前的status状态仍是'pending'。这该如何改进代码呢?
思路其实很简单:
class ObjPromise { constructor(executor) { // promise状态 this.status = 'pending'; // resolve回调成功,resolve方法里的参数值 this.successVal = null; // reject回调成功,reject方法里的参数值 this.failVal = null; // resolve的回调函数 this.onResolveCallback = []; // reject的回调函数 this.onRejectCallback = []; // 定义resolve函数 const resolve = (successVal) => { setTimeout(()=>{ if (this.status !== 'pending') { return; } this.status = 'resolve'; this.successVal = successVal; //执行全部resolve的回调函数 this.onResolveCallback.forEach(fn => fn()) }) }; // 定义reject const reject = (failVal) => { setTimeout(()=>{ if (this.status !== 'pending') { return; } this.status = 'reject'; this.failVal = failVal; //执行全部reject的回调函数 this.onRejectCallback.forEach(fn => fn()) }) }; try { // 将resolve函数给使用者 executor(resolve, reject) } catch (e) { // 执行抛出异常时 reject(e) } } // data为返回值 // newResolve为新的promise的resolve方法 // newReject为新的promise的reject方法 resolvePromise(resPromise, data, newResolve, newReject) { if (resPromise === data) { return newReject(new TypeError('循环引用')) } if (!(data instanceof ObjPromise)) { return newResolve(data) } try { let then = data.then; const resolveFunction = (newData) => { this.resolvePromise(resPromise, newData, newResolve, newReject); }; const rejectFunction = (err) => { newReject(err); }; then.call(data, resolveFunction, rejectFunction) } catch (e) { // 错误处理 newReject(e); } } then(onResolved, onRejected) { const isFunction = (fn) => { return Object.prototype.toString.call(fn) === "[object Function]" }; onResolved = isFunction(onResolved) ? onResolved : (e) => e; onRejected = isFunction(onRejected) ? onRejected : err => { throw err }; // 定义这个变量保存要返回的promise对象 let resPromise; switch (this.status) { case "resolve": resPromise = new ObjPromise((resolve, reject) => { try { // 传入的第一个函数 let data = onResolved(this.successVal); this.resolvePromise(resPromise, data, resolve, reject); } catch (e) { reject(e); } }); break; case "reject": resPromise = new ObjPromise((resolve, reject) => { try { // 传入的第二个函数 let data = onRejected(this.failVal); this.resolvePromise(resPromise, data, resolve, reject); } catch (e) { reject(e); } }); break; case "pending": resPromise = new ObjPromise((resolve, reject) => { const resolveFunction = () => { try { // 传入的第一个函数 let data = onResolved(this.successVal); this.resolvePromise(resPromise, data, resolve, reject); } catch (e) { reject(e); } }; const rejectFunction = () => { try { // 传入的第二个函数 let data = onRejected(this.failVal); this.resolvePromise(resPromise, data, resolve, reject); } catch (e) { reject(e); } }; this.onResolveCallback.push(resolveFunction); this.onRejectCallback.push(rejectFunction); }); break; } return resPromise; } }
能够用下面代码测试一下:
new ObjPromise((resolve, reject) => { setTimeout(() => { resolve(1); }, 100) }).then((resp) => { console.log(resp); // 1 return 2 }).then((resp) => { console.log(resp); // 2 return new ObjPromise((resolve, reject) => { resolve(3) }) }).then((resp) => { console.log(resp); // 3 });
咱们如今已经基本完成了Promise的then方法啦。
到如今已经完成了promise最核心的两个方法:constructor方法和then方法。不过Promise/A+还规定了一些其余的方法,我们继续来完成。
catch()方法就是能够经过回调函数拿到reject的值,这个好办,其实then方法已经实现了,转接一下then方法就好了:
catch(onRejected) { return this.then(null, onRejected) }
这样就实现了catch()方法
你们确定都见过Promise.resolve()或者Promise.resolve()用法。其实做用就是返回一个新的promise,而且内部调用resolve或者reject。
ObjPromise.resolve = (val) => { return new ObjPromise((resolve, reject) => { resolve(val) }) }; ObjPromise.reject = (val) => { return new ObjPromise((resolve, reject) => { reject(val) }) };
经过这两种方法,我们能够将现有的数据很方便的转换成promise对象
all方法
all方法也是很经常使用的方法,它能够传入promise数组,当所有resolve或者有一个reject时,执行结束,固然返回的也是promise对象,来实现一下。
ObjPromise.all = (arrPromise) => { return new ObjPromise((resolve, reject) => { // 传入类型必须为数组 if(Array.isArray(arrPromise)){ return reject(new TypeError("传入类型必须为数组")) } // resp 保存每一个promise的执行结果 let resp = new Array(arrPromise.length); // 保存执行完成的promise数量 let doneNum = 0; for (let i = 0; arrPromise.length > i; i++) { // 将当前promise let nowPromise = arrPromise[i]; if (!(nowPromise instanceof ObjPromise)) { return reject(new TypeError("类型错误")) } // 将当前promise的执行结果存入到then中 nowPromise.then((item) => { resp[i] = item; doneNum++; if(doneNum === arrPromise.length){ resolve(resp); } }, reject) } }) };
来分析一下:
race方法也偶尔会用到,它能够传入promise数组,当哪一个promise执行完,则race就直接执行完,我们来实现一下:
ObjPromise.race = (arrPromise) => { return new Promise((resolve, reject) => { for (let i = 0; arrPromise.length > i; i++) { // 将当前promise let nowPromise = arrPromise[i]; if (!(nowPromise instanceof ObjPromise)) { return reject(new TypeError("类型错误")) }; nowPromise.then(resolve, reject); } }) };
来分析一下:
手撕完promise,趁热再深刻学习一下ES7的新特性async/await。async/await至关牛逼:它是JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的状况下使用同步代码实现异步访问资源的能力,而且使得代码逻辑更加清晰。接下来我们就来深刻了解下async/await为何能这么牛逼。
async/await使用了Generator和Promise两种技术,Promise我们已经掌握了,因此要再看一看Generator究竟是什么。
生成器Generator
先了解一下生成器Generator是如何工做的,接着再学习Generator的底层实现机制——协程(Coroutine)
如何工做
生成器函数:生成器函数是一个带星号函数,并且是能够暂停执行和恢复执行的
先来看下面这段代码:
function* genFun() { console.log("第一段") yield 'generator 1' console.log("第二段") return 'generator 2' } console.log('begin') let gen = genFun() console.log(gen.next().value) console.log('main 1') console.log(gen.next().value) console.log('main 2')
执行这段代码,你会发现gen并非一次执行完的,而是全局代码和gen代码交替执行。这其实就是生成器函数的特性,它能够暂停执行,也能够恢复执行。
再来看下,它是具体是怎么暂停执行和恢复执行的:
想要搞懂生成器函数如何暂停和恢复,要先了解一下协程的概念,协程是一种比线程更加轻量级的存在,能够把协程当作是跑在线程上的任务:
从图中结合代码能够看出协程的规则:
若是协程在执行期间,遇到了return,那么JavaScript引擎会结束当前协程,并将return后面的内容返回给父协程。
其实规则总的来讲:
再看async/await
已经知道,async/await使用了Generator和Promise两种技术,其实往低层说就是微任务和协程的应用。如今Generator和Promise都已经深刻理解啦。可是微任务和协程是如何协做实现了async/await呢?
MDN:async是一个经过异步执行并隐式返回Promise做为结果的函数。”
能够执行下面代码:
async function foo() { return 1 } console.log(foo()) // Promise {<resolved>: 1}
能够看到调用async声明的foo()函数返回了一个Promise对象,而且状态是resolved。
MDN:await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。
若 Promise 正常处理(fulfilled),其回调的resolve函数参数做为 await 表达式的值,继续执行 async function。
若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常缘由抛出。”
先来看下面这段代码:
async function foo() { console.log(1) let a = await 99 console.log(a) } console.log(0) foo() console.log(3)
想要知道上面这段代码执行结果如何,就先看看这段代码的执行流程图,我已经画出来了:
结合上面这张流程图,分析一下上面代码的执行过程:
let newPromise = new Promise((resolve,reject){ resolve(99) })
而且在建立的过程当中遇到了resolve(99),JavaScript引擎会将该任务推入微任务队列。
这一次,完全弄懂 Promise 原理
面试题:说说事件循环机制(满分答案来了)
async/await 原理及执行顺序分析