Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promise 库 lie.js 源码解读 #26

Open
CommanderXL opened this issue Jan 4, 2019 · 0 comments
Open

Promise 库 lie.js 源码解读 #26

CommanderXL opened this issue Jan 4, 2019 · 0 comments

Comments

@CommanderXL
Copy link
Owner

Lie源码解读

这篇文章是通过lie.js的源码一起去了解下如何实现Promise相关的规范。

首先是Promise的核心的构造函数的实现。

function INTERNAL() {}

var REJECTED = ['REJECTED'];
var FULFILLED = ['FULFILLED'];
var PENDING = ['PENDING'];

var handlers = {}

function Promise (resolver) {
  if (typeof resolver !== 'function') {
    throw new TypeError('resolver must be a function');
  }
  this.state = PENDING;
  this.queue = [];
  this.outcome = void 0;
  /* istanbul ignore else */
  if (!process.browser) {
    this.handled = UNHANDLED;
  }
  if (resolver !== INTERNAL) {
    safelyResolveThenable(this, resolver);
  }
}

构造函数内部定义了几个promise实例的属性:

  • state.

promise的状态值.有3种:rejectedfulfilledpending

  • queue

queue数组用以保存这个promiseresolve/rejected后需要异步执行的回调

  • outcome

这个promise实例的值

对于promise,我们一般的用法是:

// 构造函数当中接收2个参数,resolve/reject,需要注意的是这2个参数是promise内部定义的,用以改变这个promise的状态和值
const promise = new Promise((resolve, reject) => {
  // 同步或者异步的去resolve一个值
  resolve(1)
})

给这个Promise构造函数内部传入的resolver由内部的方法safelyResolveThenable去执行:

function safelyResolveThenable(self, thenable) {
  // Either fulfill, reject or reject with error
  // 标志位,初始态的promise仅仅只能被resolve/reject一次
  var called = false;
  function onError(value) {
    if (called) {
      return;
    }
    called = true;
    // reject这个promise
    handlers.reject(self, value);
  }

  function onSuccess(value) {
    if (called) {
      return;
    }
    called = true;
    // resolve这个promise
    handlers.resolve(self, value);
  }

  // 用一个函数将resolver执行包裹一层
  function tryToUnwrap() {
    // 这个函数即由调用方传入的
    thenable(onSuccess, onError);
  }

  // 用以捕获resolver在执行过程可能抛出的错误
  var result = tryCatch(tryToUnwrap);
  if (result.status === 'error') {
    onError(result.value);
  }
}

function tryCatch(func, value) {
  var out = {};
  try {
    out.value = func(value);
    out.status = 'success';
  } catch (e) {
    out.status = 'error';
    out.value = e;
  }
  return out;
}

safelyResolveThenable方法中设定了一个called标志位,这是因为一旦一个promise的状态发生了改变,那么之后的状态不能再次被改变,举例:

new Promise((resolve, reject) => {
  // 一旦状态发生改变,后面的reject/resolve方法不能起作用
  resolve(1)
  reject(new Error('error'))
  resolve(2)
})

如果给Promise构造函数传入callback在执行过程中没有报错,且被resolve的话,那么这个时候即调用的onSuccess方法,这个方法内部调用了handlers.resolve方法。

接下来我们看下这个方法的定义:

handlers.resolve = function (self, value) {
  var result = tryCatch(getThen, value);
  if (result.status === 'error') {
    return handlers.reject(self, result.value);
  }
  // 判断这个value是否是个thenable对象
  var thenable = result.value;
  
  if (thenable) {
    safelyResolveThenable(self, thenable);
  } else {
    // 将这个promise的state从 pending -> fulfilled
    self.state = FULFILLED;
    // 更改这个promise对应的值
    self.outcome = value;
    var i = -1;
    var len = self.queue.length;
    // 依次执行这个promise的queue队列里面每一项queueItem的callFulfilled方法
    while (++i < len) {
      self.queue[i].callFulfilled(value);
    }
  }
  // 返回这个promise对象
  return self;
}

再回到我们上面举的这个例子:

const promise = new Promise(resolve => {
  resolve(1)
})

在这个例子当中,是同步去resolve这个promise,那么返回的这个promise实例的状态便为fulfilled,同时outcome的值也被设为1

将这个例子拓展一下:

const promise = new Promise(resolve => {
  resolve(1)
})

promise.then(function onFullfilled (value) {
  console.log(value)
})

在实例的then方法上传入一个onFullfilled回调执行上面的代码,最后在控制台输出1

接下来我们看下Promise原型上then方法的定义:

Promise.prototype.then = function (onFulfilled, onRejected) {
  if (typeof onFulfilled !== 'function' && this.state === FULFILLED ||
    typeof onRejected !== 'function' && this.state === REJECTED) {
    return this;
  }
  // 创建一个新的promise
  var promise = new this.constructor(INTERNAL);
  /* istanbul ignore else */
  if (!process.browser) {
    if (this.handled === UNHANDLED) {
      this.handled = null;
    }
  }

  // new Promise在内部resolve过程中如果是同步的
  if (this.state !== PENDING) {
    var resolver = this.state === FULFILLED ? onFulfilled : onRejected;
    unwrap(promise, resolver, this.outcome);
  } else { // 异步的resolve
    // this.queue保存了对于promise
    this.queue.push(new QueueItem(promise, onFulfilled, onRejected));
  }

  return promise;
};

then方法内部首先创建一个新的promise,接下来会根据这个promise的状态来进行不同的处理。

  1. 如果这个promise已经被resolve/reject了(即非pending态),那么会直接调用unwrap()方法来执行对应的回调函数;

  2. 如果这个promise还是处于pending态,那么需要实例化一个QueueItem,并推入到queue队列当中。

我们首先分析第一种情况,即调用then方法的时候,promise的状态已经被resolve/reject了,那么根据对应的state来取对应的回调函数,并调用unwrap函数(后面会详细讲解这个方法)。

function unwrap(promise, func, value) {
  // 异步执行这个func
  immediate(function () {
    var returnValue;
    try {
      // 捕获onFulfilled函数在执行过程中的错误
      returnValue = func(value);
    } catch (e) {
      return handlers.reject(promise, e);
    }
    // 不能返回自身promise
    if (returnValue === promise) {
      handlers.reject(promise, new TypeError('Cannot resolve promise with itself'));
    } else {
      handlers.resolve(promise, returnValue);
    }
  });
}

在这个函数中,使用immediate方法统一的将func方法异步的执行。并将这个func执行的返回值传递到下一个promise的处理方法当中。

因此在上面给的例子当中,因为Promise的状态是被同步resolve的,那么接下来立即调用then方法,并执行传入的onFullfilled方法。

第二种情况,如果promise还是处于pending态,这个时候不是立即执行callback,首先实例化一个QueueItem,并缓存到这个promisequeue队列当中,延迟执行这个queue当中保存的回调函数。

function QueueItem(promise, onFulfilled, onRejected) {
  // 首先保存这个promise
  this.promise = promise;
  // 如果onFulfilled是一个函数
  if (typeof onFulfilled === 'function') {
    this.onFulfilled = onFulfilled;
    // 那么重新赋值callFulfilled函数
    this.callFulfilled = this.otherCallFulfilled;
  }
  if (typeof onRejected === 'function') {
    this.onRejected = onRejected;
    this.callRejected = this.otherCallRejected;
  }
}
// 如果onFulfilled是一个函数,那么就会覆盖callFulfilled方法
// 如果onFulfilled不是一个函数,那么就会直接调用handlers.resolve去递归处理promise
QueueItem.prototype.callFulfilled = function (value) {
  handlers.resolve(this.promise, value);
};
QueueItem.prototype.otherCallFulfilled = function (value) {
  unwrap(this.promise, this.onFulfilled, value);
};
QueueItem.prototype.callRejected = function (value) {
  handlers.reject(this.promise, value);
};
QueueItem.prototype.otherCallRejected = function (value) {
  unwrap(this.promise, this.onRejected, value);
};

QueueItem构造函数接受3个参数:promiseonFullfilledonRejected

  1. promise

then当中新创建的promise对象

  1. onFullfilled

上一个promise被resolve后需要调用的回调

  1. onRejected

上一个promise被reject后需要调用的回调函数

接下来我们看下第二种情况是在什么样的情况下去执行的:

const promise = new Promise(resolve => {
  setTimeout(() => {
    resolve(1)
  }, 3000)
})

promise.then(data => console.log(data))

在这个例子当中,当过了3s后在控制台输出1。在这个例子当中,因为promise内部是异步去resolve这个promise。在这个promiseresolve前,promise实例通过then方法向这个promisequeue队列中添加onFullfilled方法,这个queue中保存的方法会等到promiseresolve后才会被执行。当在实际的调用resolve(1)时,即promise这个时候才被resolve,那么便会调用handlers.resolve方法,并依次调用这个promisequeue队列当中保存的onFullfilled函数

可以看到在QueueItem函数内部,会对onFullfilledonRejected的参数类型做判断,只有当它们是函数的时候,才会将这个方法进行一次缓存,同时使用otherCallFulfilled方法覆盖原有的callFulfilled方法。这也是大家经常会遇到的值穿透的问题,举个例子:

const promise = new Promise(resolve => {
  setTimeout(() => {
    resolve(2)
  }, 2000)
})

promise
.then(3)
.then(console.log)

最后在控制台打印出2,而非3。当上一个promiseresolve后,调用这个promisequeue当中缓存的queueItem上的callFulfilled方法,因为then方法接收的是数值类型,因此这个queueItem上的callFulfilled方法未被覆盖,因此这时所做的便是直接将这个queueItem中保存的promise进行resolve,同时将上一个promise的值传下去。可以这样理解,如果then方法第一个参数接收到的是个函数,那么就由这个函数处理上一个promise传递过来的值,如果不是函数,那么就像管道一样,先流过这个then方法,而将上一个值传递给下下个then方法接收到的函数去处理。

上面提到了关于unwrap这个函数,这个函数的作用就是统一的将then方法中接收到的onFullfilled参数异步的执行。主要是使用了immediate这个库。这里说明下为什么统一的要将onFullfilled方法进行异步话的处理呢。

首先,是要解决代码逻辑执行顺序的问题,首先来看一种情况:

const promise = new Promise(resolve => {
  // 情况一:同步resolve
  resolve(1)
  // 情况二:异步resolve
  setTimeout(() => {
    resolve(2)
  }, 1000)
})

promise.then(function onFullfilled() {
  // do something
  foo()
})

bar()

这个promise可能会被同步的resolve,也有可能异步的resolve,这个时候如果onFullfilled方法设计成同步的执行的话,那么foobar的执行顺序便依赖promise是被同步or异步resolve,但是如果统一将onFullfilled方法设计成异步的执行的话,那么bar方法始终在foo方法前执行,这样就保证了代码执行的顺序。

其次,是要解决同步回调stackoverflow的问题,具体的链接请戳我

我们看到lie.js的内部实现当中,每次在调用then方法的时候,内部都新创建了一个promise的对象并返回,这样也完成了promise的链式调用。即:

const Promise = require('lie')
const promise = new Promise(resolve => setTimeout(resolve, 3000))
promise.then(() => 'a').then(() => 'b').then(() => {})

需要注意的是,在每个then方法内部创建的新的promise对象的statepending态,outcomenull。可以将上面示例的promise打印到控制台,你会非常清晰的看到整个promise链的结构:

Promise {
  state: [ 'PENDING' ],
  queue:
   [ QueueItem {
       promise: {
         state: ['PENDING'],
         queue: [
           QueueItem {
             promise: {
               state: ['PENDING'],
               queue: [
                 QueueItem {
                   promise: {
                     state: ['PENDING'],
                     queue: [],
                     outcome: undefined
                   }
                 }
               ],
               outcome: undefined,
               handled: null
             },
             onFulfilled: [Function],
             callFulfilled: [Function]
           }
         ],
        outcome: undefined,
        handled: null        
       },
       onFulfilled: [Function],
       callFulfilled: [Function] } ],
  outcome: undefined,
  handled: null }

实际这个promise链是一个嵌套的结构,一旦的最外部的promise的状态发生了改变,那么就会依次执行这个promisequeue队列里保存的queueItemonFulfilled或者onRejected方法,并这样一直传递下去。因此这也是大家经常看到的promise链一旦开始,就会一直向下执行,没法在哪个promise的执行过程中中断。

不过刚才也提到了关于在then方法内部是创建的一个新的pending状态的promise,这个promise状态的改变完全是由上一个promise的状态决定的,如果上一个promise是被resolve的,那么这个promise同样是被resolve的(前提是在代码执行过程中没有报错),并这样传递下去,同样如果上一个promise是被rejected的,那么这个状态也会一直传递下去。如果有这样一种情况,在某个promise封装的请求中,如果响应的错误码不符合要求,不希望这个promise继续被resolve下去,同时想要单独的catch住这个响应的话,那么可以在then方法中直接返回一个被rejectedpromise。这样在这个promise后面的then方法中创建的promisestate都会被rejected,同时这些promise所接受的fullfilled方法不再执行,如果有传入onRejected方法的话便会执行onRejected方法,最后一直传递到的catch方法中添加的onReject方法。

someRequest()
.then(res => {
  if (res.error === 0) {
    // do something
    return res
  } else {
    return Promise.reject(res)
  }
}).then(val => {
  // do something
}).catch(err => {
  // do something
})

看完lie的源码后,觉得promise设计还是挺巧妙的,promise事实上就是一个状态机,不过状态值能发生一次转变,由于then方法内部每次都是创建了一个新的promise,这样也完成了promise的链式调用,同时then方法中的回调统一设计为异步执行也保证了代码逻辑执行顺序。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant