手写Promise

# 手写Promise

# 前言

这里主要是我的Promise手写学习记录,关于Promise的入门和使用,具体可移步阮老师的Promise对象 (opens new window)

Promise作为异步编程的一种解决方案,比传统的解决方案回调函数更合理和更强大;它由社区最早提出和实现,ES6将其写进了语言标准。

我在项目中应用比较多的场景主要是对后端接口请求的处理,还有就是前端需要执行一些异步任务的时候会用到,但我一直以来都停留在简单的使用,对于它是怎么实现的一直不清楚,出于对其实现原理的好奇,所以才有了这篇博文。

下面直接进入主题。

# 使用

// 定义
let asyncFn = new Promise((resolve, reject) => {
    console.log('pending');
    setTimeout(() => {
        let random = Math.random() * 10;
        if (random > 5) {
            resolve('result success'); // 成功回调
        } else {
            reject('result error'); // 错误回调
        }
    },100);
});

// 调用
asyncFn.then(res => {
    console.log('====then',res); // 成功回调处理
}).catch(err => {
    console.log('err',err); // 错误回调处理
})

上面就是我在项目中使用Promise进行异步任务时的简单模型,首先定义一个异步函数,一般是后端的接口请求,这里用setTimeout模拟异步操作;之后在调用这个请求的时候对成功和失败两种情况进行处理。

执行顺序:

  1. 首先是调用asyncFn方法,并立即执行里面的代码,打印pending
  2. 100ms后进行随机数判断:若大于5则执行成功回调函数resolvethen方法中打印====then result success;否则则执行失败回调rejectcatch方法中打印====then result error.

大概的使用就是这样,接下来试着手写一个Promise实现上面的效果。

# 实现

首先梳理一下要实现的主要功能:

想实现哪些功能?

  1. Promise的状态处理:Pending、Resolve、Rejectthen方法;
  2. 异步任务和链式调用
  3. 添加Promise的其他方法:catch、finally、all、race、resolve、reject;

# MyPromise基础版

这里我会按照上面三个功能点,分步实现,首先是最基本版的实现。

# 1.构造函数的定义

首先定义一个构造函数,并添加三种状态:


// 添加三个状态常量
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

// 定义MyPromise类
class MyPromise {
    constructor(executor) {
        this._status = PENDING; // 初始化状态
        this._value = undefined; // 初始化成功回调返回的值
        this._reason = undefined; // 初始化失败回调返回的值

       /*
        executor是生成Promise实例时传入的执行函数,即 new Promise((resolve,reject) => {...})中的(resolve,reject) => {...}
        因为它在生成实例的时候就要立即执行,所以这里直接开始执行:
       */
        try {
            // resolve, reject是这个执行函数里传入的参数,这两个参数本身也是函数,接下来会重点定义这两个函数
            // bind(this):绑定this指向
            executor(this.resolve.bind(this), this.reject.bind(this))
        } catch(err) { // 兼容报错情况
            this.reject(err);
        }
    }

    // 添加_resolve方法
    resolve(val) {
        ...
    }

    // 添加_reject方法
    reject(err) {
        ...
    }
}

初始化之后,接下来继续完善resolvereject里面状态的执行逻辑:

resolve(val) {
    if (this._status !== PENDING) return; // 为了保证状态从pending > fulfilled的单向流转,当状态不为pending时不让继续执行
    this._status = FULFILLED; // 切换状态为已完成:fulfilled
    this._value = val; // 并获取成功回调的值
}

// 同理,reject中也是类似的处理逻辑
reject (err) { 
    if (this._status !== PENDING) return
    this._status = REJECTED
    this._reason = err
}

以上就已经能实现Promise基本的状态流转和值的传递了,接下来定义then方法:

# 2.then方法的初定义

看了阮老师的Promise入门讲解后,我发现then方法其实是可以接受两个回调函数作为参数的,虽然我在平时项目开发中基本都是只传一个回调函数;而和then一起搭配使用的catch方法其实就是.then(null, rejection),所以这里只要定义好then方法就能实现.then().catch()

// 继续在MyPromise类中添加then方法
then (onFulfilled, onRejected) { // onFulfilled和onRejected是我们传入的两个回调函数
    if (this._status === FULFILLED) { // 成功回调
        onFulfilled(this._value);
    }
    if (this._status === REJECTED) { // 失败回调
        onRejected(this._reason);
    }
}

现在已经算是基本定义好了Promise类的状态切换和then方法,最简单的第一版Promise已经完成,可以调用一下看看是否生效,下面是完整代码:

// MyPromise基础版

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
    constructor(executor) {
        this._status = PENDING;
        this._value = undefined;
        this._reason = undefined;
        try {
            executor(this.resolve.bind(this), this.reject.bind(this))
        } catch(err) {
            this.reject(err);
        }
    }
    resolve(val) {
        if (this._status !== PENDING) return;
        this._status = FULFILLED;
        this._value = val;
    }
    reject (err) { 
        if (this._status !== PENDING) return
        this._status = REJECTED
        this._reason = err
    }
    then (onFulfilled, onRejected) {
        if (this._status === FULFILLED) {
            onFulfilled(this._value);
        }
        if (this._status === REJECTED) {
            onRejected(this._reason);
        }
    }
}

// 使用
let asyncFn = new MyPromise((resolve, reject) => {
    console.log('start'); // start
    resolve('success');
    // reject('error');
})
asyncFn.then(res => {
    console.log('then',res); // then success
}, err => {
    console.log('err', err); // 如果把上面的resolve方法注释掉,执行reject('error')方法,则打印 err error
})

执行成功,说明第一版Promise已经完成,但如果把asyncFn修改为:

let asyncFn = new MyPromise((resolve, reject) => {
    console.log('start'); // start
    setTimeout(() => { // 执行异步任务
         resolve('success');
        // reject('error');
    },0)
})

这样的话,由于.then里面的方法立即执行,还没来得及执行resolve('success').then就已经执行完了,所以目前这个版本只能运行同步任务,对于异步任务then的链式调用还不行,继续完善:

# MyPromise升级版

# 异步任务和链式调用

想要支持异步任务,首先得需要改造constructor:增加执行队列,用于保存then方法注册时的回调函数:





 
 







constructor(executor) {
    this._status = PENDING;
    this._value = undefined;
    this._reason = undefined;
    this._onResoveQueues = []; // 添加成功回调函数队列
    this._onRejectQueues = []; // 添加失败回调函数队列
    try {
        executor(this.resolve.bind(this), this.reject.bind(this))
    } catch(err) {
        this.reject(err);
    }
}

接着改造then方法:

then (onFulfilled, onRejected) {
    // if (this._status === FULFILLED) { // 成功回调
    //     onFulfilled(this._value);
    // }
    // if (this._status === REJECTED) { // 失败回调
    //     onRejected(this._reason);
    // }
    const {_value, _reason, _status} = this;
    switch(_status) {
        case PENDING: // 当状态为pending,把回调函数加入到队列中
            this._onResoveQueues.push(onFulfilled);
            this._onRejectQueues.push(onRejected);
            break;
        case FULFILLED: // 立即执行成功回调
            onFulfilled(this._value);
            break;
        case REJECTED: // 立即执行失败回调
            onRejected(this._reason);
            break;
    }
}
  • then的链式调用

接着就是给then方法添加链式调用,先来看看什么是链式调用:

asyncFn.then(res => {
    console.log('====res1',res);
    return res;
}).then(res => {
    console.log('====res2',res);
}).then(res => {...})

类似这种,一般函数的链式调用都是在执行完方法后直接return this,在这里,想实现then方法的链式调用,就得让then返回一个新的Promise对象:

then (onFulfilled, onRejected) {
    const {_value, _reason, _status} = this;
    // 返回一个Promise对象
    return new MyPromise((onFulfilledSub, onRejectedSub) => {
        // 封装一个成功回调方法
        let _fulfilled = val => {
            let res = onFulfilled(val);
            // 判断是否返回Promise对象
            if (res instanceof MyPromise) {// 如果是,必须等待其状态改变后再执行下一个回调
                res.then(onFulfilledSub, onRejectedSub)
            } else { // 不是,则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
                onFulfilledSub(res);
            }
        };

        // 封装一个失败回调方法
        let _rejected = err => {
            let res = onRejected(err);
            // 判断是否返回Promise对象
            if (res instanceof MyPromise) {// 如果是,必须等待其状态改变后再执行下一个回调
                res.then(onFulfilledSub, onRejectedSub)
            } else { // 不是,则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
                onRejectedSub(res);
            }
        };

        switch(_status) {
            case PENDING: // 当状态为pending,把回调函数加入到队列中
                this._onResoveQueues.push(_fulfilled);
                this._onRejectQueues.push(_rejected);
                break;
            case FULFILLED: // 立即执行成功回调
                _fulfilled(_value);
                break;
            case REJECTED: // 立即执行失败回调
                _rejected(_reason);
                break;
        }
    })
}

这里面代码量有点多,具体就是让then返回一个新的Promise对象,并封装了一个成功回调方法fulfilled和失败回调方法rejected,并在这两个方法里执行传入的回调函数,然后根据其返回的值是不是Promise对象,分别进行处理。

下面来改造resolve方法:

当执行该方法时,依次执行回调队列_onResoveQueues中的函数,执行完毕并清空队列





 
 
 
 
 
 
 
 
 
 
 
 
 
 
 


resolve(val) {
    if (this._status !== PENDING) return;
    // this._status = FULFILLED;
    // this._value = val;
    // 封装一个run方法
    const run = () => {
        this._status = FULFILLED
        this._value = val
        let cb;        
        while (cb = this._onResoveQueues.shift()) { // 循环遍历执行_onResoveQueues队列中函数,并清空队列
            cb(val)
        }
        /** 例:
        let a = [1,2,3,4,5],b;
        while(b = a.shift()){console.log(b)}; 依次打印 1 2 3 4 5,并把数组a清空
        * **/ 
    }
    // 模拟异步操作
    setTimeout(() => run(), 0)
}

同理,也可以对reject方法进行类似的改造:






 
 
 
 
 
 
 
 
 
 


reject (err) { 
    console.log(this);
    if (this._status !== PENDING) return
    // this._status = REJECTED
    // this._reason = err
    const run = () => {
        this._status = REJECTED
        this._reason = err
        let cb;        
        while (cb = this._onRejectQueues.shift()) { // 循环遍历执行_onRejectQueues队列中函数,并清空队列
            cb(val)
        }
    }
    // 模拟异步操作
    setTimeout(() => run(), 0)
}

到这里已经基本实现了Promise的异步任务和链式调用,接下来校验下第二版的MyPromise代码有没有问题,下面是完整代码:

// MyPromise升级版

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
    constructor(executor) {
        this._status = PENDING;
        this._value = undefined;
        this._reason = undefined;
        this._onResoveQueues = []; // 添加成功回调函数队列
        this._onRejectQueues = []; // 添加失败回调函数队列
        try {
            executor(this.resolve.bind(this), this.reject.bind(this))
        } catch(err) {
            this.reject(err);
        }
    }
    resolve(val) {
        if (this._status !== PENDING) return;
        // 封装一个run方法
        const run = () => {
            this._status = FULFILLED
            this._value = val
            let cb;        
            while (cb = this._onResoveQueues.shift()) { // 循环遍历执行
                cb(val)
            }           
        }
        // 模拟异步操作
        setTimeout(() => run(), 0)
    }
    reject (err) {
        if (this._status !== PENDING) return       
        const run = () => {
            this._status = REJECTED
            this._reason = err
            let cb;        
            while (cb = this._onRejectQueues.shift()) {
                cb(err)
            }
        }
        setTimeout(() => run(), 0)
    }
    then (onFulfilled, onRejected) {
        const {_value, _reason, _status} = this;
        // 返回一个Promise对象
        return new MyPromise((onFulfilledSub, onRejectedSub) => {
            // 封装一个成功回调方法
            let _fulfilled = val => {
                let res = onFulfilled(val);
                // 判断是否返回Promise对象
                if (res instanceof MyPromise) {// 如果是,必须等待其状态改变后再执行下一个回调
                    res.then(onFulfilledSub, onRejectedSub)
                } else { // 不是,则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
                    onFulfilledSub(res);
                }
            };

            // 封装一个失败回调方法
            let _rejected = err => {
                let res = onRejected(err);
                if (res instanceof MyPromise) {
                    res.then(onFulfilledSub, onRejectedSub)
                } else {
                    onRejectedSub(res);
                }
            };
            switch(_status) {
                case PENDING: // 当状态为pending,把回调函数加入到队列中
                    this._onResoveQueues.push(_fulfilled);
                    this._onRejectQueues.push(_rejected);
                    break;
                case FULFILLED: // 立即执行成功回调
                    _fulfilled(_value);
                    break;
                case REJECTED: // 立即执行失败回调
                    _rejected(_reason);
                    break;
            }
        })
    }
}

// 定义
let asyncFn = new MyPromise((resolve, reject) => {
    console.log('start');
    setTimeout(() => {
        console.log('====1111')
        resolve('success');
        // reject('error');
        console.log('====2222')
    }, 1000);
})
// 调用
asyncFn.then(res => {
    console.log('then',res);
    // 返回Promise对象
    return new MyPromise((resolve, reject) => {
        resolve(res + '__aaa')
    })
}, err => {
    console.log('err', err);
}).then(res => { // 链式调用
    console.log('===then',res);
    return res + '__bbb'; // 返回字符串
}).then(res => { // 链式调用
    console.log('====then',res);
})
console.log('end');

执行一下asyncFn方法,测一下异步任务和链式调用是否生效,执行后打印顺序为:

start  // 首先打印start,因为MyPromise实例声明后,里面的代码就会立即执行
end // 然后执行所有同步任务,打印end
====1111 // 同步代码执行完毕,开始执行异步任务,1000ms后,直接打印====1111
====2222 // 因为前面的resolve('success')是异步任务,同理,先不执行,直接打印====2222
then success // 等到所有同步任务执行完毕,才开始执行asyncFn的then回调函数,打印获取到的res
===then success__aaa // 上一步then返回一个Promise对象,这里链式调用获取到的res是这个新的Promise对象resolve中抛出的值,即:success__aaa
====then success__aaa__bbb // 这一步也是通过链式调用获取上一步then方法中返回的值,因为上一步直接返回字符串,所以直接打印

到这里,也算是初步实现了Promise基本方法使用、异步任务、链式调用了,下面接着继续完善:

# MyPromise轻奢版

# 1.兼容处理

  • resolve方法中传值的兼容处理

在项目开发中遇到过resolve(val)成功回调抛出的val本身也是一个Promise对象的情况,如:

let p1 = new Promise((resolve, reject) => {
    ...
})
let p = new Promise((resolve, reject) => {
    resolve(p1) // p1也是一个Promise对象
})

这种情况就需要对resolve(val)传入的val进行兼容处理:

如果valPromise对象,则执行它的.then方法,等待其状态改变再执行回调

resolve(val) {
    if (this._status !== PENDING) return;
    // 重新封装run方法
    const run = () => {
        // 定义一个成功回调
        let runFulfilled = (value) => {
            let cb;        
            while (cb = this._onResoveQueues.shift()) { // 循环遍历执行_onResoveQueues队列中函数,并清空队列
                cb(value)
            }
        }
        // 失败回调
        let runRejected = (error) => {
            let cb;        
            while (cb = this._onRejectQueues.shift()) { // 循环遍历执行_onRejectQueues队列中函数,并清空队列
                cb(error)
            }
        }
        // 判断val是否为Promise类型
        if (val instanceof MyPromise) {
            val.then(res => { // 执行.then方法,等待其状态改变
                this._status = FULFILLED; // 状态改变
                this._value = res; // 获取val返回的值
                runFulfilled(res); // 执行成功回调
            }, err => {
                this._status = REJECTED;
                this._reason = err;
                runRejected(err);
            })
        } else { // 其他类型
            this._status = FULFILLED; // 状态改变
            this._value = val; // 获取val返回的值
            runFulfilled(val); // 执行成功回调
        }
    }
    // 模拟异步操作
    setTimeout(() => run(), 0)
}

这样修改之后,就可以在asyncFn中这样使用了:

let asyncFn = new MyPromise((resolve, reject) => {
    console.log('start');
    setTimeout(() => {
        console.log('====1111')
        // resolve('success');
        resolve(new MyPromise((_resolve, _reject) => {
            _resolve('success');
        }))
        // reject('error');
        console.log('====2222')
    }, 1000);
})

resolve(val)传入的val为Promise对象时,执行结果跟刚才的也是一样的。

  • then方法中对传入的回调函数作兼容处理

增加类型判断、报错提示

// 判断变量否为function
const isFunction = variable => typeof variable === 'function'

then() {
    ...
    let _fulfilled = val => {
        try {
            if (!isFunction(onFulfilled)) {
                onRejectedSub(val); // 直接报错
                return;
            }
            let res = onFulfilled(val);
            // 判断是否返回Promise对象
            if (res instanceof MyPromise) {// 如果是,必须等待其状态改变后再执行下一个回调
                res.then(onFulfilledSub, onRejectedSub)
            } else { // 不是,则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
                onFulfilledSub(res);
            }
        } catch(err) {
            onRejectedSub(err); // 直接报错
        }
    };
    ...
}

同样的,在_rejected也加类似的判断,这里不再赘述了。

到这里其实一个基本的Promise就已经实现了,接下来添加一些其他方法。

# 2.添加其他方法

  • catch方法

前面说过,看了阮老师的入门课程后,发现其实Promisecatch方法就是直接调用的then方法:.then(null, rejection)

catch(onRejected) {
    return this.then(null, onRejected); // 直接调用then方法
}
  • 静态resolve方法
static resolve(value) {
    // 如果参数是MyPromise实例,直接返回这个实例
    return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value));
} 
  • 静态reject方法
static reject(error) {
    return new MyPromise((resolve, reject) => reject(error)); // 返回MyPromise实例
} 
  • 静态all方法

只要有一个失败就算失败,全成功才算成功,失败返回失败的那个报错信息,成功返回所有的成功信息,返回一个新的Promise对象

static all(list) {
    return new MyPromise((resolve, reject) => {
        let index = 0;
        let result = [];
        list.forEach((v,i) => {
            // 统一转换成MyPromise对象,调用then方法
            MyPromise.resolve(v).then(res => {
                index ++;
                result[i] = res;
                if (index >= list.length) { // 循环结束,全部成功则执行resolve方法
                    resolve(result);
                }
            },err => {
                reject(err); // 有报错,直接执行reject方法
            })
        })
    })
}
  • 静态race方法

race,顾名思义:竞速模式。只要有一个状态改变就执行回调,同样返回一个新的Promise对象;这个方法常用于接口请求中需要设置超时限制的场景。

staic race(list) {
    return new MyPromise((resolve, reject) => {
        for(let p of list) { // 循环遍历
            MyPromise.resolve(p).then(res => {
                resolve(res); // 有状态更改,直接执行
            }, err => {
                reject(err); // 同上
            })
        }
    })
}
  • finally方法

finally方法用于指定不管Promise对象最后状态如何,都会执行的操作

finally(cb) {
    return this.then(res => {
        MyPromise.resolve(cb()).then(() => res); // 先将cb转成Promise实例
    },err => {
        MyPromise.resolve(cb()).then(() => {throw err});
    })
}

到这里,我的手写MyPromise可算是结束了~

最终版完整代码如下:

// MyPromise轻奢版

const isFunction = fn => typeof fn === 'function'
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
    constructor(executor) {
        this._status = PENDING;
        this._value = undefined;
        this._reason = undefined;
        this._onResoveQueues = [];
        this._onRejectQueues = [];
        try {
            executor(this.resolve.bind(this), this.reject.bind(this))
        } catch(err) {
            this.reject(err);
        }
    }
    resolve(val) {
        if (this._status !== PENDING) return;
        const run = () => {
            let runFulfilled = (value) => {
                let cb;        
                while (cb = this._onResoveQueues.shift()) {
                    cb(value)
                }
            }
            let runRejected = (error) => {
                let cb;        
                while (cb = this._onRejectQueues.shift()) {
                    cb(error)
                }
            }
            if (val instanceof MyPromise) {
                val.then(res => {
                    this._status = FULFILLED;
                    this._value = res;
                    runFulfilled(res);
                }, err => {
                    this._status = REJECTED;
                    this._reason = err;
                    runRejected(err);
                })
            } else {
                this._status = FULFILLED;
                this._value = val;
                runFulfilled(val);
            }
        }
        setTimeout(() => run(), 0)
    }
    reject (err) {
        if (this._status !== PENDING) return
        const run = () => {
            this._status = REJECTED
            this._reason = err
            let cb;        
            while (cb = this._onRejectQueues.shift()) {
                cb(err)
            }
        }
        setTimeout(() => run(), 0)
    }
    then (onFulfilled, onRejected) {
        const {_value, _reason, _status} = this;
        return new MyPromise((onFulfilledSub, onRejectedSub) => {
            let _fulfilled = val => {
                try {
                    if (!isFunction(onFulfilled)) {
                        onRejectedSub(val);
                        return;
                    }
                    let res = onFulfilled(val);
                    if (res instanceof MyPromise) {
                        res.then(onFulfilledSub, onRejectedSub)
                    } else {
                        onFulfilledSub(res);
                    }
                } catch(err) {
                    onRejectedSub(err); // 直接报错
                }
                
            };

            // 封装一个失败回调方法
            let _rejected = err => {
                try {
                    if (!isFunction(onRejected)) {
                        onRejectedSub(err); // 直接报错
                        return;
                    }
                    let res = onRejected(err);
                    // 判断是否返回Promise对象
                    if (res instanceof MyPromise) {// 如果是,必须等待其状态改变后再执行下一个回调
                        res.then(onFulfilledSub, onRejectedSub)
                    } else { // 不是,则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
                        onRejectedSub(res);
                    }
                } catch(err) {
                    onRejectedSub(err); // 直接报错
                }

                
            };
            switch(_status) {
                case PENDING: // 当状态为pending,把回调函数加入到队列中
                    this._onResoveQueues.push(_fulfilled);
                    this._onRejectQueues.push(_rejected);
                    break;
                case FULFILLED: // 立即执行成功回调
                    _fulfilled(_value);
                    break;
                case REJECTED: // 立即执行失败回调
                    _rejected(_reason);
                    break;
            }
        })
    }

    catch(onRejected) {
        return this.then(null, onRejected);
    }

    /*
    * 添加静态方法
    */

    static resolve(value) {
        // 如果参数是MyPromise实例,直接返回这个实例
        return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value));
    }
    
    static reject(error) {
        return new MyPromise((resolve, reject) => reject(error));
    }

    static all(list) {
        return new MyPromise((resolve, reject) => {
            let index = 0;
            let result = [];
            list.forEach((v,i) => {
                // 兼容v不是Promise实例的情况
                MyPromise.resolve(v).then(res => {
                    index ++;
                    result[i] = res;
                    if (index >= list.length) { // 循环结束
                        resolve(result);
                    }
                },err => {
                    reject(err);
                })
            })
        })
    }

    static race(list) {
        return new MyPromise((resolve, reject) => {
            for (let p of list) {
                MyPromise.resolve(p).then(res => {
                    resolve(res);
                }, err => {
                    reject(err);
                })
            }
        })
    }

    finally(cb) {
        return this.then(res => {
            MyPromise.resolve(cb()).then(() => res); // 先将cb转成Promise实例
        },err => {
            MyPromise.resolve(cb()).then(() => {throw err});
        })
    }
}

end~

# 备注

  1. 这里MyPromise的异步调用时通过setTimeout来模拟的,但在js的执行序列里,Promise是属于微任务,而setTimeout是属于宏任务,Promise是先于setTimeout执行的,这里只是通过setTimeout来模拟异步操作。
  2. finally方法感觉还需要梳理下~
  3. 有时间研究下Promise.allSettled方法~

# 参考

上次更新: 1/24/2022, 5:38:42 PM
最近更新
01
taro开发实操笔记
09-29
02
前端跨端技术调研报告
07-28
03
Flutter学习笔记
07-15
更多文章>