跟着月影学Javascript之异步| 青训营笔记

90 阅读13分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天

异步专题

异步是在Javascript继原型链后的又一大难点,而且经常在实际开发中经常遇到异步操作,所以大家应该不陌生,在面试中也往往会配合事件循环机制等知识点结合出组合拳暴打求职者。

所以这个知识点是非常关键且重要的,本文好好捋一下异步的整个知识脉络,包括Promise和事件循环机制等内容。

好了,话不多说直接进入正题。

什么是异步?

和异步有个相对的概念叫做同步。

所谓同步,就是按照你的代码顺序进行执行,而异步不按照代码顺序执行,因此执行效率更高。

异步任务会在浏览器单独中开出一个线程来处理,等同步任务进行完在把异步任务加入执行栈。

image

Javascript的事件循环机制

前置知识铺垫

在讲事件循环之前,我觉得应该来这里简单铺垫一下浏览器环境。

浏览器有4大进程,其中有一个叫Renderer进程就是用来处理Javascript的,这个进程也是我们经常说的浏览器内核。

在这个进程下面又分为5个线程

  • GUI 渲染线程
  • Javascript引擎线程 主要在事件循环提供执行栈
  • 定时器触发线程
  • 事件触发线程 主要在事件循环提供任务队列
  • 异步http请求线程

发现没有,这里面5个线程,就有两个是来处理异步任务的😀

首先我们要知道Javascript是单线程编程,也就是Javascript在一个时间只能干一件事。

咦,之前不是说有5个线程吗?怎么现在又叫单线程了(O_o)??

实际上我们所说的多线程指的是能否有多个线程同时运行并发执行,而不是说有多少个线程,Javascript显然是做不到这点的,在这里Javascript的各个线程其实只能处理各自特定的事情。 这还不算GUI和引擎线程之间还是互斥的,因此Javascript是单线程的。

之所以这么设计的原因在于前端是于用户交互的,比如一个DOM元素被两个线程同时操作那就会造成很多麻烦。

事件循环

好了,前置知识铺垫完就可以开始进入重头戏了。

那么事件循环是啥呢?

我也不扯什么官方解释了这里直接用大白话解释,事件循环其实就是浏览器处理Javascript代码的过程。

循环机制

既然说是事件循环,那么肯定是有几个提供循环流动的关键结点:

  • 主角1:Javascript引擎线程中的执行栈,用来处理Javascript代码执行,每次从循环队列中取出代码执行。
  • 主角2: 事件触发线程中的任务队列,取出WebApi中的执行好的代码,放入队列中。
  • 主角3:WebApi,取出执行栈中Javascript的异步代码,开其它线程执行,执行完后丢到任务队列。

因此整个过程就是,执行栈按顺序执行代码,如果为同步代码则直接执行,遇到异步代码就丢WebApi中。当同步代码全部执行完后,取出WebApi执行好的异步代码,放进任务队列中。

执行栈在按顺序取出任务队列中的代码执行。 image

循环过程

在这整个循环过程,有两个如雷贯耳的概念😀,宏任务/微任务。

宏任务:

  • script代码
  • 定时器
  • I/O
  • ui交互等

微任务:

  • new Promise().then(回调) 注意这里是回调哦
  • MutationObserver(html5 新特性)

这两个概念其实是对于事件循环中异步任务的一个分类,我们的每一轮事件循环结束,都相当于进行了一次宏任务+ 微任务。

执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。

举个例子:当我们执行完同步代码后,开始执行任务队列中的异步代码,因为异步任务可能不止一个,所以就会分成一轮轮的宏任务,而假如在这个异步任务中,比如说定时器中有个Promise.then()回调的代码,那么这个任务就算是这轮宏任务的微任务,应该加到这个这轮宏任务中执行。

image

Promise

回调

回调其实是最常见的异步代码了。比如我们封装的各种代码,Ajax请求,文件请求等。在现在这个时代其实已经不推荐使用回调了,因为多重回调会导致回调地狱。

// 回调地狱代码实例
setTimeout(function () {
    console.log("First");
    setTimeout(function () {
        console.log("Second");
        setTimeout(function () {
            console.log("Third");
        }, 3000);
    }, 4000);
}, 1000);

Promise介绍

Promise就是我们用来解决回调地狱的手段。

Promise 是一个构造函数

  • 我们可以创建 Promise 的实例 const p = new Promise()
  • new 出来的 Promise 实例对象,代表一个异步操作

对于Promise我们主要掌握如何使用它,比如三个状态,then,catch,finally,返回值等。

在这里简单说明一下Promise如何使用:

let p = new Promise((resolve,reject) => {
        resolve('success');   // 调用then中的第一个参数
        // reject('fail);     // 调用then中第二个参数
    })

p.then(value => {
    console.log(value);  //打印success ,调用了resolve获取resovle中的传入的参数。
},err => {
    console.log(err);
})

例如上面这个例子,我们new一个实例p,然后再Promise中传入两个参数,一个是resolve函数,一个是reject函数。这两个函数就是用来改变p的状态,分别对应成功和失败。 当调用成功函数时,会调用回调函数then(resolve,reject),哪个状态改变分别调用哪个函数。

我们通过Promise的方法,把回调变成了用then()连接的链式调用模式,解决了回调地狱。

手写Promise

为了更好的理解Promise各种机制,在这里简单的实现一下基础的Promise。

在这里先给出基础架构:

class Promise {
    constructor(executor) {
        this.state = 'pending'   // Promise状态
        this.result = null       // 传入的参数值
        this.callback = [];      // 存储回调函数

        const _this = this;

        function resolve(data) {    
        }
        
        function reject(data) {
        }
        
        executor(resolve,reject);
    }

    then(onResovled,onRejected) {
    }
    
    catch(onRejected) {
    }
}

1.状态变化

在Promise中有三个状态:pending(等待),fulfilled(成功),rejected(失败)。

Promise的一些规定:只有为pending状态才能调用回调函数,其它状态无法调用回调函数。其实简单来说就是,还没调用之前为pending,调用完后为fulfilled或者rejected,而且只能调用其中一个。

在Promise中,调用then回调的时候分为两种情况,一种是Promise中同步运行,一种就是Promise中异步运行。

这分别会导致两种情况,第一种就是Promise种是同步运行,例如下面这个例子:

let p = new Promise((resolve,reject) => {
        resolve('success');   
    })
p.then(value => {
    console.log(value);  
})

此时的运行顺序是:先调用resolve后,状态从pending变为fulfill,然后再调用then中的回调函数。

因此我们Promise中就是这么实现的:

class Promise {
    ...
    
    function resolve(data) {
        if (_this.state !== 'pending') return ;
        _this.state = 'fulfilled';
        _this.result = data;


        setTimeout(() =>{
            //调用then中的回调   
        })
    }

    function reject(data) {
        if (_this.state !== 'pending') return ;
        _this.state = 'rejected';
        _this.result = data;

        setTimeout(() =>{
            //调用then中的回调  
        })
    }
    
    then(onResovled,onRejected) {
        if (this.state === 'fulfilled') {
            // 获取函数的返回值
            setTimeout(() =>{ 
                callback(onResovled)      // 这里说明一下这个callback函数,这里其实是一个封装的函数,下文会解释,在这里只要知道状态时怎么处理即可。
            })
        }

        if (this.state === 'rejected') {
            setTimeout(() =>{
                callback(onRejected);  // 这里说明一下这个callback函数,这里其实是一个封装的函数,下文会解释,在这里只要知道状态时怎么处理即可。
            })
        }
    }
}

那么我们再来处理异步情况,当Promise为异步情况,那么我们从之前的事件循环就知道了,异步任务会在下一次宏任务才进行,而then是微任务,因此then中的回调会先执行。

let p = new Promise((resolve,reject) => {
        setTimeout(() => {
            resolve('success');
        },1000) 
    })
p.then(value => {
    console.log(value);  
})

此时的then中的状态为pending,因此我们在then中还需要判读状态为pending的情况,接着把传进来的参数存到callback数组中,在后面异步任务进行后调用resolve改变状态时在调用。

class Promise {
    ...
    
    function resolve(data) {
        if (_this.state !== 'pending') return ;
        _this.state = 'fulfilled';
        _this.result = data;


        setTimeout(() =>{
            _this.callback.forEach(item => {   // 处理多个回调
                item.onResovled(data)
            })    
        })
    }

    function reject(data) {
        if (_this.state !== 'pending') return ;
        _this.state = 'rejected';
        _this.result = data;

        setTimeout(() =>{
            _this.callback.forEach(item => {  // 处理多个回调
                item.onRejected(data)
            })     
        })
    }
    
    then(onResovled,onRejected) {
        ...
        // 处理异步情况
        if (this.state === 'pending') {
            this.callback.push({             
                onResovled: function() {
                    callback(onResovled);    // 这里说明一下这个callback函数,这里其实是一个封装的函数,下文会解释,在这里只要知道状态时怎么处理即可。
                },
                onRejected: function() {
                    callback(onRejected);    // 这里说明一下这个callback函数,这里其实是一个封装的函数,下文会解释,在这里只要知道状态时怎么处理即可。
                }
            })
        }
    }
}

2.Promise返回值

下面我们开始Promise的第二个重点,Promise的返回值。

Promise的返回值可以做很多事,比如中断一个Promise链,获取then的结果等作用。

let p = new Promise((resolve,reject) => {
        resolve('success');   
    })
let res = p.then(value => {
    console.log(value);  
})
console.log(res); // [state:fulfilled,result:'undefinded'];

Promise的返回值也是一个Promise,对于这个返回的Promise我们主要处理返回result和返回的状态。

首先我们来看整体返回值,无论什么情况我们都是返回一个Promise,因此在then中,我们的代码也应该返回一个Promise

在这里我直接把then的完整代码给出来,和上面的改进是我们把整个内容用一个new Promise包裹起来了。在整个代码块中,其它的内容我们前面都已经实现了,这时候只要聚焦callback的这个函数,这也是我们返回值处理的核心函数。

then(onResovled,onRejected) {
    const _this = this;

    return new Promise((resolve,reject) => {  // 返回值为Promise
        function callback(fn) {
            try{
                const res = fn(_this.result);
                // 判断返回值是否为Promise
                if (res instanceof Promise) {
                    res.then(x => {
                        resolve(x);
                    },err => {
                        reject(err);
                    })
                } else {    // 如果不是Promise返回函数中的返回值
                    // 返回Promise成功状态和返回值。
                    resolve(res);  
                }
            } catch(e) {
                reject(e)
            }
        }

        if (this.state === 'fulfilled') {
            // 获取函数的返回值
            setTimeout(() =>{
                callback(onResovled)
            })
        }

        if (this.state === 'rejected') {
            setTimeout(() =>{
                callback(onRejected);  
            })
        }

        // 处理异步情况
        if (this.state === 'pending') {
            this.callback.push({
                onResovled: function() {
                    callback(onResovled);
                },
                onRejected: function() {
                    callback(onRejected);
                }
            })
        }
    })
}

我们来整理一下,返回值需要处理什么?返回的Promise的状态和Promise的result。

首先,返回值的状态是非常复杂的,这里面的情况非常多。

在这里我先来一个暴论:一般情况返回Promise时状态rjected只有两种情况会发生:第一种:出现then中抛出错误的情况,第二种:在then中return的Promise调用了reject()

上面这个结论不一定对,但是实际上会发现符合我们绝大多数情况。

为什么说不一定对呢?因为当我们省略了then中reject参数时,会发现状态变成了由调用了then种的回调函数决定,如果调用了resolve,状态则为成功;反之为失败。

看下面两个例子:返回的是截然不同的状态

let p = new Promise((resolve,reject) => {
        reject('success'); 
    })
let res = p.then(value => {
    console.log(value)
},err => {
    console.log(err)
})
console.log(res);   // [fuifilled]

// 省略参数
let p = new Promise((resolve,reject) => {
        reject('success'); 
    })
let res = p.then(value => {
    console.log(value)
})
console.log(res);  // [rejected]

接下来我们再来看返回值,返回值的情况有三种

  • 1.then中没有返回值
  • 2.then中有返回值,但是返回的不是Promise
  • 3.then中有返回值,并且返回的值为Promise

第一种情况:result为undefined 第二种情况:result为return 的值。 第三种情况:result为Promise调用时的参数。

好了,上面的情况分析完毕,我们再来看这份代码。

function callback(fn) {
    try{
        const res = fn(_this.result);
        // 判断返回值是否为Promise
        if (res instanceof Promise) {
            res.then(x => {
                resolve(x);
            },err => {
                reject(err);
            })
        } else {    // 如果不是Promise返回函数中的返回值
            // 返回Promise成功状态和返回值。
            resolve(res);  
        }
    } catch(e) {
        reject(e)
    }
}

try,catch 就是用来判断有无抛出错误的,如果有我们就直接调用rejected()。

如果我们的返回值为Promise,则去调用then中的回调,看我们返回的Promise调用了哪个函数。

最后如果放回值不是Promise类型,就直接调用resolve,把状态更新为成功,并把参数挂载到这个Promise的result上。

3.多重回调 值穿透 catch异常穿透

到这里实际上我们已经把Promise所有最核心的代码都实现了,这里的多重回调和值穿透等内容,其实都是依赖于上面两个的实现。

多重回调:如果仔细观察上面的代码,不难发现这其实就是靠一个循环和获取返回值的后不断用新的Promise迭代的过程。

_this.callback.forEach(item => {
    item.onResovled(data)
})      

值穿透概念:链式调用的参数不是函数时,会发生值穿透,就传入的非函数值忽略,传入的是之前的函数参数。

下面我们先来看个例子,在这里我们中间有一then为空,不传入函数,但是我们依旧可以在下一个then获取到value的值,这就是值的穿透。

let p = new Promise((resolve,reject) => {
    resolve('success'); 
})

p.then(value => {
    return value
}).then().then(value => {
    console.log(value);
})

那么我们应该如何实现值的穿透呢?

在这里我们补上then中最后一块内容,就是判断传入的参数是否为函数,如果不为函数则把它设为一个返回参数值的函数,接着在后new Promise中去接收来自上一个传进来的参数即可。

then(onResovled,onRejected) {

    // 判断回调函数是否存在 允许使用then的时候不写一些参数
    
    if (typeof onRejected !== 'function') {
        onRejected = err => {
            throw err;
        }
    }

    if (typeof onResovled !== 'function') {
        onResovled = value => value;
    }
    
    ...
}

这样值穿透就可以被实现,在我们传入非函数时,就返回上一个参数的值,同理我们也把异常穿透也按同样的方法实现,如果发生报错,就把onRejected设为一个抛出参数值的函数,一直把错误往下抛。

干讲可能不太好理解,还是用这个例子来说明一下流程:

let p = new Promise((resolve,reject) => {
    resolve('success'); 
})

p.then(value => {
    return value
}).then().then(value => {
    console.log(value);
})

首先调用resolve后改变状态并传入参数success,开始调用then中的回调,then中先判断是否传进来一个函数,如果为函数则不进行值穿透设置。进入返回Promise中,因为状态为fulfilled,所以在callback中调用onResolved函数,在callback中最终会走resolve,并且把res 也就是 return value 的参数传进去。(这里return value的值就是传进来的success)

这时候开始进入下一轮回调,还是跟之前一样,判断是否传入一个函数。这里发现传进来是空的,因此会调用值穿透设置,设置为返回参数的函数。再次进入新的Promise中,因为状态为fulfilled,最后会走resolve。进入下一轮then回调。

在最后一轮中,上一个空的then被设置为值穿透,因此直接在callback中获取到了success。

这就是这整个值穿透的过程,这个过程还是很绕的,需要细细体会。

最后我们再来说下catch。

catch捕获异常穿透:catch的实现其实非常容易,catch的作用是来捕获错误的,其它大致内容和then方法一致,所以只需要传入一个onRejected函数即可。

catch(onRejected) {
    return this.then(undefined,onRejected);
}