想必大家都知道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í)只傳遞了前兩個(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)有多糟糕了奔垦。
推薦:
- 020 持續(xù)更新,精品小圈子每日都有新內(nèi)容尸疆,干貨濃度極高椿猎。
- 結(jié)實(shí)人脈、討論技術(shù) 你想要的這里都有寿弱!
- 搶先入群犯眠,跑贏同齡人!(入群無(wú)需任何費(fèi)用)
- 群號(hào):779186871
- 點(diǎn)擊此處症革,與前端開發(fā)大牛一起交流學(xué)習(xí)
申請(qǐng)即送:
BAT大廠面試題筐咧、獨(dú)家面試工具包,
-
資料免費(fèi)領(lǐng)取,包括 各類面試題以及答案整理噪矛,各大廠面試真題分享量蕊!