JavaScript异步编程

347 阅读9分钟

概述

单线程:JS执行环境中负责执行代码的线程只有一个 (一个人执行代码,任务排队执行) 优点:安全、简单 缺点:耗时任务等待结束,导致程序执行拖延,造成假死

  • 同步模式 synchronous
  • 异步模式 asynchronous

同步模式

特点:排队执行 JS内部有一个调用栈call-stack,执行代码时,会首先压入一个匿名的函数(anonymous),便开始逐行执行每行的代码

  • 函数的声明并不会调用 当程序运行结束的时候,调用栈会被清空

JS在工作中维护了一个正在执行工作表,记录我们正在做的事情,被清空,就结束

缺点:某行代码执行过长,会延迟,称为堵塞

异步模式

  • 耗时任务开始便马上执行下一个任务
  • 逻辑一般由回调函数的方式定义 特点:代码执行顺序混乱

(以下没有先后执行顺序!) 内部API环境(web环境就是web APIs),异步函数一般会放入此处,当异步函数运行完成时,会将其送入消息队列中 消息队列Queue(回调队列)中的任务会排队等待事件循环 Event Loop监听调用栈和消息队列,如果调用栈结束,事件循环会从消息队列中取出第一个回调函数压入调用栈call stack 调用栈接收异步任务时又开始新的一轮调用,但也同时执行手头上的任务

同步和异步的差别:运行环境提供的API是否以同步还是异步模式的方式工作

回调函数

所有异步编程方案的根基

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

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数

缺点:复制的异步流程会使用大量的回调函数,会导致回调地狱问题

回调函数补充

  • 回调函数在使用上是把一个函数当成参数传给另一个函数,在另一个函数中作为返回结果
function outPutPhone(name) {
    console.log(name + "电话:10000");
}

//使用回调函数的函数
function searchInfo(name, callback) {
    callback(name);
}

//=>将outPutPhone作为参数传递
searchInfo("tom", outPutPhone);

Promise

概述

Promise 物件代表一個即將完成、或失敗的非同步操作,以及它所產生的值

一個 Promise 物件處於以下幾種狀態:

  • 擱置(pending):初始狀態,不是 fulfilled 與 rejected。
  • 實現(fulfilled):表示操作成功地完成。
  • 拒絕(rejected):表示操作失敗了。

一种更优的异步编程统一方案

表示一个异步任务究竟是成功还是失败,内部对外界进行的承诺

graph LR
promise-->pendind
pendind-->fulfilled
pendind-->Rejected
fulfilled-->onFulfilled
Rejected-->onRejected

好处

  • 将复杂的异步处理轻松地进行模式化
  • 代码更清晰
  • 异常处理更方便
  • 代码链式操作,

基本用法

then() 方法回傳一個 Promise 物件。它接收兩個引數: Promise 在成功及失敗情況時的回呼函式

语法:p.then(onFulfilled[, onRejected]);

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')//被率先打印

使用案例

ajax和promise结合:

// 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 () {
      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)
})

AJAX补充知识

定义:JAX 代表 Asynchronous JavaScript And XML,即非同步 JavaScript 及 XML

通俗:AJAX 使用XMLHttpRequest物件來與伺服器進行通訊

作用:AJAX可以传送并接收多种格式的咨询,JSON XML HTML等

常见误区

  • 在ajax请求中,如果有多个请求嵌套,还是会出现回调地狱,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方法链式调用的特点处理

链式调用

Promise链条

  //链式调用then方法
ajax('/api/users.json')
  .then(function (value) {
    console.log(1111)
    return ajax('/api/urls.json')//ajax执行完毕后会自动执行下一个then
  })
  .then(function (value) {
    console.log(2222)
    console.log(value)
    return ajax('/api/urls.json')
  })
  .then(function (value) {
    console.log(3333)
  })
  .then(function (value) {
    console.log(4444,value)//value为空值
    return 'foo'
  })
  .then(function (value) {
    console.log(5555)
    console.log(value)//拿到的是上一个then返回的值foo
  })

/**
* 每个then方法为上一个then方法返回的promise对象去添加状态明确过后的回调
* 每个then方法返回的是一个全新的promise对象
*/

总结:

  • Promise对象的then方法会返回一个全新的Promise对象

  • 后面的then方法是在为上一个then返回的Pormise注册回调

  • 前面then方法中回调函数的返回值会作为后面then方法回调的参数

  • 如果回调中返回的是Promise,那后面then方法的回调会等待它的结束

异常处理

手动抛出一个异常throw new Error(),onRejected也可以捕获

ajax('/api/users11.json')
  .then(function onFulfilled (value) {
    console.log('onFulfilled', value)
    return ajax('/error-url')
    //此时注册在上一个then里的onRejected就无法捕获异常
  }, function onRejected (error) {
    console.log('onRejected', error)
  })
  • 同时注册的 onRejected 只是给当前 Promise 对象注册的失败回调,它只能捕获到当前 Promise 对象的异常

  • 使用 catch 注册失败回调是更常见的

    ajax('/api/users11.json')
      .then(function onFulfilled (value) {
        console.log('onFulfilled', value)
        return ajax('/error-url')
        //此时onRejected可以捕获异常
      })
      .catch(function onRejected (error) {
        console.log('onRejected', error)
      })
    
    //then(onRejected) 实际上就相当于 then(undefined, onRejected)
    
  • 分析:因为 Promise 链条上的任何一个异常都会被一直向后传递,直至被捕获

  • 分开注册的 onRejected 相当于给整个 Promise 链条注册失败回调

  • 全局也可以注册unhandledrejection事件处理没有手动捕获的异常,process内注册。但编程习惯应该在每个代码中明确捕获每个可能的异常。

静态方法

**Promise.resolve()**快速把值转化成一个对象

此函数将类promise对象的多层嵌套展平

  • 如果这个值是一个 promise ,那么将返回这个 promise

  • 如果这个值是thenable(即带有**"then"**方法),返回的promise会“跟随”这个thenable的对象,采用它的最终状态

  • 否则返回的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)
console.log(promise === promise2)
  • 如果传入的是带有一个跟 Promise 一样的 then 方法的对象,Promise.resolve 会将这个对象作为 Promise 执行
Promise.resolve({
  then: function (onFulfilled, onRejected) {
    onFulfilled('foo')
  }//它是一个thenable的对象
})
.then(function (value) {
  console.log(value)
})

**Promise.reject()**会创建一个一定失败的Promise对象

无论传入什么参数,都会作为Promise失败的理由

并行处理

Promise.all()可以合并多个Promise对象,等待所有任务结束后才会结束

Promise.race()可以合并多个Promise对象,只会等待第一个结束的任务

执行时序

微任务

//微任务
console.log('s1')

setTimeout(() => {
    console.log('setTime')
}, 0)
//会立即进入到队列中排序,因为时间设置为0,等待一轮执行

Promise.resolve()
	.then(() => {
    console.log('p1')
})
	.then(() => {
    console.log('p2')
})
	.then(() => {
    console.log('p3')
})
console.log('s2')
//s1 s2 p1 p2 p3 setTime
//回调依次往下执行

宏任务

回调队列中的任务称之为宏任务

  • 宏任务在执行过程中可以临时加上一些额外的需求,可以选择作为一个新的宏任务进到队列中排队
  • 上述代码中,setTimeout就是作为宏任务进入到队列中排队
  • 但也可以作为当前任务的微任务,直接在当前任务结束后立即执行
  • Promise的回调作为微任务执行,会在==本轮调用结束的末尾==去自动执行

微任务优点:提高整体的响应能力

绝大多数异步调用都是作为宏任务执行的

Promise & MutationObserver / process.nextTick 都作为微任务

Generator

概念

ES2015提供,生成器函数

  • 生成器函数会自动返回一个生成器对象,调用它的next方法才会让这个函数体开始执行
  • 一旦中途遇到yield关键字,便会暂停执行,继续next便会继续执行
function * foo () {
  console.log('start')
    
    try{
        const res = yield 'foo'
    //向外返回一个foo值
    //yield暂停了函数执行,直到外界下一次去调用generator.next()
    	console.log(res)//res = bar
    }catch(e){
        console.log(e)
        //输出error
    }
    
}

const generator = foo()
//generator就是生成器对象

const result = generator.next()
//手动调用next方法才会开始执行函数的函数体
console.log(result)
//可以拿到yield返回的值,也可以拿到一个done值,表示了生成器是否已经执行完了


generator.next('bar')
//如果调用了生成器的next传了参,那么这个参数会作为yield的返回值

generator.throw(new Error('error'))
//可以对生成器内部抛出一个异常

异步方案

借助yield

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是一个promise对象,可以调用then处理请求结果
result.value.then(data => {
  const result2 = g.next(data)
  //继续请求next去进行yeild下面的代码,并且把得到的数据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)
    })
  })
})

递归无止尽地调用yeild:

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

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)

特点:使异步调用回归扁平化

Async/Await语法糖

将 * 函数替换成async,将yeild替换成await


本文首发于我的GitHub博客,其它博客同步更新。