一、JavaScript为什么要异步?
Javascript语言的执行环境是"单线程"(single thread)。所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。
二、同步模式
"同步模式" 指后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的
三、异步模式
"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。 "异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。
四、异步编程的实现方式
1、回调函数
优点:简单、容易理解
缺点:不利于维护,代码耦合高,多个异步操作下容易形成回调地狱。2、事件监听
优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数
缺点:事件驱动型,流程不够清晰3、发布/订阅(观察者模式)
类似于事件监听,但是可以通过‘消息中心’,了解现在有多少发布者,多少订阅者
4、Promise
优点:可以利用then方法,进行链式写法;可以书写错误时的回调函数;
缺点:编写和理解,相对比较难5、Generation
优点:函数体内外的数据交换、错误处理机制
缺点:流程管理不方便6、async/await
优点:内置执行器、更好的语义、更广的适用性、返回的是Promise、结构清晰。
缺点:错误处理机制
五、回调函数
传说中的 "callback hell" 就是来自回调函数。回调函数简单理解就是一个函数被作为参数传递给另一个函数,而回调函数也是最基础最常用的处理js异步操作的办法。我们来看一个简单的例子:
function fn1() {
console.log('Function 1')
}
function fn2() {
setTimeout(() => {
console.log('Function 2')
}, 500)
}
function fn3() {
console.log('Function 3')
}
fn1();
fn2();
fn3();
// 结果:
// Function 1
// Function 3
// Function 2
其在fn2可以视作一个延迟了500毫秒执行的异步函数。现在我希望可以依次执行fn1,fn2,fn3。为了保证fn3在最后执行,我们可以把它作为fn2的回调函数:
function fn1() {
console.log('Function 1')
}
function fn2(callback) {
setTimeout(() => {
console.log('Function 2')
callback()
}, 500)
}
function fn3() {
console.log('Function 3')
}
fn1();
fn2(fn3);
// 结果:
// Function 1
// Function 2
// Function 3
回调函数是异步编程最基本的方法,其优点是简单、容易理解和部署。回调函数最大的缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),如果有多个类似的函数,很有可能会出现fn1(fn2(fn3(fn4(...))))这样的情况。
六、事件监听
采用事件驱动模式,任务的执行不取决于代码的顺序,而取决于某个事件是否发生,事件监听最常用的常见在于DOM元素事件绑定触发,如果我们想在DOM元素与用户进行鼠标或其他交互之后执行某些逻辑,就可以使用事件监听了
$('body').on('done', fn2 )
function fn1() {
setTimeout(() => {
console.log('Function 1')
$('body').trigger('done')
}, 500);
}
function fn2() {
console.log('Function 2')
}
fn1()
// 结果:
// Function 1
// Function 2
上述代码中,我们使用jq的on监听了一个自定义事件done,传入了fn2回调函数,表示事件触发后立即执行函数fn2。在函数fn1中使用setTimeout模拟了耗时任务,setTimeout回调中使用trigger触发了done事件。我们可以使用on来绑定多个事件,每个事件可以指定多个回调函数
七、发布/订阅(观察者模式)
发布/订阅模式是利用一个消息中心,发布者发布一个消息给消息中心,订阅者从消息中心订阅该消息。订阅/发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某发布者对象。这个发布者对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。类似于 vue 的父子组件之间的传值。
//创建一个主题发布类
let Publisher = function () {
this.subscribers = []
}
Publisher.prototype.publish = function (data) {
this.subscribers.forEach(fn=>{
fn(data)
})
}
//订阅 —— 在Function上挂载这个些方法,所有的函数都可以调用这些方法表示所有函数都可以订阅/取消订阅相关的主题发布
Function.prototype.subscribe = function (publisher) {
let that = this;
let isExist = publisher.subscribers.some(function (el) {
if (el === that) {
return true
}
})
if (!isExist) {
publisher.subscribers.push(that)
}
//return this是为了支持链式调用
return this
}
//取消订阅
Function.prototype.unsubscribe = function (publisher) {
let that = this;
//就是将函数从发布者的订阅者列表中进行删除
publisher.subscribers = publisher.subscribers.filter(function (el) {
if (el !== that) {
return true
}
})
return this
}
let publisher = new Publisher();
let subscriberObj = function (data) {
console.log(data)
}
subscriberObj.subscribe(publisher)
八、Promise
Promise 是异步编程的一种解决方案,比传统的解决方案【回调函数】和【事件】更合理、更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。Promise说得通俗一点就是一种写代码的方式,并且是用来写JavaScript编程中的异步代码的。(详情请查看我的另一篇原文:Promise: 给我一个承诺,我还你一个承诺 )
【8.1】promise三种状态
- pending:进行中
- fulfilled :已成功
- rejected 已失败
只有异步操作的结果才能确定当前处于哪种状态,任何其他操作都不能改变这个状态,这也是Promise(承诺)的由来。
Promise对象的状态改变,只有两种可能:
- 从pending变为fulfilled
- 从pending变为rejected
这两种情况只要发生,状态就凝固了,不会再变了,这时就称为resolved(已定型)
【8.2】promise缺点
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消
- 如果不设置回调函数(没有捕获错误),Promise内部抛出的错误,不会反应到外部
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
【8.3】promise API
我们先把Promise打印出来,会发现Promise是一个构造函数,自己身上有all、reject、resolve、race等方法,原型上有then、catch、finally等方法。
※ Promise.prototype.constructor() ※
它的基本用法如下:
let promise = new Promise((resolve, reject) => {
// 在这里执行异步操作
if (/*异步操作成功*/) {
resolve(success)
} else {
reject(error)
}
})
Promise接收一个函数作为参数,函数里有resolve和reject两个参数:
- resolve方法的作用是将Promise的pending状态变为fulfilled,在异步操作成功之后调用,可以将异步返回的结果作为参数传递出去。
- reject方法的作用是将Promise的pending状态变为rejected,在异步操作失败之后调用,可以将异步返回的结果作为参数传递出去。
他们之间只能有一个被执行,不会同时被执行,因为Promise只能保持一种状态。
※ Promise.prototype.then() ※
Promise实例确定后,可以用then方法分别指定fulfilled状态和rejected状态的回调函数。它的基本用法如下:
promise.then((success) => {
// 异步操作成功在这里执行
// 对应于上面的resolve(success)方法
}, (error) => {
// 异步操作失败在这里执行
// 对应于上面的reject(error)方法
})
// 还可以写成这样 (推荐使用这种写法)
promise.then((success) => {
// 异步操作成功在这里执行
// 对应于上面的resolve(success)方法
}).catch((error) => {
// 异步操作失败在这里执行
// 对应于上面的reject(error)方法
})
then(onfulfilled,onrejected)方法中有两个参数,两个参数都是函数,第一个参数执行的是resolve()方法(即异步成功后的回调方法),第二参数执行的是reject()方法(即异步失败后的回调方法)(第二个参数可选)。它返回的是一个新的Promise对象。
※ Promise.prototype.catch() ※
catch方法是.then(null,onrejected)的别名,用于指定发生错误时的回调函数。作用和then中的onrejected一样,不过它还可以捕获onfulfilled抛出的错,这是onrejected所无法做到的:
function createPromise(p, arg) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (arg === 0) {
reject(p + ' fail')
} else {
resolve(p + ' ok')
}
}, 0);
})
}
createPromise('p1', 1).then((success) => {
console.log(success) // p1 ok
return createPromise('p2', 0)
}).catch((error) => {
console.log(error) // p2 fail
})
createPromise('p1', 1).then((success) => {
console.log(success) // p1 ok
return createPromise('p2', 0)
}, (error) => {
console.log(error) // Uncaught (in pomise) p2 fail
})
Promise错误具有"冒泡"的性质,如果不被捕获会一直往外抛,直到被捕获为止;而无法捕获在他们后面的Promise抛出的错。
※ Promise.prototype.finally() ※
finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。该方法是 ES2018 引入的标准:
createPromise('p1', 0).then((success) => {
console.log(success)
}).catch((error) => {
console.log(error) // p1 fail
}).finally(() => {
console.log('finally') // finally
})
createPromise('p1', 1).then((success) => {
console.log(success) // p1 ok
}).catch((error) => {
console.log(error)
}).finally(() => {
console.log('finally') // finally
})
finally方法不接受任何参数,故可知它跟Promise的状态无关,不依赖于Promise的执行结果。
※ Promise.all() ※
Promise.all方法接受一个数组作为参数,但每个参数必须是一个Promise实例。Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作都执行完毕后才执行回调,只要其中一个异步操作返回的状态为rejected那么Promise.all()返回的Promise即为rejected状态,此时第一个被reject的实例的返回值,会传递给Promise.all的回调函数:
function createPromise(p, arg) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (arg === 0) {
reject(p + ' fail')
} else {
resolve(p + ' ok')
}
}, 0);
})
}
// test: 两个Promise都成功
Promise.all([createPromise('p1', 1), createPromise('p2', 1)])
.then((success) => {
console.log(success) // ['p1 ok', 'p2 ok']
}).catch((error) => {
console.log(error)
})
// test: 其中一个Promise失败
Promise.all([createPromise('p1', 0), createPromise('p2', 1)])
.then((success) => {
console.log(success)
}).catch((error) => {
console.log(error) // p1 fail
})
// test: 两个Promise都失败
Promise.all([createPromise('p1', 0), createPromise('p2', 0)])
.then((success) => {
console.log(success)
}).catch((error) => {
console.log(error) // p1 fail 只打印第一个失败的异步操作信息
})
※ Promise.race() ※
Promise的race方法和all方法类似,都提供了并行执行异步操作的能力。顾名思义,race就是赛跑的意思,意思就是说Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态,以下就是race的执行过程:
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('failed')
}, 500)
})
Promise.race([p1, p2]).then((success) => {
console.log(success)
}).catch((error) => {
console.log(error) // failed
})
※ Promise.resolve() ※
有时需要将现有对象转为 Promise 对象Promise.resolve()方法就起到这个作用。
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
※ Promise.reject() ※
Promise.reject()方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
【8.4】例子
Promise 翻译成中文为“承诺, 诺言”, 例如: 你承诺这个月挣钱了给你老婆买一个包, 那么你先去挣钱, 等挣钱了就立刻给老婆买包, 实现你的诺言, 没挣到钱就立马道歉。换成代码就是:
// 买包就是一个Promise,Promise的意思就是承诺
// 这时候老公给老婆一个承诺
// 在未来的一个月,不管挣没挣到钱,都会给老婆一个答复
let buyBag = new Promise((resolve, reject) => {
// Promise 接受两个参数
// resolve: 异步事件成功时调用(挣到钱)
// reject: 异步事件失败时调用(没挣到钱)
// 模拟挣钱概率事件
let result = function makeMoney() {
return Math.random() > 0.5 ? '挣到钱' : '没挣到钱'
}
// 下面老公给出承诺,不管挣没挣到钱,都会给老婆一个答复
if (result == '挣到钱')
resolve('我买包了')
else
reject('不好意思,我这个月没挣到钱')
})
buyBag().then(res => {
// 返回 "我买包了"
console.log(res)
}).catch(err => {
// 返回 "不好意思,我这个月没挣到钱"
console.log(err)
})
解释一下
第一段调用了Promise构造函数,第二段是调用了promise实例的.then方法
1. 构造实例
- 构造函数接受一个函数作为参数
- 调用构造函数得到实例buyBag的同时,作为参数的函数会立即执行
- 参数函数接受两个回调函数参数resolve和reject
- 在参数函数被执行的过程中,如果在其内部调用resolve会将buyBag的状态变成fulfilled,或者调用reject会将buyBag的状态变成rejected
2. 调用.then
- 调用.then可以为实例buyBag注册两种状态回调函数
- 当实例buyBag的状态为fulfilled,会触发第一个回调函数执行
- 当实例buyBag的状态为rejected,则触发第二个回调函数执行
九、Generation
顾名思义,Generation 是一个生成器,它也是一个状态机,内部拥有值及相关的状态,生成器返回一个迭代器Iterator对象,我们可以通过这个迭代器,手动地遍历相关的值、状态,保证正确的执行顺序。Generation最大特点就是可以交出函数的执行权(即暂停执行)
【9.1】声明
Generator的声明方式类似一般的函数声明,只是多了个*号,并且一般可以在函数内看到yield关键字
function* showWords() {
yield 'one';
yield 'two';
return 'three';
}
let show = showWords();
console.log(show.next()) // {done: false, value: "one"}
console.log(show.next()) // {done: false, value: "two"}
console.log(show.next()) // {done: true, value: "three"}
console.log(show.next()) // {done: true, value: undefined}
如上代码,定义了一个showWords的生成器函数,调用之后返回了一个迭代器对象(即show)。调用next方法后,函数内执行第一条yield语句,输出当前的状态done(迭代器是否遍历完成)以及相应值(一般为yield关键字后面的运算结果)。每调用一次next,则执行一次yield语句,并在该处暂停,return完成之后,就退出了生成器函数,后续如果还有yield操作就不再执行了
【9.2】yield和yield*
yield就是说明next函数调用时返回的值,yield还有一个有趣的地方,就是在每个yield调用之后,后面的代码都会停止执行。其实从某种程度来说,yield和return是非常相似的。有时候,我们会看到yield之后跟了一个*号,它是什么,有什么用呢?我们修改一下上面的代码:
function* showWords() {
yield 'one';
yield showNumbers();
return 'four';
}
function* showNumbers() {
yield 2;
yield 3;
}
let show = showWords();
console.log(show.next()) // {done: false, value: "one"}
console.log(show.next()) // {done: false, value: showNumbers}
console.log(show.next()) // {done: true, value: "three"}
console.log(show.next()) // {done: true, value: undefined}
增添了一个生成器函数showNumbers(),我们在showWords中调用一次showNumbers()之后发现并没有执行函数里面的yield,因为yield只能原封不动地返回右边运算后的值,但现在的showNumbers()不是一般的函数调用,返回的是迭代器对象,所以换个yield* 让它自动遍历进该对象。修改代码如下:
function* showWords() {
yield 'one';
yield* showNumbers();
return 'four';
}
function* showNumbers() {
yield 2;
yield 3;
}
let show = showWords();
console.log(show.next()) // {done: false, value: "one"}
console.log(show.next()) // {done: false, value: 2}
console.log(show.next()) // {done: false, value: 3}
console.log(show.next()) // {done: true, value: "three"}
yield和yield* 只能在generator函数内部使用,一般的函数内使用会报错
// 普通函数中使用yield
function showWords() {
yield 'one';
}
showWords() // Uncaught SyntaxError: Unexpected string
// 普通函数中使用yield*
function showNums() {
yield* 1;
}
let show = showNums();
console.log(show.next()) // Uncaught ReferenceError: yield is not defined
【9.3】 next()传参
参数值有注入的功能,可改变上一个yield的返回值
function* showNumbers() {
let one = yield 1;
let two = yield 2 * one;
yield 3 * two;
}
let show = showNumbers();
console.log(show.next().value) // 1
console.log(show.next().value) // NaN
console.log(show.next(2).value) // 6
第一次调用next之后返回值one为1,但在第二次调用next的时候one其实是undefined的,因为generator不会自动保存相应变量值,我们需要手动的指定,这时two值为NaN,在第三次调用next的时候执行到yield 3 * two,通过传参将上次yield返回值two设为2,得到结果为6。 若是传入3返回的就是 9
【9.4】 for...of循环代替.next()
除了使用.next()方法遍历迭代器对象外,通过ES6提供的新循环方式for...of也可遍历,但与next不同的是,它会忽略return返回的值
function* showNumbers() {
yield 1;
yield 2;
return 3;
}
let show = showNumbers();
for (let n of show) {
console.log(n) // 1 2
}
除了for...of循环,具有调用迭代器接口的方法方式也可遍历生成器函数,如扩展运算符...的使用
function* showNumbers() {
yield 1;
yield 2;
return 3;
}
let show = showNumbers();
console.log([...show]) // [1, 2]
【9.5】 注意
生成器函数不能当构造器使用
function* Person() {}
let person = new Person; // throws "TypeError: Person is not a constructor"
yield是不能穿透函数的,不能使用forEach代替for循环遍历
function* showNumbers(array){
// 正确写法
for( let i=0; i<array.length; i++ ){
yield array[i]
}
// 错误写法
// array.forEach(item=>{
// yield item
// })
}
let show = showNumbers([2,5,7]);
console.log(show.next()) // {value: 2, done: false}
console.log(show.next()) // {value: 5, done: false}
console.log(show.next()) // {value: 7, done: false}
console.log(show.next()) // {value: undefined, done: true}
可以使用变量来定义函数,也就是函数表达式。但是不能用箭头函数进行创建
let showNumbers = function* () {
yield 1;
yield 2;
return 3;
}
let show = showNumbers();
console.log([...show]) // [1, 2]
十、async/await
【10.1】async关键字
async 是 ES7 才有的与异步操作有关的关键字,和 Promise,Generator 有很大关联的。
※ 特点 ※
1、建立在 promise 之上。所以,不能把它和回调函数搭配使用。但它会声明一个异步函数,并隐式地返回一个Promise。因此可以直接return变量,无需使用 Promise.resolve 进行转换。
2、和 promise 一样,是非阻塞的。但不用写 then 及其回调函数,这减少代码行数,也避免了代码嵌套。而且,所有异步调用,可以写在同一个代码块中,无需定义多余的中间变量。
3、它的最大价值在于,可以使异步代码,在形式上,更接近于同步代码。
4、它总是与 await 一起使用的。并且await 只能在 async 函数体内。
5、await 是个运算符,用于组成表达式,它会阻塞后面的代码。如果等到的是 Promise 对象,则得到其 resolve 值。否则,会得到一个表达式的运算结果。
※ 用法 ※
先说一下async的用法,它作为一个关键字放到函数前面,用于表示函数是一个异步函数,因为async就是异步的意思, 异步函数也就意味着该函数的执行不会阻塞后面代码的执行。 下面我们就来写一个async 函数
async function test() {
return 'Hello World';
}
console.log(test())
语法很简单,就是在函数前面加上async 关键字,来表示它是异步的,那怎么调用呢?async 函数也是函数,平时我们怎么使用函数就怎么使用它,直接加括号调用就可以了
查看控制台打印结果
原来async 函数返回的是一个promise 对象,如果要获取到promise 返回值,我们应该用then 方法, 继续修改代码
async function test() {
return 'Hello World';
}
test().then(res=>{
console.log(res) // Hello World
})
console.log('我在后面,但是我先执行')
上面代码中通过then()方法获取到promise的返回值,假设promise内部抛出异常,我们同样可以通过catch()方法来捕获异常。
我们获取到了"Hello World', 同时test()异步函数的执行也没有阻塞后面代码的执行,"我在后面,但是我先执行",这条语句会先执行
看到这,小伙伴们可能要纳闷了,就是封装一个Promise的对象返回而已,要这有个鬼用啊。别急,接下来有请async黄金搭档 await关键字闪亮登场。
【10.2】await关键字
await是等待的意思,那么它等待什么呢,它后面跟着什么呢?
正常情况下,await 命令后面是一个 Promise 对象,它也可以跟其他值,如字符串,布尔值,数值以及普通函数。await表达式会使async函数暂停执行,等待promise的结果出来,然后恢复async的执行并返回解析值(resolved)
注意,await 关键字仅仅在async 函数中才有效,如果在async函数外使用await,则会抛出一个语法错误(SyntaxError)
function testAwait() {
return new Promise((resolve) => {
setTimeout(function () {
console.log("Test Await");
resolve();
}, 1000);
});
}
async function test() {
await testAwait();
console.log("Hello World");
}
test();
// Test Await
// Hello World
我们来分析下上面这段代码
现在我们看看代码的执行过程,调用test函数,它里面遇到了await, await 表示等一下,代码就暂停到这里,不再向下执行了,它等什么呢?等后面的testAwait函数中的promise对象执行完毕,然后拿到promise resolve 的值并进行返回,返回值拿到之后,它继续向下执行。执行console.log语句。
注意:await 命令后面的 Promise 对象,运行结果不一定都是resolve,也可能是 rejected。当promise返回结果为rejected状态时,会终止后面的代码执行。所以最好把 await 命令放在 try...catch 代码块中。异常被try...catch捕获后,继续执行下面的代码,不会导致中断
function testAwait() {
return new Promise((resolve) => {
setTimeout(function () {
console.log("Test Await");
resolve();
}, 1000);
});
}
async function test() {
try {
await testAwait();
} catch (err) {
console.log(err)
}
console.log("Hello World");
}
test();
文章每周持续更新,可以微信搜索「 前端大集锦 」第一时间阅读,回复【视频】【书籍】领取200G视频资料和30本PDF书籍资料
网友评论