异步编程-学习总结


概述

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

这种模式最大的优势就是更安全,更简单,缺点也很明确,就是如果中间有一个特别耗时的任务,其他的任务就要等待很长的时间,出现假死的情况。

为了解决这种问题,js有两种任务的执行模式:同步模式(Synchronous)和异步模式(Asynchronous)

同步模式

同步模式 :指的是代码的任务依次执行,后一个任务必须等待前一个任务结束才能开始执行。程序的执行顺序和代码的编写顺序是完全一致的。在单线程模式下,大多数任务都会以同步模式执行。同时,为了避免耗时函数让页面卡顿和假死,所以还有异步模式。

异步模式

异步模式 不会去等待这个任务的结束才开始下一个任务,都是开启过后就立即往后执行下一个任务。耗时函数的后续逻辑会通过回调函数的方式定义。在内部,耗时任务完成过后就会自动执行传入的回调函数。对开发者而言,单线程下面的异步最大的难点就是代码执行的顺序混乱。

宏任务、微任务及EventLoop:

js线程某个时刻发起了一个异步调用,它紧接着继续执行其他的任务,此时异步线程会单独执行异步任务,执行过后会将回调放到消息队列中,js主线程执行完任务过后会依次执行消息队列中的任务。这里要强调,js是单线程的,浏览器不是单线程的,有一些API是有单独的线程去做的。

补充:浏览器内核多线程包含:

1.GUI 渲染线程:

  • (1)负责浏览器界面的渲染,解析 HTML、CSS,构建 DOM 树和 RenderObject 树,布局和绘制等;
  • (2) 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时该线程会执行;
  • 注意:GUI 渲染线程和 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起,GUI 更新会保存在一个队列中等 JS 引擎空闲时立即执行。

2.JS 引擎线程:

  • JS 内核,负责处理 JavaScript 脚本程序(V8 引擎)
  • 负责解析 JavaScript 脚本,运行代码;
  • JS 引擎一直等待着任务队列中的任务到来,然后加以处理,一个 tab 页面(renderer 进程)中无论什么时候都只有一个 JS 线程在运行 JS 程序;
  • 注意:由于 GUI 渲染线程和 JS 引擎线程是互斥的,所以如果 JS 程序运行时间过长,这样会导致页面渲染不连贯,导致页面渲染加载阻塞;

3. 事件触发线程:

  • 归属于浏览器,而不是 JS 引擎,用来控制事件循环;
  • 当 JS 引擎执行代码块如 setTimeOut 时(也可以来自浏览器内核的其他线程,如鼠标单击事件、AJAX 异步请求等),会将对应的任务添加到事件线程中;
  • 当对应的事件符合触发条件被触发时,该线程就会把事件添加到 JS 的待处理队列的队尾,等待 JS 引擎的处理;
  • 注意:由于 JS 的单线程的关系所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行)。

4. 定时触发器线程:

  • setTimeOut 与 setInterval 所在的线程;
  • 浏览器的定时计数器并不是由 JavaScript 引擎计数的,(因为 JavaScript 是单线程,如果处于阻塞状态就会影响计时的准确)因此通过单独的线程来计时并触发定时(计时完毕后,添加到事件队列,等待 JS 引擎空闲时执行)

5. 异步 http 请求线程:

  • 在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求的
  • 将检测到状态变更时,如果设置有回调函数,异步线程就将产生状态变更事件,将这个回调在放到事件队列中,再由 JavaScript 引擎执行。

回调函数

回调函数是所有异步编程的根基。回调函数是由调用者定义,交给执行者执行的函数。

Promise概述

虽然回调函数是所有异步编程方案的根基。但是如果直接使用传统回调方式去完成复杂的异步流程,就会无法避免大量的回调函数嵌套。导致回调地狱的问题。为了避免这个问题。CommonJS社区提出了Promise的规范,ES6中称为语言规范。

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

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

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

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

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

Promise常见误区

嵌套使用的方式是使用Promise最常见的误区。要使用promise的链式调用的方法尽可能保证异步任务的扁平化。(最佳解决方案是用async和await。)

Promise链式调用

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

Promise异常处理

promise中如果有异常,都会调用reject方法,还可以使用.catch(),而使用.catch方法更为常见,因为.catch是给整个promise链条注册的一个失败回调。更推荐使用!

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函数处理中有报错是捕获不到的。

Promise静态方法

Promise.resolve():传入一个对象,其中有then属性以及对应的回调函数并且可被.then调用(thenable)。可用来将第三方的Promise对象转成原生的Promise。

Promise.reject():无论传入什么参数,在.catch调用后其参数就是其失败的原因。

Promise并行执行

Promise.all():传入一个Promise数组,该promise数组中若没有reject则resolve对应的Promise数组中的所有Promise并返回结果数组,有reject则reject那个原因。(等待所有任务结束)

Promise.race():同样传入一个Promise数组,只会等待第一个结束的任务并返回结果。

Promise执行时序

Promise的回调是作为微任务执行的,会在本轮宏任务结束后执行。

常见的宏任务有: script(整体代码),setTimeout,setInterval,I/O,UI交互事件,postMessage,MessageChannel,setImmediate(Node.js 环境) 。

常见的微任务有: Promise.then,Object.observe,MutaionObserver,process.nextTick(Node.js 环境)。

Generator异步方案

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。

基本用法:

function* doSomething() {
    yield '吃饭'
    return '睡觉'
}

let newDoSomething = doSomething() // 自己执行不了,需要指向一个状态机

console.log(newDoSomething.next()) // {value: "吃饭", done: false}
console.log(newDoSomething.next()) // {value: "睡觉", done: true}

从上面的例子可以看出来,Generator 函数有四个特点:

1、function 后面有个小 *,这个地方有两种写法,没啥太大区别;

function* doSomething(){}

function *doSomething(){}

2、函数里面会有一个 yield,把函数截成不同的状态;

一个yield可以截成两个状态,也就需要两个next()触发;

3、Generator 函数自己不会执行,而是会返回一个生成器对象;

4、生成器对象会通过. next() 方法依次调用各个状态。

消息传递:

Generator 函数除了能控制函数分状态的执行,还有一个非常重要的作用就是消息传递。

举个简单栗子:

function *doSomething() {
    let x = yield 'hhh'
    console.log(x)
    return (x * 2)
}

let newDoSomething = doSomething()

console.log(newDoSomething.next(1))  //{value: "hhh", done: false}
console.log(newDoSomething.next(2))  //{value: 4, done: true}

分析打印内容:

//{value: "hhh", done: false}
第一个next()是Generator函数的启动器
这个时候打印的是yield后面的值
注意:yield后面的值并不会赋值给x

 //{value: 4, done: true}
暂停执行的时候,yield表达式处可以接收下一个启动它的next(...)传进来的值
可以理解为:这时第二个next传入的参数会把第一个yield的值替换掉
这个时候,x被赋值2,所以打印2*2

注意几个问题:

第一个 next() 是用来启动 Generator 函数的,传值会被忽略,没用

yield 的用法和 return 比较像,你可以当做 return 来用,如果 yield 后没值,return undefined

最后一个 next() 函数,得到的是函数 return 的值,如果没有,也是 undefined。

看个特殊情况:

function *doSomething() {
    let x = yield 'hhh'
    console.log(x)
    let y = yield (x + 3)
    console.log(y)
    let z = yield (y * 3)
    return (x * 2)
}

let newDoSomething = doSomething()

console.log(newDoSomething.next(1))
console.log(newDoSomething.next(2))
console.log(newDoSomething.next())
console.log(newDoSomething.next())

输出打印结果:

{value: "hhh", done: false}
{value: 5, done: false}
undefined
{value: NaN, done: false}
{value: 4, done: true}

为什么打印 undefined?分析:

1、第一个next(1)传进去的1,没有起任何意义,打印的{value: “hhh”, done: false};

2、第二个next(2)传进去的2,所以x会打印2,第二个next(2)打印2+3=5;

3、第三个next()传进去的是空,那么y打印的就是undefined,undefined*3那肯定就是NaN;

4、第四个next()传进去的是空,但是return的是x,刚才说了x是2,那打印的是2*2=4。

async、await函数

async、await 是 Generator 函数的语法糖,原理是通过 Generator 函数加自动执行器来实现的,这就使得 async、await 跟普通函数一样了,不用再一直 next 执行了。

他吸收了 Generator 函数的优点,可以通过 await 来把函数分状态执行,但是又不用一直 next,可以自动执行。

函数前加async修饰符,代表这个函数是一个异步函数。async函数返回的是一个promise对象,函数内部return返回的值,会成为then方法回调函数的参数。

await是一个运算符,用于组成表达式,它会阻塞后面的代码,且await只能写在async函数内。await如果等到的是Promise对象,则得到其resolve值。await如果等到的不是promise对象,就得到一个表达式的运算结果。


文章作者: 智升
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 智升 !
评论
  目录