0
点赞
收藏
分享

微信扫一扫

Promise从手写到扩展 | Promise/Generator/async | [Promise系列二]


作者:寒草

前情提要

Promise从入门到手写:juejin.cn/post/693968…

上一篇文章主要分为四个部分:

  • ​Promise​​介绍
  • ​Promise​​特点
  • ​Promise​​使用
  • ​Promise​​手写

如果有需要得可以点击上面的链接,原本这篇文章我个人认为是比较完整的,但是经过同事,以及掘金评论区的小伙伴的反馈,我决定再对之前的文章进行一些补全。

本篇文章内容如下:

  • ​Promise补充​​:包括Promise A+规范 | 用类的方式对Promise进行手写
  • ​Generator​​: Generator介绍与用法 | Generator与协程 | Generate自动执行(执行器)
  • ​async/await​​:async/await用法 | 对比Generator的改进项
  • ​知识补充​​:Thunk函数 | co模块介绍与源码
  • ​私货​​:结束语 | 属于我风格的经典文章结束闲扯淡

ok,那我们正式开始我们的旅程吧~

Promise补充

Promise A+规范

在前文我们已经实现过Promise了,后面我们还会用类的方式再写一次,但是我貌似并没有去介绍Promise A+规范官网。

大家其实只需要去关注上面链接的第二章。第二章分为三个部分:

  • ​Promise States​​ 即 Promise的状态
  • ​The then Method​​ 即 then方法
  • ​The Promise Resolution Procedure​​ 即 Promise的处理过程

在这里我搜索到了一个​​Promise A+规范​​的​​中英文对照翻译​​ 我也在这里做一个简单总结:


  1. ​Promise​​​本质是一个状态机,且状态只能为以下三种:​​Pending​​​(等待)、​​Fulfilled​​​(成功)、​​Rejected​​​(失败),状态的变更是单向且不可逆的,只能从​​Pending​​​ ->​​Fulfilled​​​ 或​​Pending​​​ ->​​Rejected​​。
  2. ​then​​​方法接收两个可选参数,分别对应状态改变时触发的回调。​​then​​​方法返回一个​​promise​​​。​​then​​​ 方法可以被同一个​​promise​​ 调用多次。


当然,其实​​Promise A+规范​​​还有很多细节,我的总结其实很不完整,比如官网的第二章第三节,Promise处理过程我很难去简单总结,而且上面那个中英文对照翻译的很好,大家有时间建议看一看,其实网络上很多关于​​Promise A+规范​​的介绍,也其实像我这般简单说说,具体细节还是要去看官网或者翻译​​中英文对照翻译​​的~


而且其实按照完整的​​Promise A+​​​规范,其实之前的代码在一些细节上不能完全通过​​Promise A+​​规范。


Promise手写(类的方式)

变动和之前文章变动不大。但是有一点要注意,像​​resolve​​​,​​reject​​​,​​all​​​,​​race​​​是类的静态方法,所以要加上​​static​​关键字。


关于各个方法的写法,思路,在上一篇文章有完整的介绍,如果有不懂的,可以去看看之前的文章。相信可以解答大部分问题。


//Promise类的写法
class Promise {
// 构造方法
constructor(executor) {
//保存promise状态
this.promiseState = 'pending';
//保存promise结果
this.promiseResult = null;
//用于保存异步回调函数列表
this.callbackList = [];
const resolve = val => {
// 状态只能修改一次
if (this.promiseState !== 'pending') return;
// 1. 要去修改Promise对象的状态([[promiseState]]),置为成功fulfilled
this.promiseState = 'fulfilled';
// 2. 要去修改Promise对象的状态([[promiseResult]])
this.promiseResult = val;
setTimeout(() => {
// 调用成功的回调【callbackList存起来的】
for (let callback of this.callbackList) {
callback.onResolved(val);
}
})
}

const reject = err => {
// 状态只能修改一次
if (this.promiseState !== 'pending') return;
// 1. 要去修改Promise对象的状态([[promiseState]]),置为失败rejected
this.promiseState = 'rejected';
// 2. 要去修改Promise对象的状态([[promiseResult]])
this.promiseResult = err;
setTimeout(() => {
// 调用失败的回调【callbackList存起来的】
for (let callback of this.callbackList) {
callback.onRejected(err);
}
})
}
// 为什么要加try catch 是因为,throw err也相当于调用reject了【前面说过没看过的去补课】
try {
/*
* 同步执行执行器函数
* 执行器函数接收两个参数,一个是resolve,一个是reject
*/
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
// then方法
then(onResolved, onRejected) {
const self = this;
//处理异常穿透 并且为onResolved,onRejected设置默认值。因为这两个参数可以都不传
if (typeof onRejected !== 'function') {
onRejected = err => {
throw err;
}
}
if (typeof onResolved !== 'function') {
onResolved = val => val;
}
// then方法会返回Promise
return new Promise((resolve, reject) => {
// 对返回值的处理进行封装
const handleCallback = (callback) => {
// 如果回调函数中抛出错误,则reject
try {
// 需要依据回调的返回结果确定then方法的返回值
// 现在的this会指向return的promise对象,所以使用self
const res = callback(self.promiseResult);
if (res instanceof Promise) {
//如果回调返回结果是个Promise
res.then(val => {
resolve(val);
}, err => {
reject(err);
})
} else {
// 返回结果不是Promise
resolve(res);
}
} catch (err) {
reject(err);
}
}
//调用回调函数
if (this.promiseState === 'fulfilled') {
setTimeout(() => {
handleCallback(onResolved);
})
}
if (this.promiseState === 'rejected') {
setTimeout(() => {
handleCallback(onRejected);
})
}
/*
* 如果是pending状态,则异步任务,在改变状态的时候去调用回调函数
* 所以要保存回调函数
* 因为promise实例阔以指定多个回调,于是采用数组
*/
if (this.promiseState === 'pending') {
this.callbackList.push({
onResolved: () => {
handleCallback(onResolved);
},
onRejected: () => {
handleCallback(onRejected);
}
})
}
})
}
// catch方法
catch(onRejected) {
// 我们可以直接使用then方法实现
return this.then(undefined, onRejected);
}
// resolve方法
static resolve(val) {
//返回值的情况在前文说过,可以在 Promise的使用一章找到
return new Promise((resolve, reject) => {
if (val instanceof Promise) {
val.then(val => {
resolve(val);
}, err => {
reject(err);
});
} else {
resolve(value);
}
})
}
// reject方法
static reject(err) {
//返回值的情况在前文说过,可以在 Promise的使用一章找到
return new Promise((resolve, reject) => {
reject(err);
})
}
// all方法
static all(promiseList) {
let count = 0;
let res = [];
const length = promiseList.length;
return new Promise((resolve, reject) => {
for (let i = 0; i < length; i++) {
promiseList[i].then(val => {
count++;
res[i] = val;
if (count === length) {
resolve(res);
}
}, err => {
reject(err);
});
}
})
}
// race方法
static race(promiseList) {
const length = promiseList.length;
//谁先完成谁就决定结果!
return new Promise((resolve, reject) => {
for (let i = 0; i < length; i++) {
promiseList[i].then(val => {
resolve(val);
}, err => {
reject(err);
});
}
})
}
}

Generator

引子

其实我们写多了​​Promise​​​,其实我们也会发现一个问题,​​Promise​​虽然解决了回调地狱的问题,但是我们作为开发者,比如我这种有着半年开发经验的资深前端工程师(手动狗头),其实也遇到了很多比较复杂的情况,导致代码中有很多难读的then方法引起的嵌套结构。


吐槽:当然,虽然很多导致难于理解的原因是历史因素或者开发者的随意导致的。当时从结果出发,我还是不太喜欢会出现嵌套的​​Promise.prototype.then()​​的异步处理方式。


但是我们还是期望一种技术可以解决then导致的嵌套结构。把异步代码像同步代码一样处理,于是我在开发中最常使用的​​async/await​​​在ES7引入了,​​async/await​​提供了以同步代码实现异步任务的能力,使得写异步代码像同步代码一样清晰。

但是,在我们研究​​async/await​​​之前,我们需要去研究一下​​Generator​​​,因为​​async​​​是​​Generator​​的语法糖。

Generator简单介绍


网上关于Generator的用法有很多,大家可以看更详细的文档,我不会在此详细介绍用法~抱歉啦


​Generator​​是​​ECMAScript 2015​​即​​ES6​​提出的规范。

首先遇事不决,先说用法:


​function*​​​ 这种声明方式(function关键字后跟一个星号)会定义一个生成器函数 ​​(generator function​​​),它返回一个 ​​Generator​​ 对象。


我们直接对MDN上面的例子加以修改:

function* generator(i) {
console.log('第一次')
yield i;
console.log('第二次')
yield i + 10;
console.log('第三次')
}
const c = generator(1);

为了比较清晰,我直接把执行过程的打印结果截图:

Promise从手写到扩展 | Promise/Generator/async | [Promise系列二]_JavaScript

我们仔细观察这个图片,我们不难发现几件事情:

  • 首先就是​​function*​​​ 这种声明方式定义的生成器函数返回一个对象。这个对象我们直观的发现里面有一个叫做​​[[GeneratorState]]​​的属性,里面代表着这个生成器对象的状态。
  • ​Generator.prototype.next()​​​方法会返回一个由​​yield​​​表达式生成的值,并使得函数执行到该​​yield​​​的位置并暂停。该值是一个对象,有​​value​​​和​​done​​​两个属性,​​value​​​是返回结果,即​​yield​​​表达式后面那个表达式的值,​​done​​代表生成器对象是否已经油尽灯枯,即生成器对象是否遍历结束。[伙伴们,油尽灯枯这个词是不是用的很妙~]

到这里,其实小伙伴们一定会好奇,js是单线程执行的,生成器是如何让函数暂停,之后又恢复的捏,那我们就不得不提一下​​协程​​了。

Generator与协程

  1. 什么是协程?


协程是一种比线程更加轻量级的存在,协程处在线程的环境中,一个线程可以存在多个协程,可以将协程理解为线程中的一个个任务。不像进程和线程,协程并不受操作系统的管理,而是被具体的应用程序代码所控制。


  1. 生成器函数是如何让函数暂停,之后又恢复的呢?


一个线程一次只能执行一个协程。比如当前执行 A 协程,另外还有一个 B 协程,如果想要执行 B 的任务,就必须在 A 协程中将JS 线程的控制权转交给 B协程,那么现在 B 执行,A 就相当于处于暂停的状态。


总之,大概的流程就是这样:

协程A协程B协程C我先休息一下,交给你了!我也力竭了!还是你来吧!啊,我也不行了!A你来!协程A协程B协程C

  1. ​协程A​​开始执行
  2. ​协程A​​​暂停,​​协程B​​开始执行
  3. ​协程B​​​执行完,交给​​协程C​​执行
  4. ​协程C​​​执行完,执行权交回给​​协程A​​继续执行

Generate自动执行(执行器)

ok,那我们现在去做一件事,我们利用​​generator​​把异步的任务封装成一个方法,之后自动的去按顺序(即类似同步方式)去进行调用。

function* asyncFun() {
let resA = yield PromiseA //promise对象
let resB = yield PromiseB //promise对象
let resC = yield PromiseC //promise对象
}

如上面代码所示,我们把我们要依次执行的异步任务按照同步任务的写法写了出来,现在我们要去写一个方法,让他们自动按顺序执行~

function asyncToSyncAndRun(gen){
var g = gen(); //此时g为生成器对象

function next(data){
var result = g.next(data);
//注意:前面说过 result的结构,result是一个对象,里面的value对应yield后表达式的返回值
//所以result.value是一个Promise对象
if (result.done) return result.value;//如果遍历结束,return
//未遍历结束,就把下一个next执行放在现在的Promise对象的回调中去
result.value.then(function(data){
next(data);
});
}
next();//触发next方法~
}

//自动执行
asyncToSyncAndRun(asyncFun)

于是,通过上面的工具方法​​asyncToSyncAndRun​​​,并借助​​generator​​,我们可以把异步任务已同步任务的写法串联起来~

async/await

async/await用法

​此处采用MDN的介绍和示例​

介绍:


async函数是使用async关键字声明的函数。 async函数是AsyncFunction构造函数的实例, 并且其中允许使用await关键字。async和await关键字让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用promise。


用法:

function resolveAfter2Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}

async function asyncCall() {
console.log('calling');
const result = await resolveAfter2Seconds();
console.log(result);
// expected output: "resolved"
}

asyncCall();
// 输出
//"calling"
//"resolved"

ok,我们一起看一下这个示例,顺便介绍一下​​async/await​​的用法。

首先async声明的函数里面可以使用await关键字,await其后接着一个​​Promise​​,这样就完成了异步转同步的过程~

但是这就有小朋友会问了,如果​​await​​后面接的不是​​Promise​​,会发生什么?让我们尝试一下。

const asyncFn1 = async () => {
let res = await 1;
console.log(res,111111111111111);
}
asyncFn1()
// 输出
1 111111111111111


所以,如此来看,如果await后面接的不是Promise,就会返回其后表达式的执行结果。


ok,接下来我们对之前这个方法简单改写。

//generator
function* asyncFun() {
let resA = yield PromiseA //promise对象
let resB = yield PromiseB //promise对象
let resC = yield PromiseC //promise对象
console.log([resA, resB, resC])
}
//asyn/await
const asyncFun = async () => {
let resA = await PromiseA //promise对象
let resB = await PromiseB //promise对象
let resC = await PromiseC //promise对象
console.log([resA, resB, resC])
}

一比较就会发现,​​async​​​函数就是将 ​​Generator​​​ 函数的星号​​(*)​​​替换成​​async​​​,将​​yield​​​替换成​​await​​,仅此而已。

ok,现在我们来看一下​​async/await​​​ 与 ​​generator​​区别,他们的区别不仅仅写法不同这么简单~

async/await 与 generator区别

​async​​​函数对 ​​Generator​​ 函数的改进,体现在以下四点:

  • ​内置执行器​​。Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样。
  • ​更好的语义​​。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  • ​更广的适用性​​。co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
  • ​返回值是 Promise​​。async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

扩展知识

此处扩展知识,是在前文中​​async/await​​​ 与 ​​generator​​区别处提及问题的扩展

Thunk 函数

前文说到了​​Thunk 函数​​,想必大家对这个名词并不了解,其实是我不了解,非要拉上你们,哈哈哈。

详细内容可以参考阮一峰的Thunk 函数的含义和用法,这里只会对其进行简单的介绍。


​定义​​:它是"传名调用"的一种实现策略,用来替换某个表达式。


//传名调用的例子
function f(m){
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};

function f(thunk){
return thunk() * 2;
}


​JavaScript 语言的 Thunk 函数​​:在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。


// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var readFileThunk = Thunk(fileName);
readFileThunk(callback);

var Thunk = function (fileName){
return function (callback){
return fs.readFile(fileName, callback);
};
};


​简单总结​​:无论是Promise还是thunk函数,都是通过传入回调的方式来实现Generator的自动执行。这里具体的的案例可以查看​​Thunk 函数的含义和用法​​的第五章。


co模块介绍及源码

前文我们提到了​​co模块​​[​​此处链接是源码,大家可以自行查看~​​],相比大家都对它并不了解,所以在此为大家简单介绍一下~


​co模块​​: co模块可以让你不用编写 Generator函数的执行器。Generator函数只要传入co函数,就会自动执行


用法如下:

var co = require('co');
var gen = function* () {
var resA = yield PromiseA;
var resB = yield PromiseB;
};
co(gen);

ok,我们了解到​​co模块​​的用法之后,我们就可以去理解一下​​co模块​​的源码了,源码部分我将只会去介绍主要部分,目的是是理解​​co模块​​开发者思路。

此处先粘​​co方法​​的完整代码,大家可以先看看,我在后面进行拆分讲解~

/**
* Execute the generator function or a generator
* and return a promise.
*
* @param {Function} fn
* @return {Promise}
* @api public
*/

function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);

// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);

onFulfilled();

/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/

function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}

/**
* @param {Error} err
* @return {Promise}
* @api private
*/

function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}

/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/

function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}

大家一看发现其实也没有几行,所以我们摘出主要部分一起看一下~

请大家留意我的注释和代码前的描述。


首先,​​co函数​​​接受​​Generator函数​​​作为参数,返回一个​​Promise对象​​。


function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
});
}


在返回的​​Promise对象​​​里面,​​co​​​先检查参数​​gen​​​是否为​​Generator函数​​​。如果是,就执行该函数,得到一个内部指针对象;如果不是就返回,并将​​Promise对象​​​的状态改为​​resolved​​。


function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
// 注意此时得到gen已经是Generator对象,而不是Generator函数了
if (!gen || typeof gen.next !== 'function') return resolve(gen);
});
}


接着,执行​​onFulfilled函数​​。看我的注释吧~


function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
// 注意此时得到gen已经是Generator对象,而不是Generator函数了
if (!gen || typeof gen.next !== 'function') return resolve(gen);

onFulfilled();
function onFulfilled(res) {
var ret;
try {
//此时第一次执行Generator对象的next方法,赋值给ret变量(该变量相当于Generator对象的内部指针)
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
});
}


最后,就是关键的​​next函数​​,它会反复调用自身。


function next(ret) {
//如果Generator对象遍历结束,就返回
if (ret.done) return resolve(ret.value);
//toPromise是一个内部方法,方法描述为:Convert a `yield`ed value into a promise。
//我们就理解它是把值转换成Promise的工具方法就好
var value = toPromise.call(ctx, ret.value);
//使用then方法,为返回值加上回调函数,然后通过onFulfilled函数再次调用next函数,以此递归的方式可以把后续yield后面的异步操作放在前面的Promise执行结束的回调中去。
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
//在参数不符合要求的情况下,将 Promise 对象的状态改为rejected,从而终止执行。
return onRejected(
new TypeError(
'You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "'
+ String(ret.value)
+ '"'
)
);
}


其实整体逻辑和咱们前文中的自动执行器也是一致的,所以仔细读一下还是很好理解的~


结束语

就这样,Promise的两篇就完整的结束了。感谢各位掘金小伙伴的阅读~

大家可以在评论区批评指正,也可以加我微信和我深入交流,如果感兴趣还阔以去阅读我之前的文章。


欢迎阅读和关注~
也可以加我的微信 ​​hancao97​​ 和我交流


Promise从入门到手写[Promise系列(一)]

关于整洁代码与重构的摸爬滚打

我,24岁,展望一下?

【精简版】浏览器渲染机制(完整流程概述)

浏览器的渲染机制(一)

浏览器的渲染机制(二)

...

结尾聊聊天[番外]


​强调:此次正经​


背景

最近,我有重要的伙伴陷入低谷,通过对话交流我能感受到伙伴正在经历很困难的一个阶段,我也有很多想法,记录在此。

其实刚刚步入社会的我们,肯定早就发现了,工作之后和学生时代的生活真的是翻天覆地的变化。之前我们在大学生活中,其实大部分时间是自由自在的。压力?几乎没有。 然而我们工作之后其实压力是多种多样的,压力来源直接翻了很多很多倍,会感觉到责任重大,工作不快乐呀各种各样的问题。

关于工作中处理问题方式

但是,说着压力大,说着不快乐,但是我们总会需要一个方法去解决问题,让自己有一个做事的方法论为依托,让自己的工作“快乐”起来,毕竟遇到问题直面并解决才是积极的处事风格。

所以我列出以下几点,并说出自己的想法,如果各位读者有什么想法可以评论区补充。

​关于工作中的交流风格:​


简洁,明确,直白,省去不必要的环节,该怼就怼,该说就说。


​突然同时有好几个事情要做怎么处理:​


首先,我们明确知道一个点,人可以并发处理问题,而不是并行,遇到很多要解决的事,我们还是要一个一个做的。之后,我们按照事情的紧急程度和重要程度进行排列,一般情况下,紧急程度更加值得关注,我们优先按照紧急程度由高到低去处理问题,同等紧急的事情再去根据重要性和耗时长短去考量事件的优先级。如果事情都不太紧急,再去按照重要性去安排任务。
而且,我眼里,一旦开启一件事情,除非发生难以个人推动的阻塞性问题,或者极其紧急的事情需要处理,就尽量不要中途切换到别的任务。因为事件的切换是需要时间的。


​如何解决参会者时间冲突问题:​


首先,参会者是有优先级的,我们先要去搞清楚,哪些人是必须参会的,哪些人是可以不参加的,之后我们再去优先安排对会议内容更关键的角色的时间,较不重要的参会人可以通过其他方式了解会议结论,比如会议纪要或者讨论内容的相关文档更新记录。


​如果工作遇到问题超出自己认知范畴怎么办:​


说白了,这还是个时间问题,我认为,优先解决问题,如果可以快速Google到解决方案,就直接自己解决掉,并记录问题【最好自己有一个markdown】。如果不能,优先去问身边的有经验者或者技术大佬(如果他有时间并愿意搭理你的话),不要把较长工作时间浪费在探索问题原因这件事情上,我眼里这个时间我会控制在一小时,当然如果任务少,我就死磕,任务重时间紧的时候,我看一看就直接暴露,快速找人解决。反正根据任务量,工作紧张程度,去自己衡量一个阈值。留给自己去探索解决问题时间的阈值。
但是,最后,我们一定要记录问题!并尽量本周事,本周毕,不要留尾巴,把问题弄清楚,搞明白,这也是提升的过程。


鼓励与感谢

我这个人总是张口闭口​​冲冲冲​​​,总是乐于说着​​无敌无敌​​​,​​加油加油​​​这样的话,当然其实也带动着什么的同事和朋友和我一起说着​​热血中二​​话,我其实比较词穷,所以翻来覆去也就这几句。

在这工作半年多时间,遇到了很多友善的人,有人给我指引前进的路,有人给我鼓励,其实我工作这半年多还是很幸福的,在此感谢:

  • 感谢曾经的导师,给过我很多指导。
  • 感谢工作中的同事,以及几个甚至不是前端的伙伴也在支持着我这些稚嫩的文章。hhh,虽然每次看到大大的​​小葛后援会​​群名我也很羞耻。
  • 感谢重要的小伙伴,和我互为程序员鼓励师。

所以,伙伴们,大家一起加油呀,有困难一起战胜~

葱呀!!​​你们要知道作为一个不善言辞的人,葱呀其实饱含深意~​

​伙伴,你也是无敌的,愿你更快乐的生活与工作~ ​

最后,头图与尾图是玉渊潭晚上看到的,好看吧!

Promise从手写到扩展 | Promise/Generator/async | [Promise系列二]_生成器_02

感谢阅读~

举报

相关推荐

0 条评论