Javascript异步编程

Javascript异步编程

持续更新,文章内容较长,建议收藏食用,如果觉得内容可以,求点赞哦。本文章总结自拉钩教育-大前端高薪训练营(PS:班班让我加的~)



概述

众所周知,目前主流的javascript环境都是以单线程模式执行javascript代码,而采用单线程模式的原因,跟它的设计初衷有关,javascript最早是运行在浏览器端的脚本语言,主要用来实现页面上的动态交互,而页面上的交互主要就是实现dom操作,而这就决定了javascript必须采用单线程模式,否则就会出现复杂的线程同步问题,试想一下,如果采用多线程模式,一个线程对dom进行了取值操作,另一个线程对这个dom进行了删除操作,这个在实际场景中就会出问题,因为浏览器不知道应该以哪个线程的结果为准,而为了避免这种问题,所以javascript最开始就设计成了单线程模式,来解决这种问题。单线程模式的优点就是代码从上到下依次执行,安全简单,缺点同样也很明显,就是如果有一个任务非常耗时,那么后续任务必须等待这个任务结束才能继续执行。而为了解决耗时任务导致的问题,javascript将任务的执行模式分成了同步模式和异步模式。


同步模式

同步模式,就是后一个任务必须等待前一个任务执行完才能开始执行,不论前一个任务耗时多久,也就是说,同步任务的执行顺序,跟我们代码编写的顺序是完全一致的。

console.log('start'); // 压入到调用栈,执行完弹出调用栈

function foo() { // 函数声明不会产生调用
    console.log('foo')
}

function bar() { // 函数声明不会产生调用
    foo();
    console.log('bar')
}

bar(); /* bar函数执行,压人调用栈,而bar函数里面又调用了foo函数,
        所以继续把foo函数压入调用栈,foo函数执行完输出foo
        ,然后弹出调用栈,接下来,输出bar,bar函数执行完,然后bar函数弹出调用栈 
        */

console.log('end'); //压入调用栈,然后弹出

输出顺序跟我们书写代码执行顺序完全一致
在这里插入图片描述


异步模式

掌握异步编程,需要先掌握几个概念:消息队列、Event loop、宏任务、微任务。

异步模式概念

不会等待当前任务完成,直接执行下一个任务,当前任务的后续操作会以回调函数的方式定义,当前任务完成之后,自动执行回调函数。优点就是不会阻塞进程,也就是说不会让页面进入假死状态。缺点就是代码执行顺序混乱,需要多加练习才能掌握。

  • 消息队列:暂时存储异步操作的地方。
  • Eventloop:监听调用栈和消息队列,当调用栈清空之后,会将消息队列中的第一个任务推到调用栈中执行。
  • 宏任务:每次调用栈中的中的执行代码就是宏任务。例如setTimeout
  • 微任务:当前任务执行结束之后立即执行的任务就是微任务。例如promise
console.log('start'); 

setTimeout(function timer1() { 
    console.log('time1') 
}, 1800);

setTimeout(function time2() { 
    console.log('timer2') 
    setTimeout(function time3() {
        console.log('timer3');
    }, 1000)
}, 1000)

console.log('end');

输出结果
在这里插入图片描述
可能比较难理解,可以看下面这张图来帮助理解。

在这里插入图片描述

回调函数

回调函数是所有异步编程方案的根本。回调函数可以理解为,你想要做的一件事情,你知道这件事情该怎么做,并且,你也知道这个事情应该在哪件事情之后做,但是你不知道的是,上一件事情需要花费多久才能完成,偏偏你又想继续做其他事情,不想傻等着上一件事情结束。所以最好的办法是,你把这件事情的执行步骤记录下来,交给任务的执行者,而任务的执行者是知道上一件事情什么时候完成,等上一件事情完成之后,任务的执行者就会将你想做的这件事情完成。

  • 优点:非阻塞
  • 缺点:回调地狱

Promise

为了避免回调地狱,CommonJs提出了Promise规范,后来在ES2015中被标准化,成为ECMAScript语言规范。Promise有3中状态,Pending(等待)、Fulfilled(成功)、Rejected(失败)。初始状态为Pending,可以变成成功状态Fulfilled,执行一个成功回调onFufilled;也可以变为失败状态Rejected,然后执行一个失败回调onRejected。需要注意的是,Promise的状态一旦变化,不可二次更改。promise的本质就是定义异步任务结束之后需要执行的任务。
在这里插入图片描述

简单示例

const promise = new Promise((resolve, reject) => {
    
    resolve('100'); // 成功回调

    // reject(new Error('promise rejected')); // 失败回调

});
promise.then((value) => {
    console.log(value); => 100
}, (error) => {
    console.log(error);
})
const promise = new Promise((resolve, reject) => {
    
    // resolve('100'); // 成功回调

    reject(new Error('promise rejected')); // 失败回调

});
promise.then((value) => {
    console.log(value);
}, (error) => {
    console.log(error); // => promise rejected
})
const promise = new Promise((resolve, reject) => {
    
    resolve('100'); // 成功回调

    reject(new Error('promise rejected')); // 失败回调

});
promise.then((value) => {
    console.log(value); // => 100
}, (error) => {
    console.log(error);
})

通过new关键字来创建Promise实例,promise.then接收2个函数参数,第一个函数执行成功的回调,第二个参数执行失败的回调。从上面例子也可以看出,当执行resolve的时候,then会执行第一个成功回调;当执行reject的时候,then会执行第二个失败回调;并且当resolve和reject同时执行的时候,then只会执行第一个成功,并没有执行第二个失败的回调,这也可以总结出,Promise的状态一旦变化,不可二次更改。

Promise封装ajax

const ajax = url => {
    return new Promise((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();
    })
}
ajax('/foo.json').then((value) => {
    console.log(value);
}, (reason) => {
    console.log(reason);
});

Promise链式调用

promise的then可以链式调用,then返回的是一个全新的promise对象,每个then方法都是为上一个then方法返回的promise对象状态确定之后,指定回调函数,这些promise会依次执行,所以这些回调函数也会依次执行,我们也可以在回调函数中手动返回一个promise对象。

const ajax = url => {
    return new Promise((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();
    })
}
const promise1 = ajax('/foo.json')
console.log(promise) // => 返回一个promise对象
const promise2 = promise1.then(function() {

}, function() {

})
console.log(promise2) // => 返回一个promise
console.log(promise1 === promise2); // => 返回false,说明then方法返回的是一个全新的promise对象
ajax('/foo.json').then(res => {
    console.log(11111)
    return ajax('/user.json')
})
.then(res => {
    console.log(22222)
    console.log(res)
})
.then(res => {
    console.log(33333)
})

在这里插入图片描述

相比传统回调函数的写法,promise链式调用在写法和可读性上面有很大的提升。

Promise并行执行

Promise里面提供了一个all和race方法,可以让我们并行执行接口请求。不同的是,all方法是等所有的请求都成功完成之后才执行then方法,并且一旦有一个请求失败,整个Promise执行就是失败的。而race方法是,只要有一个请求完成,就会直接调用回调函数。

Promise执行时序/宏任务VS微任务

console.log('start');
setTimeout(function() {
    console.log(2222)
}, 0)
Promise.resolve().then(res => {
    console.log(11111)
})
console.log('end')

在这里插入图片描述
从输入结果可以看出,promise的回调是作为微任务来执行的。

Promise总结

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

强烈建议,每个then方法都写一下第二个失败的回调函数,这样就算promise内部出现问题,也不会阻塞我们程序的执行

Generator异步方案

ES2015提供的Generator(生成器函数),通过*来标记生成器函数,生成器函数调用的时候不会立即执行,而是会返回一个生成器对象,必须执行生成器对象的next方法之后才会执行。

function * foo() {
    console.log('start');
}
const generator = foo(); // => 不会打印出start
generator.next(); // => start

并且我们可以在生成器函数的内部,使用yield来指定函数的返回值。

function * foo() {
    console.log('start');
    yield 'aaa'
}
const generator = foo();
const result = generator.next();
console.log(result)

在这里插入图片描述
如果next方法里面传入一个参数的话,我们可以在生成器函数里面捕获这个参数。

function * foo() {
    console.log('start');
    const res = yield 'aaa'
    console.log(res)
}
const generator = foo();
generator.next()
generator.next('bbb')

在这里插入图片描述
还可以通过try,catch来捕获异常操作

function* foo() {
    try {
        const res = yield 'aaa';
        console.log(res)
    } catch (e) {
        console.log(e)
    }
}
const generator = foo();
generator.next()
generator.throw(new Error('失败了'))

在这里插入图片描述
生成器函数,会在yield关键词处暂停执行,如果想让它继续执行,需要继续调用next方法。

Generator实现异步

const ajax = url => {
    return new Promise((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();
    })
}
function * main() {
    const res = yield ajax('/foo.json');
    console.log(res);
    const res2 = yield ajax('/foo2.json');
    console.log(res2);
}
const g = main();
const result = g.next();
result.value.then(res => {
    const result2 = g.next(res);
    if (result.done) return;
    console.log(res)
    result2.value.then(res2 => {
        console.log(res2)
    })
})

在这里插入图片描述
思考一下上面为什么返回3个结果。

接下来通过递归来改造一下上面的异步操作。

const ajax = url => {
    return new Promise((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();
    })
}
function* main() {
    try {
        const res = yield ajax('/foo.json');
        console.log(res);
        const res2 = yield ajax('/foo22.json');
        console.log(res2);
    } catch (e) {
        console.log(e)
    }
}
const g = main();
function handleResult(result) {
    if (result.done) return;
    result.value.then(res => {
        handleResult(g.next(res))
    }, err => {
        g.throw(err)
    })
}
handleResult(g.next())

Async/Await语法糖

有了generator之后呢,其实异步操作写起来就已经非常像同步函数了,但是我们每次用的时候,都要自己封装生成器函数,不是很方便。而在ECMAScript2017中新增了一个async的函数,是语言层面的异步编程语法。可以大大简化我们的代码,我们用async来改写一下上面的代码

const ajax = url => {
    return new Promise((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();
    })
}
async function main() {
    try {
        const res = await ajax('/foo.json');
        console.log(res);
        const res2 = await ajax('/foo22.json');
        console.log(res2);
    } catch (e) {
        console.log(e)
    }
}
main()

本文章总结自拉钩教育-大前端高薪训练营(PS:班班让我加的~)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值