javascript异步及解决方案

203 阅读6分钟

单线程的 javascript

javascript是一门单线程的编程语言,它同一时间只能处理一件事情,这样的概念应该在每一个使用或了解javascript的程序员的脑海中应该都根深蒂固,也是js最基础的知识之一。
在业务越来越复杂,网络连接和页面交互操作越来越密切,为了将单线程的js实现为异步,各种方法层出不迭。但我脑海中的第一个问题是:为什么javascript是单线程的?

答:javascript最初被设计为运行在浏览器中的脚本语言,主要负责页面的各种操作,尤其是dom的操作。试想一下,如果同时有一个操作添加了dom,但同时又有一个操作删除了dom,是不是就会带来紊乱?

单线程模式的一个缺点是,如果有三个任务ABC,A一直没有完成,堵塞了线程,BC任务就陷入了永远的等待...所以为了解决这个问题,js虽然是单线程的,但它也有异步,js的执行机制是event-loop(事件轮循)

event-loop机制

同步:执行顺序和任务的排列顺序一致
异步:不确定是不是立刻执行,可能要满足一定条件后才会执行

js的执行顺序:

  1. 在执行一段代码时,首先判断它是同步还是异步,是可以立即执行,还是需要满足一定条件才能执行?如果是同步代码,则放入执行栈异步代码则开始执行后,不等待结果,先挂起事件。
  2. 当异步执行操作返回结果后,这个异步操作的结果会被加入消息队列(callback queue),等当前执行栈中的同步代码运行完后,按照顺序从callback queue中取任务(异步的回调)来运行。
  3. 以上的过程可以进行无限的循环,这个过程就是event-loop事件轮询机制

回调函数

function say(value) {
    console.log('say:', value);
}

function exec(func, param) {
    func(param);
}

exec(say, 'today is Friday');

将一个函数A作为参数传递给另一个函数B,并在函数B内部调用函数A,就称为回调函数。这也是起初能解决js异步的方法之一,但随着要等待的结果越来越多,要处理的状态越来越复杂,回调可能会出现:A1->A2->A3->A4这样子需要按照顺序执行的情况,每一个回调函数都作为参数传给上一个,造成了回调地狱的问题。

micro-task && macro-task

怎么会区分宏任务和微任务? 宏任务是由宿主(node、浏览器)发起的,而微任务是由JS引擎自行发起的 个人没有怎么使用过I/O和process()等,所以主要讨论常用的。

  1. 宏任务:setTimeout,setInterval
  2. 微任务:Promise

执行顺序:主线程任务->遇到异步挂起->异步进入任务队列(区分宏任务和微任务)->微任务执行直到为空->执行宏任务......
现实类比:在银行办事时,宏任务是每个要办事的人,微任务是每个人到了柜台后要处理完的所有个人事宜。

Promise

通过手写promise的实现,来了解Promise:

/**
 * promise接受一个函数作为参数,函数有reject和resolve两个参数
 * resolve负责将执行结果作为参数传出,且将promise的状态变更为fulfilled
 * reject负责将如果执行出现了错误,就将错误传出,且将状态变更为rejected
 */
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

/** 
 * x是then的返回结果
 * newPromise是默认返回的promise
 * resolve是newPromise的resolve,也就是默认promise的resolve,需要根据x决定是什么内容
 * reject是newPromise的reject,也就是默认promise的reject,需要根据x决定是什么内容
 * 1. x不能和newPromise相等,因为newPromise等待x,如果x === newPromise,会循环
 * 2. x不能为null
 * 3. x如果是普通类型的值,直接将这个值透传,作为newPromise的resolve要传递出去的参数
 * 4. x如果是函数或者对象,看能不能let then = x.then;
 * 4.1 then报错,则走reject()
 * 4.2 可以成功的then(),则视x为thenable,执行then(),第一个参数是this,然后是成功和失败的回调
 * 4.3 如果成功的回调还是promise,继续递归
 * 5. 不管成功还是失败,都只调用一次
 */
function resolvePromise (x, newPromise, resolve, reject) {
    if (x === newPromise) {
        // 使用reject抛出错误
        return reject(new TypeError('endless loop with same x and newPromise'));
    }
    // 因为上方return了,不用else,声明called防止重复调用resolve或reject
    let called;
    if (x !== null && (typeof x === 'function' || typeof x === 'object')) {
        // 看看thenable,取then的时候注意防止报错
        try {
            let then = x.then;
            if (typeof then === 'function') {
                then.call(x, (newValue) => {
                    // resolve的结果如果还是promise,继续解析
                    if (called) return;
                    called = true;
                    resolvePromise(newValue, newPromise, resolve, reject);
                }, (newReason) => {
                    // reject
                    if (called) return;
                    called = true;
                    reject(newReason);
                })
            } else {
                // 不是thenable,当成基本类型透传
                resolve(x);
            }
        } catch(e) {
            if (called) return;
            called = true;
            reject(e);
        }
    } else {
        // 直接透传
        resolve(x);
    }
}

class MyPromise {
    /** 
     * Promise 1: 执行传入的函数,使用resolve和reject作为参数,改变promise的状态
     */
    constructor(executor) {
        this.state = PENDING;
        this.value = undefined;
        this.reason = undefined;
        this.onFulFilledCallback = [];
        this.onRejectedCallback = [];
        /**
         * executor需要两个参数: resolve和reject
         */
        let resolve = (value) => {
            this.value = value;
            this.state = FULFILLED;
            // 开始将onFulFilledCallback里的方法都取出来执行
            this.onFulFilledCallback.forEach(fn => fn());
        }
        let reject = (reason) => {
            this.reason = reason;
            this.state = REJECTED;
            this.onRejectedCallback.forEach(fn => fn());
        }
        try {
            executor(resolve, reject);
        } catch (err) {
            reject(err);
        }
    }

    /**
     * Promise 2: 
     * promise有一个叫then的方法,传入onFulFilled和onRejected两个参数
     * 成功则执行onFulFilled,传入value
     * 报错则执行onRejected,传入reason
     */
    then(_onFulFilled, _onRejected) {
        /**
         * Promise 3:
         * 为了实现链式调用,不再出现第二个then的目标对象为undefined的情况,then要返回一个newPromise
         * 但then自己会返回一个x(onFulFilled或onRejected返回的值),x可能是任何值
         * 判断x和newPromise的方法是resolvePromise,resolvePromise有自己的规则
         */
        let onFulFilled = typeof _onFulFilled === 'function' ? _onFulFilled : value => value;
        let onRejected = typeof _onRejected === 'function' ? _onRejected : err => { throw err };
        let newPromise = new MyPromise((resolve, reject) => {
            // 将重复的方法抽出来
            let autoResolve = () => {
                let x = onFulFilled(this.value);
                resolvePromise(x, newPromise, resolve, reject);
            };
            let autoReject = () => {
                let x = onRejected(this.reason);
                resolvePromise(x, newPromise, resolve, reject);
            };
            /**
             * Promise 4: 
             * onFulfilled如果不是函数,直接value => value
             * onRejected如果不是函数,直接reason 
             * onFulfilled和onRejected必须被异步调用,因为需要保持代码的一致性:如果它们不是异步的,在某些场合下同步,某些场合下异步,无法保证执行顺序,如果是异步的,根据event-loop,它们必将在同步代码都执行后执行
             */
            if (this.state === FULFILLED) {
                // x是onFulFilled的结果
                // let x = onFulFilled(this.value);
                // resolvePromise(x, newPromise, resolve, reject);
                setTimeout(() => {
                    try {
                        console.log('autoResolve');
                        autoResolve();
                    } catch (err) {
                        reject(err);
                    }
                }, 0);
            }
            if (this.state === REJECTED) {
                console.log(this.state);
                // x是onRejected的结果
                // let x = onRejected(this.reason);
                // resolvePromise(x, newPromise, resolve, reject);
                setTimeout(() => {
                    try {
                        autoReject();
                    } catch (err) {
                        reject(err);
                    }
                }, 0);
            }
            if (this.state === PENDING) {
                /**
                 * 将onFulFilled和onRejected放入callback中
                 * 为了链式调用,push进去的待执行的,也需要有x和resolvePromise的处理
                 */
                this.onFulFilledCallback.push(() => {
                    setTimeout(() => {
                        try {
                            autoResolve();
                        } catch (err) {
                            reject(err);
                        }
                    }, 0);
                });
                this.onRejectedCallback.push(() => {
                    setTimeout(() => {
                        try {
                            autoReject();
                        } catch (err) {
                            reject(err);
                        }
                    }, 0);
                });
            }
        });
        return newPromise;
    }
}


// promises-aplus-tests的测试脚本
MyPromise.deferred  = function() {
    const defer = {}
    defer.promise = new MyPromise((resolve, reject) => {
        defer.resolve = resolve;
        defer.reject = reject;
    })
    return defer;
}

try {
    module.exports = MyPromise;
} catch (e) {}

通过测试npx promises-aplus-tests promise.js的872个测试用例

image.png

还有常用的Promise.all(),Promise.race()两种方法,一个是等待Promise内所有任务的完成,一个是竞争完成。

async、await、yield

根据es6的定义,async和await其实是Generator函数的语法糖,假如我们想用async和await实现下面的Generator函数:

思考

  1. 有一个页面,从上到下分为ABC三个区域,都需要通过接口请求返回数据显示。三个请求同时发出,需要达成如下场景:A接口成功,显示A;B接口成功,显示B;C接口成功,显示C。在A显示前,BC不显示;在AB显示前,C不显示。