在ES6中我最喜欢的的两个特性,一个是模块化,另一个是promise。

promise其实和观察者模式非常类似,他们都有自身的订阅者。所不同的是promise决议后事件是异步触发的(即订阅者的回调是异步调用的),而且在决议后再订阅该事件依然可以触发订阅者的回调(观察者模式事件触发后再订阅事件只能等下一次事件触发)。

我所要采用的promise的polyfill是github上的一个项目提供的,ajax库axios也推荐这个promise的polyfill。

异步

异步是promise很重要的一点,这个第三方库的异步相关代码在 asap.js 中。

类似于Vue中nextTick方法的实现,这里首先是根据环境尝试选择不同的异步方案,兜底方案就是使用setTimeout方法。

具体实现上维护了一个队列,我们需要异步调用的方法都往这个队列里面放,然后异步处理完整个队列:

const queue = new Array(1000);
export var asap = function asap(callback, arg) {
    // 将任务放入到队列中
    queue[len] = callback;
    queue[len + 1] = arg;
    len += 2;

    // 当异步队列为空时向队列添加任务,此时才需要启动异步处理队列
    // 当不为空时添加任务,已经开启了异步处理队列,无需重复处理
    if (len === 2) {
        if (customSchedulerFn) {
            customSchedulerFn(flush);
        } else {
            scheduleFlush();
        }
    }
}

// 处理异步队列的任务
function flush() {
    for (let i = 0; i < len; i+=2) {
        let callback = queue[i];
        let arg = queue[i+1];
        callback(arg);
        queue[i] = undefined;
        queue[i+1] = undefined;
    }

    len = 0;
}

构造函数

promise的构造函数在 promise.js 中,它比较简单,主要是声明了几个私有属性_result(反映promise的结果)、_state(反映promise的状态)、_subscribers(这个promise未处理的订阅者)。

export default function Promise(resolver) {
    this[PROMISE_ID] = nextId();
    this._result = this._state = undefined;
    this._subscribers = [];

    if (noop !== resolver) {
        typeof resolver !== 'function' && needsResolver();
        this instanceof Promise ? initializePromise(this, resolver) : needsNew();
    }
}

比较难理解的是initializePromise方法:

function initializePromise(promise, resolver) {
    try {
        resolver(function resolvePromise(value){
            resolve(promise, value);
        }, function rejectPromise(reason) {
            reject(promise, reason);
        });
    } catch(e) {
        reject(promise, e);
    }
}

在promise世界有个词叫 revealing constructor ,构造器是由外界传进来的(resolver方法),resolver会立即执行,并且会传入两个函数作为参数。

订阅 决议

之前提到过,promise和观察者模式有相似之处,他们都有自己的订阅者。上面已经提到,订阅者相关信息被存放到了_subscribers这一私有属性中。订阅是在then方法中调用的,我们看一下订阅的实现:

function subscribe(parent, child, onFulfillment, onRejection) {
    let { _subscribers } = parent;
    let { length } = _subscribers;

    parent._onerror = null;

    _subscribers[length] = child;
    _subscribers[length + FULFILLED] = onFulfillment;
    _subscribers[length + REJECTED]  = onRejection;

    if (length === 0 && parent._state) {
        asap(publish, parent);
    }
}

promise的then方法会返回一个新promise,对应这里的child,onFulfillment、onRejection分别对应then方法的两个参数。

注册完我们就可以等决议了:

function resolve(promise, value) {
    if (promise === value) {
        reject(promise, selfFulfillment());
    } else if (objectOrFunction(value)) {
        handleMaybeThenable(promise, value, getThen(value));
    } else {
        fulfill(promise, value);
    }
}
function reject(promise, reason) {
    if (promise._state !== PENDING) { return; }
    promise._state = REJECTED;
    promise._result = reason;

    asap(publishRejection, promise);
}
function fulfill(promise, value) {
    if (promise._state !== PENDING) { return; }

    promise._result = value;
    promise._state = FULFILLED;

    if (promise._subscribers.length !== 0) {
        asap(publish, promise);
    }
}

这里请允许我先发明两个概念,一个是临时决议值,一个是最终决议值。临时决议值是我们在promise构造器中调用resolve方法传入的值,或者then方法resolve回调中返回的值,临时决议值可以是最终决议值,也可以不是,这主要取决于临时决议值是不是一个promise(传入thenable没遇到过,不考虑)。如果临时决议值不是promise,就会通过fulfill转换成最终决议值,同时promise的状态也更新为resolved。类似于观察值模式,已决议的promise会通知订阅者已决议这个事件:

function publish(promise) {
    let subscribers = promise._subscribers;
    let settled = promise._state;

    if (subscribers.length === 0) { return; }

    let child, callback, detail = promise._result;

    // 遍历订阅者,执行回调
    for (let i = 0; i < subscribers.length; i += 3) {
        // child是then方法创造出来的新promise
        child = subscribers[i];
        callback = subscribers[i + settled];

        if (child) {
            invokeCallback(settled, child, callback, detail);
        } else {
            callback(detail);
        }
    }

    // _subscribers维护的是为执行的订阅者,所以这里要清空
    promise._subscribers.length = 0;
}

你可能会好奇invokeCallback起到了什么作用。这个方法是把决议按照promise链向后传递的关键:

function invokeCallback(settled, promise, callback, detail) {
    let hasCallback = isFunction(callback),
        value, error, succeeded, failed;

    if (hasCallback) {
        value = tryCatch(callback, detail);

        if (value === TRY_CATCH_ERROR) {
            failed = true;
            error = value.error;
            value.error = null;
        } else {
            succeeded = true;
        }

        if (promise === value) {
            reject(promise, cannotReturnOwn());
            return;
        }

    } else {
        value = detail;
        succeeded = true;
    }

    // 决议向后传递的关键
    if (promise._state !== PENDING) {
    // noop
    } else if (hasCallback && succeeded) {
        resolve(promise, value);
    } else if (failed) {
        reject(promise, error);
    } else if (settled === FULFILLED) {
        fulfill(promise, value);
    } else if (settled === REJECTED) {
        reject(promise, value);
    }
}

这里的一个问题是有没有对应的回调函数,毕竟决议状态有两种。如果有对应的回调函数,回调函数的返回值就是临时决议值,需要通过resolve方法进一步处理,如果没有,这个then方法产生的promise的决议值和决议状态由其订阅的promise(父promise)决定,即可以简单使用fulfill方法(因为父promise已经决议)。

到现在还有两个问题,一个是如果调用then方法时promise已决议怎么办,另一个是临时决议值如果是promise怎么办。

第一个方法的解决方案其实很简单,就是判断一下promise的状态,如果未决议走订阅的路子,如果已决议走异步执行回调的路子。

export default function then(onFulfillment, onRejection) {
    const parent = this;

    const child = new this.constructor(noop);

    if (child[PROMISE_ID] === undefined) {
        makePromise(child);
    }

    const { _state } = parent;

    // 如果已决议异步执行回调
    if (_state) {
        const callback = arguments[_state - 1];
        asap(() => invokeCallback(_state, child, callback, parent._result));
    } else {
        subscribe(parent, child, onFulfillment, onRejection);
    }

    return child;
}

第二个问题就比较复杂了:

// 这个方法关联两个promise
function handleOwnThenable(promise, thenable) {
    if (thenable._state === FULFILLED) {
        fulfill(promise, thenable._result);
    } else if (thenable._state === REJECTED) {
        reject(promise, thenable._result);
    } else {
        subscribe(
            thenable, 
            undefined, 
            value  => resolve(promise, value),
            reason => reject(promise, reason)
        )
    }
}

上面代码中thenable是类型为promise的临时决议值,在这种情况下原始promise的最终决议值由这个临时决议promise决定。请注意这种模式下订阅者是关联到父promise而不是返回值promise,但是父promise的状态由子promise决定。

Promise.resolve Promise.reject

这两个静态方法其实只是语法糖罢了,Promise.resolve(obj)相当于:

new Promise((resolve,reject)=>{
    resolve(obj)
})

所以Promise的实现就非常简单:

Promise.resolve = function(obj){
    return new Promise(function(resolve,reject){
        resolve(obj);
    })
}

类似的,我们可以得到Promise.reject的polyfill:

Promise.reject = function(obj){
    return new Promise(function(resolve,reject){
        reject(obj);
    });
}

上面提到的实现和这个提到的第三方库的实现有所出入,但最终结果都是返回已决议的promise。

Promise.reject我用得不多,Promise.resolve用处相对较多,后者的功能除了看做是返回一个已决议promise,还可以看做是把非promise包装成promise,这一特性在静态方法Promise.race和Promise.all中得到了应用。

Promise.race Promise.all

Promise.race是传入一个数组,返回的是一个新promise,新promise的状态(resolve还是reject)以及决议值由传入的数组对应的promise中第一个已决议的promise决定。

export default function race(entries) {
    let Constructor = this;

    if (!isArray(entries)) {
        return new Constructor((_, reject) => reject(new TypeError('You must pass an array to race.')));
    } else {
        return new Constructor((resolve, reject) => {
            let length = entries.length;
            for (let i = 0; i < length; i++) {
                Constructor.resolve(entries[i]).then(resolve, reject);
            }
        });
    }
}

这段代码的核心是Constructor.resolve(entries[i]).then(resolve, reject);,首先是把传入数组中所有元素包装成promise(事实上如果传入的是promise也会被包装一层),然后等待第一个决议的promise,由这个promise决定返回的promise的状态和值。

这个第三方库给出的Promise.all的polyfill我感觉过于复杂,下面是我给出的方案:

Promise.all = function(arr){
    return new Promise((resolve,reject)=>{
        // 此处应有对arr数据类型的校验

        let rst = [];
        let counter = 0;
        let len = arr.length;
        for(let i=0;i<len;i++){
            Promise.resolve(arr[i]).then((value)=>{
                rst[i] = value;
                counter++;
                if(counter===len){
                    resolve(rst);
                }
            }).catch((value)=>{
                reject(value)
            })
        }
    })
}

虽说是polyfill但是依然用到了一些ES6的特性,尤其是let关键字。

results matching ""

    No results matching ""