學(xué)習(xí) lodash 源碼整體架構(gòu)肠阱,打造屬于自己的函數(shù)式編程類庫

前言

這是學(xué)習(xí)源碼整體架構(gòu)系列第三篇。整體架構(gòu)這詞語好像有點大朴读,姑且就算是源碼整體結(jié)構(gòu)吧屹徘,主要就是學(xué)習(xí)是代碼整體結(jié)構(gòu),不深究其他不是主線的具體函數(shù)的實現(xiàn)衅金。文章學(xué)習(xí)的是打包整合后的代碼噪伊,不是實際倉庫中的拆分的代碼簿煌。

上上篇文章寫了jQuery源碼整體架構(gòu)學(xué)習(xí) jQuery 源碼整體架構(gòu)鉴吹,打造屬于自己的 js 類庫

上一篇文章寫了underscore源碼整體架構(gòu)姨伟,學(xué)習(xí) underscore 源碼整體架構(gòu),打造屬于自己的函數(shù)式編程類庫

感興趣的讀者可以點擊閱讀豆励。

underscore源碼分析的文章比較多夺荒,而lodash源碼分析的文章比較少。原因之一可能是由于lodash源碼行數(shù)太多良蒸。注釋加起來一萬多行技扼。

分析lodash整體代碼結(jié)構(gòu)的文章比較少,筆者利用谷歌嫩痰、必應(yīng)剿吻、github等搜索都沒有找到,可能是找的方式不對串纺。于是打算自己寫一篇丽旅。平常開發(fā)大多數(shù)人都會使用lodash,而且都或多或少知道纺棺,lodashunderscore性能好魔招,性能好的主要原因是使用了惰性求值這一特性。

本文章學(xué)習(xí)的lodash的版本是:v4.17.15五辽。unpkg.com地址 https://unpkg.com/lodash@4.17.15/lodash.js

文章篇幅可能比較長,可以先收藏再看外恕。

導(dǎo)讀:

文章主要學(xué)習(xí)了runInContext() 導(dǎo)出_ lodash函數(shù)使用baseCreate方法原型繼承LodashWrapperLazyWrapper杆逗,mixin掛載方法到lodash.prototype、后文用結(jié)合例子解釋lodash.prototype.value(wrapperValue)Lazy.prototype.value(lazyValue)惰性求值的源碼具體實現(xiàn)鳞疲。

匿名函數(shù)執(zhí)行

;(function() {

}.call(this));

暴露 lodash

var _ = runInContext();

runInContext 函數(shù)

這里的簡版源碼罪郊,只關(guān)注函數(shù)入口和返回值。

var runInContext = (function runInContext(context) {
    // 瀏覽器中處理context為window
    // ...
    function lodash(value) {}{
        // ...
        return new LodashWrapper(value);
    }
    // ...
    return lodash;
});

可以看到申明了一個runInContext函數(shù)尚洽。里面有一個lodash函數(shù)悔橄,最后處理返回這個lodash函數(shù)。

再看lodash函數(shù)中的返回值 new LodashWrapper(value)腺毫。

LodashWrapper 函數(shù)

function LodashWrapper(value, chainAll) {
    this.__wrapped__ = value;
    this.__actions__ = [];
    this.__chain__ = !!chainAll;
    this.__index__ = 0;
    this.__values__ = undefined;
}

設(shè)置了這些屬性:

__wrapped__:存放參數(shù)value癣疟。

__actions__:存放待執(zhí)行的函數(shù)體func, 函數(shù)參數(shù) args潮酒,函數(shù)執(zhí)行的this 指向 thisArg睛挚。

__chain__undefined兩次取反轉(zhuǎn)成布爾值false急黎,不支持鏈?zhǔn)秸{(diào)用扎狱。和underscore一樣侧到,默認(rèn)是不支持鏈?zhǔn)秸{(diào)用的。

__index__:索引值 默認(rèn) 0淤击。

__values__:主要clone時使用匠抗。

接著往下搜索源碼,LodashWrapper污抬,
會發(fā)現(xiàn)這兩行代碼汞贸。

LodashWrapper.prototype = baseCreate(baseLodash.prototype);
LodashWrapper.prototype.constructor = LodashWrapper;

接著往上找baseCreate、baseLodash這兩個函數(shù)壕吹。

baseCreate 原型繼承

//  立即執(zhí)行匿名函數(shù)
// 返回一個函數(shù)著蛙,用于設(shè)置原型 可以理解為是 __proto__
var baseCreate = (function() {
    // 這句放在函數(shù)外,是為了不用每次調(diào)用baseCreate都重復(fù)申明 object
    // underscore 源碼中耳贬,把這句放在開頭就申明了一個空函數(shù) `Ctor`
    function object() {}
    return function(proto) {
        // 如果傳入的參數(shù)不是object也不是function 是null
        // 則返回空對象踏堡。
        if (!isObject(proto)) {
            return {};
        }
        // 如果支持Object.create方法,則返回 Object.create
        if (objectCreate) {
            // Object.create
            return objectCreate(proto);
        }
        // 如果不支持Object.create 用 ployfill new
        object.prototype = proto;
        var result = new object;
        // 還原 prototype
        object.prototype = undefined;
        return result;
    };
}());

// 空函數(shù)
function baseLodash() {
    // No operation performed.
}

// Ensure wrappers are instances of `baseLodash`.
lodash.prototype = baseLodash.prototype;
// 為什么會有這一句咒劲?因為上一句把lodash.prototype.construtor 設(shè)置為Object了顷蟆。這一句修正constructor
lodash.prototype.constructor = lodash;

LodashWrapper.prototype = baseCreate(baseLodash.prototype);
LodashWrapper.prototype.constructor = LodashWrapper;

筆者畫了一張圖,表示這個關(guān)系腐魂。


lodash 原型關(guān)系圖

衍生的 isObject 函數(shù)

判斷typeof value不等于null帐偎,并且是object或者function

function isObject(value) {
    var type = typeof value;
    return value != null && (type == 'object' || type == 'function');
}

Object.create() 用法舉例

面試官問:能否模擬實現(xiàn)JS的new操作符 之前這篇文章寫過的一段蛔屹。

筆者之前整理的一篇文章中也有講過削樊,可以翻看JavaScript 對象所有API解析

MDN Object.create()

Object.create(proto, [propertiesObject])
方法創(chuàng)建一個新對象,使用現(xiàn)有的對象來提供新創(chuàng)建的對象的proto兔毒。
它接收兩個參數(shù)漫贞,不過第二個可選參數(shù)是屬性描述符(不常用,默認(rèn)是undefined)育叁。

var anotherObject = {
    name: '若川'
};
var myObject = Object.create(anotherObject, {
    age: {
        value:18,
    },
});
// 獲得它的原型
Object.getPrototypeOf(anotherObject) === Object.prototype; // true 說明anotherObject的原型是Object.prototype
Object.getPrototypeOf(myObject); // {name: "若川"} // 說明myObject的原型是{name: "若川"}
myObject.hasOwnProperty('name'); // false; 說明name是原型上的迅脐。
myObject.hasOwnProperty('age'); // true 說明age是自身的
myObject.name; // '若川'
myObject.age; // 18;

對于不支持ES5的瀏覽器,MDN上提供了ployfill方案豪嗽。

if (typeof Object.create !== "function") {
    Object.create = function (proto, propertiesObject) {
        if (typeof proto !== 'object' && typeof proto !== 'function') {
            throw new TypeError('Object prototype may only be an Object: ' + proto);
        } else if (proto === null) {
            throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
        }

        if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

        function F() {}
        F.prototype = proto;
        return new F();
    };
}

lodash上有很多方法和屬性谴蔑,但在lodash.prototype也有很多與lodash上相同的方法」昝危肯定不是在lodash.prototype上重新寫一遍隐锭。而是通過mixin掛載的。

mixin

mixin 具體用法

_.mixin([object=lodash], source, [options={}])

添加來源對象自身的所有可枚舉函數(shù)屬性到目標(biāo)對象计贰。 如果 object 是個函數(shù)成榜,那么函數(shù)方法將被添加到原型鏈上。

注意: 使用 _.runInContext 來創(chuàng)建原始的 lodash 函數(shù)來避免修改造成的沖突蹦玫。

添加版本

0.1.0

參數(shù)

[object=lodash] (Function|Object): 目標(biāo)對象赎婚。

source (Object): 來源對象刘绣。

[options={}] (Object): 選項對象。

[options.chain=true] (boolean): 是否開啟鏈?zhǔn)讲僮鳌?/p>

返回

(*): 返回 object.

mixin 源碼

mixin源碼挣输,后文注釋解析

function mixin(object, source, options) {
    var props = keys(source),
        methodNames = baseFunctions(source, props);

    if (options == null &&
        !(isObject(source) && (methodNames.length || !props.length))) {
        options = source;
        source = object;
        object = this;
        methodNames = baseFunctions(source, keys(source));
    }
    var chain = !(isObject(options) && 'chain' in options) || !!options.chain,
        isFunc = isFunction(object);

    arrayEach(methodNames, function(methodName) {
        var func = source[methodName];
        object[methodName] = func;
        if (isFunc) {
            object.prototype[methodName] = function() {
                var chainAll = this.__chain__;
                if (chain || chainAll) {
                    var result = object(this.__wrapped__),
                        actions = result.__actions__ = copyArray(this.__actions__);

                    actions.push({ 'func': func, 'args': arguments, 'thisArg': object });
                    result.__chain__ = chainAll;
                    return result;
                }
                return func.apply(object, arrayPush([this.value()], arguments));
            };
        }
    });

    return object;
}

接下來先看衍生的函數(shù)纬凤。

其實看到具體定義的函數(shù)代碼就大概知道這個函數(shù)的功能。為了不影響主線撩嚼,導(dǎo)致文章篇幅過長停士。具體源碼在這里就不展開。

感興趣的讀者可以自行看這些函數(shù)衍生的其他函數(shù)的源碼完丽。

mixin 衍生的函數(shù) keys

mixin 函數(shù)中 其實最終調(diào)用的就是 Object.keys

function keys(object) {
    return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
}

mixin 衍生的函數(shù) baseFunctions

返回函數(shù)數(shù)組集合

function baseFunctions(object, props) {
    return arrayFilter(props, function(key) {
        return isFunction(object[key]);
    });
}

mixin 衍生的函數(shù) isFunction

判斷參數(shù)是否是函數(shù)

function isFunction(value) {
    if (!isObject(value)) {
        return false;
    }
    // The use of `Object#toString` avoids issues with the `typeof` operator
    // in Safari 9 which returns 'object' for typed arrays and other constructors.
    var tag = baseGetTag(value);
    return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
}

mixin 衍生的函數(shù) arrayEach

類似 [].forEarch

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

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

mixin 衍生的函數(shù) arrayPush

類似 [].push

function arrayPush(array, values) {
    var index = -1,
        length = values.length,
        offset = array.length;

    while (++index < length) {
    array[offset + index] = values[index];
    }
    return array;
}

mixin 衍生的函數(shù) copyArray

拷貝數(shù)組

function copyArray(source, array) {
    var index = -1,
        length = source.length;

    array || (array = Array(length));
    while (++index < length) {
        array[index] = source[index];
    }
    return array;
}

mixin 源碼解析

lodash 源碼中兩次調(diào)用 mixin

// Add methods that return wrapped values in chain sequences.
lodash.after = after;
// code ... 等 153 個支持鏈?zhǔn)秸{(diào)用的方法

// Add methods to `lodash.prototype`.
// 把lodash上的靜態(tài)方法賦值到 lodash.prototype 上
mixin(lodash, lodash);

// Add methods that return unwrapped values in chain sequences.
lodash.add = add;
// code ... 等 152 個不支持鏈?zhǔn)秸{(diào)用的方法


// 這里其實就是過濾 after 等支持鏈?zhǔn)秸{(diào)用的方法恋技,獲取到 lodash 上的 add 等 添加到lodash.prototype 上。
mixin(lodash, (function() {
    var source = {};
    // baseForOwn 這里其實就是遍歷lodash上的靜態(tài)方法逻族,執(zhí)行回調(diào)函數(shù)
    baseForOwn(lodash, function(func, methodName) {
        // 第一次 mixin 調(diào)用了所以賦值到了lodash.prototype
        // 所以這里用 Object.hasOwnProperty 排除不在lodash.prototype 上的方法蜻底。也就是 add 等 152 個不支持鏈?zhǔn)秸{(diào)用的方法。
        if (!hasOwnProperty.call(lodash.prototype, methodName)) {
            source[methodName] = func;
        }
    });
    return source;
// 最后一個參數(shù)options 特意注明不支持鏈?zhǔn)秸{(diào)用
}()), { 'chain': false });

結(jié)合兩次調(diào)用mixin 代入到源碼解析如下
mixin源碼及注釋

function mixin(object, source, options) {
    // source 對象中可以枚舉的屬性
    var props = keys(source),
        // source 對象中的方法名稱數(shù)組
        methodNames = baseFunctions(source, props);

    if (options == null &&
        !(isObject(source) && (methodNames.length || !props.length))) {
        // 如果 options 沒傳為 undefined  undefined == null 為true
        // 且 如果source 不為 對象或者不是函數(shù)
        // 且 source對象的函數(shù)函數(shù)長度 或者 source 對象的屬性長度不為0
        // 把 options 賦值為 source
        options = source;
        // 把 source 賦值為 object
        source = object;
        // 把 object 賦值為 this 也就是 _ (lodash)
        object = this;
        // 獲取到所有的方法名稱數(shù)組
        methodNames = baseFunctions(source, keys(source));
    }
    // 是否支持 鏈?zhǔn)秸{(diào)用
    // options  不是對象或者不是函數(shù)聘鳞,是null或者其他值
    // 判斷options是否是對象或者函數(shù)薄辅,如果不是或者函數(shù)則不會執(zhí)行 'chain' in options 也就不會報錯
    //  且 chain 在 options的對象或者原型鏈中
    // 知識點 in [MDN in :  https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/in
    // 如果指定的屬性在指定的對象或其原型鏈中,則in 運算符返回true抠璃。

    // 或者 options.chain 轉(zhuǎn)布爾值
    var chain = !(isObject(options) && 'chain' in options) || !!options.chain,
        // object 是函數(shù)
        isFunc = isFunction(object);

    // 循環(huán) 方法名稱數(shù)組
    arrayEach(methodNames, function(methodName) {
        // 函數(shù)本身
        var func = source[methodName];
        // object 通常是 lodash  也賦值這個函數(shù)站楚。
        object[methodName] = func;
        if (isFunc) {
            // 如果object是函數(shù) 賦值到  object prototype  上,通常是lodash
            object.prototype[methodName] = function() {
                // 實例上的__chain__ 屬性 是否支持鏈?zhǔn)秸{(diào)用
                // 這里的 this 是 new LodashWrapper 實例 類似如下
                /**
                 {
                    __actions__: [],
                    __chain__: true
                    __index__: 0
                    __values__: undefined
                    __wrapped__: []
                 }
                 **/

                var chainAll = this.__chain__;
                // options 中的 chain 屬性 是否支持鏈?zhǔn)秸{(diào)用
                // 兩者有一個符合鏈?zhǔn)秸{(diào)用  執(zhí)行下面的代碼
                if (chain || chainAll) {
                    // 通常是 lodash
                    var result = object(this.__wrapped__),
                    // 復(fù)制 實例上的 __action__ 到 result.__action__ 和 action 上
                    actions = result.__actions__ = copyArray(this.__actions__);

                    // action 添加 函數(shù) 和 args 和 this 指向搏嗡,延遲計算調(diào)用窿春。
                    actions.push({ 'func': func, 'args': arguments, 'thisArg': object });
                    //實例上的__chain__ 屬性  賦值給 result 的 屬性 __chain__
                    result.__chain__ = chainAll;
                    // 最后返回這個實例
                    return result;
                }

                // 都不支持鏈?zhǔn)秸{(diào)用。直接調(diào)用
                // 把當(dāng)前實例的 value 和 arguments 對象 傳遞給 func 函數(shù)作為參數(shù)調(diào)用采盒。返回調(diào)用結(jié)果旧乞。
                return func.apply(object, arrayPush([this.value()], arguments));
            };
        }
    });

    // 最后返回對象 object
    return object;
}

小結(jié):簡單說就是把lodash上的靜態(tài)方法賦值到lodash.prototype上。分兩次第一次是支持鏈?zhǔn)秸{(diào)用(lodash.after153個支持鏈?zhǔn)秸{(diào)用的方法)纽甘,第二次是不支持鏈?zhǔn)秸{(diào)用的方法(lodash.add152個不支持鏈?zhǔn)秸{(diào)用的方法)。

lodash 究竟在.prototype掛載了多少方法和屬性

再來看下lodash究竟掛載在_函數(shù)對象上有多少靜態(tài)方法和屬性抽碌,和掛載_.prototype上有多少方法和屬性悍赢。

使用for in循環(huán)一試便知』踽悖看如下代碼:

var staticMethods = [];
var staticProperty = [];
for(var name in _){
    if(typeof _[name] === 'function'){
        staticMethods.push(name);
    }
    else{
        staticProperty.push(name);
    }
}
console.log(staticProperty); // ["templateSettings", "VERSION"] 2個
console.log(staticMethods); // ["after", "ary", "assign", "assignIn", "assignInWith", ...] 305個

其實就是上文提及的 lodash.after153個支持鏈?zhǔn)秸{(diào)用的函數(shù) 左权、lodash.add152不支持鏈?zhǔn)秸{(diào)用的函數(shù)賦值而來。

var prototypeMethods = [];
var prototypeProperty = [];
for(var name in _.prototype){
    if(typeof _.prototype[name] === 'function'){
        prototypeMethods.push(name);
    }
    else{
        prototypeProperty.push(name);
    }
}
console.log(prototypeProperty); // []
console.log(prototypeMethods); // ["after", "all", "allKeys", "any", "assign", ...] 317個

相比lodash上的靜態(tài)方法多了12個痴颊,說明除了 mixin 外赏迟,還有12個其他形式賦值而來。

支持鏈?zhǔn)秸{(diào)用的方法最后返回是實例對象蠢棱,獲取最后的處理的結(jié)果值锌杀,最后需要調(diào)用value方法甩栈。

筆者畫了一張表示lodash的方法和屬性掛載關(guān)系圖。

`lodash`的方法和屬性掛載關(guān)系圖

請出貫穿下文的簡單的例子

var result = _.chain([1, 2, 3, 4, 5])
.map(el => {
    console.log(el); // 1, 2, 3
    return el + 1;
})
.take(3)
.value();
// lodash中這里的`map`僅執(zhí)行了`3`次糕再。
// 具體功能也很簡單 數(shù)組 1-5 加一量没,最后獲取其中三個值。
console.log('result:', result);

也就是說這里lodash聰明的知道了最后需要幾個值突想,就執(zhí)行幾次map循環(huán)殴蹄,對于很大的數(shù)組,提升性能很有幫助猾担。

underscore執(zhí)行這段代碼其中map執(zhí)行了5次袭灯。
如果是平常實現(xiàn)該功能也簡單。

var result = [1, 2, 3, 4, 5].map(el => el + 1).slice(0, 3);
console.log('result:', result);

而相比lodash這里的map執(zhí)行了5次绑嘹。

// 不使用 map稽荧、slice
var result = [];
var arr = [1, 2, 3, 4, 5];
for (var i = 0; i < 3; i++){
    result[i] = arr[i] + 1;
}
console.log(result, 'result');

簡單說這里的map方法,添加 LazyWrapper 的方法到 lodash.prototype存儲下來圾叼,最后調(diào)用 value時再調(diào)用蛤克。
具體看下文源碼實現(xiàn)。

添加 LazyWrapper 的方法到 lodash.prototype

主要是如下方法添加到到 lodash.prototype 原型上夷蚊。

// "constructor"
["drop", "dropRight", "take", "takeRight", "filter", "map", "takeWhile", "head", "last", "initial", "tail", "compact", "find", "findLast", "invokeMap", "reject", "slice", "takeRightWhile", "toArray", "clone", "reverse", "value"]

具體源碼及注釋

// Add `LazyWrapper` methods to `lodash.prototype`.
// baseForOwn 這里其實就是遍歷LazyWrapper.prototype上的方法构挤,執(zhí)行回調(diào)函數(shù)
baseForOwn(LazyWrapper.prototype, function(func, methodName) {
    // 檢測函數(shù)名稱是否是迭代器也就是循環(huán)
    var checkIteratee = /^(?:filter|find|map|reject)|While$/.test(methodName),
        // 檢測函數(shù)名稱是否head和last
        // 順便提一下 ()這個是捕獲分組 而加上 ?:  則是非捕獲分組 也就是說不用于其他操作
        isTaker = /^(?:head|last)$/.test(methodName),
        // lodashFunc 是 根據(jù) isTaker 組合 takeRight take methodName
        lodashFunc = lodash[isTaker ? ('take' + (methodName == 'last' ? 'Right' : '')) : methodName],
        // 根據(jù)isTaker 和 是 find 判斷結(jié)果是否 包裝
        retUnwrapped = isTaker || /^find/.test(methodName);

    // 如果不存在這個函數(shù),就不往下執(zhí)行
    if (!lodashFunc) {
        return;
    }
    // 把 lodash.prototype 方法賦值到lodash.prototype
    lodash.prototype[methodName] = function() {
        // 取實例中的__wrapped__ 值 例子中則是 [1,2,3,4,5]
        var value = this.__wrapped__,
            // 如果是head和last 方法 isTaker 返回 [1], 否則是arguments對象
            args = isTaker ? [1] : arguments,
            // 如果value 是LayeWrapper的實例
            isLazy = value instanceof LazyWrapper,
            // 迭代器 循環(huán)
            iteratee = args[0],
            // 使用useLazy isLazy value或者是數(shù)組
            useLazy = isLazy || isArray(value);

        var interceptor = function(value) {
            // 函數(shù)執(zhí)行 value args 組合成數(shù)組參數(shù)
            var result = lodashFunc.apply(lodash, arrayPush([value], args));
            // 如果是 head 和 last (isTaker) 支持鏈?zhǔn)秸{(diào)用 返回結(jié)果的第一個參數(shù) 否則 返回result
            return (isTaker && chainAll) ? result[0] : result;
        };

        // useLazy true 并且 函數(shù)checkIteratee 且迭代器是函數(shù)惕鼓,且迭代器參數(shù)個數(shù)不等于1
        if (useLazy && checkIteratee && typeof iteratee == 'function' && iteratee.length != 1) {
            // Avoid lazy use if the iteratee has a "length" value other than `1`.
            // useLazy 賦值為 false
            // isLazy 賦值為 false
            isLazy = useLazy = false;
        }
        // 取實例上的 __chain__
        var chainAll = this.__chain__,
            // 存儲的待執(zhí)行的函數(shù) __actions__ 二次取反是布爾值 也就是等于0或者大于0兩種結(jié)果
            isHybrid = !!this.__actions__.length,
            // 是否不包裝 用結(jié)果是否不包裝 且 不支持鏈?zhǔn)秸{(diào)用
            isUnwrapped = retUnwrapped && !chainAll,
            // 是否僅Lazy 用isLazy 和 存儲的函數(shù)
            onlyLazy = isLazy && !isHybrid;

        // 結(jié)果不包裝 且 useLazy 為 true
        if (!retUnwrapped && useLazy) {
            // 實例 new LazyWrapper 這里的this 是 new LodashWrapper()
            value = onlyLazy ? value : new LazyWrapper(this);
            // result 執(zhí)行函數(shù)結(jié)果
            var result = func.apply(value, args);

            /*
            *
            // _.thru(value, interceptor)
            // 這個方法類似 _.tap筋现, 除了它返回 interceptor 的返回結(jié)果。該方法的目的是"傳遞" 值到一個方法鏈序列以取代中間結(jié)果箱歧。
            _([1, 2, 3])
            .tap(function(array) {
                // 改變傳入的數(shù)組
                array.pop();
            })
            .reverse()
            .value();
            // => [2, 1]
            */

            // thisArg 指向undefined 或者null 非嚴(yán)格模式下是指向window矾飞,嚴(yán)格模式是undefined 或者nll
            result.__actions__.push({ 'func': thru, 'args': [interceptor], 'thisArg': undefined });
            // 返回實例 lodashWrapper
            return new LodashWrapper(result, chainAll);
        }
        // 不包裝 且 onlyLazy 為 true
        if (isUnwrapped && onlyLazy) {
            // 執(zhí)行函數(shù)
            return func.apply(this, args);
        }
        // 上面都沒有執(zhí)行,執(zhí)行到這里了
        // 執(zhí)行 thru 函數(shù)呀邢,回調(diào)函數(shù) 是 interceptor
        result = this.thru(interceptor);
        return isUnwrapped ? (isTaker ? result.value()[0] : result.value()) : result;
    };
});

小結(jié)一下洒沦,寫了這么多注釋,簡單說:其實就是用LazyWrapper.prototype 改寫原先在lodash.prototype的函數(shù)价淌,判斷函數(shù)是否需要使用惰性求值申眼,需要時再調(diào)用。

讀者可以斷點調(diào)試一下蝉衣,善用斷點進(jìn)入函數(shù)功能括尸,對著注釋看,可能會更加清晰病毡。

斷點調(diào)試的部分截圖

例子的chain和map執(zhí)行后的debugger截圖

例子的chain和map執(zhí)行后的結(jié)果截圖

鏈?zhǔn)秸{(diào)用最后都是返回實例對象濒翻,實際的處理數(shù)據(jù)的函數(shù)都沒有調(diào)用,而是被存儲存儲下來了,最后調(diào)用value方法有送,才執(zhí)行這些函數(shù)淌喻。

lodash.prototype.value 即 wrapperValue

function baseWrapperValue(value, actions) {
    var result = value;
    // 如果是lazyWrapper的實例,則調(diào)用LazyWrapper.prototype.value 方法娶眷,也就是 lazyValue 方法
    if (result instanceof LazyWrapper) {
        result = result.value();
    }
    // 類似 [].reduce()似嗤,把上一個函數(shù)返回結(jié)果作為參數(shù)傳遞給下一個函數(shù)
    return arrayReduce(actions, function(result, action) {
        return action.func.apply(action.thisArg, arrayPush([result], action.args));
    }, result);
}
function wrapperValue() {
    return baseWrapperValue(this.__wrapped__, this.__actions__);
}
lodash.prototype.toJSON = lodash.prototype.valueOf = lodash.prototype.value = wrapperValue;

如果是惰性求值,則調(diào)用的是 LazyWrapper.prototype.valuelazyValue届宠。

LazyWrapper.prototype.value 即 lazyValue 惰性求值

lazyValue源碼及注釋

function LazyWrapper(value) {
    // 參數(shù) value
    this.__wrapped__ = value;
    // 執(zhí)行的函數(shù)
    this.__actions__ = [];
    this.__dir__ = 1;
    // 過濾
    this.__filtered__ = false;
    // 存儲迭代器函數(shù)
    this.__iteratees__ = [];
    // 默認(rèn)最大取值個數(shù)
    this.__takeCount__ = MAX_ARRAY_LENGTH;
    // 具體取值多少個烁落,存儲函數(shù)和類型
    this.__views__ = [];
}
/**
* Extracts the unwrapped value from its lazy wrapper.
*
* @private
* @name value
* @memberOf LazyWrapper
* @returns {*} Returns the unwrapped value.
*/
function lazyValue() {
    // this.__wrapped__ 是 new LodashWrapper 實例 所以執(zhí)行.value 獲取原始值
    var array = this.__wrapped__.value(),
        //
        dir = this.__dir__,
        // 是否是函數(shù)
        isArr = isArray(array),
        // 是否從右邊開始
        isRight = dir < 0,
        // 數(shù)組的長度。如果不是數(shù)組豌注,則是0
        arrLength = isArr ? array.length : 0,
        // 獲取 take(3) 上述例子中 則是 start: 0伤塌,end: 3
        view = getView(0, arrLength, this.__views__),
        start = view.start,
        end = view.end,
        // 長度 3
        length = end - start,
        // 如果是是從右開始
        index = isRight ? end : (start - 1),
        // 存儲的迭代器數(shù)組
        iteratees = this.__iteratees__,
        // 迭代器數(shù)組長度
        iterLength = iteratees.length,
        // 結(jié)果resIndex
        resIndex = 0,
        // 最后獲取幾個值,也就是 3
        takeCount = nativeMin(length, this.__takeCount__);

    // 如果不是數(shù)組轧铁,或者 不是從右開始 并且 參數(shù)數(shù)組長度等于take的長度 takeCount等于長度
    // 則直接調(diào)用 baseWrapperValue 不需要
    if (!isArr || (!isRight && arrLength == length && takeCount == length)) {
        return baseWrapperValue(array, this.__actions__);
    }
    var result = [];

    // 標(biāo)簽語句 label
    // MDN label 鏈接
    // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/label
    // 標(biāo)記語句可以和 break 或 continue 語句一起使用每聪。標(biāo)記就是在一條語句前面加個可以引用的標(biāo)識符(identifier)。
    outer:
    while (length-- && resIndex < takeCount) {
        index += dir;

        var iterIndex = -1,
            // 數(shù)組第一項
            value = array[index];

        while (++iterIndex < iterLength) {
            // 迭代器數(shù)組 {iteratee: function{}, typy: 2}
            var data = iteratees[iterIndex],
                iteratee = data.iteratee,
                type = data.type,
                // 結(jié)果 迭代器執(zhí)行結(jié)果
                computed = iteratee(value);

            if (type == LAZY_MAP_FLAG) {
                // 如果 type 是 map 類型齿风,結(jié)果 computed 賦值給value
                value = computed;
            } else if (!computed) {
                if (type == LAZY_FILTER_FLAG) {
                    // 退出當(dāng)前這次循環(huán)药薯,進(jìn)行下一次循環(huán)
                    continue outer;
                } else {
                    // 退出整個循環(huán)
                    break outer;
                }
            }
        }
        // 最終數(shù)組
        result[resIndex++] = value;
    }
    // 返回數(shù)組 例子中則是 [2, 3, 4]
    return result;
}
// Ensure `LazyWrapper` is an instance of `baseLodash`.
LazyWrapper.prototype = baseCreate(baseLodash.prototype);
LazyWrapper.prototype.constructor = LazyWrapper;

LazyWrapper.prototype.value = lazyValue;

筆者畫了一張 lodashLazyWrapper的關(guān)系圖來表示。

`lodash`和`LazyWrapper`的關(guān)系圖

小結(jié):lazyValue簡單說實現(xiàn)的功能就是把之前記錄的需要執(zhí)行幾次救斑,把記錄存儲的函數(shù)執(zhí)行幾次童本,不會有多少項數(shù)據(jù)就執(zhí)行多少次,而是根據(jù)需要幾項脸候,執(zhí)行幾項穷娱。
也就是說以下這個例子中,map函數(shù)只會執(zhí)行3次运沦。如果沒有用惰性求值泵额,那么map函數(shù)會執(zhí)行5次。

var result = _.chain([1, 2, 3, 4, 5])
.map(el => el + 1)
.take(3)
.value();

總結(jié)

行文至此携添,基本接近尾聲嫁盲,最后總結(jié)一下。

文章主要學(xué)習(xí)了runInContext() 導(dǎo)出_ lodash函數(shù)使用baseCreate方法原型繼承LodashWrapperLazyWrapper烈掠,mixin掛載方法到lodash.prototype羞秤、后文用結(jié)合例子解釋lodash.prototype.value(wrapperValue)Lazy.prototype.value(lazyValue)惰性求值的源碼具體實現(xiàn)。

分享一個只知道函數(shù)名找源碼定位函數(shù)申明位置的VSCode 技巧Ctrl + p向叉。輸入 @functionName 定位函數(shù)functionName在源碼文件中的具體位置锥腻。如果知道調(diào)用位置嗦董,那直接按alt+鼠標(biāo)左鍵即可跳轉(zhuǎn)到函數(shù)申明的位置母谎。

如果讀者發(fā)現(xiàn)有不妥或可改善之處,再或者哪里沒寫明白的地方京革,歡迎評論指出奇唤。另外覺得寫得不錯幸斥,對您有些許幫助,可以點贊咬扇、評論甲葬、轉(zhuǎn)發(fā)分享,也是對筆者的一種支持懈贺。萬分感謝经窖。

推薦閱讀

lodash github倉庫
lodash 官方文檔
lodash 中文文檔
打造一個類似于lodash的前端工具庫
惰性求值——lodash源碼解讀
luobo tang:lazy.js 惰性求值實現(xiàn)分析
lazy.js github 倉庫
本文章學(xué)習(xí)的lodash的版本v4.17.15 unpkg.com鏈接

筆者往期文章

學(xué)習(xí) underscore 源碼整體架構(gòu),打造屬于自己的函數(shù)式編程類庫
學(xué)習(xí) jQuery 源碼整體架構(gòu)梭灿,打造屬于自己的 js 類庫
面試官問:JS的繼承
面試官問:JS的this指向
面試官問:能否模擬實現(xiàn)JS的call和apply方法
面試官問:能否模擬實現(xiàn)JS的bind方法
面試官問:能否模擬實現(xiàn)JS的new操作符
前端使用puppeteer 爬蟲生成《React.js 小書》PDF并合并

關(guān)于

作者:常以若川為名混跡于江湖画侣。前端路上 | PPT愛好者 | 所知甚少,唯善學(xué)堡妒。
個人博客-若川配乱,使用vuepress重構(gòu)了,閱讀體驗可能更好些
掘金專欄@若川皮迟,歡迎關(guān)注~
segmentfault前端視野專欄搬泥,歡迎關(guān)注~
知乎前端視野專欄,歡迎關(guān)注~
github blog伏尼,相關(guān)源碼和資源都放在這里忿檩,求個star_~

微信公眾號 若川視野

可能比較有趣的微信公眾號,長按掃碼關(guān)注烦粒。也可以加微信 lxchuan12休溶,注明來源,拉您進(jìn)【前端視野交流群】扰她。

若川視野

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末兽掰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子徒役,更是在濱河造成了極大的恐慌孽尽,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忧勿,死亡現(xiàn)場離奇詭異杉女,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)鸳吸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門熏挎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人晌砾,你說我怎么就攤上這事坎拐。” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵哼勇,是天一觀的道長都伪。 經(jīng)常有香客問我,道長积担,這世上最難降的妖魔是什么陨晶? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮帝璧,結(jié)果婚禮上先誉,老公的妹妹穿的比我還像新娘。我一直安慰自己的烁,他們只是感情好谆膳,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著撮躁,像睡著了一般漱病。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上把曼,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天杨帽,我揣著相機(jī)與錄音,去河邊找鬼嗤军。 笑死注盈,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的叙赚。 我是一名探鬼主播老客,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼震叮!你這毒婦竟也來了胧砰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤苇瓣,失蹤者是張志新(化名)和其女友劉穎尉间,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體击罪,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡哲嘲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了媳禁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片眠副。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖竣稽,靈堂內(nèi)的尸體忽然破棺而出囱怕,到底是詐尸還是另有隱情槽唾,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布光涂,位于F島的核電站,受9級特大地震影響拧烦,放射性物質(zhì)發(fā)生泄漏忘闻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一恋博、第九天 我趴在偏房一處隱蔽的房頂上張望齐佳。 院中可真熱鬧,春花似錦债沮、人聲如沸炼吴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽硅蹦。三九已至,卻和暖如春闷煤,著一層夾襖步出監(jiān)牢的瞬間童芹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工鲤拿, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留假褪,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓近顷,卻偏偏與公主長得像生音,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子窒升,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容