今天看啥  ›  专栏  ›  Holyforsaken_FHC

JavaScript 异步编程

Holyforsaken_FHC  · CSDN  ·  · 2021-03-16 14:18

单线程的优势和弊端

js是单线程的,在浏览器中js的执行栈跟渲染线程是相互阻塞的。
单线程模式最大的优势就是 更安全,更简单
缺点也很明确,就是如果中间有一个特别耗时的任务,其他的任务就要等待很长的时间,出现假死的情况。
为了解决这种问题,js有两种任务的执行模式: 同步模式(Synchronous)和异步模式(Asynchronous)

同步模式与异步模式

同步模式 Synchronous

指的是代码的任务依次执行,后一个任务必须等待前一个任务结束才能开始执行。
程序的执行顺序和代码的编写顺序是完全一致的。
在单线程模式下,大多数任务都会以同步模式执行。
缺点 :因为是类似于排队的按次序执行,如果一旦某一个操作执行特别耗时,就会让页面卡顿,卡死,所以需要异步模式。

异步模式 Asynchronous

异步模式 不会去等待这个任务的结束才开始下一个任务,都是开启过后就立即往后执行下一个任务。 耗时函数的后续逻辑会通过回调函数的方式定义 。任务完成后,就会调用回调函数。
异步模式对于单线程的JavaScript 非常重要,如果没有这种模式,单线程的JavaScript 就无法同时处理大量的耗时任务。
而单线程下异步模式的最大难点在于代码执行的顺序混乱。

异步代码的执行方式简单来说:

  • js线程某个时刻发起了一个异步调用,它紧接着继续执行其他的任务,
  • 此时异步线程会单独执行异步任务,执行过后会将回调放到消息队列中,
  • js主线程执行完任务过后会依次执行消息队列中的任务。

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

回调函数

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

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

foo(function() {
    console.log('这就是一个回调函数')
    console.log('调用者定义这个函数,执行者执行这个函数')
    console.log('其实就是调用者告诉执行者异步任务结束后应该做什么')
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

Promise 异步方案

Promise概述

回调的多重嵌套,会导致代码可读低、编写费劲、容易出错,故而被称为 callback hell。
为了避免这个问题。CommonJS社区提出了Promise的规范,ES6中称为语言规范。

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

  • Pending 待定状态, 表示还不清楚异步执行的结果 是成功还是失败。
  • Fulfilled 完成状态, 表示操作已经完成
  • Rejected 操作失败

Rromise状态

Promise基本用法

返回resolve

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

promise.then((value) => {
  console.log('resolved', value) // resolve 100
},(error) => {
  console.log('rejected', error)
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

返回reject

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

promise.then((value) => {
  console.log('resolved', value)
},(error) => {
  console.log('rejected', error)
  // rejected Error: promise rejected
  //  at E:\professer\lagou\Promise\promise-example.js:4:10
  //  at new Promise (<anonymous>)
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

即便promise中没有任何的异步操作,then方法的回调函数仍然会进入到事件队列中排队。

Promise 的链式调用

  • promise对象then方法,返回了全新的promise对象。可以再继续调用then方法,如果return的不是promise对象,而是一个值,那么这个值会作为resolve的值传递,如果没有值,默认是undefined
  • 后面的then方法就是在为上一个then返回的Promise注册回调
  • 前面then方法中回调函数的返回值会作为后面then方法回调的参数
  • 如果回调中返回的是Promise,那后面then方法的回调会等待它的结束
// promise假设是一个Promise实例
// then中的回调return了一个Promise实例
promise.then(()=>{
	return new Promise((resolve,reject)=>{
		// some code
		resolve("实参A")
	} )
}).then( (接收实参A)=>{ 作为上一个then中的回调 } )


// then中回调return的不是promise实例
promise.then(()=>{ return 123 })
	   .then(val=>{ console.log(val) // 123 })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

Promise异常处理

then中回调的onRejected方法

也就是then中的第二个回调函数

.catch()(推荐)

如果then中没有传入第二个回调 那么异常会进入catch的回调处理

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

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

全局对象上的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对象
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

两个静态方法

Promise.resolve

  • 不带参数 返回一个Fulfilled状态的Promise对象 后面.then执行 then中的第一个回调
  • 参数为简单数据类型 返回一个Fulfilled状态的Promise对象 后面.then执行 then中的第一个回调,并且把该参数作为实参传入给回调
  • 如果传入的是一个 Promise 对象,Promise.resolve 方法原样返回
  • 如果传入的是带有一个跟 Promise 一样的 then 方法的对象,Promise.resolve 会将这个对象作为 Promise 执行
Promise.resolve('foo')
  .then(function (value) {
    console.log(value)
  })

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)
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

Promise.reject

Promise.reject()方法返回一个带有拒绝原因的Promise对象

// Promise.reject 传入任何值,都会作为这个 Promise 失败的理由

 Promise.reject(new Error('rejected'))
  .catch(function (error) {
     console.log(error)
   })

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

Promise 并行执行

.all()
  • Promise.all 方法接受一个数组作参数,数组中的对象(p1、p2、p3)均为promise实例(如果不是一个promise,该项会被用Promise.resolve转换为一个promise)。返回一个全新的Promise实例,它的状态由这三个promise实例决定 只有都fulfilled 新的Promise才会进入 fulfilled
.race()
  • Promise.race方法同样接受一个数组作参数。当p1, p2, p3中有一个实例的状态发生改变(变为fulfilled或rejected),p的状态就跟着改变。并把第一个改变状态的promise的返回值,传给p的回调函数

Promise的执行时序

如何简单理解宏任务和微任务
比如:你在银行排队办理业务
那么队列中的每一个人都可以看做是一个宏任务
当排到你的时候 你告诉柜员你需要 办卡,存钱,转账,这些就是微任务。柜员不会让你办完一个业务就重新排一次队 而是一次性把你的微任务全处理完。然后才会轮到下一个人,也就是下一个宏任务。

宏任务 (macro) task

(macro) task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
宏任务包括

script(整体代码,同步代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

微任务 (micro) task

(micro)task,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
微任务包括

Promise.then
Object.observe
MutaionObserver
process.nextTick(Node.js 环境)
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

判断代码执行结果

// 微任务

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')

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

结果是:

// 同步代码看做是一次宏任务,执行结束后 执行该宏任务阶段产生的所有微任务 也就是promise中的then, 然后再执行下一个宏任务
global start
global end
promise
promise 2
promise 3
setTimeout
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Generator 异步方案、Async/Await 语法糖

Generator 生成器函数

Generator 函数可以暂停执行和恢复执行。除此之外,它还有两个特性:函数体内外的数据交换和错误处理机制。
next方法不传递参数返回一个对象,next方法传入参数把参数传给函数内部
next 方法返回值的 value 属性,是 Generator 函数向外输出数据;next 方法还可以接受参数,这是向 Generator 函数体内输入数据。

function* gen(x){
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

上面代码中,第一个 next 方法的 value 属性,返回表达式 x + 2 的值(3)。第二个 next 方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量 y 接收。因此,这一步的 value 属性,返回的就是2(变量 y 的值)。

Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。

function* gen(x){
  try {
    var y = yield x + 2;
  } catch (e){ 
    console.log(e);
  }
  return y;
}

var g = gen(1);
g.next();
g.throw'出错了';
// 出错了
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

上面代码的最后一行,Generator 函数体外,使用指针对象的 throw 方法抛出的错误,可以被函数体内的 try … catch 代码块捕获。

Generator 配合 Promise 的异步方案在开发中 可以使用 co 这个库
当然, **“大人,时代变了”**我们现在用 async / await

Async/Await 语法糖

这个大家已经用的比较多了 就不再赘述了

基本用法

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)
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

异常处理

  • 让await后面的Promise对象自己catch
  • 也可以让外面的async函数返回的Promise对象统一catch
  • 像同步代码一样,放在一个try…catch结构中;



原文地址:访问原文地址
快照地址: 访问文章快照