JavaScript 异步编程

Stella981
• 阅读 653

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

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

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

同步模式与异步模式

同步模式

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

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

JavaScript 异步编程

1.gif

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

异步模式

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

JavaScript 异步编程

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

JavaScript 异步编程

Untitled 1.png

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

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

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

JavaScript 异步编程

Untitled 2.png

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

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]方法的实现如下代码:

`

              Document `

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

`

              Document `

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->rejected3. resolve reject 函数用来更改状态    resolve:fulfilled    reject:rejected4. then方法内部做的事情就是判断状态 如果状态成功调用成功回调函数如果状态失败就回调失败的回调函数5. then成功或失败都有一个参数分别表示成功的值和失败的原因6. 记录成功的值和失败的值7. 处理执行器内部异步情况的处理 调用resolve或reject8. 处理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;
`

本文分享自微信公众号 - FrontMagic(JakePrim)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Easter79 Easter79
3年前
swap空间的增减方法
(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
JavaScript回调函数的高手指南
摘要:本文将会解释回调函数的概念,同时帮你区分两种回调:同步和异步。回调函数是每个前端程序员都应该知道的概念之一。回调可用于数组、计时器函数、promise、事件处理中。本文将会解释回调函数的概念,同时帮你区分两种回调:同步和异步。1.回调函数首先写一个向人打招呼的函数。只需要创建一个接受name参数的函数gree
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这