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

1、JavaScript 异步机制

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

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

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

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

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

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

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

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

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

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

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

JavaScript学习笔记(六) 异步问题_第1张图片

在 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 为空
  • 重复上述步骤,直至结束

JavaScript学习笔记(六) 异步问题_第2张图片

(4)关于定时器

JavaScript 中的定时器有两种,一种是 setTimeout,另一种是 setInterval

两者在内部运行机制上没有什么本质区别,区别仅在于前者是执行一次,而后者是多次执行

下面讨论 setTimeout 函数,大家对于这个函数的第一印象大概就是:在指定的时间后马上执行规定的动作

但是,这种理解是有所偏差的,请看下面的例子

console.log(1)
setTimeout(function(){ console.log(2) }, 0)
console.log(3)

按照一般的理解,打印的顺序应该是 1 2 3,但是真正的执行结果却是 1 3 2

因为在 HTML 标准中规定,setTimeout 的第二个参数的最小值不得小于 4ms,若小于 4ms 则会当作 4ms

其次,就算还是 0ms,也将会是同样的结果

其实关于 setTimeout 函数更准确的理解应该是:在指定的时间后将回调函数推入任务队列中,而非马上执行

那么只有当执行栈中为空时,才会执行任务队列中的事件,所以毫无疑问,代码的打印结果应该是 1 3 2

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 result = asynchronous()
console.log(result)

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

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

那 await 关键字又有什么用呢?

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

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

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

async function main() {
    let promise = asynchronous()
    console.log(promise)
    let result = await asynchronous()
    console.log(result)
}

main()

/*
 * 执行结果:
 * Promise {: "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
**/

await 关键字也并非一定要等待异步操作的结果,实际上它在等待一个表达式

function synchronous() {
    let data = "synchronous"
    return data
}

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

(async function() {
    let result1 = await synchronous()
    let result2 = await asynchronous()
    console.log(result1)
    console.log(result2)
})()

/*
 * 执行结果:
 * synchronous
 * asynchronous
**/

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

你可能感兴趣的:(JavaScript学习笔记(六) 异步问题)