JavaScript学习笔记(六) 异步问题

1、JavaScript 异步机制

(1)JavaScript 为什么是单线程的?

JavaScript 最先的用途是与用户交互和操作 DOM,如果 JavaScript 被设计成异步,那么就会导致复杂的同步问题

所以为了避免复杂性,JavaScript 被设计成单线程的(哈哈哈这个问题并没有标准答案,这个只是比较广泛的说法

(2)JavaScript 为什么还需要异步?

单线程就意味着所有任务都要排队,也就是说,只有前一个任务完成之后,后一个任务才能开始执行

如果前一个任务耗时很长,那么后一个任务只能一直等着,导致很差的用户体验

并且很多时候,耗时很长的任务一般都是 IO 操作,而非 CPU 计算,所以十分浪费 CPU 资源

异步操作就是要先挂起处于等待状态的任务,而先执行排在后面的任务,等结果返回后,才继续执行挂起的任务

(3)单线程的 JavaScript 怎么实现异步?

JavaScript 通过 事件循环 (event loop) 实现异步,事件循环的执行机制如下:

  • 对于同步任务,将会直接进入执行栈中,由主线程执行
  • 对于异步任务,只有当异步任务返回结果后,才会在任务队列中放置一个事件(回调函数)
  • 当执行栈中的同步任务全部执行完成后,就去读取任务队列中的下一个事件,将其加入到执行栈中开始执行
  • 重复上述步骤,直至结束

在 ES6 中,我们对事件循环有一个更加细致的理解

首先我们将任务划分为两个类别,一个是宏任务 (MacroTask,又称 Task),一个是微任务 (MicroTask,又称 Jobs)

  • MacroTask:setTimeout,setInterval,I/O,UI rendering 等
  • MicroTask:process.nextTick(node),Promise 等

在一次事件循环中,运行机制如下:

  • 从 MacroTask Queue 中提取一个 MacroTask 开始执行(若执行栈为空,则从任务队列中获取)
  • 若在 MacroTask 执行过程中遇到 MicroTask,则将其加入到 MicroTask Queue 中
  • 从 MicroTask Queue 中提取所有 MicroTask 执行,直至 MicroTask Queue 为空
  • 重复上述步骤,直至结束

结合两种理解,最后总结一下:

  • 将代码放到执行栈中,按顺序执行
  • 如果遇到同步任务,就马上执行
  • 如果遇到异步任务,首先在 Event Table 中注册回调函数,然后等异步任务完成后将回调函数放到相应的队列
    • 如果该异步任务是宏任务,那么等任务完成后要将对应的回调函数放到宏任务队列中
    • 如果该异步任务是微任务,那么等任务完成后要将对应的回调函数放到微任务队列中
  • 当执行栈为空,执行微任务队列中所有的回调函数,至此完成一个事件循环
  • 然后,提取宏任务队列中的第一个回调函数放到执行栈中,重复上述过程

这里给大家出一道题目,在 Node 运行环境下,请写出下面程序的执行结果

setTimeout(function() {
    console.log(1)
}, 0)
new Promise(function(resolve,reject){
    console.log(2)
    resolve()
}).then(function() {
    console.log(3)
}).then(function() {
    console.log(4)
})
process.nextTick(function(){
    console.log(5)
})
console.log(6)

输出的结果是 2 6 5 3 4 1,大家答对了吗,完整的执行逻辑如下:

  • 将这段代码放到执行栈中,按顺序执行

  • 遇到 setTimeout,在 Event Table 注册回调函数,等任务完成后将回调函数放到宏任务队列

    由于等待的时间是 0,所以任务完成,马上可以放到宏任务队列中

  • 遇到 Promise,构造函数的逻辑是同步任务,马上执行,输出 2

    对于 then,在 Event Table 注册回调函数,等任务完成后将回调函数放到微任务队列中

    由于在构造函数中已经调用 resolve() ,所以任务完成,可以马上放到微任务队列中

  • 遇到 process.nextTick,在 Event Table 注册回调函数,并将回调函数放到微任务队列中

  • 遇到 console.log,它是同步任务,马上执行,输出 6

  • 至此执行栈清空,然后我们再来执行微任务队列中的所有回调函数

    由于在微任务队列中,process.nextTick 的优先级高于 Promise,所以先输出 5 再输出 34

  • 第一轮事件循环到此结束,接下来进入第二轮事件循环

  • 从宏任务队列中取第一个回调函数放到执行栈中,按顺序执行

    宏任务队列中第一个回调函数就是 setTimeout 对应的回调函数,输出 1

2、异步问题的解决方案

在 JavaScript 编程中,异步始终都是绕不开的一个话题,如何优雅地解决异步问题十分值得探讨

(1)回调函数

这个方案的本质其实就是在异步函数中传入一个函数作为参数,当异步逻辑执行完成后才执行这个函数

先来一个例子,感受一下使用回调函数处理异步问题的方式

// 回调函数
function callback() {
    console.log("执行回调函数")
}

// 异步函数
function asynchronous(callback){
    console.log("执行异步逻辑")
    callback()
}

// 调用函数,保证在异步逻辑执行完成后才去执行回调函数
asynchronous(callback)

比如我们常用的 setTimeout 函数

// 回调函数
function sayHello() {
    console.log("Hello World")
}

// 异步函数
// 将回调函数作为参数传给异步函数,等待 1000ms 后才执行
setTimeout(sayHello, 1000)

比如我们常用的 JQuery,里面大量使用回调函数技术

$.post("http://www.httpbin.org/post", {
    "username": "admin",
    "password": "123456"
}, function(data, status) { // 回调函数,保证在得到 data 和 status 的结果后才去执行
    console.log(status)
    console.log(data)
})

大量使用回调函数将会出现回调地狱(callback hell),其实就是过多嵌套,使得程序难以阅读和理解

(2)Promise

Promise 是什么?Promise 其实就是一个对象,用于表示一个异步操作的状态和结果

Promise 一共有三种状态,分别是 pending(等待),resolved(成功) 和 rejected(失败)

Promise 对象初始处于 pending 状态,在整个生命周期中 有且只有一次 状态转移,变成 resolved 或者 rejected

① 创建 Promise

  • Promise()

我们可以通过 Promise 构造函数创建一个 Promise 对象

构造函数接受一个函数作为参数,这个函数接受两个参数 resolve 和 reject,它们都是函数类型

  1. resolve:在操作成功时调用,将 Promise 对象的状态变成 resolved,并将操作结果作为参数传递出去

  2. reject:在操作失败时调用,将 Promise 对象的状态变成 rejected,并将错误信息作为参数传递出去

// 伪代码
let promise = new Promise(function(resolve, reject) { // resolve 和 reject 都是回调函数
    // 异步操作
    if (/* 操作成功 */) {
        return resolve(value) // 使用 resolve 回调函数,把操作结果 value 作为参数传递出去
    } else { // 操作失败
        return reject(error) // 使用 reject 回调函数,把错误信息 error 作为参数传递出去
    }
})
  • Promise.resolve()

用于将现有对象转化为 Promise 对象,根据参数类型可以分为四种情况

  1. Promise 对象:不做操作,直接返回这个 Promise 对象
  2. thenable 对象:变成 Promise 对象,并且马上执行 thenable 对象的 then 方法
  3. 普通值:返回一个状态为 resolved 的 Promise 对象,该值作为参数传递给回调函数
  4. 无参数:返回一个状态为 resolved 的 Promise 对象
console.log(1)

let obj = {
    then: function(resolve, reject) {
        console.log("obj then")
        resolve()
    }
}

console.log(2)

let pro = Promise.resolve(obj)

console.log(3)

pro.then(function() {
    console.log("pro then")
}).catch(function() {
    console.log("pro catch")
})

console.log(4)

/*
 * 执行结果:
 * 1
 * 2
 * 3
 * 4
 * obj then
 * pro then
**/
  • Promise.reject()

用于将现有对象转化为 Promise 对象

不管参数是什么类型,总是返回一个状态为 rejected 的 Promise 对象,参数将直接传递给回调函数

console.log(1)

let obj = {
    then: function(resolve, reject) {
        console.log("obj then")
        reject()
    }
}

console.log(2)

let pro = Promise.reject(obj)

console.log(3)

pro.then(function(value) {
    console.log("pro then")
    console.log(value === obj)
}).catch(function(error) {
    console.log("pro catch")
    console.log(error === obj)
})

console.log(4)

/*
 * 执行结果:
 * 1
 * 2
 * 3
 * 4
 * pro catch
 * true
**/

② 使用 Promise

  • Promise.prototype.then()

该方法接受两个函数作为参数,用于指定 resolved 状态和 rejected 状态的回调函数

第一个函数在状态变成 resolved 时调用,第二个函数在状态变成 rejected 时调用,其中第二个函数是可选的

// 伪代码
promise.then(function(value){ // resolved 状态的回调函数
    // 操作成功,处理 value
}, function(error){ // rejected 状态的回调函数
    // 操作失败,处理 error
})

Promise 在创建后马上执行,而 then 方法指定的回调函数则在当前事件循环的最后才会执行

console.log(1)

let promise = new Promise(function(resolve, reject) {
    console.log("Promise begin")
    let error = "fail"
    reject(error)
    console.log("Promise end")
})

console.log(2)

promise.then(function(value) {
    console.log(value)
}, function(error) {
    console.log(error)
})

console.log(3)

/*
 * 执行结果:
 * 1
 * Promise begin
 * Promise end
 * 2
 * 3
 * fail
**/

Promise 对象的 then 方法可以调用多次,这是十分特别的一点

let promise = new Promise(function(resolve, reject) {
    let value = "success"
    resolve(value)
})

promise.then(function(value){
    console.log(value)
})

promise.then(function(value){
    console.log(value)
})

/*
 * 执行结果:
 * success
 * success
**/

then 方法返回一个新的 Promise 对象(不是原来的 Promise 对象),所以可以链式调用

let promise = new Promise(function(resolve, reject) {
    let data = 2
    resolve(data)
})

promise.then(function(value) {
    console.log(value)
    return value*2
}).then(function(value) {
    console.log(value)
})

/*
 * 执行结果:
 * Promise
 * 2
 * 4
**/
  • Promise.prototype.catch()

这个函数用于指定当错误发生时的回调函数,等价于 then(null, reject)

Promise 内部如果发生错误 reject()throw new Error(),错误将会一直往后传递

直至遇到可以处理它的语句 then(resolve, reject)catch(reject)

如果后面没有可以处理它的语句,错误也不会传递到外层代码

let promise = new Promise(function(resolve, reject) {
    reject("fail")
})

promise.then(function(value) {
    console.log(value)
}).catch(function(error) {
    console.log(error)
})

/*
 * 执行结果:
 * fail
**/
  • Promise.prototype.finally()

这个函数用于指定不管最后状态如何都会执行的回调函数

let promise = new Promise(function(resolve, reject) {
    let success = (Math.random() >= 0.5)
    if (success) {
        resolve("success")
    } else {
        reject("fail")
    }
})

promise.then(function(value) {
    console.log(value)
}).catch(function(error) {
    console.log(error)
}).finally(function() {
    console.log("finally")
})

/*
 * 执行结果:
 * success/fail
 * finally
**/
  • Promise.all()

接受一个数组作为参数,其中的每一个元素都是一个 Promise 实例,返回一个新的 Promise 实例

只有当所有传入的 Promise 实例的状态都变成 resolved 时,新的 Promise 实例的状态才会变成 resolved

如果任意一个传入的 Promise 实例的状态变成 rejected,那么新的 Promise 实例的状态也会变成 rejected

console.time("all")

let pro1 = new Promise(function(resolve, reject) {
    setTimeout(function() { resolve() }, 2000)
})

let pro2 = new Promise(function(resolve, reject) {
    setTimeout(function() { reject() }, 5000)
})

let pro = Promise.all([pro1, pro2])

pro.then(function() {
    console.log("Promise resolve")
}).catch(function() {
    console.log("Promise reject")
}).finally(function() {
    console.timeEnd("all")
})

/*
 * 执行结果:
 * Promise reject
 * all: 5002.387939453125ms
**/
  • Promise.race()

这个方法同样接受一个数组作为参数,其中的每一个元素都是一个 Promise 实例,返回一个新的 Promise 实例

不同的是,只要任意一个传入的 Promise 实例的状态发生改变,新的 Promise 实例的状态就会改变

也就是说,新的 Promise 实例的状态由最快得到结果的 Promise 实例的状态决定

console.time("race")

let pro1 = new Promise(function(resolve, reject) {
    setTimeout(function() { resolve() }, 2000)
})

let pro2 = new Promise(function(resolve, reject) {
    setTimeout(function() { reject() }, 5000)
})

let pro = Promise.race([pro1, pro2])

pro.then(function() {
    console.log("Promise resolve")
}).catch(function() {
    console.log("Promise reject")
}).finally(function() {
    console.timeEnd("race")
})

/*
 * 执行结果:
 * Promise resolve
 * race: 2001.555908203125ms
**/

(3)async/await

async 关键字有什么用呢?由 async 定义的函数返回一个 Promise 对象,后续可以通过 Promise 的相关操作处理

async function asynchronous() {
    let data = "success"
    return data // 转化成 Promise 对象后返回
}

let promise = asynchronous()
console.log(promise)

promise.then(function(value) {
    console.log(value)
})

/*
 * 执行结果:
 * Promise {<resolved>: "success"}
 * success
**/

async 提供创建 Promise 的语法糖,而 await 则提供使用 Promise 的语法糖,让我们可以进一步简化代码

await 关键字用于等待一个异步操作的结果,只有当异步操作完成并返回结果后,才能继续执行后面的代码

注意 await 只能用在由 async 定义的函数的内部

async function asynchronous() {
    let data = "success"
    return data
}

async function main() {
    // 没有使用 await,返回一个 Promise
    let promise = asynchronous()
    console.log(promise)
    // 使用 await,返回 Promise 的结果
    let result = await asynchronous()
	console.log(result)
}

main()

/*
 * 执行结果:
 * Promise {<resolved>: "success"}
 * success
**/

await 后面的 Promise 对象的状态可能会转变成 rejected,这时候我们需要使用 try/catch 去处理这种情况

async function asynchronous() {
    return new Promise(function(resolve, reject) {
        let data = "fail"
        reject(data)
    })
}

async function main() {
    try {
        let result = await asynchronous()
    } catch(error) {
        console.log(error)
    }	
}

main()

/*
 * 执行结果:
 * fail
**/

【 阅读更多 JavaScript 系列文章,请看 JavaScript学习笔记

posted @ 2020-01-06 23:44  半虹  阅读(185)  评论(0编辑  收藏  举报