Vue3.0 放棄 Object.defineProperty 你了解多少幌羞?

想必大家都知道Vue3.0 把數(shù)據(jù)對(duì)象偵測(cè)的API 從Object.defineProperty 換成 proxy求冷,原因是使用了Proxy 初始性能更好。

因?yàn)閜roxy是真正的在對(duì)象層面做了proxy不會(huì)去改變對(duì)象的結(jié)構(gòu)辱挥,Object.defineProperty需要轉(zhuǎn)化數(shù)據(jù)對(duì)象屬性為getter、setter 這是比較昂貴的操作边涕。

如果你想從源碼去了解 Object.defineProperty 有多么糟糕那我們開始...

數(shù)據(jù)偵測(cè):

function observe(value, asRootData) {
    if (!isObject(value) || value instanceof VNode) {
        return
    }
    var ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__;
    } else if (
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
    ) {
        ob = new Observer(value);
    }
    if (asRootData && ob) {
        ob.vmCount++;
    }
    return ob
}

observe工廠函數(shù)是整個(gè)數(shù)據(jù)響應(yīng)式系統(tǒng)的入口晤碘,它會(huì)做幾個(gè)事情:

  • value 如果不是對(duì)象 或者是VNode的實(shí)例直接終止函數(shù)的執(zhí)行。
  • value如有"ob"屬性功蜓,或者 value.ob 的值是 Observer實(shí)例直接把 value.ob的值作為observe返回值。(注:當(dāng)一個(gè)數(shù)據(jù)對(duì)象被觀測(cè)之后將會(huì)在該對(duì)象上定義ob屬性)
  • 檢測(cè)value的合法性。(不能是Vue實(shí)例梦裂、必須是數(shù)組對(duì)象或者純對(duì)象告匠、必須為可配置...)
  • 創(chuàng)建Observer實(shí)例,將value作為參數(shù)傳遞著隆。

Observer構(gòu)造函數(shù)

var Observer = function Observer(value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
        if (hasProto) {
            protoAugment(value, arrayMethods);
        } else {
            copyAugment(value, arrayMethods, arrayKeys);
        }
        this.observeArray(value);
    } else {
        this.walk(value);
    }
};

將數(shù)據(jù)對(duì)象轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)是Observer構(gòu)造函數(shù)任務(wù)之一叠洗,一會(huì)我們會(huì)重點(diǎn)講this.walk這個(gè)方法。現(xiàn)在先來(lái)認(rèn)識(shí)下 Dep 旅东。

this.dep = new Dep();

很多人會(huì)把 Dep 理解為訂閱者構(gòu)造函數(shù)灭抑,但訂閱者本身就是一個(gè)很抽象的概念,理解上難免會(huì)增加心智負(fù)擔(dān)抵代。 我更愿意把Dep 理解成一個(gè)"容器" 這個(gè)"容器"中存儲(chǔ)的就是觀察者腾节。 什么是觀察者一會(huì)我們來(lái)講。先說(shuō)下這里的this.dep 就是創(chuàng)建了一個(gè)"容器" 這個(gè)"容器"中存儲(chǔ)的就是某個(gè)對(duì)象或者數(shù)組依賴的觀察者。

現(xiàn)在進(jìn)入到walk中看看:

Observer.prototype.walk = function walk(obj) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
        defineReactive$$1(obj, keys[i]);
    }
};

walk 方法很簡(jiǎn)單案腺,使用 Object.keys(obj) 獲取對(duì)象所有可枚舉的屬性庆冕,然后通過(guò) for 循環(huán)遍歷這些屬性,同時(shí)為每個(gè)屬性調(diào)用了 defineReactive$$1 函數(shù)劈榨。

function defineReactive$$1(
    obj,
    key,
    val,
    customSetter,
    shallow
) {
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
        return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key];
    }

    var childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            var value = getter ? getter.call(obj) : val;
            if (Dep.target) {
                dep.depend();
                if (childOb) {
                    childOb.dep.depend();
                    if (Array.isArray(value)) {
                        dependArray(value);
                    }
                }
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            var value = getter ? getter.call(obj) : val;
            /* eslint-disable no-self-compare */
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            /* eslint-enable no-self-compare */
            if (customSetter) {
                customSetter();
            }
            // #7981: for accessor properties without setter
            if (getter && !setter) {
                return
            }
            if (setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            childOb = !shallow && observe(newVal);
            dep.notify();
        }
    });
}

defineReactive1 函數(shù)核心就是將數(shù)據(jù)對(duì)象的數(shù)據(jù)屬性轉(zhuǎn)換為訪問(wèn)器屬性访递,但其中做了很多處理邊界條件的工作這里我們將不會(huì)做過(guò)多的闡述。defineReactive 接收五個(gè)參數(shù)同辣,但是在 walk 方法中調(diào)用 defineReactive1函數(shù)時(shí)只傳遞了前兩個(gè)參數(shù)拷姿,數(shù)據(jù)對(duì)象和屬性的鍵名。

重要代碼:

var dep = new Dep(); //1
var childOb = !shallow && observe(val); //2
Object.defineProperty(obj, key, { //3
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
            dep.depend();
            ...
        }
        return value
    },
    set: function reactiveSetter(newVal) {
        var value = getter ? getter.call(obj) : val;
        ...
        if (setter) {
            setter.call(obj, newVal);
        } else {
            val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
    }
});

需要注意的是:每次調(diào)用 defineReactive$$1 都會(huì)創(chuàng)建一個(gè) Dep 實(shí)例即之前我們講過(guò)的"容器"旱函,這里通過(guò)閉包的方法讓數(shù)據(jù)對(duì)象中每個(gè)屬性都會(huì)對(duì)應(yīng)有一個(gè) "容器" , 這個(gè)"容器"會(huì)在依賴收集的過(guò)程中存儲(chǔ)對(duì)應(yīng)的Watcher對(duì)象响巢。

shallow 這個(gè)屬性未傳值,它的作用是當(dāng)未傳值或者傳遞false 那是需要進(jìn)行深度觀測(cè)棒妨。接下來(lái)會(huì)在遞歸調(diào)用observe 檢測(cè) val 的數(shù)據(jù)類型是不是引用類型踪古,如果是在把 val 對(duì)象中的字段加入到響應(yīng)式系統(tǒng)當(dāng)中,用重新調(diào)用walk 券腔、defineReactive$$1函數(shù)伏穆。

看到這里大家就知道為什么說(shuō)Object.defineProperty很糟糕了吧? 當(dāng)項(xiàng)目復(fù)雜度上升纷纫、數(shù)據(jù)對(duì)象結(jié)構(gòu)過(guò)于復(fù)雜蜈出、初始性能將會(huì)變得越來(lái)越差,而Proxy 能完美的規(guī)避掉這些東西涛酗。如果你只是看標(biāo)題進(jìn)來(lái)這里應(yīng)該能解決你的疑惑了铡原。

但是現(xiàn)在我想跟大家講講關(guān)于Dep "容器"的事情。

get: function reactiveGetter() {
    var value = getter ? getter.call(obj) : val;
    if (Dep.target) {
        dep.depend();
        ...
    }
    return value
}

這是我們給數(shù)據(jù)對(duì)象屬性設(shè)置的getter商叹, 首先判斷Dep.target是否存在燕刻,那么Dep.target 是什么呢? 直言不諱的講剖笙,Dep.target中保存的值就是要被收集的依賴(觀察者)卵洗。所以如果 Dep.target 存在的話說(shuō)明有依賴需要被收集,這個(gè)時(shí)候才需要執(zhí)行 if 語(yǔ)句塊內(nèi)的代碼弥咪,如果 Dep.target 不存在就意味著沒(méi)有需要被收集的依賴过蹂,所以當(dāng)然就不需要執(zhí)行 if 語(yǔ)句塊內(nèi)的代碼了。

在 if 語(yǔ)句塊內(nèi)第一句執(zhí)行的代碼就是:dep.depend()聚至,它會(huì)將依賴收集到 dep 這個(gè)"容器"中酷勺,這里的 dep 對(duì)象就是屬性的 getter/setter 通過(guò)閉包關(guān)聯(lián)它自身的那個(gè)"容器"。

Dep構(gòu)造函數(shù)代碼很簡(jiǎn)單大家自行去閱讀扳躬,我們重點(diǎn)放在Dep.target 上脆诉。

Dep.target = null;

Dep.target初始值為null甚亭, 那什么時(shí)候賦值呢?那我們需要從模板編譯組件掛載說(shuō)起击胜,模板編譯專欄系列文章亏狰,接下來(lái)重點(diǎn)放在組件掛載。

function mountComponent(vm, el, hydrating) {
    vm.$el = el;
    ...
    callHook(vm, 'beforeMount');

    var updateComponent = function() {
        vm._update(vm._render(), hydrating);
    }
         ...
    new Watcher(vm, updateComponent, noop, {
            before: function before() {
            if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate');
            }
        }
    }, true /* isRenderWatcher */ );
    ...
}

mountComponent就是組件掛載的核心函數(shù)了偶摔,其內(nèi)部定義了 updateComponent 函數(shù)暇唾,該函數(shù)的作用是以 vm._render() 函數(shù)的返回值作為第一個(gè)參數(shù)調(diào)用 vm._update() 函數(shù)。沒(méi)有看過(guò)專欄的朋友可能還不了解 vm._render 函數(shù)和 vm._update 函數(shù)的作用辰斋,但可以先簡(jiǎn)單地認(rèn)為:

  • vm._render 函數(shù)的作用是調(diào)用 vm.$options.render 函數(shù)并返回生成的虛擬節(jié)點(diǎn)(VNode)
  • vm._update 函數(shù)的作用是把 vm._render 函數(shù)生成的虛擬節(jié)點(diǎn)渲染成真正的 DOM

再往下策州,我們將遇到創(chuàng)建觀察者(Watcher)實(shí)例的代碼:

new Watcher(vm, updateComponent, noop, {
    before: function before() {
        if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate');
        }
    }
}, true /* isRenderWatcher */ );

簡(jiǎn)單說(shuō)下Watcher的作用,他在數(shù)據(jù)響應(yīng)式系統(tǒng)中扮演的是對(duì)表達(dá)式的求值的角色亡呵,觸發(fā)數(shù)據(jù)屬性的 get 攔截器函數(shù)(做到這一步不困難想想我們之前講到的 render 函數(shù))抽活,從而收集到了依賴硫戈,當(dāng)數(shù)據(jù)變化時(shí)能夠觸發(fā)響應(yīng)锰什。

在上面的代碼中 Watcher 觀察者實(shí)例將對(duì) updateComponent 函數(shù)求值,updateComponent 函數(shù)執(zhí)行會(huì)間接觸發(fā)渲染函數(shù)(vm.$options.render)的執(zhí)行丁逝,而渲染函數(shù)的執(zhí)行則會(huì)觸發(fā)數(shù)據(jù)屬性的 get 攔截器函數(shù)汁胆,從而將依賴(觀察者)收集,當(dāng)數(shù)據(jù)變化時(shí)將重新執(zhí)行 updateComponent 函數(shù)霜幼,這就完成了重新渲染嫩码。同時(shí)我們把上面代碼中實(shí)例化的觀察者對(duì)象稱為 渲染函數(shù)的觀察者。

Watcher構(gòu)造函數(shù)

var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
    ...
    if (options) {
        this.deep = !!options.deep;
        this.user = !!options.user;
        this.lazy = !!options.lazy;
        this.sync = !!options.sync;
        this.before = options.before;
    } else {
        this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$1; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    ...
    this.value = this.lazy ? undefined : this.get();
};

我們目光只放在主線代碼上罪既,創(chuàng)建 Watcher 實(shí)例時(shí)可以傳遞五個(gè)參數(shù)铸题,分別是:組件實(shí)例對(duì)象vm、要觀察的表達(dá)式 expOrFn琢感、當(dāng)被觀察的表達(dá)式的值變化時(shí)的回調(diào)函數(shù) cb丢间、一些傳遞給當(dāng)前觀察者對(duì)象的選項(xiàng) options 以及一個(gè)布爾值 isRenderWatcher 用來(lái)標(biāo)識(shí)該觀察者實(shí)例是否是渲染函數(shù)的觀察者。

這里特別注意的是expOrFn 參數(shù)驹针, 我們?cè)趧?chuàng)建實(shí)例的時(shí)候?qū)?yīng)傳遞給它的是updateComponent 函數(shù)烘挫,剛剛我們講到 Watcher 的原理是通過(guò)對(duì)"被觀測(cè)目標(biāo)"的求值,觸發(fā)數(shù)據(jù)屬性的get 攔截器函數(shù)從而收集依賴, 那當(dāng)數(shù)據(jù)變化的時(shí)候呢柬甥? 數(shù)據(jù)一但是發(fā)生變化會(huì)執(zhí)行cb回調(diào)饮六,還會(huì)重新對(duì)"被觀察目標(biāo)"求值,也就是說(shuō) updateComponent 也會(huì)被調(diào)用苛蒲,在此過(guò)程中生成新的VNode卤橄。 說(shuō)到這里大家或許又產(chǎn)生了一個(gè)疑問(wèn):"再次執(zhí)行updateComponent函數(shù)難道不會(huì)導(dǎo)致再次觸發(fā)數(shù)據(jù)屬性的get攔截器函數(shù)導(dǎo)致重復(fù)收集依賴嗎?" 不用擔(dān)心臂外,因?yàn)?Vue 已經(jīng)實(shí)現(xiàn)了避免收集重復(fù)依賴的處理虽风,稍后會(huì)講到的棒口。

if (options) {
    this.deep = !!options.deep;
    this.user = !!options.user;
    this.lazy = !!options.lazy;
    this.sync = !!options.sync;
    this.before = options.before;
} else {
    this.deep = this.user = this.lazy = this.sync = false;
}

重點(diǎn)講講一會(huì)用到的lazy,options.lazy 用來(lái)標(biāo)識(shí)當(dāng)前觀察者實(shí)例對(duì)象是否是計(jì)算屬性的觀察者辜膝。 在低版本代碼中它還有另外一個(gè)時(shí)髦的名字 "options.computed" 无牵。計(jì)算屬性的觀察者并不是指一個(gè)觀察某個(gè)計(jì)算屬性變化的觀察者,而是指 Vue 內(nèi)部在實(shí)現(xiàn)計(jì)算屬性這個(gè)功能時(shí)為計(jì)算屬性創(chuàng)建的觀察者厂抖。后面用到了在詳細(xì)解釋茎毁。

this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.computed // for computed watchers

  • this.cb屬性,它的值為cb回調(diào)函數(shù)忱辅。
  • this.id屬性七蜘,它是觀察者實(shí)例對(duì)象的唯一標(biāo)識(shí)。
  • this.active屬性墙懂,它標(biāo)識(shí)著該觀察者實(shí)例對(duì)象是否是激活狀態(tài)橡卤,默認(rèn)值為true代表激活。
  • this.dirty屬性损搬,該屬性的值與this.lazy屬性的值相同碧库,也就是說(shuō)只有計(jì)算屬性的觀察者實(shí)例對(duì)象的this.dirty屬性的值才會(huì)為真,因?yàn)橛?jì)算屬性是惰性求值巧勤。
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()

重點(diǎn)關(guān)注: this.newDeps 與 this.newDepIds 它們兩就是用來(lái)避免收集重復(fù)依賴嵌灰,且移除無(wú)用依賴。

this.value = this.lazy ? undefined : this.get();

最后一句代碼意思是除計(jì)算屬性的觀察者之外的所有觀察者實(shí)例對(duì)象都將執(zhí)行 this.get() 方法颅悉。

依賴收集的過(guò)程

this.get()它的作用就是求值沽瞭。求值的目的有兩個(gè),第一個(gè)是能夠觸發(fā)訪問(wèn)器屬性的get攔截器函數(shù)剩瓶,第二個(gè)是能夠獲得被觀察目標(biāo)的值驹溃。而且能夠觸發(fā)訪問(wèn)器屬性的get攔截器函數(shù)是依賴被收集的關(guān)鍵,下面我們具體查看一下this.get()方法的內(nèi)容:

Watcher.prototype.get = function get() {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
        value = this.getter.call(vm, vm);
    } catch (e) {
        if (this.user) {
            handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
        } else {
            throw e
        }
    } finally {
        // "touch" every property so they are all tracked as
        // dependencies for deep watching
        if (this.deep) {
            traverse(value);
        }
        popTarget();
        this.cleanupDeps();
    }
    return value
};

this.get()方法調(diào)用了pushTarget(this) 函數(shù)延曙,并將當(dāng)前觀察者實(shí)例對(duì)象作為參數(shù)傳遞豌鹤。

Dep.target = null;
var targetStack = [];

function pushTarget(target) {
    targetStack.push(target);
    Dep.target = target;
}

function popTarget() {
    targetStack.pop();
    Dep.target = targetStack[targetStack.length - 1];
}

到目前為止已經(jīng)解決了之前的疑惑。Dep.target 是什么呢搂鲫? 直言不諱的講傍药,Dep.target中保存的值就是要被收集的依賴(觀察者)。

總結(jié)下:

Dep.target屬性初始值為null魂仍,pushTarget函數(shù)的作用就是用來(lái)為Dep.target屬性賦值的拐辽,pushTarget函數(shù)會(huì)將接收到的參數(shù)賦值給Dep.target屬性,傳遞給pushTarget函數(shù)的參數(shù)就是調(diào)用該函數(shù)的觀察者對(duì)象擦酌,所以Dep.target保存著一個(gè)觀察者對(duì)象俱诸,其實(shí)這個(gè)觀察者對(duì)象就是即將要收集的目標(biāo)。

接下來(lái)在回到get方法中:

Watcher.prototype.get = function get() {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
        value = this.getter.call(vm, vm);
    } catch (e) {
        if (this.user) {
            handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
        } else {
            throw e
        }
    } finally {
        // "touch" every property so they are all tracked as
        // dependencies for deep watching
        if (this.deep) {
            traverse(value);
        }
        popTarget();
        this.cleanupDeps();
    }
    return value
};

在調(diào)用pushTarget函數(shù)之后赊舶,定義了value變量睁搭,該變量的值為this.getter函數(shù)的返回值赶诊,你先簡(jiǎn)單認(rèn)定this.getter的值就是我們剛剛傳過(guò)來(lái)的updateComponent 函數(shù),這個(gè)函數(shù)的執(zhí)行就意味著對(duì)被觀察目標(biāo)的求值园骆,將得到的值賦值給value變量舔痪。而且我們可以看到this.get方法的最后將value返回。

this.value = this.lazy ? undefined : this.get();

在Watcher構(gòu)造函數(shù)中我們看到被觀察目標(biāo)的值锌唾,最終都會(huì)存儲(chǔ)在實(shí)例的value屬性上锄码。this.get()方法除了對(duì)被觀察目標(biāo)求值之外,大家別忘了正是因?yàn)閷?duì)被觀察目標(biāo)的求值才得以觸發(fā)數(shù)據(jù)屬性的get攔截器函數(shù)晌涕,還是以渲染函數(shù)的觀察者為例滋捶,假設(shè)我們有如下模板:

<div id="app">
  {{message}}
</div>

這段模板被編譯將生成如下渲染函數(shù):

function anonymous () {
  with (this) {
    return _c('div',
      { attrs:{ "id": "app" } },
      [_v(_s(message))]
    )
  }
}

這個(gè)過(guò)程在專欄的編譯器中都講過(guò)不在重述,可以發(fā)現(xiàn)渲染函數(shù)的執(zhí)行會(huì)讀取數(shù)據(jù)屬性message 的值余黎,這將會(huì)觸發(fā)message 屬性的 get 攔截器函數(shù)重窟。

執(zhí)行如下代碼defineReactive$$1部分源碼:

Object.defineProperty(obj, key, { 
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
            dep.depend();
            ...
        }
        return value
    },
    set: function reactiveSetter(newVal) {
        var value = getter ? getter.call(obj) : val;
        ...
        if (setter) {
            setter.call(obj, newVal);
        } else {
            val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
    }
});

由于渲染函數(shù)讀取了 message 屬性的值,所以 message 屬性的 get 攔截器函數(shù)將被執(zhí)行惧财,執(zhí)行過(guò)程中首先判斷了 Dep.target 是否存在巡扇,如果存在則調(diào)用dep.depend方法收集依賴。那么 Dep.target 是否存在呢可缚?答案是存在霎迫,這就是為什么 pushTarget 函數(shù)要在調(diào)用this.getter 函數(shù)之前被調(diào)用的原因斋枢。既然 dep.depend 方法被執(zhí)行帘靡,那么我們就找到dep.depend方法。

Dep.prototype.depend = function depend() {
    if (Dep.target) {
        Dep.target.addDep(this);
    }
};

順藤摸瓜去找下addDep的源碼:

Watcher.prototype.addDep = function addDep(dep) {
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id);
        this.newDeps.push(dep);
        if (!this.depIds.has(id)) {
            dep.addSub(this);
        }
    }
};

可以看到addDep方法接收一個(gè)參數(shù)瓤帚,這個(gè)參數(shù)是一個(gè)Dep對(duì)象描姚,在 addDep 方法內(nèi)部首先定義了常量id,它的值是Dep實(shí)例對(duì)象的唯一 id 值戈次。接著是一段 if 語(yǔ)句塊轩勘,該 if 語(yǔ)句塊的代碼很關(guān)鍵,因?yàn)樗淖饔镁褪怯脕?lái)避免收集重復(fù)依賴的怯邪,既然是用來(lái)避免收集重復(fù)的依賴绊寻,那么就不得不用到我們前面提到過(guò)的兩組屬性,即newDepIds悬秉、newDeps以及depIds澄步、deps。

什么叫收集重復(fù)的依賴和泌?舉個(gè)栗子有模板如下:

<div id="app">
  {{message}}{{message}}
</div>

這段模板被編譯將生成如下渲染函數(shù):

function anonymous () {
  with (this) {
    return _c('div',
      { attrs:{ "id": "app" } },
      [_v(_s(message)+_s(message))]
    )
  }
}

渲染函數(shù)的執(zhí)行將讀取兩次數(shù)據(jù)對(duì)象 message 屬性的值村缸,這必然會(huì)觸發(fā)兩次 message 屬性的 get 攔截器函數(shù),同樣的道理武氓,dep.depend 也將被觸發(fā)兩次梯皿,最后導(dǎo)致dep.addSub 方法被執(zhí)行了兩次仇箱,且參數(shù)一模一樣,這樣就產(chǎn)生了同一個(gè)觀察者被收集多次的問(wèn)題东羹。

if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    ...
}

在 addDep 內(nèi)部并不是直接調(diào)用 dep.addSub 收集觀察者剂桥,而是先根據(jù) dep.id屬性檢測(cè)該Dep實(shí)例對(duì)象是否已經(jīng)存在于 newDepIds 中,如果存在那么說(shuō)明已經(jīng)收集過(guò)依賴了属提,什么都不會(huì)做渊额。如果不存在才會(huì)繼續(xù)執(zhí)行if語(yǔ)句塊的代碼,同時(shí)將 dep.id 屬性和 Dep 實(shí)例對(duì)象本身分別添加到 newDepIds 和 newDeps 屬性中垒拢,這樣無(wú)論一個(gè)數(shù)據(jù)屬性被讀取了多少次旬迹,對(duì)于同一個(gè)觀察者它只會(huì)收集一次。

呼出一口長(zhǎng)氣求类,文章到這結(jié)束了....現(xiàn)在你是否有點(diǎn)明白用Object.defineProperty 構(gòu)建數(shù)據(jù)響應(yīng)式系統(tǒng)有多糟糕了奔垦。

推薦:

申請(qǐng)即送:

  • BAT大廠面試題筐咧、獨(dú)家面試工具包,

  • 資料免費(fèi)領(lǐng)取,包括 各類面試題以及答案整理噪矛,各大廠面試真題分享量蕊!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市艇挨,隨后出現(xiàn)的幾起案子残炮,更是在濱河造成了極大的恐慌,老刑警劉巖缩滨,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件势就,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡脉漏,警方通過(guò)查閱死者的電腦和手機(jī)苞冯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鸠删,“玉大人抱完,你說(shuō)我怎么就攤上這事∪信荩” “怎么了巧娱?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵碉怔,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我禁添,道長(zhǎng)撮胧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任老翘,我火速辦了婚禮芹啥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘铺峭。我一直安慰自己墓怀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布卫键。 她就那樣靜靜地躺著傀履,像睡著了一般。 火紅的嫁衣襯著肌膚如雪莉炉。 梳的紋絲不亂的頭發(fā)上钓账,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音絮宁,去河邊找鬼梆暮。 笑死,一個(gè)胖子當(dāng)著我的面吹牛绍昂,可吹牛的內(nèi)容都是我干的啦粹。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼治专,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼卖陵!你這毒婦竟也來(lái)了遭顶?” 一聲冷哼從身側(cè)響起张峰,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎棒旗,沒(méi)想到半個(gè)月后喘批,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡铣揉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年饶深,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逛拱。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡敌厘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出朽合,到底是詐尸還是另有隱情俱两,我是刑警寧澤饱狂,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站宪彩,受9級(jí)特大地震影響休讳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜尿孔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一俊柔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧活合,春花似錦雏婶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至侵续,卻和暖如春倔丈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背状蜗。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工需五, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人轧坎。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓宏邮,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親缸血。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蜜氨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

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