JavaScript 异步编程

544 阅读12分钟

掌握JavaScript主流的异步任务处理 ( 本篇文章内容输出来源:《拉钩教育大前端训练营》参阅《你不知道的JavaScript中卷》异步章节)

JavaScrip 采用单线程模式工作的原因,需要进行DOM操作,如果多个线程同时修改DOM浏览器无法知道以哪个线程为主。

JavaScirpt分为:同步模式、异步模式

同步模式与异步模式

同步模式

同步模式其实就是:排队执行,下面根据一个Gif动画来演示同步模式,非常简单理解,js维护了一个正在执行的工作表,当工作表的任务被清空后就结束了。

如下打开调试模式,注意观察Call Stack调用栈的情况,当执行foo方法的是否foo会进入Call Stack调用栈之后打印'foo task',然后执行bar()方法bar进入调用栈打印'bar task',bar执行完后被移除调用栈,foo被移除调用栈然后打印'global end'执行结束。

1.gif

存在的问题:如果其中的某一个任务执行的时间过长,后面的任务就会被阻塞,界面就会被卡顿,所以就需要使用异步模式去执行避免界面被卡死。

异步模式

通过一个图来演示异步任务,用到事件循环与消息队列机制实现

Untitled 0.png

Promise异步方案

常见的异步方案就是通过回调函数来实现,导致回调地狱的问题,CommonJS社区提出了Promise方案并在ES6中采用了。如下代码实现一个环绕动画如果通过回调会嵌套多次。

案例演示地址

let box = document.querySelector('#box');
    move(box, 'left', 300, () => {
        move(box, 'top', 300, () => {
            move(box, 'left', 0, () => {
                move(box, 'top', 0, () => {
                    console.log('运动完成');
                });
            });
        });
    });

Promise 的使用案例演示代码如下:

//应用案例
function ajax(url) {
    return new Promise(function (resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        xhr.responseType = 'json';
        xhr.onload = function () {
            if (this.status === 200) {
                resolve(this.response);
            } else {
                reject(new Error(this.statusText));
            }
        }
        xhr.send();
    });
}

let promise2 = ajax('./api/user.json');
let newPromise = promise2.then((res) => {
    console.log(res);
});
console.log(promise2 === newPromise);//false 每一个then都返回一个新的promise对象
//then 仍然会导致回调地狱 尽量保证异步任务的扁平化

//也可以在then方法中返回一个promise对象
ajax('./api/user.json').then(res=>{
    console.log(111);
    return ajax('./api/user.json');
}).then(res=>{
    console.log(222);
    return 'foo';
}).then(res=>{
    console.log(res);
})

//OUT:
false
Array(2)
111
222
foo

Promise 链式调用注意一下几点

  • Promise对象的then方法会返回一个全新的Promise对象
  • 后面的then方法就是在为上一个then返回的Promise注册回调
  • 前面then方法中回调函数的返回值会作为后面then方法回调的参数
  • 如果回调中返回的是Promise,那后面then方法的回调会等待它的结束

Promise异常处理

Promise 执行过程中出现错误onRejected回调会执行,一般通过catch方法注册失败回调,跟在then方法第二个参数注册回调结果是一样的。

const promise = new Promise(function (resolve, reject) {
    //只能调用两者中的一个 一旦设置了某个状态就不允许修改了
    // resolve(100);//成功
    reject(new Error('promise rejected'));//失败
});

promise.then(function (value) {
    console.log('resolved', value);
}, function (err) {
    console.log('rejected', err);
}).catch(err=>{
    console.log("catch",err);
});

console.log('end');

推荐使用catch方法作为错误的回调,不推荐使用then方法的第二个参数作为错误回调,原因如下:

当我们在收到正确的回调又返回一个Promise对象但是在执行过程中出现了错误,而这时无法收到错误回调的。

ajax('./api/user.json').then(res=>{
    console.log('onresolved',res);
    return ajax('/error.json');
},err=>{
    console.log("onRejected",err);
});

我们再来看catch方法:

ajax('./api/user.json').then(res=>{
    console.log('onresolved',res);
    return ajax('/error.json');
}).catch(err=>{
    console.log("onRejected",err);
});

打印结果如下:catch方法可以捕捉到then方法return的新的Promise对象的执行错误。

onresolved (2) [{…}, {…}]
onRejected Error: Not Found
    at XMLHttpRequest.xhr.onload (promise.js:28)

除此之外全局对象注册unhandlerdrejection 事件,处理代码中没有被手动捕获处理的异常。下面是node中的方法

process.on('unhandledRejection',(reason,promise)=>{
    //reason => Promise 失败原因,一般是一个错误对象
		//promise => 出现异常的Promise对象
})

一般不推荐使用,应该在代码中明确捕获每一个可能的异常,而不是丢给全局处理

Promise 的静态方法

//一个成功状态的Promise 对象
Promise.resolve('foo').then(res=>{
    console.log(res);
});
var promise = ajax('./api/user.json');

var promise2 = Promise.resolve(promise);//如果传入一个Prmose对象会原样返回相同的Promise对象

console.log(promise === promise2);//true

//如下传入的一个对象带有then方法的对象一样可以执行
Promise.resolve({
    then:function(onFulfilled,onRejected){
        onFulfilled('f00');
    }
}).then(res=>{
    console.log(res);//f00
});

//创建一个失败状态的Promise对象
Promise.reject(new Error('rejected')).catch(err=>{
    console.log(err);
})

Promise并行执行:all race 将多个Promise对象组合到一起

var promise = Promise.all([ajax('./api/user.json'),ajax('./api/user.json')]);

promise.then(res=>{
    console.log(res);
})
//都成功才会成功 有一个失败就会返回失败状态回调
ajax('./api/user.json')
.then(res=>{
    const urls = Object.values(res);
    console.log('??',urls);
    const tasks = urls.map(url=>{
        console.log(url);
        return ajax(url);
    });
    console.log(tasks);
    return Promise.all(tasks);
}).then(res=>{
    console.log(res);
});

//race 只会等待第一个结束的任务
const request = ajax('./api/user.json');

const timeout = new Promise((resolve,reject)=>{
    setTimeout(() => {
        reject(new Error('timeout'));
    }, 500);
});

Promise.race([request,timeout]).then(res=>{
    console.log(res);
}).catch(err=>{
    console.log(err);    
});

模仿网络慢的情况,可以看到race会执行reject

Untitled 1.png

Promise 执行时序:宏任务与微任务

Promise的回调会作为微任务执行。微任务:提高整体的响应能力。目前大部分异步回调作为宏任务

常见的宏任务与微任务如下图所示:

Untitled 2.png

下面是JavaScript执行异步任务的执行时序图:

Untitled 3.png

看下面的例子来进行理解: 下列例子中输出: 2 4 1 3 5

这其实也符合了上图事件循环的原理,先主任务执行输出: 2 4 之后查询是否有微观任务没有就新建宏观任务执行

然后宏观任务执行输出:1 3

之后查询是否之后查询是否有微观任务没有就新建宏观任务执行

执行输出: 5

let time = 0;
    setTimeout(()=>{
        time = 1;
        console.log(time);
        //宏任务嵌套宏任务
        setTimeout(()=>{
            time = 5;
            console.log(time);
        },1000);
    },1000);
    time = 2;
    console.log(time);
    setTimeout(()=>{
        time=3;
        console.log(time);
    },1000);
    time = 4;
    console.log(time);

下面我们在看一个带有微任务的例子: 下面例子输出的结果: 2 4 6 1 3 5.主任务执行完毕之后先执行微任务.

let time = 0;
    setTimeout(()=>{
        time = 1;
        console.log(time);
        //宏任务嵌套宏任务
        setTimeout(()=>{
            time = 5;
            console.log(time);
        },1000);
    },1000);
    time = 2;
    console.log(time);
    setTimeout(()=>{
        time=3;
        console.log(time);
    },1000);
    time = 4;
    console.log(time);
    //微任务
    let observer = new MutationObserver(()=>{
        time = 6;
        console.log(6);
    });
    observer.observe(document.body,{
        attributes:true
    });
    document.body.setAttribute('kkb',Math.random());

Generator异步方案

首先需要连接一下迭代器的

迭代器

for...in : 以原始插入的顺序迭代对象的可枚举属性for...of : 根据迭代对象的迭代器具体实现迭代对象数据 可迭代对象 - 实现了[Symbol.iterator]方法数组结构有[Symbol.iterator]方法,但是如果要迭代Object就需要添加[Symbol.iterator]方法的实现如下代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
</body>
<script>
    //for of/ for in 迭代器
    //for...in : 以原始插入的顺序迭代对象的可枚举属性
    //for...of : 根据迭代对象的迭代器具体实现迭代对象数据 可迭代对象 - 实现了[Symbol.iterator]方法
    let arr = ['a','b','c','d'];
    let obj = {
        a:1,
        b:2,
        c:3
    }
    for(let attr in arr){
        console.log(attr);//0 1 2 3
    }
    for(let val of arr){
        console.log(val);//a b c d
    }
    console.dir(arr);//symbol(Symbol.iterator): ƒ values()
    console.dir(obj);//没有Symbol.iterator方法
    
    //如果要对象使用for of需要加一个属性 自定义迭代器
    obj[Symbol.iterator] = function(){
        //迭代协议
        //将对象value转换为数组
        let values = Object.values(obj);
        //将对象key转为数组
        let keys = Object.keys(obj);
        // let values = [...obj];
        console.log(values);
        let index = 0;
        //必须返回一个对象 同时必须有一个next方法
        return {
            next(){
                //done:表示循环是否完成
                //value:for a of obj a就是value
                //必须返回一个对象
                if (index >= values.length) {
                    return{
                        done:true
                    }
                }else{
                    return{
                        done:false,
                        value:{
                            key:keys[index],
                            value:obj[keys[index++]]
                        }
                    }
                }
            }
        }
    }
    //可以测试直接迭代方法
    let objIterator = obj[Symbol.iterator]();
    //执行next方法
    console.log(objIterator.next());
    
    //其实for...of 一直调用objIterator.next() 直到done:true就会停止
    for(let o of obj){
        //obj is not iterable
        console.log(o);
    }
</script>
</html>

Generator函数比普通函数多了一个*号,函数内部使用yield语句,定义遍历器的每个成员,即不同的内部状态. 实现可迭代的函数.Generator函数一般很少会使用了解即可.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
</body>
<script>
    /**
     * 定义可迭代函数 yield 每次迭代的返回值 
     */
    function* fn() {
        yield new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log("a");
                resolve("1");
            }, 500);
        });
        yield new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log("b");
                resolve("2");
            }, 500);
        });
        yield new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log("c");
                resolve("3");
            }, 500);
        });
    }
    let f = fn();
    // console.log(f.next());
    // for (const iterator of f) {
    //     console.log(iterator);
    // }
    function co(fn) {
        let f = fn();
        next();
        function next(data){
            let result = f.next();
            console.log(result);
            if(!result.done){
                //上一个异步执行完毕
                result.value.then((info)=>{
                    console.log(info,data);
                    next(info);
                });
            }
        }
    }
    co(fn);
    // for (let fn of f) { 一同执行 不能异步调用
    // }
</script>
</html>

Generator 生成器函数的使用

//Generator 生成器函数
function* foo() {
    try {
        console.log('start');
        const res = yield 'foo';
        console.log(res);
    } catch (e) {
        console.log(e);
    }

}

const generator = foo();

const result = generator.next();

console.log(result);

generator.next('bar');

generator.throw(new Error('Generator Error'));//抛出一个异常

Generator 异步使用的案例如下代码:

function* main() {
    try{
        const users = yield ajax('./api/user.json');
        console.log(users);
        const posts = yield ajax('./api/user.json');
        console.log(posts);
    }catch(e){
        console.log(e);
    }
}
//通用的异步生成器方法
function co(generator){
    const g = generator();
    function handleResult(result){
        if(result.done) return;
        result.value.then(data=>{
            handleResult(g.next(data));
        }).catch(err=>{
            g.throw(err);
        })
    }
    handleResult(g.next());
}

co(main);

Async/Await 语法糖

推荐使用异步编程的标准.需要注意await 后面必须是一个Promise对象,await只能出现在async函数内部目前还不支持(以后可能会支持)

async function  main2() {
    try{
        const users = await ajax('./api/user.json');
        console.log(users);
        const posts = await ajax('./api/user.json');
        console.log(posts);
    }catch(e){
        console.log(e);
    }
}
main2();

Promise 源码手写实现

1. Promise 是一个类 在执行这个类的时候 需要传递一个执行器进去 这个执行器会立即执行
2. Promise 中有三种状态分别为:pending -> fulfilled pending->rejected
3. resolve reject 函数用来更改状态
    resolve:fulfilled
    reject:rejected
4. then方法内部做的事情就是判断状态 如果状态成功调用成功回调函数
如果状态失败就回调失败的回调函数
5. then成功或失败都有一个参数分别表示成功的值和失败的原因
6. 记录成功的值和失败的值
7. 处理执行器内部异步情况的处理 调用resolve或reject
8. 处理then方法可以被多次调用
9. then方法可以被链式调用 后面then方法回调函数拿到的值是上一个then方法
回调函数的返回值
10. then 返回值是普通值还是Promise对象
11. then 返回相同的Promise对象循环调用的判断
12. 执行器内部发生错误 回调给reject,then 内部发生错误的处理
13. then无参数的链式调用实现
14. all等静态方法实现

const PENDING = 'pending';//等待
const FULFILLED = 'fulfilled';//成功
const REJECTED = 'rejected';//失败

class MyPromise {
    constructor(executor) {
        try {
            executor(this.resolve, this.reject);//执行器立即执行
        } catch (e) {
            this.reject(e);
        }
    }
    status = PENDING;//定义状态
    //成功之后的值
    value = undefined;
    //失败之后的值
    error = undefined;

    //成功回调
    onFulfilled = [];
    //失败回调
    onRejected = [];
    //箭头函数 this指向不会被更改 this还会指向MyPromise对象
    resolve = (value) => {
        //0 判断状态是不是pending 阻止向下执行
        if (this.status !== PENDING) {
            return;
        }

        //1 状态更改
        this.status = FULFILLED;
        //2 保存成功之后的值
        this.value = value;
        //3 成功回调是否存在
        // this.onFulfilled && this.onFulfilled(this.value);
        while (this.onFulfilled.length) {
            this.onFulfilled.shift()();
        }
    }
    reject = (error) => {
        //0 判断状态是不是pending 阻止向下执行
        if (this.status !== PENDING) {
            return;
        }
        //1 状态更改
        this.status = REJECTED;
        //2 保存失败之后的值
        this.error = error;
        //3 失败回调是否存在
        // this.onRejected && this.onRejected(this.error);
        while (this.onRejected.length) {
            this.onRejected.shift()();
        }
    }
    then(onFulfilled, onRejected) {
        onFulfilled = onFulfilled ? onFulfilled : value => value;
        onRejected = onRejected ? onRejected : error => { throw error };
        //1. 实现链式调用
        let p = new MyPromise((resolve, reject) => {
            if (this.status === FULFILLED) {
                setTimeout(() => {
                    try {
                        //拿到回调函数的返回值
                        let result = onFulfilled(this.value);
                        //传递给下一个Promise对象
                        //判断result是普通值还是Promise对象
                        //如果是普通值 直接调用resolve
                        //如果是promise对象 查看promise对象返回的结果
                        //再根据promise对象返回的结果 决定调用resolve还是reject
                        // resolve(result);
                        //需要等待同步代码执行完毕拿到p在执行
                        this.resolvePromise(p, result, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            } else if (this.status == REJECTED) {
                setTimeout(() => {
                    try {
                        //拿到回调函数的返回值
                        let result = onRejected(this.error);
                        this.resolvePromise(p, result, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            } else {
                //由于异步代码没有立即执行 先存储回调 等异步代码执行完成后再执行回调
                this.onFulfilled.push(() => {
                    setTimeout(() => {
                        try {
                            //拿到回调函数的返回值
                            let result = onFulfilled(this.value);
                            this.resolvePromise(p, result, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    }, 0);
                });
                this.onRejected.push(() => {
                    setTimeout(() => {
                        try {
                            //拿到回调函数的返回值
                            let result = onRejected(this.error);
                            this.resolvePromise(p, result, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    }, 0);
                });
            }
        });
        return p;
    }
    resolvePromise(p, result, resolve, reject) {
        if (p === result) {
            return reject(new TypeError('TypeError: Chaining cycle detected for'));
        }
        if (result instanceof MyPromise) {
            //Promise对象 把新的Promise对象的值传递下去
            result.then(resolve, reject);
        } else {
            //普通值
            resolve(result);
        }
    }
    static all(array) {
        let result = [];
        let index = 0;
        return new MyPromise((resolve, reject) => {
            function addData(key, value) {
                result[key] = value;
                index++;
                if (index === array.length) {
                    //需要等待异步操作完成再调用resolve
                    resolve(result);
                }
            }
            for (let i = 0; i < array.length; i++) {
                let cur = array[i];
                if (cur instanceof MyPromise) {
                    cur.then((value) => {
                        addData(i, value);
                    }, (err) => {
                        reject(err);
                    });
                } else {
                    addData(i, cur);
                }
            }
        });
    }
    static resolve(value) {
        if (value instanceof MyPromise) {
            return value;
        }
        return new MyPromise((resolve, reject) => {
            resolve(value);
        });
    }
    static reject(error){
        return new MyPromise((resolve,reject)=>{
            reject(error);
        })
    }
    finally(callback){
        return this.then(res=>{
            return MyPromise.resolve(callback()).then(()=>res);
        },err=>{
            return MyPromise.resolve(callback()).then(()=>{throw err});
        });
    }
    catch(error){
        return this.then(undefined,error);
    }
}

//
module.exports = MyPromise;