JavaScript异步编程

我心飞翔 分类:javascript

在前端快速发展的今天,如果不能时刻保持学习就会很快被淘汰。分享一下对JavaScript异步编程相关知识的学习,文章有点长,希望对大家有所帮助。每天进步一点点。

1、背景知识

​ 在最早的时候JavaScript就是运行在浏览器端的语言,主要目的是为了实现页面上的动态交互。实现页面交互的核心就是DOM操作,决定了它必须使用单线程模型,否则就会出现很复杂的线程同步问题。

​ 如果在JavaScript中有多个线程,一个线程修改了这个DOM元素,同时另一个线程删除了这个元素,此时浏览器就无法明确该以哪个工作线程为准。所以为了避免这种问题,从一开始,JavaScript就设计成了单线程的工作模式。【即:JavaScript执行环境中负责执行代码的线程只有一个】

2、单线程的优点和缺点

优点:更安全【执行环境相对单纯】,更简单【实现起来比较简单】

缺点:如果中间有一个特别耗时的任务,其他的任务就要等待很长的时间,出现卡顿、假死的情况

为了解决单线程的缺点,JavaScript将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

3、异步编程的内容概要

  • 同步模式与异步模式
  • 事件循环与消息队列(JavaScript如何实现异步模式)
  • 异步编程的几种方式
  • Promise异步方案、宏任务/微任务队列
  • Generator异步方案、Async/Await语法糖

<1> 同步模式与异步模式

"同步模式"就是后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的,在单线程模式下,大多数任务都会以同步模式执行。

console.log('global begin')
function bar () {
    console.log('bar task') 
}
function foo () {
    console.log('foo task')
    bar()
}
foo()
console.log('global end')
// global begin
// foo task
// bar task
//global end
// 使用调用栈的逻辑
 

"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。

“异步模式”对于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
//除了调用栈,还用到了消息队列和事件循环
 

执行过程分析:同步任务进栈,执行,出栈;异步任务进栈,放入运行环境 API 开始倒计时,倒计时结束后放入消息队列等待进栈;当调用栈中没有任务后,事件循环从消息队列取出异步任务,将异步任务进栈,执行,出栈,事件循环重复上面的步骤

这里要强调,JavaScript是单线程的,浏览器不是单线程的,有一些API是有单独的线程去做的。

这里的同步和异步不是指写代码的方式,而是运行环境提供的API是以同步或异步模式的方式工作。

同步模式的API的特点就是任务执行完代码才会继续往下走,例如:console.log

异步模式的API的特点就是下达这个任务开启的指令之后代码就会继续执行,代码不会等待任务的结束,例如:setTimeout

<2> 回调函数

【回调函数是所有异步编程方案的根基】

回调函数就是由调用者定义,交给执行者执行的函数

// callback就是回调函数
// 就是把函数作为参数传递,缺点是不利于阅读,执行顺序混乱。
function foo(callback) {
    setTimeout(function(){
        callback()
    }, 3000)
}

foo(function() {
    console.log('这就是一个回调函数')
    console.log('调用者定义这个函数,执行者执行这个函数')
    console.log('其实就是调用者告诉执行者异步任务结束后应该做什么')
})
 

还有其他的一些实现异步的方式,例如:事件机制和发布订阅。这些也都是基于回调函数之上的变体。

<3> 宏任务/微任务

宿主环境提供的方法是宏任务,JavaScript引擎自身提供的是微任务

宏任务:I/O、setTimeout、setInterval、requestAnimationFrame、setImmediate

微任务:Promise、MutationObserver、process.nextTick

微任务在先,宏任务在后。在当前执行过程中,如果加入了微任务,会自动放到当前执行栈的最后,依次向下执行;在当前执行过程中,如果加入了宏任务,则会放到运行环境API中开始计时,等待计时结束后放到消息队列里面,直到当前执行栈中的所有任务都执行完毕后,事件循环会拿出消息队列里面的宏任务放入到当前执行栈中开始执行。

4、异步编程统一方案promise

<1> Promise概述

虽然回调函数是所有异步编程方案的根基。但是如果我们直接使用传统回调方式去完成复杂的异步流程,就会无法避免大量的回调函数嵌套。导致回调地狱的问题。

为了避免这个问题。CommonJS社区提出了Promise的规范,ES6中称为语言规范。

Promise是一个对象,用来表述一个异步任务执行之后是成功还是失败

<1> Promise基本用法

返回resolve

const promise = new Promise((resolve, reject) => {
  resolve(100)
})

promise.then((value) => {
  console.log('resolved', value) // resolve 100
},(error) => {
  console.log('rejected', error)
})
 

返回reject

const promise = new Promise((resolve, reject) => {
  reject(new Error('promise rejected'))
})

promise.then((value) => {
  console.log('resolved', value)
},(error) => {
      console.log('rejected', error.message)	// rejected promise rejected
})
 

<2> Promise案例

使用Promise去封装一个ajax的案例

function ajax (url) {
  return new Promise((resolve, rejects) => {
    // 创建一个XMLHttpRequest对象去发送一个请求
    const xhr = new XMLHttpRequest()
    // 先设置一下xhr对象的请求方式是GET,请求的地址就是参数传递的url
    xhr.open('GET', url)
    // 设置返回的类型是json,是HTML5的新特性
    // 我们在请求之后拿到的是json对象,而不是字符串
    xhr.responseType = 'json'
    // html5中提供的新事件,请求完成之后(readyState为4)才会执行
    xhr.onload = () => {
      if(this.status === 200) {
        // 请求成功将请求结果返回
        resolve(this.response)
      } else {
        // 请求失败,创建一个错误对象,返回错误文本
        rejects(new Error(this.statusText))
      }
    }
    // 开始执行异步请求
    xhr.send()
  })
}

ajax('/api/user.json').then((res) => {
  console.log(res)
}, (error) => {
  console.log(error)
})
 

<3> Promise的本质

本质上也是使用回调函数的方式去定义异步任务结束后所需要执行的任务。这里的回调函数是通过then方法传递过去的

<4> Promise链式调用

常见误区

  • 嵌套使用的方式是使用Promise最常见的误区。要使用promise的链式调用的方法尽可能保证异步任务的扁平化。

链式调用的理解

  • promise对象then方法,返回了全新的promise对象。可以再继续调用then方法,如果return的不是promise对象,而是一个值,那么这个值会作为resolve的值传递,如果没有值,默认是undefined
  • 后面的then方法就是在为上一个then返回的Promise注册回调
  • 前面then方法中回调函数的返回值会作为后面then方法回调的参数
  • 如果回调中返回的是Promise,那后面then方法的回调会等待它的结束

<5> Promise异常处理

then中回调的onRejected方法

.catch()(推荐

promise中如果有异常,都会调用reject方法,还可以使用.catch()

使用.catch方法更为常见,因为更加符合链式调用

ajax('/api/user.json')
  .then(function onFulfilled(res) {
    console.log('onFulfilled', res)
  }).catch(function onRejected(error) {
    console.log('onRejected', error)
  })
  
// 相当于
ajax('/api/user.json')
  .then(function onFulfilled(res) {
    console.log('onFulfilled', res)
  })
  .then(undefined, function onRejected(error) {
    console.log('onRejected', error)
  })
 

.catch形式和前面then里面的第二个参数的形式,两者异常捕获的区别:

  • .catch()是对上一个.then()返回的promise进行处理,不过第一个promise的报错也顺延到了catch中,而then的第二个参数形式,只能捕获第一个promise的报错,如果当前then的resolve函数处理中有报错是捕获不到的。

所以.catch是给整个promise链条注册的一个失败回调。推荐使用!!!!

全局对象上的unhandledrejection事件

还可以在全局对象上注册一个unhandledrejection事件,处理那些代码中没有被手动捕获的promise异常,当然并不推荐使用

更合理的是:在代码中明确捕获每一个可能的异常,而不是丢给全局处理

// 浏览器
window.addEventListener('unhandledrejection', event => {
  const { reason, promise } = event
  console.log(reason, promise)

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

  event.preventDefault()
}, false)

// node
process.on('unhandledRejection', (reason, promise) => {
  console.log(reason, promise)

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

5、手写Promise源码

思路步骤:

1、Promise核心逻辑实现

分析其原理

<1> promise就是一个类, 在执行类的时候需要传递一个执行器进去,执行器会立即执行
<2> Promise中有三种状态,分别为成功-fulfilled 失败-rejected 等待-pending
	pending -> fulfilled
	pending -> rejected
	一旦状态确定就不可更改
<3> resolve 和 reject函数是用来更改状态的
	resolve:fulfilled
	reject:rejected
<4> then方法内部做的事情就是判断状态
	如果状态是成功,调用成功回调函数
	如果状态是失败,就调用失败回调函数
	then方法是被定义在原型对象中的
<5> then成功回调有一个参数,表示成功之后的值;then失败回调有一个参数,表示失败后的原因
 

2、在 Promise 类中加入异步逻辑

3、实现 then 方法多次调用添加多个处理函数

4、实现then方法的链式调用

5、then方法链式调用识别 Promise 对象自返回

6、捕获错误及 then 链式调用其他状态代码补充

<1> 捕获执行器的错误
<2> then执行的时候报错捕获
<3> 错误之后的链式调用
<4> 异步状态下链式调用
 

7、将then方法的参数变成可选参数

8、promise.all方法的实现

9、Promise.resolve方法的实现

10、finally 方法的实现

11、catch方法的实现

代码如下:

// 定义常量是为了复用且代码有提示
const PENDING = 'pending'     // 等待
const FULFILLED = 'fulfilled' // 成功
const REJECTED = 'rejected'   // 失败
// 定义一个构造函数
class MyPromise {
// 构造器
constructor (exector) {
// 捕获错误,如果有错误就执行reject
try {
// exector 是一个执行器,进入会立即执行,并传入resolve 和 reject 方法
exector(this.resolve, this.reject)
} catch (e) {
this.reject(e)
}
}
status = PENDING        // 实例对象的一个属性,初始化为等待状态
value = undefined       // 成功之后的值
reason = undefined      // 失败之后的原因
successCallback = []    // 定义成功的回调,可能存在多个回调,使用数组保存
failCallback = []       // 定义失败的回调,可能存在多个回调,使用数组保存
// 使用箭头函数可以让this 指向当前实例对象,如果直接调用,普通函数this指向的是Windows或者undefined
resolve = value => {
// 判断当前状态,如果不是等待就直接返回
if(this.status !== PENDING) return
this.status = FULFILLED   // 将状态改为成功
this.value = value        // 保存成功之后的值
// 判断成功的回调是否存在,存在就调用
// 调用的时候不需要传值,因为下面push到里面的时候已经处理好了
while(this.successCallback.length) this.successCallback.shift()()
}
reject = reason => {
// 判断当前状态,如果不是等待就直接返回
if(this.status !== PENDING) return
this.status = REJECTED    // 将状态改为成功
this.reason = reason        // 保存成功之后的值
// 判断失败的回调是否存在,存在就调用
// 调用的时候不需要传值,因为下面push到里面的时候已经处理好了
while(this.failCallback.length) this.failCallback.shift()()
}
// 如果有回调就选择回调,如果没有回调就传一个函数,把参数传递
then (successCallback = value => value, failCallback = reason => {throw reason}) {
let promise2 = new MyPromise((resolve, reject) => {
// 状态判断
if(this.status === FULFILLED) {     // 状态为成功调用成功的回调
// 因为执行之后才有promise2,所以需要异步执行
setTimeout(() => {
try {
// x是上一个promise回调函数的return返回值
let x = successCallback(this.value)
// 去判断x是一个普通值还是promise对象,并决定调用resolve还是reject
// 判断 新创建的promise2 和 上一个 x 是否相等
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
} else if (this.status === REJECTED) {     // 状态为失败调用失败的回调
// 因为执行之后才有promise2,所以需要异步执行
setTimeout(() => {
try {
// x是上一个promise回调函数的return返回值
let x = failCallback(this.reason)
// 去判断x是一个普通值还是promise对象,并决定调用resolve还是reject
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
} else {
// 状态为等待,需要将成功的回调和失败的回调都先暂时存储起来
// 等到执行成功或者失败的时候再传递执行
this.successCallback.push(() => {   // 将成功的回调保存在数组中
setTimeout(() => {
try {
let x = successCallback(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
this.failCallback.push(() => {   // 将失败的回调保存在数组中
setTimeout(() => {
try {
let x = failCallback(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
}
});
return promise2
}
finally (callback) {
// 使用then方法拿到当前的promise的状态,而且不管怎样都返回callback
// then方法就是返回一个promise对象,所以直接返回then方法调用之后的结果即可
// 在回调callback之后,成功的回调返回value,失败的回调抛出原因
// 如果callback是一个异步的promise对象,我们还需要等待其执行完毕,所以需要用到静态方法resolve
return this.then(value => {
// 把callback调用之后返回的promise传递过去,并且执行promise,且在成功之后返回value
return MyPromise.resolve(callback()).then(() => value)
}, reason => {
// 失败之后调用的then方法,然后把失败的原因返回出去
return MyPromise.resolve(callback()).then(() => { throw reason })
})
}
catch (failCallback) {
return this.then(undefined, failCallback)
}
static all (array) {
let result = []     // 结果数组
let index = 0       // 计数器
return new Promise((resolve, reject) => {
let addData = (key, value) => {
result[key] = value
index ++
// 如果计数器和数组长度相同,说明所有的元素都执行完毕了,就可以输出了
if(index === array.length) {
resolve(result)
}
}
// 对传递的数组进行遍历
for (let i = 0; i < array.lengt; i++) {
let current = array[i]
if (current instanceof MyPromise) {
current.then(value => addData(i, value), reason => reject(reason))
} else {
addData(i, array[i])
}
}
})
}
static resolve (value) {
// 如果是promise对象,就直接返回
if(value instanceof MyPromise) return value
// 如果是普通值就返回一个promise对象,并返回值
return new MyPromise(resolve => resolve(value))
}
}
function resolvePromise(promise2, x, resolve, reject) {
// 如果相等,说明return的是自己,抛出类型错误并返回
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
if(x instanceof MyPromise) {    // x 是promise对象
x.then(resolve, reject)
} else{     // x 是普通值
resolve(x)
}
}
module.exports = MyPromise

回复

我来回复
  • 暂无回复内容