关于前端:关于JS事件循环-这一篇就够啦

38次阅读

共计 20198 个字符,预计需要花费 51 分钟才能阅读完成。

前言

在上篇我曾经讲过了 JS 世界是如何诞生的,然而如何能力让世界井井有条的运行起来呢?
本文将从 万物初始 讲起 JS 世界的运行规定,也就是 事件循环 ,在这个过程中你就能明确为什么须要这些规定。有了规定 JS 世界能力稳稳的运行起来,所以这些规定十分重要,然而你真的理解它们了吗?
浏览本文前能够思考上面几个问题:

  • 你了解中的事件循环是怎么的?
  • 有宏工作了,为什么还要有微工作,它们又有什么关系?
  • promise十分重要,你能够手撕 promise/A+ 标准了吗?
  • async/await底层实现原理是什么?

本文将会由浅入深的解答这些问题

万物初始

本文基于 chromium 内核 V8 引擎解说
刚开始让万物运行是件挺容易的事件,毕竟刚开始嘛,也没什么简单事,比方有如下一系列工作:

  • 工作 1:1 + 2
  • 工作 2:3 / 4
  • 工作 3:打印出 工作 1 和 工作 2 后果

把工作转换成 JS 代码长这样:

function MainThread() {
    let a = 1 + 2;
    let b = 3 / 4;
    console.log(a + b)
}

JS 世界拿到这个工作一看很简略啊:首先建一条流水线(一个单线程),而后顺次解决这三个工作,最初执行完后撤掉流水线(线程退出)就行了。

当初咱们的事件循环系统很容易就能解决这几个工作了,能够得出:

  • 单线程解决了解决工作的问题 :如果有一些确定好的工作,能够应用一个单线程来 依照程序 解决这些工作。

然而有一些问题:

  • 但并不是所有的工作都是在执行之前统一安排好的,很多时候,新的工作是在线程运行过程中产生的
  • 在线程执行过程中,想退出一个新工作,然而当初这个线程执行完以后记录的工作就间接退出了

世界循环运行

要想解决下面的问题,就须要引入 循环机制 ,让线程继续运行,再来工作就能执行啦
转换成代码就像这样

function MainThread() {while(true){······}
}


当初的 JS 的事件循环系统就能继续运行起来啦:

  • 循环机制解决了不能循环执行的问题:引入了循环机制,通过一个 while 循环语句,线程会始终循环执行

不过又有其余问题呈现了:

  • 别的线程要交给我这个主线程工作,并且还可能短时间内交给很多的工作。这时候该如何优化来解决这种状况呢?

工作放入队列

交给主线程的这些工作,必定得按肯定程序执行,并且还要得主线程闲暇能力做这些工作,所以就须要先将这些工作按程序存起来,等着主线程有空后一个个执行。
然而如何按顺序存储这些工作呢?
很容易想到用 队列 ,因为这种状况合乎队列“ 先进先出 ”的特点,也就是说 要增加工作的话,增加到队列的尾部;要取出工作的话,从队列头部去取。

有了队列之后,主线程就能够从音讯队列中读取一个工作,而后执行该工作,主线程就这样始终循环往下执行,因而只有音讯队列中有工作,主线程就会去执行。
咱们要留神的是:

  • JavaScript V8 引擎是在 渲染过程 主线程 上工作的

后果如下图所示:

其实渲染过程会有一个 IO 线程:IO 线程负责和其它过程 IPC 通信,接管其余过程传进来的音讯 ,如图所示:

咱们当初晓得页面主线程是如何接管内部工作了:

  • 如果其余过程想要发送工作给页面主线程,那么先通过 IPC 把工作发送给渲染过程的 IO 线程,IO 线程再把工作发送给页面主线程

到当初,其实曾经实现 chromium 内核 根本的事件循环系统了:

  • JavaScript V8 引擎 渲染过程 主线程 上工作
  • 主线程有 循环机制,能在线程运行过程中,能接管并执行新的工作
  • 交给主线程执行的工作会先放入 工作队列 中,期待主线程闲暇后顺次调用
  • 渲染过程会有一个 IO 线程:IO 线程负责和其它过程 IPC 通信,接管其余过程传进来的音讯

欠缺运行规定

当初曾经晓得:页面线程所有执行的工作都来自于工作队列。工作队列是“先进先出”的,也就是说放入队列中的工作,须要 期待后面的工作被执行完,才会被执行
这就导致两个问题了:

  • 如何解决高优先级的工作?
  • 如何解决执行工夫长的工作?

如何解决这两个问题呢?

解决高优先级的工作 - 微工作

以监听 dom 变动为例,如果 dom 变动则触发工作回调,然而如果将这个工作回调放到队列尾部,等到轮到它出队列,可能曾经过来一段时间了,影响了监听的 实时性 。并且如果变动很频繁的话,往队列中插入了这么多的工作,必然也升高了 效率
所以须要一种既能兼顾实时性,又能兼顾效率的办法。
解决方案 V8 引擎 曾经给出了:在每个工作外部,开拓一个属于该工作的队列,把须要兼顾实时性和效率的工作,先放到这个工作外部的队列中期待执行,等到当前任务快执行完筹备退出前,执行该工作外部的队列。咱们把放入到这个非凡队列中的工作称为 微工作
这样既不会影响以后的工作又不会升高多少实时性。
如图所示以 工作 1 为例:

能够总结一下:

  • 工作队列中的工作都是 宏观工作
  • 每个宏观工作都有一个本人的宏观工作队列
  • 微工作在以后宏工作中的 JavaScript 快执行实现时,也就在 V8 引擎 筹备退出全局执行上下文并清空调用栈 的时候,V8 引擎 查看全局执行上下文中的微工作队列,而后依照程序执行队列中的微工作。
  • V8 引擎 始终循环执行微工作队列中的工作,直到队列为空才算执行完结。也就是说在执行微工作 过程中产生的新的微工作并不会推延到下个宏工作中执行,而是在以后的宏工作中继续执行。

咱们来看看 微工作 怎么产生?在古代浏览器外面,产生微工作 只有两种形式

  • 第一种形式是应用 MutationObserver监控某个 DOM 节点,而后再通过 JavaScript 来批改这个节点,或者为这个节点增加、删除局部子节点,当 DOM 节点发生变化时,就会产生 DOM 变动记录的微工作。
  • 第二种形式是应用 Promise,当调用 Promise.resolve()或者 Promise.reject() 的时候,也会产生微工作。

而常见的 宏工作 又有哪些呢?

  • 定时器类:setTimeout、setInterval、setImmediate
  • I/ O 操作:比方读写文件
  • 音讯通道:MessageChannel

并且咱们要晓得:

  • 宿主(如浏览器)发动的工作称为宏观工作
  • JavaScript 引擎发动的工作称为宏观工作

解决执行工夫长的工作 - 回调

要晓得 排版引擎 BlinkJavaScript 引擎 V8 都工作在渲染过程的主线程上并且是互斥的。
在单线程中,每次只能执行一个工作,而其余工作就都处于期待状态。如果其中一个工作执行工夫过久,那么下一个工作就要期待很长时间。
如果页面上有动画,当有一个 JavaScript 工作运行工夫较长的时候(比方大于 16.7ms),主线程无奈交给 排版引擎 Blink来工作,动画也就无奈渲染来,造成卡顿的成果。这当然是十分蹩脚的用户体验。想要防止这种问题,就须要用到 回调 来解决。

从底层看 setTimeout 实现

到当初曾经晓得了,JS 世界是由 事件循环和工作队列 来驱动的。
setTimeout大家都很相熟,它是一个定时器,用来指定某个函数在多少毫秒后执行。那浏览器是怎么实现 setTimeout 的呢?
要搞清楚浏览器是怎么实现 setTimeout 就先要弄明确两个问题:

  • setTimeout工作存到哪了?
  • setTimeout到工夫后怎么触发?
  • 勾销 setTimeout 是如何实现的?

setTimeout工作存到哪了

首先要分明,工作队列不止有一个,Chrome 还保护着一个 提早工作队列 ,这个队列保护了须要提早执行的工作,所以当你通过Javascript 调用 setTimeout 时,渲染过程会将该定时器的 回调工作 增加到 提早工作队列 中。
回调工作的信息蕴含:回调函数、以后发动工夫、提早执行工夫
具体我画了个图:

setTimeout到工夫后怎么触发

当主线程执行完工作队列中的一个工作之后,主线程会对提早工作队列中的工作,通过以后发动工夫和提早执行工夫计算出曾经到期的工作,而后顺次的执行这些到期的工作,等到期的工作全副执行完后,主线程就进入到下一次循环中。具体呢我也画了个图:

ps:为了讲清楚,画配图好累哦,点个赞吧!
到这就分明 setTimeout 是如何实现的了:

  • setTimeout存储到提早工作队列中
  • 当主线程执行完工作队列中的一个工作后,计算提早工作队列中到期到工作,并执行所有到期工作
  • 执行完所有到期工作后,让出主线程,进行下一次事件循环

手撕 promise

promise十分重要,新退出的原生 api 和前端框架都大量应用了 promisepromise 未然成为前端的“水”和“电”。
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/A+ 标准

咱们当初想写一个 promise,然而谁来通知怎么才算一个合格的promise,不必放心,业界是通过一个规定指标来实现promise 的,这就是 Promise / A+,还有一篇翻译可供参考【翻译】Promises / A+ 标准。
接下来就开始逐渐实现吧!

同步的 promise

先从一个最简略的 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
接下来剖析一下:

  • 调用 ObjPromise 传入一个函数命名为 executorexecutor 函数承受两个参数 resolve、reject,能够了解为别离代表胜利时的调用和失败时的调用。executor函数个别长这样(resolve,reject)=>{...}
  • status代表以后 promise 的状态,有三种 'pending'、'resolve'、'reject'(注:从状态机思考的话还有一个额定的初始状态,示意promise 还未执行)
  • successValfailVal 别离代表 resolve 回调和 reject 回调携带的参数值
  • 函数 resolve:初始化的时候通过作为 executor 的参数传递给使用者,用来让使用者须要的时候调用,将 status 状态从 'pending' 改成'resolve'
  • 函数 reject:初始化的时候通过作为 executor 的参数传递给使用者,将 status 状态从 'pending' 改成'reject'
  • 你可能还发现了 函数 resolve 函数 reject 外面都有 if (this.status !== 'pending') {return;},这是因为resolvereject只能调用一次,也就是 status 状态只能扭转一次。

then办法

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;
    }
}

来剖析一下:

  • then 办法能够传入两个参数,两个参数都是函数,俩函数就像这样(val)=>{...}
  • status 状态为 'resolve' 则调用第一个传入的函数,传入的 valsuccessVal
  • status 状态为 'reject' 则调用第二个传入的函数,传入的 valfailVal

然而 then 办法还须要反对链式调用的,也就是说能够这样:

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;
}

再剖析一下:

  • status'resolve'时,将 promise 胜利 resolve 的后果 successVal,传递给第一个办法onResolved(),而后执行onResolved(this.successVal) 函数
  • status'reject'时,过程统一,就不多说啦
  • 重点看它们都会把新创建的 promise 赋值给 then 办法,执行完后 then 办法会返回这个新的 promise,这样就能实现then 的链式调用了

3. 使 then 办法的链式调用能够传参
然而你没有发现一个问题,我 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;
}

很简略:

  • 先保留函数执行的后果,也就是函数的返回值
  • 而后,将返回值传递给新的用来返回的 promiseresolve(),就能够将返回值保留到新的 promisesuccessVal
  • 执行出错的话,当然要将谬误传递给新的用来返回的 promisereject(),将谬误保留到新的 promisefailVal

4.then 传入参数解决
再看看这段常见的代码:

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};
    ······
}

剖析一下:

  • 判断传入参数的类型是不是函数
  • 传入类型是函数的话,那没故障,间接用就行
  • 传入类型不是函数的话,那蹩脚啦,咱们得别离用 (e) => e(err)=>{throw err}来替换

到当初 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)
        }
    }
    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 时,间接 resolve 就行
  • 当是 promise 类型时,用 this.resolvePromise(newData, newResolve, newReject) 来递归的调用 then 办法,直到 data 不为 promise,而后resolve 后果就行啦

6. 解决 then 返回值循环援用
当初又有问题了:
如果新的 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,然而很多状况下,promiseresolvereject 是被异步调用的,异步调用的话,执行到 then() 办法时,以后的 status 状态还是 'pending'。这该如何改良代码呢?
思路其实很简略:

  • 设置两个数组,别离存起来 then() 办法的回调函数 onResolvedonRejected
  • 当等到调用了 resolve 或者 reject 时,执行对应数组内存入的回调函数即可
  • 另外为了保障执行程序, 期待以后执行栈执行实现,咱们还须要给 constructorresolvereject 函数外面应用 setTimeout 包裹起来,防止影响以后执行的工作。

依据这个思路来革新一下promise

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
});

咱们当初曾经根本实现了 Promisethen办法啦。

欠缺 promise

到当初曾经实现了 promise 最外围的两个办法:constructor办法和 then 办法。不过 Promise/A+ 还规定了一些其余的办法,咱们持续来实现。

catch办法

catch()办法就是能够通过回调函数拿到 reject 的值,这个好办,其实 then 办法曾经实现了,转接一下 then 办法就行了:

catch(onRejected) {return this.then(null, onRejected)
}

这样就实现了 catch() 办法

Promise.resolve()/reject()办法

大家必定都见过 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)
        }
    })
};

来剖析一下:

  • 传入 promise 数组,返回一个新的 promsie 对象
  • resp用来保留所有 promise 的执行后果
  • instanceof 来判断是否是 promise 类型
  • 通过调用每个 promise 的 then 办法拿到返回值,并且要传入 reject 办法
  • doneNum 来保留执行实现的 promise 数量,全副执行完后,通过 resolve 传递执行后果 resp,并且将以后promise 状态改为 'resolve',后续就能够通过then 办法取值

race办法

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 数组,返回一个新的 promsie 对象
  • 用 instance 来判断是否是 promise 类型
  • 调用每个 promisethen办法,并传递 resolve、reject 办法,哪个先执行完就间接完结了,后续就能够通过 then 办法取值

OK, 到当初曾经实现了一个本人的 promise 对象!

从底层看 async/await 实现

手撕完 promise,趁热再深刻学习一下ES7 的新个性 async/awaitasync/await 相当牛逼:它是 JavaScript 异步编程的一个重大改良,提供了在不阻塞主线程的状况下应用同步代码实现异步拜访资源的能力,并且使得代码逻辑更加清晰 。接下来咱们就来深刻理解下async/await 为什么能这么牛逼。
async/await应用了 GeneratorPromise两种技术,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 代码交替执行。这其实就是生成器函数的个性,它能够暂停执行,也能够复原执行。
再来看下,它是具体是怎么暂停执行和复原执行的:

  • 在生成器函数外部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎 将返回关键字前面的内容给内部,并暂停该函数的执行。
  • 内部函数能够通过 next 办法 复原生成器函数的执行。

然而 JavaScript 引擎 V8 是如何实现生成器函数的暂停和复原呢,接着往下看

生成器原理

想要搞懂生成器函数如何暂停和复原,要先理解一下协程的概念,协程是一种比线程更加轻量级的存在,能够把协程看成是跑在线程上的工作:

  • 一个线程上能够存在多个协程,然而在线程上同时只能执行一个协程。
  • 如果从 A 协程启动 B 协程,咱们就把 A 协程称为 B 协程的父协程。
  • 一个过程能够领有多个线程一样,一个线程也能够领有多个协程。
  • 协程不是被操作系统内核所治理,而齐全是由程序所管制(也就是在用户态执行)。因而协程在性能上要远高于线程。

小知识点:线程 外围态,协程 用户态。也就是说线程被内核调度,协程是由用户的程序本人调度,零碎并不知道有协程的存在
上面我画了个图来演示下面代码的执行过程:

从图中联合代码能够看出协程的规定:

  • 通过调用生成器函数 genFun 来创立一个协程 gen,创立之后,gen 协程 并没有立刻执行
  • 要让 gen 协程执行,须要通过调用gen.next()
  • 当协程正在执行的时候,能够通过 yield 关键字来暂停 gen 协程的执行,并返回次要信息给父协程。
  • 如果协程在执行期间,遇到了 return,那么JavaScript 引擎 会完结以后协程,并将 return 前面的内容返回给父协程。

其实规定总的来说:

  • 父协程中执行next(),线程控制权就让给子协程了
  • 子协程中遇到yield,线程控制权就让给父协程了
  • 能够看出父协程和子协程还是相互谦让的

然而用 Generator 生成器还是不太好用,咱们心愿写代码的时候,不要手动管制协程之间的切换,该切换时,JavaScript 引擎 帮我间接切换好多省事。这时候 async/await 就退场啦!

再看async/await

曾经晓得,async/await应用了 GeneratorPromise两种技术,其实往低层说就是 微工作和协程 的利用。当初 GeneratorPromise都曾经深刻了解啦。然而微工作和协程是如何合作实现了 async/await 呢?
1. async是什么:

MDN:async是一个通过异步执行并隐式返回 Promise 作为后果的函数。
能够执行上面代码:

async function foo() {return 1}
console.log(foo())  // Promise {<resolved>: 1}

能够看到调用 async 申明的 foo() 函数返回了一个 Promise 对象,并且状态是 resolved
2. await 是什么:

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)

想要晓得下面这段代码执行后果如何,就先看看这段代码的执行流程图,我曾经画进去了:

联合下面这张流程图,剖析一下下面代码的执行过程:

  • 首先,执行 console.log(0) 这个语句,打印进去0
  • 因为 foo 函数是被 async 标记过的,所以当进入该函数的时候,JavaScript 引擎 会保留父协程调用栈等信息,而后切换到子协程,执行 foo 函数 中的 console.log(1) 语句,并打印出 1
  • 当执行到 await 99 时,会默认创立一个 Promise 对象,如下:
let newPromise = new Promise((resolve,reject){resolve(99)
})

并且在创立的过程中遇到了 resolve(99)JavaScript 引擎 会将该工作推入微工作队列。

  • 而后 JavaScript 引擎 暂停以后子协程的执行,将主线程控制权交给父协程。并且还会把这个新创建的 Promise 返回给父协程
  • 父协程拿到主线程控制权后,首先调用 newPromise.then,把回调函数放入到Promise 中,这个回调函数是什么?其实就是相当于 生成器函数 next(),调用这个回调函数会调用next(),会将父协程的控制权再交给子协程。
  • 接下来继续执行父协程的流程,这里执行console.log(3),并打印进去3
  • 之后父协程将执行完结,在完结之前,会进入微工作的检查点,查看微工作,而后执行微工作队列,微工作队列中有 resolve(99) 的工作期待执行。
  • 执行 resolve(99),触发了之前存入的回调函数,回调函数内有next(),父协程的控制权再交给子协程,并同时将 value99传给该子协程。
  • 子协程 foo 激活之后,会把接管到的 value99赋给了变量 a,而后foo 协程执行console.log(a),打印出99,执行实现之后,将控制权归还给父协程。

下面的就是 async/await 具体的执行过程啦,能够看出 JavaScript 引擎 帮咱们做了好多工作,能力让咱们将异步代码写成同步代码的格局。

参考

  • 浏览器工作原理与实际
  • Promise 之你看得懂的 Promise
  • MDN-async
  • MDN-await

小结

  • 从零开始理解了 JS 世界的事件循环机制
  • 明确了为什么会有微工作,以及宏工作与微工作的关系
  • 把握了如何手撕合乎 Promise/A+ 标准的Promise
  • 晓得 async/await 应用了 GeneratorPromise两种技术,也就是说它是 微工作和协程 的利用

看完两件事

  • 欢送加我微信(iamyyymmm),拉你进技术群,长期交流学习
  • 关注公众号「呆鹅实验室」,和呆鹅一起学前端,进步技术认知

正文完
 0