前端百日进阶-2.异步编程

134 阅读4分钟

在js语言设计之初,主要是用来操作dom。假设js是多线程的,那么在一个线程里对dom进行修改操作,另一个线程进行删除操作,这样就会造成冲突。所以js被设计成了单线程。

由于js单线程的原因,如果在代码执行过程中碰到了耗时长的任务页面就会卡住。为了避免这种情况的产生,js采用了事件循环+消息队列来异步执行代码从而解决像网络IO这样耗时长的任务。

虽然js是单线程的但是浏览器并不是只有一个线程

20922-20181115003917102-1019941005.png

js会在对应的时刻交给对应线程去执行

事件循环和消息队列

1596548052271-341abd5f-cfaf-45ba-81d4-446117bcc29b.png

const fetch = require('node-fetch')

console.log('fetch before...')
fetch('https://www.baidu.com/')
    .then(res => res.text())
    .then(res => console.log(res))
console.log('fetch after...')

我们用上面的代码来模拟一次js中的异步工作:

  1. 主线程依次往下执行遇到console.log('fetch before')直接打印
  2. 主线程遇到fetch,由于fetch是个异步操作。为了不再这里继续等待,js主线程会将fetch操作交给网络线程。
  3. 主线程依次往下执行遇到console.log('fetch after...')直接打印
  4. 主线程执行完后会去不断监听消息队列里面有没有任务可以执行。当异步请求线程获取到需要的数据后,会把任务推到消息队列里面。主线程监听到消息队列有任务后就取出来执行。
  5. 主线程不断的监听消息队列并且执行的过程叫做事件循环

消息队列工作方式

事件循环中存在着两种任务:

宏任务(Macro Task):setTimeout、setInterval、I/O、UI 交互事件(onclick之类的)

微任务(Micro Task):Promise、process.nextTick(Node.js 环境)

当主线程执行完会首先去取事件循环中的微任务来执行,之后才会去取宏任务执行

异步解决方案

Promise

可以把promise当作一个承诺,代码执行过程为pending成功返回fulfilled,失败返回rejected。

Snipaste_2021-04-18_14-24-13.png

const dosome = new Promise((resolve, reject) => {
    setTimeout(() => {
        if (Math.random() > 0.5) {
            resolve('fulfilled')
        }
        reject('rejected')
    }, 1000)
})

dosome
    .then((res) => {
        console.log(res)
        return 'fulfilled2'
    })
    .then((res) => {
        console.log(res) 
    })
    .catch((err) => {
        console.log(err)
    })

// 如果promise成功则打印
// fulfilled
// fulfilled2

// 如果promise成功则打印
// rejected

promise中的静态方法

  • promise.resolve(): 返回一个成功后的promise对象
  • prmise.all([task1, task2...]): 当所有任务都成功返回一个成功结果的数组
  • promise.race([task1, task2...]): 当数组中的第一个任务成功返回第一个任务执行成功后的数据

Generator

generator(生成器)是ES6标准引入的新的数据类型。看起来像一个可以返回多次的函数。

function * foo () {
    // 第一次带哦用next才执行
    console.log('start')

    const res = yield 'foo'

    console.log(res)
}

// 并没有立即执行foo而是得到一个生成器对象
const generator = foo()

// 在只想到yield停止并返回
const result = generator.next()
console.log(result) // { value: 'foo', done: false }

// 接着yield之后执行并将传入参数给res
const result2 = generator.next('bar')

使用generator处理异步任务

const fetch = require('node-fetch')

function * foo () {
    const url = yield fetch('https://www.baidu.com/')
    yield fetch(url)
}

const g = foo()

const result = g.next()
result.value
    .then(res => res.text())
    .then(res => console.log(res))

const result2 = g.next('https://www.baidu.com/')
result2.value
    .then(res => res.text())
    .then(res => console.log(res))

Async和Await

现在最流行的写法

const fetch = require('node-fetch')

// 用async修饰的函数会变成promise对象
;(async function() {
    // await必须用在async函数里面
    const res = await fetch('https://www.baidu.com/').then(res=>res.text())
    console.log(res)
})()

Promise源码

自己实现了一个简单的promise

const STATUS = {
    PENDING: 'PENDING',
    REJECTED: 'REJECTED',
    FULFILLED: 'FULFILLED'
}

class MyPromise {
    status = STATUS.PENDING
    reason = undefined
    value = undefined
    rejectedQuene = []
    fullfilledQuene = []

    // new Promise传入的函数进来后会直接进行
    constructor(callback) {
        if (callback instanceof Function) {
           try {
               callback && callback(this.resolve.bind(this), this.reject.bind(this))
           } catch(err) {
               this.reject(err)
           }
        }
    }

    // 用于处理链式调用中then的返回值
    // 如果then返回是普通值或者是prmise值的操作
    // 这里不应该以static暴露到外面,这里这样写只是为了方便
    static handlePromise(p1, value, resolve, reject) {
        // then返回的promise和当前返回的promise一样会照成循环引用
        if (p1 === value) {
            throw 'can not do this: return promise';
        }

        if (value instanceof MyPromise) {
            value.then(v => resolve(v), r => reject(r))
        } else {
            resolve(value)
        }
    }

    resolve(value) {
        if (this.status === STATUS.PENDING) {
            this.status = STATUS.FULFILLED
            this.value = value

            // 如果对应的事件队列有操作则取出消费
            while(this.fullfilledQuene.length !== 0) {
                this.fullfilledQuene.shift()(this.value)
            }
        }
    }

    reject(reason) {
        // status值只有在Pending的时候才会进行转换
        if (this.status === STATUS.PENDING) {
            this.status = STATUS.REJECTED
            this.reason = reason
            // 如果对应的事件队列有操作则取出消费
            while(this.rejectedQuene.length !== 0) {
                this.rejectedQuene.shift()(this.reason)
            }
        }
    }

    then(successCallback, rejectCallback) {
        const promise = new MyPromise((resolve, reject) => {
            if (this.status === STATUS.FULFILLED) {
                setTimeout(() => {
                    try {
                        const value = successCallback(this.value)
                        MyPromise.handlePromise(promise, value, resolve, reject)
                    } catch(err) {
                        reject(err)
                    }
                }, 0)
            } else if (this.status === STATUS.REJECTED) {
                setTimeout(() => {
                    try {
                        const value = rejectCallback(this.reason)
                        MyPromise.handlePromise(promise, value, resolve, reject)
                    } catch(err) {
                        reject(err)
                    }
                }, 0)
            } else {
                // 如果是异步操作就传入对应的事件队列
                this.fullfilledQuene.push(() => {
                    setTimeout(() => {
                        try {
                            const value = successCallback(this.value)
                            MyPromise.handlePromise(promise, value, resolve, reject)
                        } catch(err) {
                            reject(err)
                        }
                    }, 0)
                })
                this.rejectedQuene.push(() => {
                    setTimeout(() => {
                        try {
                            const value = rejectCallback(this.reason)
                            MyPromise.handlePromise(promise, value, resolve, reject)
                        } catch(err) {
                            reject(err)
                        }
                    }, 0)
                })
            }
        })

        // 返回promise才可以进行链式调用
        return promise
    }
}

今日总结

  1. 事件循环和消息队列的概念,更多浏览器内部执行逻辑可以参阅书籍:webkit技术内幕
  2. 基于异步而产生异步代码的解决方案
  3. promise源码