2023-08-13
JavaScript
00
请注意,本文编写于 193 天前,最后修改于 25 天前,其中某些信息可能已经过时。

目录

Promise
基本语法
错误捕捉
Promise.prototype.finally
Promise.all([])
Promise.race([])
Promise.allSettled()
Promise.any()
Promise.resolve()
Promise.reject()
Promise.try()
Promise手写题
手写Promise包装Ajax
手写Promise.all
手写promisec串行调用
手写Promise调度器
实现Promise.abort()
Iterator
Iterator简介
手写Iterator接口
手写扩展运算符、for...of
对象的遍历
for...of、for...in、forEach比较
Generator
如何理解Generator
Generator基本语法
Generator的原型图
Generator与Iterator
Gererator.prototype.throw()
Generator.prototype.return()
yield * 表达式
为什么要使用Generator
对Generator协程的理解
应用
Generator的异步应用
自执行demo
co核心代码
async 函数
相对于Generator的优势
基本语法
await命令
顶层await
异步遍历器

本文是基于阮一峰老师《es6入门教程》异步编程几个章节的学习笔记,包括IteratorPromiseGeneratorAsync/Await异步Generator几个章节的知识总结,改写及加入了一些案例,以练来学,融入了一些自己的思考和拓展。

详细思维导图点这里

ES6之前,异步编程大约有一下四种

  • 回调函数
  • 事件监听
  • 发布/订阅
  • 基于Promise对象的社区方案bluebirdQWhen
    ES6 提供了三种方案
  • Promise
  • Generator
  • Async/Await

Promise

  • 一种异步编程解决方案,避免嵌套的回调地狱问题
  • 由社区提出和实现,最后写入语言标准

特点

  • 三种状态 pending fulfilled rejected, 状态不受外界影响
  • 一旦改变状态就不会再变

缺点

  • 无法取消Promie 手写abort方法
  • 内部的错误不会反应到外部,
    • 相当于内置了try/catch,会变成reject状态
  • pending状态无法得知具体进展

基本语法

js
const p = new Promise((resolve, reject) => { // 同步执行 // 刚开始是pending状态 resolve(123); // 将变成fullfilled状态 // setTimeout(resolve, 1000); // 无效 一旦改变状态就不会再变 // resolve或reject之后不应有代码,应写到.then里面 }); p.then((val) => { // 异步执行 微任务 console.log(val); }); const p2 = new Promise(() => { console.log(a+3); // 报错 resolve(); }) console.log('promise内部的错误不会反应到外部');

错误捕捉

  • .then的第二个参数
  • .catch 推荐用这个方案, 能够捕捉前面所有报错
  • 外部代码如何捕捉promise错误:
    • 浏览器window.addEventListener('unhandledrejection')
    • NodeJS process.on('unhandledrejection', cb)
  • .catch 后面返回一个fullfiled状态的promise 可以继续使用.then方法
  • 如果没有错误,会跳过.catch方法
  • .then .catch 是原型方法

Promise.prototype.finally

  • 不管 Promise 对象最后状态如何,都会执行的操作
  • 不接受任何参数
  • 后面还会返回promise,使用之前的状态和value
  • 它是then的特殊语法
js
const myFinally = () => { // todo something } Promise.prototype.then( (arg) => { myFinally(); return Promise.resolve(arg) }, (arg) => { myFinally(); return Promise.reject(arg) }, );

Promise.all([])

  • 将多个 Promise 实例,包装成一个新的 Promise 实例
  • 只有数组元素都变成fullfilled状态,包装实例才会变成fullfilled状态
  • 如果有一元素变成rejected状态,包装实例就会变成rejected状态
js
function createPromise(val, ms = 100, isReject = false) { return new Promise((resolve, reject) => { setTimeout(() => { isReject ? reject(val) : resolve(val); }, ms) }) } Promise.all([ createPromise(1), createPromise(2, 200), ]).then((val) => { console.log(val); // [1 ,2] }) Promise.all([ createPromise(1,10, true), createPromise(2, 200), ]).catch((val) => { // 10ms后执行这里 console.log(val); // 1 })

Promise.race([])

  • 只要数组元素有一个状态发生改变,包装实例状态就会跟着改变

Promise.allSettled()

  • 只有等到数组元素都返回(不管成功失败),包装实例状态才跟着改变(只会变成fullfilled状态)
js
Promise.allSettled([ createPromise(1,10, true), createPromise(2, 200, true), ]).then((val) => { console.log(val); // [{reason: 1, status: "rejected"}, {...}] })

Promise.any()

  • 只要数组元素有一个变成fullfilled状态,包装实例就会变成fullfilled状态

Promise.resolve()

  • 将一个对象转换为Promise
  • 如果参数是一个Promise实例,将原封不动地返回这个实例
  • 如果参数是一个thenable对象,会转换为Promise对象,然后执行then方法
  • 其他情况包装为Promise实例即可

Promise.reject()

  • 返回一个rejected状态的Promise实例

Promise.try()

  • 它用来包装一个方法,即不影响它的同步执行又能捕捉它的错误。
  • 等价于(async () => f())()
  • 这时候错误捕捉就可以用promise.catch

Promise手写题

手写Promise包装Ajax

js
function getJSON (options) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(options.methods || 'GET', options.url, false); xhr.onreadystatechange = function() { // 0 未初始化, 还没有调用open // 1 已建立连接还没有调用send // 2 请求已接收,正在处理中 // 3 请求处理中. 响应中已有部分数据了 // 4 请求已经完成 if(xhr.readyState === 4) { if(xhr.status >=200 && xhr.status<300 || xhr === 304) { resolve(xhr.responseText) } else { reject(xhr.responseText) } } } xhr.setRequestHeader('Accept', 'application/json'); // xhr.responseType = 'json' let data = undefined; if(options.methods && options.methods !== 'GET') { data = JSON.stringify(options.data); } xhr.send(data) }) }

手写Promise.all

js
Promise.myAll = function(arr) { return new Promise((resolve, reject) => { const results = Array(arr.length); let count = 0; arr.forEach((inst, index) => { inst.then(res => { ++count; results[index] = res; if(count === arr.length) { resolve(results) } }).catch(reject); }); }); }

手写promisec串行调用

js
// 串行实现 const promiseChain = (promises) => { promises.reduce((sum, p) => { return sum.then(() => p()) }, Promise.resolve()); } // 测试用例 genPromises(10); promiseChain(genPromises(10)); // 辅助代码 function genPromises(num) { return Array.from( {length: num}, (i, index) => () => new Promise( (resolve) => { const time = Math.random() * 1000 setTimeout(() => { console.log(index, time); resolve() }, time); } ) ); }

手写Promise调度器

js
run(genPromises(10), 2); function run(promises, num) { Array.from({length: num}).forEach(async () => { while(promises.length) { const p = promises.shift(); await p(); } }); } run2(genPromises(10), 2); function run2(promises, num) { function exec(p) { if(!p) return; p().then(() => exec(promises.shift())) } Array.from({length: num}).forEach(async () => { exec(promises.shift()) }); }

实现Promise.abort()

js
Promise.abort = function(p) { let abort; const pAbort = new Promise((resolve, reject) => { abort = reject; }); const result = Promise.race([pAbort, p]); result.abort = abort; return result; } const p1 = Promise.abort(createPromise(22, 3000)); p1.then(console.log).catch(); setTimeout(() => { p1.abort() }, 2000);

Iterator

由于异步编程Generator依赖Iterator,所以暂且把Iterator也归类到异步编程中吧

Iterator简介

  • 遍历器是一种接口,它为各种不同的数据结构提供统一的访问机制
  • 任何对象只要部署Iterator接口,就可以遍历
    • 该对象的Symbol.iterator属性是生成器函数

Iterator的作用

  • 为各种数据结构提供统一的接口
    • 只要部署Iterator接口,就可以遍历
    • 即要求该对象的Symbol.iterator属性是生成器函数
  • 使得数据结构的成员能够按照某种次序排列
  • 可以通过扩展运算符for...of进行遍历

手写Iterator接口

js
function makeIterator(obj){ const keys = Reflect.ownKeys(obj); obj[Symbol.iterator] = function (){ let nextIndex = 0; return { next() { return nextIndex < keys.length ? ({ value: obj[keys[nextIndex++]], done: false }) : ({ value: false, done: true }) } } } return obj } // 测试 var a = makeIterator({a:1}); [...a]; // ok for(let i of a){ console.log(i) }; // ok

手写扩展运算符、for...of

js
// 注: 扩展运算符只能近似模拟, 模拟是为了了解它的执行细节 function kb(obj) { const g = obj[Symbol.iterator](); const result = []; let item = g.next(); while(!item.done) { result.push(item.value); item = g.next(); } return result; } kb(makeIterator({a:1,b:2})); kb([1,2]); var obj1 = {0:1,1:'a', length:2}; obj1[Symbol.iterator] = Array.prototype[Symbol.iterator]; kb(obj1) // 模拟 for...of function forOf(objI, cb) { const g = objI[Symbol.iterator](); let item = g.next(); while(!item.done) { cb(item.value); item = g.next(); } } forOf([1,2,3], console.log);

对象的遍历

  • 大多数数据结构都由Iterator接口,都可以遍历, 但是对象不能遍历
  • 可以借助 Object.keys Object.entries
  • 或者实现Iterator接口 obj[Symbole.iterator] = function * gen(){}

for...of、for...in、forEach比较

  1. for...of 只有实现了Iterator接口才能遍历,如对象就不能遍历;for...in是遍历对象的属性名
  2. 对数组来对象的属性说就是索引, 因此for...in不适合遍历数组
  3. for...in 还会遍历原型链上可枚举属性
  4. 某些情况下,for...in循环会以任意顺序遍历键名
  5. forEach是对for...in的封装,使用更简便,
  6. forEach的缺点
    • 不能跳出循环(不能使用break return continue
    • 不能使用暂停(不能使用async await

Generator

如何理解Generator

Generator 也就是协程

  • 协程可以理解为跑在线程上的任务,一种比线程更轻量级的存在

  • 一个线程可以有多个协程,单个线程只能同时运行一个协程

  • 协程不被操作系统内核所管理,完全由程序控制。好处是不像线程切换那样消耗资源

  • 从语法上看,Generator是一种状态机,封装了多个内部状态

Generator基本语法

  • * 位置随意
  • yield表达式 的作用
    • 暂停的标志
    • 提供与外界的通信
  • 通过函数包裹,解决首次调用next()不能传递参数问题
  • Generator 函数的声明与函数声明语句有一样的特性,即声明提前、函数体提前, 可以重复声明。
js
// function关键字与函数名之间有一个星号 * 位置你随意 function* createGenerator(){ // 函数体内部使用yield表达式 yield 'hello'; yield 'world'; return 'ending'; } const g = createGenerator(); // g 遍历器对象 g.next(); // {value: 'hello', done: false} g.next(); // {value: 'world', done: false} g.next(); // {value: 'ending', done: true} g.next(); // {value: undefined, done: true} // yield 表达式 暂停的标志 function * gen(){ // 只有调用next方法,下面的语句才会执行, // 相当于JS提供了惰性求值的心语法 yield console.log('exced'), 123 + 234; } const g2 = gen(); // 没有打印 g2.next(); // 打印 'exced' // 返回 {value: 357, done: false} // generator提供了一种与外界实时通信的机制 // 举个例子:计算矩形的周长 function * rectGen() { // console.log('第一次调用next()时执行,无法传参数'); const width = (yield) || 0; // 第二次调用next()执行,支持传参数,这里赋值给width const height = (yield) || 0; yield; return 2*width + 2*height; } // 解决第一调用不能传参的问题 function wrapGen(gen) { const g = gen(); g.next(); return g; } const rect = wrapGen(rectGen); rect.next(10); // 传入宽 rect.next(20); // 传入高 const perimeter = rect.next().value; // 计算 console.log('矩形周长', perimeter); // 60

执行顺序

js
function *gen() { console.log('start'); yield 1; console.log('end'); } const g = gen(); console.log(g.next().value); console.log(g.next().value); console.log('同步代码'); // 'start' 1 'end' '同步代码'

Generator的原型图

由于学习Generator时,有一些奇怪的现象,于是尝试探寻了下Generator的原型链, 先来说一下我的疑惑,在讲解前,假设你已经熟悉JavaScript原型链(如不熟悉请点击这里)。

js
function * gen(){} var g = gen(); g.__proto__ === gen.prototype; // true // 这是不是很像new吗? g = new gen(); // 报错 , 为什么不让new? // this问题 function * gen(){ this.aa = 1; } var g = gen(); g.next(); g.aa; // undefined window.aa // 1 // 发现跑到window下面了,严格模式下会报错 /* 为什么 不存在实例属性aa? 这个也许是 不让 new 的原因,避免当成new理解 */ // 那么怎样解决this问题呢,阮一峰老师的es6一书给了两个方案 // 1. 使用call var g = gen.call(obj); // 这样是解决了this问题,但是使用实例属性要用obj 而不是g // 2. 绑定到gen.prototype不就行了? var g = gen.call(gen.prototype); g.next(); g.aa; // 1 // 貌似解决了问题,但是JS原型是共享了,这样会污染原型 var g2 = gen(); g2.aa; // 1

因此,我绘制了Generator的原型图

generator.drawio.svg

简单总结一下
GeneratorFunction是在Function的基础上包装了一层, 只是这个对象没有暴露出来

js
function * gen(){} const GF = gen.constructor // gen.__proto__.constructor const gf = new GF('yield "hello"'); // 这是不是和 new Function 很像 gf().next();

如何理解g[Symbol.iterator]() === g

  • 通过Reflect.ownKeys(g)检测 g并没有[Symbol.iterator]属性, 见过层层检查原型链, 最终在 Reflect.ownKeys(g.__proto__.__proto__.__proto__) 检测到[Symbol.iterator]属性
  • g[Symbol.iterator]()
    • g.__proto__.__proto__.__proto__[Symbole.iterator].call(g)
    • Generator.__proto__[Symbole.iterator].call(g)
    • Generator[Symbole.iterator].call(g)
  • 也就是说 Generator[Symbole.iterator].call(obj) === objGenerator[Symbole.iterator]函数会返回当前this

Generator与Iterator

image.png

  • 上图右边的对象实现了Iterator接口是可遍历对象,左边只是模拟Iterator接口 是类遍历对象
  • 也就是说如果一个对象可遍历那么它的Symbol.iterator属性是一个高阶函数,高阶函数返回的对象必须有next()方法,这个函数就是生成器函数
  • 生成器函数function * gen(){} 用来生成一个遍历器对象var g = gen(),也就是g[Symbol.iterator]是满足上述要求的高阶函数,即生成器函数
    • 因为 g 是遍历器对象; g[Symbol.iterator]()也是遍历器对象,它们的遍历结果必须一样,那么它们的关系是怎样的呢?
    • 事实上 g === g[Symbol.iterator]()

Gererator.prototype.throw()

  • throw方法被捕获后,会附带执行下一条yield表达式
  • 如果内部没有捕获,会终止内部、外部代码块的运行
  • 再次调用next会返回 {value: undefined, done: true}
  • 也可以被外部捕获
  • finally中的yield语句具有神奇作用;会暂停错误(正常返回,外部代码可以继续运行),再次调用next方法才释放错误,终止外部代码运行
js
function *genT() { console.log(0, '启动'); try{ yield 1; console.log('一', '抛出错误,后面的代码不会执行'); // 这里不会打印 }catch(e){ console.log('进入catch', e); console.log('二', 'g.throw()会附带执行一条语句') yield 2; console.log('三'); // 这里不会打印 } finally{ yield 'finally'; } } var gt = genT(); try { console.log(gt.next()); // {value: 1, done: false} console.log(gt.throw(123)); // {value: 2, done: false} console.log(gt.throw(456)); // {value: 'finally', done: true} console.log('因为finally存在,yield暂停了错误,外部代码回继续执行'); } catch(e) { console.log('再次抛出错误会被外部捕获', e); } console.log(gt.next()); // Uncaught 456 // 释放错误,终止外部代码运行 console.log('这里不会打印');

Generator.prototype.return()

  • 立即终结Generator函数运行, 并返回值{done:true, value: 'returnVal'}。可以理解为把当前yield语句替换为return语句
  • 如果Generator函数内部有finally语句, 会进入finally语句,执行到下一个yield语句并返回。
js
function *numbers2(){ try{ yield 1; console.log('这里不会执行'); } finally { console.log('finally'); yield 2; console.log('done'); return 123; } } var gn2 = numbers2(); gn2.next(); // 进入try语句 // 返回{ value: 1, done: false } gn2.return('a'); // 打印finally, 然后后返回{ value: 2, done: false } gn2.next(); // 打印done 然后返回 {value: 123, done: true}

yield * 表达式

  • 用在一个Generator函数里执行另一个Generator函数
  • 相当于 用for...of展开另一个生成器到当前生成器

yield* 案例 拉平数组 image.png 与 普通方式比较 好像 yield*更好理解一些。

为什么要使用Generator

image.png

  • 通过上图可以看出generator少了用于保存状态的外部变量,
  • 这样更简介、更安全(状态不会被非法篡改)、更符合函数式编程思想,写法上也更优雅

对Generator协程的理解

  1. 协程与子例程的差异
    • 子例程采用堆栈式“后进先出”的执行方法
    • 协程直接可以交换执行权交替执行,可理解为多个栈
    • 本质上讲,协程多占用内存为代价,实现多任务的并行
  2. 协程与普通线程的差异
    • 多线程在多核CPU可以并行执行
    • 协程在是串行
  3. Generator是“半协程” 只有Generator的调用者才能将程序的执行权还给 Generator 函数
  4. Generator与上下文 yield语句是保存当前上下文,暂时退出,等next()调用时在回复

应用

  1. 异步表达式同步化
  2. 控制流管理
  3. 部署Iterator接口
  4. 作为数据结构
  5. 购物车价格,缓存中间计算结果 (个人总结)
js
// ajax function * main(){ const res = yield request('test.json'); console.log(res) } const it = main(); it.next(); function request(url){ makeAjaxCall(url, it.next); } // 文件逐行读取 function *numbers(){ const file = new FileReader('numbers.txt'); try { while(file.eof) { yield parseInt(file.readLine(), 10) } } finally { file.close(); } }

Generator的异步应用

Thunk函数, 惰性求值, 本质是将参数编程函数来模拟惰性求值

js
var Trunk = function (fn) { return function (...args) { return (cb) => fn.call(this, ...args, cb); }; };

自执行demo

js
function createPromise(val, ms = 1000, isReject = false) { return new Promise((resolve, reject) => { setTimeout(() => { // console.log(val); (isReject ? reject : resolve)(val); }, ms); }); } function * gen(){ console.log('start'); yield createPromise(1,1000); console.log(1); yield createPromise(2, 2000); console.log('end'); } function run(gen) { const g = gen(); next(); function next(val) { const res = g.next(val); if(res.done) return; res.value.then(next); } } run(gen);

co核心代码

js
function * gen() { console.log('start'); const a = yield createPromise(1,1000); // // 如果报错会附带执行下一条语句 // const a = yield createPromise(1,1000, 1); console.log(1); const b = yield createPromise(2, 2000); console.log('end'); return a+b*2; } co(gen).then(console.log) // 5 function co(gen) { var ctx = this; return new Promise((resolve, reject) => { if(typeof gen === "function") { gen = gen.call(ctx); } if(!gen && typeof gen !== 'function' || typeof gen.next !== "function") { return resolve(gen) }; onFillfilled(); function onFillfilled(res) { let ret try { ret = gen.next(res); } catch(e) { reject(e) } next(ret) } function next(ret) { if(ret.done) return resolve(ret.value); ret.value.then(onFillfilled, onReject) } function onReject(res) { try{ gen.throw(res); } catch(e){ reject(e); } } }) }

async 函数

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

相对于Generator的优势

  1. 内置执行器。
  2. 更好的语义。
  3. 更广的适用性。
  4. 返回值是Promise

基本语法

js
async function f1(){} var a = f1(); // async 函数返回值是Promise Reflect.toString.call(a); // '[object Promise]' a.then(console.log); // undefined async function f2(){ 12+12n; } f2(); console.log('asyn函数报错,不会中断外部代码的执行');

await命令

  • 后面跟的如果是一个Promise 对象,会等待然后返回该对象的结果
  • 如果是一个thenable对象, 会转换为promise,执行then方法然后返回结果
  • 其他情况直接返回该值
  • 如果报错就停止运行,直接返回错误结果
  • 捕捉错误 .catch try...catch
  • await后面的语句代码块是在微任务中执行
js
async function fn(){ console.log('start'); let res1 = await createPromise(1,1000); // co中Promise报错会附带执行一条语句, // async函数中不会,会直接把错误结果返回 // let res1 = await createPromise(1,1000, 1); console.log('1', res1); await createPromise(2,1000); console.log('end'); } fn();

使用注意事项:

  • 建议使用try catch捕捉Promise错误
  • 多个await语句,如果不存在继发关系,最好让它们并行 Promise.all
  • await 不能出现在普通函数中
  • await 在数组循环中 无效
  • async函数可以保留错误栈

顶层await

  • ES2022新特性, 主要目的是使用await解决模块异步加载问题
  • 会等待该模块所有顶层代码块都执行完才能加载

异步遍历器

  • 异步遍历器 obj[Symbol.syncIterator]().next() 返回值是Promise<{value: any, done:boolean}>
  • for await...of 遍历 异步遍历器对象
  • 异步Generator
  • yield * 后面可以跟异步遍历器对象
js
f(); async function f() { // function * gen(){ // 写async回编程同步执行 async function * gen(){ console.log('start'); // yield createPromise(1, 2000); // 等价于下面的方式 yield await createPromise(1, 2000); console.log(1); yield createPromise(2, 200); yield createPromise(3, 1200); console.log('end'); } const g = gen(); return await taskAsync(g); } async function taskAsync(iterator) { const result = []; let it = await iterator.next(); while(!it.done) { result.push(it.value); it = await iterator.next(); } // 等同于下面的写法 /* for await (let it of iterator) { result.push(it); } */ return result; }

本文作者:郭敬文

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!