异步编程篇(promise、generator、async)原理

260 阅读9分钟

1.promise

回调模式:我们用回调回调函数来封装程序中的continuation,然后把回调交给第三方,接着期待其能够调用回调,实现正确的功能。通过这种形式,我们要表达的意思是:“这是将来要做的事情,要在当前的步骤完成之后发生。”(即把控制权交出去,由程序决定何时调用)

promise:不把自己程序的continuation传给第三方,而是第三方给我们提供了解其任务何时结束的能力(resolve,reject),然后由我们自己的代码来决定下一步做什么(then)。(把控制权返回给代码,让其自行决定如何使用)

1.1 基本特性

promise对象具有两个内部属性:

  • state —— 最初是 pending (进行中),reslove被调用时变成 fulfilled(已完成),reject被调用时变成 rejected(已失败)
  • result —— 最初是 undefined,resolve(value) 被调用时变为 value,reject(error) 被调用时变为 error

1.2 API

Promise构建出来的实例方法:then()、catch()、finally()

Promise 构造函数方法:all()、race()、allSettled()、resolve()、reject()

Promise.resolve()

用于确保返回的是一个promise。如果传递一个非promise的立即值,就会得到一个用这个值填充的promise。而如果传递的是一个真正的promise,就会返回这个promise。

Promise.all()

用途:假设我们希望并行执行多个 promise,并等待所有 promise 都准备就绪。

promise.all 接收一个 promise 数组作为参数,并返回一个新的 promise。

返回的 promise 的状态由传入的所有 promise 的状态共同决定,只有都 fulfilled 才会 fulfilled,此时有一个被 rejected 。

Promise.allSettled()

如果任意的 promise reject,则 Promise.all 整个将会 reject。当我们需要所有结果都成功时,它对这种“全有或全无”的情况很有用。

当对结果不太关注,只关心所有的 promise 是否被 settle 时,可用 promise.allSettled()。

Promise.race()

race:竞赛。只等待其中一个 settled 的 promise 并获取其结果。

这个API可以用来解决promise本身永远不被settled的情况。

//用于超时一个promise的工具
function timeoutPromise(delay){
    return new Promise((resolve,reject) => {
        setTimeout(() => {
            reject("Timeout!")
        },delay);
    });
}
​
//决议foo()是否超时
Promise.race([
    foo(), 
    timeoutPromise(3000)
])
.then(
    onResolved(){
        //foo()及时完成
    },
    onRejected(err){
        //可能foo()被拒绝,也可能foo()没能按时完成
        //通过查看err来了解是哪种情况
    }
)

1.3 未来值

promise是一个与时间无关的值,不需要考虑其在时间方面是否可用。promise充当了一个可以在未来任意时间节点得到结果的值的占位符。未来值还有一个重要特性:它可能成功,也可能失败。

1.4 完成事件

单独的Promise展示了未来值的特性。但是,也可以从另一个角度看待Promise的settled:一种在异步任务中作为两个或更多步骤的流程控制机制,时序上的this-then-that。

promise的settled结果可能是拒绝也可能是完成。相对于完成值总是由编程给出来说,拒绝原因reason可能是程序逻辑直接设置的,也可能是从运行异常隐式得出的值。

并且一旦promise settled,它就永远保持在这个状态,成为不变值,可以根据需求多次查看。

promise至多有一个settled值(完成或拒绝)。如果没有用任何值显式settled,那么这个值就是undefined。并且不管这个值是什么,无论当前或未来,它都会被传给所有注册的回调。

1.5 链式调用

我们可以把多个Promise连接到一起表示一系列异步操作。

链式调用实现的关键在于,不管从then调用的onFulfilled返回值是什么,它都会被自动设置为链接Promise(中间变量)的完成。

MyPromise.prototype.then = function (onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : result => result;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
    return new MyPromise((resolve, reject) => {
        if (this.state === PENDING) {
            this.onFulfilledCallbacks.push(() => {
                try {
                    let res = onFulfilled(this.result);
                    resolve(res);
                } catch (error) {
                    reject(res);
                }
            });
            this.onRejectedCallbacks.push(() => {
                try {
                    let reason = onFulfilled(this.result);
                    resolve(reason);
                } catch (error) {
                    reject(error);    
                }
                }
            );
        }
        ......
    })
}

1.6 错误处理

  1. 隐式 try...catch

    promise的executor周围有一个隐式的try...catch,如果发生异常,异常就会被捕获,并视为reject进行处理。

    同样的promise的处理程序(onFulfilled,onRejected)周围也有一个隐式的try...catch,当发生异常,异常就会被捕获,并视为reject处理(因为then隐式返回promise,处理程序实际上是在这个promise的executor中执行的)。当promise被rejected后,控制权移交给最近的error处理程序。

    try {
        executor(resolve, reject);
    } catch (error) {
        reject(error);
    }
    
  2. 再次抛出

    如果我们在then中没有指定处理程序onRejected,即无法处理error,那么会将其再次抛出,控制权移交给最近的处理程序(即错误冒泡)。

    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
    

注:

  1. promise实际上并没有完全摆脱回调,它只是改变了传递回调的位置。我们并不是把回调传递给foo(),而是从foo()中得到一个promise,然后把回调传给这个promise。
  2. promise虽然能很好的解决回调地狱的问题,但是如果处理流程比较复杂的话,那么整段代码将充斥着then,语义化不明显,代码不能很好的表示执行流程。

2.可迭代对象

可迭代(Iterable) 对象是数组的泛化。这个概念是说任何对象都可以被定制为可在 for..of 循环中使用的对象。

Symbol.iterator 可迭代接口

我们有一个对象,它并不是数组,但是看上去很适合使用 for..of 循环。

比如一个 range 对象,它代表了一个数字区间:

let range = {
  from: 1,
  to: 5
};
​
// 我们希望 for..of 这样运行:
// for(let num of range) ... num=1,2,3,4,5

为了让 range 对象可迭代我们需要为对象添加一个名为 Symbol.iterator 的方法。

  1. 当 for..of 循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 迭代器(iterator) —— 一个有 next 方法的对象。
  2. 从此开始,for..of 仅适用于这个被返回的对象
  3. for..of 循环希望取得下一个数值,它就调用这个对象的 next() 方法。
  4. next() 方法返回的结果是 {done: Boolean, value: any},当 done=true 时,表示循环结束。
let range = {
  from: 1,
  to: 5,
​
  [Symbol.iterator]() {
    this.current = this.from;
    return this;
  },
​
  next() {
    if (this.current <= this.to) {
      return { done: false, value: this.current++ };
    } else {
      return { done: true };
    }
  }
};

js中很多内置类型都实现了Iterable接口:字符串、数组、map、set、arguments、NodeList等。

接收可迭代对象的原生语言特性:

for ...of、数组解构、拓展运算符、创建map,set、

Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable);

这些原生语言特性会在底层调用可迭代对象的工厂函数,从而创建一个迭代器。

3.生成器

严格说来,生成器本身并不是可迭代对象,但执行一个生成器,就得到了一个迭代器。

  • 生成器会在每次迭代中暂停,通过yield返回到主程序或事件循环队列中。
  • 并且生成器在每个yield处暂停时,生成器*foo()的状态(作用域)会被保持,不需要闭包来保持变量状态。
  • 生成器能使我们能够写出更短的迭代代码。
let range = {
  from: 1,
  to: 5,
​
  *[Symbol.iterator]() {
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

3.1 打破完全运行

生成器是ES6中新增的一种函数控制、使用的方案。

3.1.1 输入和输出

生成器具有控制函数什么时候继续执行、暂停执行的新的执行模式。但是它仍然是一个函数,因此它仍然具有函数的基本特性,如接受参数和返回值。

3.1.2 迭代消息传递

消息是双向传递的 ——(yield..)作为一个表达式可以发出消息响应next(..)调用,next(..)也可以向暂停的yield表达式发送值。

3.2 生成器的实现机制——协程

协程是一种比线程更加轻量级的存在。可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是A协程,要启动B协程,那么A协程就需要将主线程的控制权交给B协程,这就体现在A协程暂停执行,B协程恢复执行。

通常,如果从A协程启动B协程,我们就把A协程称为B协程的父协程。

协程不是被操作系统内核所管理,而完全是由程序所控制。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

function* genDemo() {
    console.log("开始执行第一段")
    yield 'generator 2'
​
    console.log("开始执行第二段")
    yield 'generator 2'
​
    console.log("开始执行第三段")
    yield 'generator 2'
​
    console.log("执行结束")
    return 'generator 2'
}
​
console.log('main 0')
let gen = genDemo() // 创建了一个迭代器,把它赋给了一个变量gen,用于控制生成器* genDemo(..)
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')

image-20220420202557664.png

从图中可以看出协程的四点规则:

  1. 通过调用生成器 genDemo 来创建一个协程 gen,创建之后,gen 协程并没有立即执行。
  2. 要让协程执行,需要调用 next()。
  3. 当协程正在执行的时候,可以通过 yield 关键字来暂停 gen 协程的执行,并返回信息给父协程。
  4. 如果协程在执行期间遇到 return 关键字,那么 JavaScript 引擎会结束当前协程,并将 return 后面的内容返回给父协程。

那么协程切换时调用栈如何切换呢?

当在gen协程中调用yield方法时,JavaScript引擎会保存gen协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行next()时,JavaScript引擎会保存父协程的调用栈信息,并恢复gen协程的调用栈信息。

image-20220420204521404.png

3.3 生成器+promise

生成器+promise最大效用的最自然的方法就是yield出来一个Promise,然后通过这个Promise来控制生成器的迭代器。(即async/await的原理)

此时并不在消息传递的意义上使用yield,而只是将其用于流程控制——实现暂停。

4.async/await

ES7引入async/await,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。

1.async function

通过 async 标记函数会表达一件事情:即这个函数总是返回一个promise。对于不是promise的返回值会自动包装在一个 resolved 的 promise 中。

async function foo() {
    return 2
}
console.log(foo())  // Promise {<resolved>: 2}

2.执行流程

async function foo() {
    console.log(1)
    let a = await 100
    console.log(a)
    console.log(2)
}
console.log(0)
foo()
console.log(3)

image-20220420210601120.png

  1. 首先执行 console.log(0) 这个语句,打印出来0。

  2. 接着执行foo(),由于 foo 函数被 async 标记过,所以当进入该函数的时候,JavaScript 引擎会保存当前的调用栈等信息,然后执行 foo 函数中的 console.log(1) 语句,打印出1。

  3. 接着执行 foo 函数中的 await 100 这个语句,这里是我们分析的重点,因为在执行 await 100 这个语句时,JavaScript 引擎在背后做了大量的工作:

    • 首先,当执行 await 100 时,会默认创建一个 Promise 对象

      let promise_ = new Promise((resolve,reject){
        resolve(100)
      })
      
    • 在这个promise_对象创建过程中,我们可以看到在 executor 函数中调用了 resolve 函数,JavaScript 引擎会将该任务提交给微任务队列

    • 然后JavaScript引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将promise_对象返回给父协程。

  4. 接着执行父协程的流程,这里执行console.log(3)。随后父协程将执行结束,在结束之前,会检查微任务队列,微任务队列有 resolve(100) 的任务等待执行,执行到这里的时候,会触发 promise_.then 中的回调函数

    promise_.then((value)=>{
       //回调函数被激活后
      //将主线程控制权交给foo协程,并将vaule值传给协程
    })
    
  5. 该回调函数被激活后,会将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程。

  6. foo 协程激活之后,会把刚才的值赋值给变量 a,然后 foo 协程继续执行后续语句,执行完成之后,将控制权归还给父协程。

3.Error 处理

如果一个 promise 正常 resolve,await promise 返回的就是其结果。但是如果 promise 被 reject,它将 throw 这个 error,就像在这一行有一个 throw 语句那样。

async function f() {
  await Promise; // 
}

如果promise被reject,那么相当于:

async function f() {
  throw new Error("Whoops!");
}

因此,在真实开发中,可以用 try...catch 来捕获这个error:

async function f() {
​
  try {
    let response = await fetch('http://no-such-url');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}
​
f();

4.async/await 可以和 Promise.all 一起使用

// 等待结果数组
let results = await Promise.all([
  fetch(url1),
  fetch(url2),
  ...
]);

5.异步解决方案

  • 回调函数
  • Promise 对象
  • generator 函数
  • async/await

这里使用网络请求案例,将几种解决异步的方案进行一个比较:

// 需求: 
// 1> url: why -> res: why
// 2> url: res + "aaa" -> res: whyaaa
// 3> url: res + "bbb" => res: whyaaabbbfunction requestData(url) {
  return new Promise((resolve, reject) => {
    // 模拟网络请求
    setTimeout(() => {
      // 拿到请求的结果
      resolve(url)
    }, 2000);
  })
}

回调函数

requestData("why").then(res => {
  requestData(res + "aaa").then(res => {
    requestData(res + "bbb").then(res => {
      console.log(res)
    })
  })
})

Promise

requestData("why")
.then(res => {
  return requestData(res + "aaa")
})
.then(res => {
  return requestData(res + "bbb")
})
.then(res => {
  console.log(res)
})

generator + promise

function* getData() {
  const res1 = yield requestData("why")
  const res2 = yield requestData(res1 + "aaa")
  const res3 = yield requestData(res2 + "bbb")
  const res4 = yield requestData(res3 + "ccc")
  console.log(res4)
}
​
// 1> 手动执行生成器函数
const gen = getData()
function getGenPromise(gen, data) {
    //恢复gen协程并拿到返回值
    return gen.next(data).value;
}
​
getGenPromise(gen)
.then(res1 => {
  return getGenPromise(gen, res1);
})
.then(res2 => {
  return getGenPromise(gen, res2);
})
.then(res3 => {
  return getGenPromise(gen, res3);
})
​
// 2> 自己封装了一个自动执行的函数
function run(gen) {
  const next = (res) => {
    let res = gen.next(res);
    if (res.done) return res.value;
    res.value.then(res => {
      next(res);
    })
  }
  next();
}
run(gen);

手动执行生成器函数的大致流程:

  1. 首先执行 const gen = getData() ,创建了 gen 协程。
  2. 然后在父协程中通过执行 gen.next 把主线程的控制权交给 gen 协程。
  3. gen 协程获取到主线程的控制权后,就调用 fetch 函数创建了一个 Promise 对象,然后通过 yield 暂停 gen 协程的执行,并将 promise 对象返回给父协程。
  4. 父协程恢复执行后,链式调用 then 方法依次添加回调。
  5. 等待网络请求完成后,会调用 then 中添加的回调,通过 gen.next 放弃主线程的控制权,将控制权交给 gen 协程继续执行下个请求。

async/await

async function getData() {
  const res1 = await requestData("why")
  const res2 = await requestData(res1 + "aaa")
  const res3 = await requestData(res2 + "bbb")
  const res4 = await requestData(res3 + "ccc")
  console.log(res4)
}

\