Lodash 源碼分析(一)「Function」 Methods

前言

Lodash一直是我很喜歡用的一個庫,代碼也十分簡潔優美,一直想抽時間好好分析一下Lodash的源代碼。最近抽出早上的一些時間來分析一下Lodash的一些我以爲比較好的源碼。由於函數之間可能會有相互依賴,因此不會按照文檔順序進行分析,而是根據依賴關係和簡易程度由淺入深地進行分析。由於我的能力有限,若是理解有誤差,還請直接指出,以便我及時修改。javascript

源碼都是針對4.17.4版本的,源docs寫得也很好,還有不少樣例。java

_.after

_.after函數幾乎是Lodash中最容易理解的一個函數了,它一共有兩個參數,第一個參數是調用次數n,第二個參數是n次調用以後執行的函數funcweb

function after(n, func) {
      if (typeof func != 'function') {
        throw new TypeError(FUNC_ERROR_TEXT);
      }
      n = toInteger(n);
      return function() {
        if (--n < 1) {
          return func.apply(this, arguments);
        }
      };
    }

這個函數的核心代碼就是:數組

func.apply(this,arguments);

可是必定要注意,這個函數中有閉包的應用,就是這個參數nn本應該在函數_.after返回的時候就應該從棧空間回收,但事實上它還被返回的函數引用着,一直在內存中:閉包

return function() {
        if (--n < 1) {
          return func.apply(this, arguments);
        }
      };

因此一直到返回的函數執行完畢,n所佔用的內存空間都沒法被回收。app

咱們再來看看這個apply函數,咱們知道apply函數能夠改變函數運行時的做用域,那麼問題來了,_.afterfunc.apply函數的this究竟是誰呢?其實這個東西咱們沒有辦法從源碼中看出來,由於this是在運行時決定的。那麼this會變嗎?若是會的話怎麼變呢?要知道這個問題的答案,咱們須要先弄懂_.after函數怎麼用。ide

_.after函數調用後返回了另外一個函數,因此對於_.after函數的返回值,咱們是須要再次調用的。因此最好的場景多是在延遲加載等場景中。固然爲了簡單起見我給出一個很簡單的例子:函數

const _ = require("lodash");

function foo(func ){
    console.log("invoked foo.");
    func();
}


var done = _.after(2,function bar(){
    console.log("invoke bar");
});

for( var i = 0; i <  4; i++ ){
   foo(done);
}

正如咱們前面說的,n的做用域是_.after函數內部,因此在執行過程當中n會一直遞減,所以輸出結果應該是在調用兩次foo以後調用一次bar,以後每次調用foo,都會調用一次bar。結果和咱們預期的一致:性能

invoked foo
invoked foo
invoke bar
invoked foo
invoke bar
invoked foo
invoke bar

那麼咱們再看看this指向的問題,咱們修改一下上面的調用函數,讓bar函數輸出一下內部的this的一些屬性:ui

const _ = require("lodash");

function foo(func ){
    this.name = "foo";
    console.log("invoked foo: " + this.name );
    func();
}


var done = _.after(2,function bar(){
    console.log("invoke bar: " + this.name);
});

for( var i = 0; i <  4; i++ ){
   foo(done);
}

其實想來你們也應該可以猜到,在bar函數中輸出的this.name也是foo

invoked foo: foo
invoked foo: foo
invoke bar: foo
invoked foo: foo
invoke bar: foo
invoked foo: foo
invoke bar: foo

這是由於barthis應該指向的是_.after建立的函數的this,而這個函數是window調用的,所以this實際上指向就是window,可是爲何會輸出foo呢?由於foo函數的調用者也是window,而在foo函數中,將window.name設置成了foo,因此bar函數輸出的也是foo(多謝評論指出!)。

_.map

_.map函數咱們幾乎隨處可見,這個函數應用也至關普遍。

function map(collection, iteratee) {
      var func = isArray(collection) ? arrayMap : baseMap;
      return func(collection, getIteratee(iteratee, 3));
}

爲了簡化問題,咱們分析比較簡單的狀況:用一個func函數處理數組。

_.map([1,2,3],func);

在處理數組的時候,lodash是分開處理的,對於Array採用arrayMap進行處理,對於對象則採用baseMap進行處理。

咱們先看數組arrayMap

function arrayMap(array, iteratee) {
    var index = -1,
        length = array == null ? 0 : array.length,
        result = Array(length);

    while (++index < length) {
      result[index] = iteratee(array[index], index, array);
    }
    return result;
  }

這個函數是一個私有函數,第一個參數是一個須要遍歷的數組,第二個參數是在遍歷過程中進行處理的函數;返回一個進行map處理以後的函數。

在看咱們須要進行遍歷處理的函數iteratee,這個函數式經過getIteratee函數獲得的:

function getIteratee() {
      var result = lodash.iteratee || iteratee;
      result = result === iteratee ? baseIteratee : result;
      return arguments.length ? result(arguments[0], arguments[1]) : result;
    }

若是lodash.iteratee被從新定義,則使用用戶定義的iteratee,不然就用官方定義的baseIteratee。須要強調的是,result(arguments[0],arguments[1])是柯里化的函數返回,返回的仍舊是一個函數。不可避免地,咱們須要看看官方定義的baseIteratee的實現:

function baseIteratee(value) {
      // Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9.
      // See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details.
      if (typeof value == 'function') {
        return value;
      }
      if (value == null) {
        return identity;
      }
      if (typeof value == 'object') {
        return isArray(value)
          ? baseMatchesProperty(value[0], value[1])
          : baseMatches(value);
      }
      return property(value);
    }

咱們能夠看出來,這個iteratee迭代者其實就是一個函數,在_.mapgetIteratee(iteratee, 3),給了兩個參數,按照邏輯,最終返回的是一個baseIterateebaseIteratee的第一個參數value就是iteratee,這是一個函數,因此,baseIteratee函數在第一個判斷就返回了。

因此咱們能夠將map函數簡化爲以下版本:

function map(collection,iteratee){
    return arrayMap(collection,getIteratee(iteratee,3));
}

function arrayMap(array, iteratee) {
    var index = -1,
        length = array == null ? 0 : array.length,
        result = Array(length);

    while (++index < length) {
      result[index] = iteratee(array[index], index, array);
    }
    return result;
}

function getIteratee() {
      var result =  baseIteratee;
      return arguments.length ? result(arguments[0], arguments[1]) : result;
}

function baseIteratee(value) {
      if (typeof value == 'function') {
        return value;
      }
}

能夠看到,最終調用函數func的時候會傳入3個參數。array[index],index,array。咱們能夠實驗,將func實現以下:

function func(){
   console.log(「arguments[0] 」 + arguments[0]);
   console.log(「arguments[1] 」 + arguments[1]);
   console.log(「arguments[2] 」 + arguments[2]);
   console.log("-----")
}

輸出的結果也和咱們的預期同樣,輸出的第一個參數是該列表元素自己,第二個參數是數組下標,第三個參數是整個列表:

arguments[0] 6
arguments[1] 0
arguments[2] 6,8,10
-----
arguments[0] 8
arguments[1] 1
arguments[2] 6,8,10
-----
arguments[0] 10
arguments[1] 2
arguments[2] 6,8,10
-----
[ undefined, undefined, undefined ]

上面的分析就是拋磚引玉,先給出數組的分析,別的非數組,例如對象的遍歷處理則會走到別的分支進行處理,各位看官有興趣能夠深刻研究。

_.ary

這個函數是用來限制參數個數的。這個函數咋一看好像沒有什麼用,但咱們考慮以下場景,將一個字符列表['6','8','10']轉爲整型列表[6,8,10],用_.map實現,咱們天然而然會寫出這樣的代碼:

const _ = require("lodash");
_.map(['6','8','10'],parseInt);

好像很完美,咱們輸出看看:

[ 6, NaN, 2 ]

很詭異是否是,看看內部到底發生了什麼?其實看了上面的-.map函數的分析,其實緣由已經很明顯了。對於parseInt函數而言,其接收兩個參數,第一個是須要處理的字符串,第二個是進制:

/**
* @param string    必需。要被解析的字符串。
* @param radix    
* 可選。表示要解析的數字的基數。該值介於 2 ~ 36 之間。
* 若是省略該參數或其值爲 0,則數字將以 10 爲基礎來解析。若是它以 「0x」 或 「0X」 開頭,將以 16 爲基數。
* 若是該參數小於 2 或者大於 36,則 parseInt() 將返回 NaN
*/
parseInt(string, radix)
/**
當參數 radix 的值爲 0,或沒有設置該參數時,parseInt() 會根據 string 來判斷數字的基數。

舉例,若是 string 以 "0x" 開頭,parseInt() 會把 string 的其他部分解析爲十六進制的整數。若是 string 以 0 開頭,那麼 ECMAScript v3 容許 parseInt() 的一個實現把其後的字符解析爲八進制或十六進制的數字。若是 string 以 1 ~ 9 的數字開頭,parseInt() 將把它解析爲十進制的整數。
*/

那麼這樣的輸出也就不難理解了:

處理第一個數組元素6的時候,parseInt實際傳入參數(6,0),那麼按照十進制解析,會獲得6,處理第二個數組元素的時候傳入的實際參數是(8,1),返回NaN,對於第三個數組元素,按照2進制處理,則10返回的是2

因此在上述需求的時候咱們須要限制參數的個數,這個時候_.ary函數就登場了,上面的函數這樣處理就沒有問題了:

const _ = require("lodash");
_.map(['6','8','10'],_.ary(parseInt,1));

咱們看看這個函數是怎麼實現的:

function ary(func, n, guard) {
      n = guard ? undefined : n;
      n = (func && n == null) ? func.length : n;
      return createWrap(func, WRAP_ARY_FLAG, undefined, undefined, undefined, undefined, n);
    }

這個函數先檢查n的值,須要說明的是func.length返回的是函數的聲明參數個數。而後返回了一個createWrap包裹函數,這個函數能夠說是髒活累活處理工廠了,負責不少函數的包裹處理工做,並且爲了提高性能,還將不一樣的判斷用bitflag進行與/非處理,能夠說是很用盡心機了。

/**
     * Creates a function that either curries or invokes `func` with optional
     * `this` binding and partially applied arguments.
     *
     * @private
     * @param {Function|string} func The function or method name to wrap.
     * @param {number} bitmask The bitmask flags.
     *    1 - `_.bind` 1                      0b0000000000000001
     *    2 - `_.bindKey`                       0b0000000000000010
     *    4 - `_.curry` or `_.curryRight`...  0b0000000000000100
     *    8 - `_.curry`                       0b0000000000001000
     *   16 - `_.curryRight`                  0b0000000000010000
     *   32 - `_.partial`                     0b0000000000100000
     *   64 - `_.partialRight`                0b0000000001000000
     *  128 - `_.rearg`                       0b0000000010000000
     *  256 - `_.ary`                            0b0000000100000000
     *  512 - `_.flip`                           0b0000001000000000
     * @param {*} [thisArg] The `this` binding of `func`.
     * @param {Array} [partials] The arguments to be partially applied.
     * @param {Array} [holders] The `partials` placeholder indexes.
     * @param {Array} [argPos] The argument positions of the new function.
     * @param {number} [ary] The arity cap of `func`.
     * @param {number} [arity] The arity of `func`.
     * @returns {Function} Returns the new wrapped function.
     */
    function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) {
      var isBindKey = bitmask & WRAP_BIND_KEY_FLAG;
      if (!isBindKey && typeof func != 'function') {
        throw new TypeError(FUNC_ERROR_TEXT);
      }
      var length = partials ? partials.length : 0;
      if (!length) {
        bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG);
        partials = holders = undefined;
      }
      ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0);
      arity = arity === undefined ? arity : toInteger(arity);
      length -= holders ? holders.length : 0;

      if (bitmask & WRAP_PARTIAL_RIGHT_FLAG) {
        var partialsRight = partials,
            holdersRight = holders;

        partials = holders = undefined;
      }
      var data = isBindKey ? undefined : getData(func);

      var newData = [
        func, bitmask, thisArg, partials, holders, partialsRight, holdersRight,
        argPos, ary, arity
      ];

      if (data) {
        mergeData(newData, data);
      }
      func = newData[0];
      bitmask = newData[1];
      thisArg = newData[2];
      partials = newData[3];
      holders = newData[4];
      arity = newData[9] = newData[9] === undefined
        ? (isBindKey ? 0 : func.length)
        : nativeMax(newData[9] - length, 0);

      if (!arity && bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG)) {
        bitmask &= ~(WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG);
      }
      if (!bitmask || bitmask == WRAP_BIND_FLAG) {
        var result = createBind(func, bitmask, thisArg);
      } else if (bitmask == WRAP_CURRY_FLAG || bitmask == WRAP_CURRY_RIGHT_FLAG) {
        result = createCurry(func, bitmask, arity);
      } else if ((bitmask == WRAP_PARTIAL_FLAG || bitmask == (WRAP_BIND_FLAG | WRAP_PARTIAL_FLAG)) && !holders.length) {
        result = createPartial(func, bitmask, thisArg, partials);
      } else {
        result = createHybrid.apply(undefined, newData);
      }
      var setter = data ? baseSetData : setData;
      return setWrapToString(setter(result, newData), func, bitmask);
    }

看上去太複雜了,把無關的代碼削減掉:

function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) {
      //      0000000100000000 & 0000000000000010
      // var isBindKey = bitmask & WRAP_BIND_KEY_FLAG;
      var isBindKey = 0;
      var length =  0;
      // if (!length) {
        //              0000000000100000 | 0000000001000000
        //            ~(0000000001100000)
        //              1111111110011111
        //             &0000000100000000
        //              0000000100000000 = WRAP_ARY_FLAG 
        // bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG);
      //  bitmask = WRAP_ARY_FLAG;
      //  partials = holders = undefined;
      // }
      bitmask = WRAP_ARY_FLAG;
      partials = holders = undefined;
      ary = undefined;
      arity = arity === undefined ? arity : toInteger(arity);
      // because holders == undefined
      //length -= 0;
      // because isBindKey  == 0
      // var data = isBindKey ? undefined : getData(func);
      var data = getData(func);
      var newData = [
        func, bitmask, thisArg, partials, holders, partialsRight, holdersRight,
        argPos, ary, arity
      ];
      if (data) {
        mergeData(newData, data);
      }
      func = newData[0];
      bitmask = newData[1];
      thisArg = newData[2];
      partials = newData[3];
      holders = newData[4];
      arity = newData[9] = newData[9] === undefined
        ? func.length : newData[9];
      result = createHybrid.apply(undefined, newData);
      var setter = data ? baseSetData : setData;
      return setWrapToString(setter(result, newData), func, bitmask);
    }

簡化了一些以後咱們來到了createHybrid函數,這個函數也巨複雜,因此咱們仍是按照簡化方法,把咱們用不到的邏輯給簡化:

function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) {
      var isAry = bitmask & WRAP_ARY_FLAG,
          isBind = bitmask & WRAP_BIND_FLAG,
          isBindKey = bitmask & WRAP_BIND_KEY_FLAG,
          isCurried = bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG),
          isFlip = bitmask & WRAP_FLIP_FLAG,
          Ctor = isBindKey ? undefined : createCtor(func);

      function wrapper() {
        var length = arguments.length,
            args = Array(length),
            index = length;

        while (index--) {
          args[index] = arguments[index];
        }
        if (isCurried) {
          var placeholder = getHolder(wrapper),
              holdersCount = countHolders(args, placeholder);
        }
        if (partials) {
          args = composeArgs(args, partials, holders, isCurried);
        }
        if (partialsRight) {
          args = composeArgsRight(args, partialsRight, holdersRight, isCurried);
        }
        length -= holdersCount;
        if (isCurried && length < arity) {
          var newHolders = replaceHolders(args, placeholder);
          return createRecurry(
            func, bitmask, createHybrid, wrapper.placeholder, thisArg,
            args, newHolders, argPos, ary, arity - length
          );
        }
        var thisBinding = isBind ? thisArg : this,
            fn = isBindKey ? thisBinding[func] : func;

        length = args.length;
        if (argPos) {
          args = reorder(args, argPos);
        } else if (isFlip && length > 1) {
          args.reverse();
        }
        if (isAry && ary < length) {
          args.length = ary;
        }
        if (this && this !== root && this instanceof wrapper) {
          fn = Ctor || createCtor(fn);
        }
        return fn.apply(thisBinding, args);
      }
      return wrapper;
    }

把不須要的邏輯削減掉:

function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) {
      var isAry = 1;
      function wrapper() {
        var length = arguments.length,
            args = Array(length),
            index = length;
        while (index--) {
          args[index] = arguments[index];
        }
        var thisBinding = this, fn = func;
        length = args.length;
        if (isAry && ary < length) {
          args.length = ary;
        }
        return fn.apply(thisBinding, args);
      }
      return wrapper;
    }

好了,繞了一大圈,終於看到最終的邏輯了,_.ary函數其實就是把參數列表從新賦值了一下,並進行了長度限制。想一想這個函數實在是太麻煩了,咱們本身能夠根據這個邏輯實現一個簡化版的_.ary

function ary(func,n){
    return function(){
        var length = arguments.length,
            args = Array(length),
            index = length;
          while(index--){
            args[index] = arguments[index];
        }
        args.length = n;
        return func.apply(this,args);
    }
}

試試效果:

console.log(_.map(['6','8','10'],ary(parseInt,1)));

工做得很不錯:

[ 6, 8, 10 ]

小結

今天分析這三個函數就花了一成天的時間,可是收穫頗豐,可以靜下心來好好分析一個著名的開源庫,並可以理解透裏面的一些邏輯,確實是一件頗有意思的事情。我會在有時間的時候把Lodash這個我很喜歡的庫都好好分析一遍,盡我最大的努力將裏面的邏輯表述清楚,但願可以簡明易懂。

敬請期待

最後,最晚下週一將會更新第二篇分析文章,敬請期待。

© 版權全部,未經容許不得轉載,宣傳一下我的博客 chenquan.me

相關文章
相關標籤/搜索