走一步再走一步,揭開co的神秘面紗

前言

原文地址

源碼地址

了解co的前提是?已經(jīng)知曉generator是什么,可以看軟大神的Generator 函數(shù)的語法,
co是TJ大神寫的?能夠使generator自動執(zhí)行的函數(shù)庫,而我們熟知的koa也用到了它管理異步流程控制,將異步任務(wù)書寫同步化,爽的飛起,也擺脫了一直以來的回調(diào)地獄問題。

如何使用

首先我們根據(jù)co的官方文檔來稍做改變看下,到底如何使用co,再一步步進行源碼分析工作(這篇文章分析的co版本是4.6.0)。

yield 后面常見的可以跟的類型

  1. promises
  2. array (parallel execution)
  3. objects (parallel execution)
  4. generator functions (delegation)

promises

let co = require('co')
let genTimeoutFun = (delay) => {
  return () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(`delayTime: ${delay}`)
      }, delay)
    })
  }
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200)

co(function * () {
  let a = yield timeout1()
  console.log(a) // delayTime: 1000
  let b = yield timeout2()
  console.log(b) // delayTime: 200

  return 'end'
}).then((res) => {
  console.log(res)
})

array

let co = require('co')
let genTimeoutFun = (delay) => {
  return () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(`delayTime: ${delay}`)
      }, delay)
    })
  }
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200)

co(function * () {
  let a = yield [timeout1(), timeout2()]
  console.log(a) // [ 'delayTime: 1000', 'delayTime: 200' ]
  return 'end'
}).then((res) => {
  console.log(res) // end
})

objects

let co = require('co')
let genTimeoutFun = (delay) => {
  return () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(`delayTime: ${delay}`)
      }, delay)
    })
  }
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200)

co(function * () {
  let a = yield {
    timeout1: timeout1(),
    timeout2: timeout2()
  }
  console.log(a) // { timeout1: 'delayTime: 1000',timeout2: 'delayTime: 200' }
  return 'end'
}).then((res) => {
  console.log(res) // end
})

generator functions

let co = require('co')
let genTimeoutFun = (delay) => {
  return () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(`delayTime: ${delay}`)
      }, delay)
    })
  }
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200)

function * gen () {
  let a = yield timeout1()
  console.log(a) // delayTime: 1000
  let b = yield timeout2()
  console.log(b) // delayTime: 200
}

co(function * () {
  yield gen()

  return 'end'
}).then((res) => {
  console.log(res) // end
})

最后說一下,關(guān)于執(zhí)行傳入的generator函數(shù)接收參數(shù)的問題

let co = require('co')

co(function * (name) {
  console.log(name) // qianlongo
}, 'qianlongo')

從co函?數(shù)的第二個參數(shù)開始,便是傳入的generator函數(shù)可以接收的實參

開始分析源碼

你可以把以上代碼拷貝至本地測試一番看看效果,接下來我們一步步開始分析co的源碼

首先經(jīng)過上面的例子可以發(fā)現(xiàn),co函數(shù)本身接收一個generator函數(shù),并且co執(zhí)行后返回的是Promise

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);

    // xxx
  });
}

在Promise的內(nèi)部,先執(zhí)行了外部傳入的gen,執(zhí)行的結(jié)果如果不具備?next屬性(且要是一個函數(shù)),就直接返回,并將執(zhí)行成功回調(diào)resolve(gen),否則得到的是一個指針對象。

接下來繼續(xù)看...


onFulfilled();

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

function onFulfilled(res) {
  var ret;
  try {
    ret = gen.next(res); // 用上面執(zhí)行g(shù)en之后的generator生成器將指針指向下一個位置
  } catch (e) {
    return reject(e);
  }
  next(ret); // 緊接著執(zhí)行next,正是它實現(xiàn)了反復(fù)調(diào)用自己,自動流程控制,注意ret(即上一次gen.next執(zhí)行后返回的對象{value: xxx, done: true or false})
}

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

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

我覺得可以把 onFulfilledonRejected 看成是返回的Promise的resolvereject。

onFulfilled也是將原生的generator生成器的next方法包裝了一遍,大概是為了抓取錯誤吧(看到內(nèi)部的try catch了嗎)

好,我們看到了co內(nèi)部將指針移動到了第一個位置之后,接著執(zhí)行了內(nèi)部的next方法,接下來聚焦在該函數(shù)上


function next(ret) {
  // 如果整個generator函數(shù)的內(nèi)部狀態(tài)已經(jīng)表示走完,便將Promise的狀態(tài)設(shè)置為成功狀態(tài),并執(zhí)行resolve
  if (ret.done) return resolve(ret.value);
  // 這一步是將ret的value轉(zhuǎn)換為Promise形式
  var value = toPromise.call(ctx, ret.value);
  // 這里非常關(guān)鍵,是co實現(xiàn)自己調(diào)用自己,實現(xiàn)流程自動化的關(guān)鍵
  // 注意這里使用value.then,即為返回值添加成功和失敗的回調(diào),在成功的回調(diào)里面再去執(zhí)行onFulfilled,緊接著就是調(diào)用內(nèi)部的next函數(shù)
  // 那不是就保證了流程完全按照你寫的順序來了?
  if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
  // 拋出錯誤,yield后只能跟著指定的下列這幾種類型
  return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
    + 'but the following object was passed: "' + String(ret.value) + '"'));
}

聰明的你,是不是已經(jīng)明白了co是怎么將異步流程自動管理起來了

但是我對next函數(shù)中的toPromise函數(shù)還有疑問,他到底做了什么事?使得co(generatorFun)中yield可以支持數(shù)組、對象、generator函數(shù)等形式。

一步步來看


function toPromise(obj) {
  // obj不存在,直接返回
  if (!obj) return obj;
  // 如果obj已經(jīng)是Promise,則也是直接返回
  if (isPromise(obj)) return obj;
  // 如果是個generator函數(shù)或者generator生成器,那就像你自己調(diào)用co函數(shù)一樣,手動傳到co里面去執(zhí)行
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  // 如果obj既不是Promise,也不是isGeneratorFunction和isGenerator,要是一個普通的函數(shù)(需要符合thunk函數(shù)規(guī)范),就將該函數(shù)包裝成?Promise的形式
  if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  // 如果是一個數(shù)組的形式,就去arrayToPromise包裝一番
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}

首先如果obj不存在,就直接返回,你想啊,co本來就是依賴上一次指針返回的value是Promise或者其他,這個時候如果返回

{
  value: false,
  done: false
}

那就沒有必要再給一個false值轉(zhuǎn)成Promise形式了吧。

接著,如果obj本身就是個Promise也是直接返回,用了內(nèi)部的isPromise函數(shù)進行判斷,我們看下他怎么實現(xiàn)的。


function isPromise(obj) {
  return 'function' == typeof obj.then;
}

其實就是判斷了obj的then屬性是不是個函數(shù)

再接著,如果是個generator函數(shù)或者generator生成器,那就像你自己調(diào)用co函數(shù)一樣,手動傳到co里面去執(zhí)行。

isGeneratorFunction


function isGeneratorFunction(obj) {
  var constructor = obj.constructor;
  if (!constructor) return false;
  if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
  return isGenerator(constructor.prototype);
}

通過obj的constructor屬性去判斷其是否屬于GeneratorFunction,最后如果constructor屬性沒判斷出來,再去用isGenerator,判斷obj的原型是不是generator生成器

function isGenerator(obj) {
  return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}

判斷的條件也比較直接,?需要符合兩個條件,一個是obj.next要是一個函數(shù),一個是obj.throw要是一個函數(shù)

接下來繼續(xù)看

如果obj既不是Promise,也不是isGeneratorFunction和isGenerator,要是一個普通的函數(shù),就將該函數(shù)包裝成?Promise的形式,這里我們主要需要看thunkToPromise

function thunkToPromise(fn) {
  var ctx = this;
  // 將thunk函數(shù)包裝成Promise
  return new Promise(function (resolve, reject) {
      // 執(zhí)行這個thunk函數(shù)
    fn.call(ctx, function (err, res) { 
      // 注意thunk函數(shù)內(nèi)部接收的回調(diào)函數(shù)中傳入的第一個參數(shù)是err,出現(xiàn)了err,?當然需要走reject了
      if (err) return reject(err); 
      // 參數(shù)是兩個以上的情況下,將參數(shù)整成一個數(shù)組
      if (arguments.length > 2) res = slice.call(arguments, 1);
      // 最后執(zhí)行成功的回調(diào)
      resolve(res);
    });
  });
}

接下來是重頭戲了,co中如果處理yield后面跟一個數(shù)組呢?主要是arrayToPromise函數(shù)的作用

function arrayToPromise(obj) {
  // 使用到了Promise.all,將obj中多個promise實例(當然你也可以在數(shù)組中填thunk函數(shù),generator函數(shù)等)重新包裝成一個。最后返回一個新的Promise
  return Promise.all(obj.map(toPromise, this));
}

還有最后一個判斷,如果obj是個對象怎么辦?

function objectToPromise(obj){
  // 構(gòu)造一個和傳入對象有相同構(gòu)造器的對象, results也是
  var results = new obj.constructor();
  // 獲取obj的keys
  var keys = Object.keys(obj);
  // 存儲obj中是Promise的屬性
  var promises = [];
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var promise = toPromise.call(this, obj[key]);
    // 如果是結(jié)果是Promise,則用defer函數(shù)對results進行修改
    if (promise && isPromise(promise)) defer(promise, key);
    // 如果是非Promise就按原樣返回
    else results[key] = obj[key];
  }
  // 最后 使用到了Promise.all,將obj中多個promise實例
  return Promise.all(promises).then(function () {
    return results;
  });

  function defer(promise, key) {
    // predefine the key in the result
    results[key] = undefined;
    promises.push(promise.then(function (res) {
      // 運行成功之后再講結(jié)果賦值給results
      results[key] = res;
    }));
  }
}

結(jié)尾

到這里,co源碼分析就告一段落了。總感覺有些沒有說到位,?歡迎大家拍磚,晚安。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 異步編程對JavaScript語言太重要。Javascript語言的執(zhí)行環(huán)境是“單線程”的,如果沒有異步編程,根本...
    呼呼哥閱讀 7,400評論 5 22
  • 簡介 基本概念 Generator函數(shù)是ES6提供的一種異步編程解決方案,語法行為與傳統(tǒng)函數(shù)完全不同。本章詳細介紹...
    呼呼哥閱讀 1,135評論 0 4
  • 在此處先列下本篇文章的主要內(nèi)容 簡介 next方法的參數(shù) for...of循環(huán) Generator.prototy...
    醉生夢死閱讀 1,486評論 3 8
  • 本文為阮一峰大神的《ECMAScript 6 入門》的個人版提純! babel babel負責(zé)將JS高級語法轉(zhuǎn)義,...
    Devildi已被占用閱讀 2,127評論 0 4
  • 弄懂js異步 講異步之前,我們必須掌握一個基礎(chǔ)知識-event-loop。 我們知道JavaScript的一大特點...
    DCbryant閱讀 2,869評論 0 5

友情鏈接更多精彩內(nèi)容