l

laor

V1

2023/03/12阅读:17主题:全栈蓝

JS中的异步编程

前言

JavaScript 在浏览器端运行是单线程的,这是由浏览器(运行环境)决定的,这种行为是为了避免多线程执行不同任务会发生冲突的情况。

  • JavaScript 代码只在一个线程上运行,称之为主线程。
  • 即使HTML5提供了web worker API可以让浏览器开一个线程运行比较复杂耗时的 JavaScript任务,但是这个线程仍受主线程的控制。

当然,这不是这篇文章讨论的重点。

为了解决耗时任务阻塞执行的问题,从而有了同步模式和异步模式。

同步模式和异步模式

同步模式(Synchronous)

  • 大多数情况下的代码都是在同步模式执行,请注意不是同时执行,而是按照顺序执行。
  • 当某一段代码执行时间过长,后面的代码的执行就会被阻塞。对于用户来说就会有页面卡顿卡死。
console.log('global begin')

function bar ({
  console.log('bar task')
}

function foo ({
  console.log('foo task')
  bar()
}

foo()

console.log('global end')

异步模式(Asynchronous)

  • 不会去等待这个任务执行结束才开始执行下一个任务。
  • 任务开启过后立即开启下一个任务。
  • 后续逻辑一般通过定义回调函数的方式去处理。
  • 这样单线程的JavaScript语言可以同时处理大量耗时任务。
  • 但是,代码执行顺序可能比较混乱,跳跃,可读性差。
console.log('global begin')

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

setTimeout(function timer2() {
console.log('timer2 invoke')

setTimeout(function inner() {
console.log('inner invoke')
}, 1000)
}, 1000)

console.log('global end')

// 依次打印 global begin、global end、timer2 invoke、timer1 invoke、inner invoke

相关概念

  • 执行JavaScript代码的线程只有一个,但浏览器并不是单线程, setTimeout倒计时的工作交给浏览器的异步调用线程
  • 函数调用栈
  • 消息队列与事件循环,事件循环机制会监听调用栈及消息队列,当调用栈为空,就去消息队列取第一个任务来入栈执行,若消息队列也为空,则暂停执行,当消息队列入栈任务时,又重新开始从消息队列取第一个任务入栈执行。

代码分析

  1. 入栈console.log函数,打印global begin,出栈;

  2. 入栈setTimeout函数执行并出栈,同时为timer1开启一个倒计时器放入异步调用线程计时;

  3. 入栈setTimeout函数执行并出栈,同时为timer2开启一个倒计时器放入异步调用线程计时;

  4. 入栈console.log函数,打印global end,出栈;

    1000ms后...

  5. timer2放入消息队列,事件循环机制监听到后,将timer2函数入栈并执行;

  6. 入栈console.log函数,打印timer2 invoke,出栈;

  7. 入栈setTimeout函数执行并出栈,同时为inner开启一个倒计时器放入异步调用线程计时,然后timer2出栈;

    再过800ms...

  8. timer1放入消息队列,事件循环机制监听到后,将timer1函数入栈并执行;

  9. 入栈console.log函数,打印timer1 invoke,出栈,然后timer1出栈;

    再过200ms...

  10. inner放入消息队列,事件循环机制监听到后,将inner函数入栈并执行;

  11. 入栈console.log函数,打印inner invoke,出栈,然后inner出栈;

回调函数

  • 回调函数是所有异步编程方案的根基。
  • 由调用者定义,交给执行者执行的函数称为回调函数
// 回调函数
function foo(callback) {
setTimeout(function () {
callback()
}, 3000)
}

foo(function () {
console.log('这就是一个回调函数')
console.log('调用者定义这个函数,执行者执行这个函数')
console.log('其实就是调用者告诉执行者异步任务结束后应该做什么')
})
  • 直接使用传统回调方式去完成复杂的异步流程
// 回调地狱,只是示例,不能运行

$.get('/url1', function (data1) {
$.get('/url2', data1, function (data2) {
$.get('/url3', data2, function (data3) {
$.get('/url4', data3, function (data4) {
$.get('/url5', data4, function (data5) {
$.get('/url6', data5, function (data6) {
$.get('/url7', data6, function (data7) {
// 略微夸张了一点点
})
})
})
})
})
})
})

因此,CommonJS社区率先提出了Promise的规范,在ES2015中被规范化,成为语言规范。

Promise

基本用法

const promise = new Promise(function (resolve, reject) {
// 这里用于“兑现”承诺

// resolve(100) // 承诺达成

reject(new Error('promise rejected')) // 承诺失败
})

promise.then(function (value) {
// 即便没有异步操作,then 方法中传入的回调仍然会被放入队列,等待下一轮执行
console.log('resolved', value)
}, function (error) {
console.log('rejected', error)
})

console.log('end')

Promise 方式的 AJAX

function ajax(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json'
xhr.onload = function () { // readyState = 4
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}

ajax('/api/foo.json').then(function (res) {
console.log(res)
}, function (error) {
console.log(error)
})
  • 嵌套使用 Promise 是最常见的误区
  • 正确做法是使用Promise then方法链式调用的特点保证异步任务扁平化
// 嵌套使用 Promise 是最常见的误区
ajax('/api/urls.json').then(function (urls) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {

})
})
})
})
})

链式调用

  • Promise的then方法会返回一个全新的Promis对象,后面的then方法就是在为上一个then返回的Promise注册的回调
  • 前面then方法中回调函数的返回值会作为后面then方法回调的参数
  • 如果回调中返回的还是Promise,那后面then方法的回调会等待它的结束,并接收它返回的结果
ajax('/api/users.json')
.then(function (value) {
console.log(1111)
return ajax('/api/urls.json')
}) // => Promise
.then(function (value) {
console.log(2222)
console.log(value)
return ajax('/api/urls.json')
}) // => Promise
.then(function (value) {
console.log(3333)
return ajax('/api/urls.json')
}) // => Promise
.then(function (value) {
console.log(4444)
return 'foo'
}) // => Promise
.then(function (value) {
console.log(5555)
console.log(value) // foo
})

Promise异常处理

  1. then方法的第二个参数onRejected函数接收当前Promise的异常(Promise失败或者出现报错等)。

  2. Promise实例的catch方法注册onRejected回调,适合链式调用,更常用。

  3. 区别

    a. catch方法是对then方法返回的Promise注册的异常回调,而then方法的第二个参数只是对当前Promise注册的异常回调。

    b. Promise链条上任何的异常都会被向后传递直到被捕获。then的第二个参数只捕获当前Promise的异常,而catch能捕获到链条里的所有异常,所以最后一个then里面的异常只能被catch捕获。

  4. 全局注册unhandledrejection进行全局捕获Promise异常。

  5. 应该在代码中明确的捕获每一个可能的异常,而不是丢给全局去统一处理。

function ajax() {
return new Promise(function (resolve, reject) {
foo()
// throw new Error()
setTimeout(() => {
resolve()
// reject(new Error('error msg'))
})
})
}

ajax()
.then(function onFulfilled(value) {
foo1()
console.log('onFulfilled', value)
}, function onRejected(error) {
console.log('onRejected', error) // onRejected ReferenceError: foo is not defined
}).catch(err => {
// foo()注释时,err ReferenceError: foo1 is not defined
// then没onRejected回调时,err ReferenceError: foo is not defined
console.log('err', err)
})

// 全局捕获 Promise 异常,类似于 window.onerror
window.addEventListener('unhandledrejection', event => {
const { reason, promise } = event

console.log(reason, promise)
// reason => Promise 失败原因,一般是一个错误对象
// promise => 出现异常的 Promise 对象

event.preventDefault()
}, false)

// Node.js 中使用以下方式
process.on('unhandledRejection', (reason, promise) => {
console.log(reason, promise)
// reason => Promise 失败原因,一般是一个错误对象
// promise => 出现异常的 Promise 对象
})

Promise静态方法

  1. Promise.resolve
  • 传入一个值。转换为一个状态为fulfilled的Promise对象
  • 如果传入的是一个 Promise 对象,Promise.resolve方法原样返回
  • 如果传入的是带有一个跟 Promise 一样的 then 方法的对象,Promise.resolve 会将这个对象作为 Promise 执行
Promise.resolve('foo')
.then(function (value) {
console.log(value) //foo
})
// 等同于 ---->
new Promise(function (resolve, reject) {
resolve('foo')
})

// 如果传入的是一个 Promise 对象,Promise.resolve 方法原样返回

var promise = ajax('/api/users.json')
var promise2 = Promise.resolve(promise)
console.log(promise === promise2)

// 如果传入的是带有一个跟 Promise 一样的 then 方法的对象,
// Promise.resolve 会将这个对象作为 Promise 执行

Promise.resolve({
then: function (onFulfilled, onRejected) {
onFulfilled('foo')
}
})
.then(function (value) {
console.log(value) // foo
})
  1. Promise.reject:传入任何值,都会作为这个 Promise 失败的理由
Promise.reject(new Error('rejected'))
.catch(function (error) {
console.log(error) // Error: rejected
})

Promise.reject('anything')
.catch(function (error) {
console.log(error) //anything
})

Promise 并行执行

  • Promise.all将多个Promise合并成一个Promise处理,都成功才成功,有一个失败就失败。
  • Promise.race方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。
var promise = Promise.all([
ajax('/api/users.json'),
ajax('/api/posts.json')
])

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

// Promise.race 实现超时控制
const request = ajax('/api/posts.json')
const timeout = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('timeout')), 500)
})

Promise.race([
request,
timeout
])
.then(value => {
console.log(value)
})
.catch(error => {
console.log(error)
})

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

  • 回调队列中的任务称为宏任务
  • 当前宏任务的微任务,会在当前任务执行结束后立即执行,常见的微任务:PromiseMutationObserver、Node中的process.nextTick
console.log('global start')

// setTimeout 的回调是 宏任务,进入回调队列排队
setTimeout(() => {
console.log('setTimeout')
}, 0)

// Promise 的回调是 微任务,本轮调用末尾直接执行
Promise.resolve()
.then(() => {
console.log('promise')
})
.then(() => {
console.log('promise 2')
})
.then(() => {
console.log('promise 3')
})

console.log('global end')
  • promise的链式调用解决了嵌套的回调函数地狱。
  • 但是链式调用的回调函数的可读性不够好,不如同步代码。

Generator异步执行方案(ES2015)

  1. 调用生成器函数。生成生成器对象generator,不会立即执行生成器函数函数体
  2. 调用generator.next,函数体开始执行,直到yield关键字,yield后面的值,作为返回对象的value进行返回,函数体暂停执行。
  3. 再调用next方法,并传递参数,此参数将作为yield语句的返回值,并继续执行函数体,直到下一个yield位置。
  4. generator.throw抛出一个异常
// 生成器函数

function * foo () {
console.log('start')

try {
const res = yield 'foo' // yield暂停生成器函数的执行
console.log(res) // bar
} catch (e) {
console.log(e) // Error: Generator error
}
}

const generator = foo() // 不会立即执行这个函数的函数体,而是得到一个生成器对象

const result = generator.next()
console.log(result) // {value: 'foo', done: false}

// generator.next('bar')

let result1 = generator.throw(new Error('Generator error')) // done 为false时才能调用该语句进行捕获
console.log(result1) // {value: undefined, done: true}

用同步的方法实现异步逻辑:



function* main() {
try {
const users = yield ajax('/api/users.json')
console.log(users)

const posts = yield ajax('/api/posts.json')
console.log(posts)

const urls = yield ajax('/api/urls11.json')
console.log(urls)
} catch (e) {
console.log(e)
}
}

const result = g.next()
result.value.then(data => {
const result2 = g.next(data)

if (result2.done) return

result2.value.then(data => {
const result3 = g.next(data)

if (result3.done) return

result3.value.then(data => {
g.next(data)
})
})
})

以上代码进行一个递归优化:

function co(generator) {
const g = generator()

function handleResult(result) {
if (result.done) return // 生成器函数结束
result.value.then(data => {
handleResult(g.next(data))
}, error => {
g.throw(error)
})
}

handleResult(g.next())
}

co(main)

可见用Generator的方式需要一个co执行器辅助函数

Async / Await 语法糖:语言层面的异步编程标准(ES2017)

  • Async函数实际生成器函数一种更加方便的语法糖,写法和上面的实现非常类似
  • 返回一个Promise对象
async function main () {
try {
const users = await ajax('/api/users.json')
console.log(users)

const posts = await ajax('/api/posts.json')
console.log(posts)

const urls = await ajax('/api/urls.json')
console.log(urls)
} catch (e) {
console.log(e)
}
}

分类:

前端

标签:

JavaScript

作者介绍

l
laor
V1