從JavaScript屬性描述器剖析Vue.js響應式視圖
學習每一門語言,一般都是從其數據結構開始,JavaScript也是一樣,而JavaScript的數據結構中對象(Object)是最基礎也是使用最頻繁的概念和語法,坊間有言,JavaScript中,一切皆對象,基本可以描述對象在JavaScript中的地位,而且JavaScript中對象的強大也使其地位名副其實,本篇介紹JavaScript對象屬性描述器接口及其在數據視圖綁定方向的實踐,然后對Vue.js的響應式原理進行剖析。
前言
JavaScript的對象,是一組鍵值對的集合,可以擁有任意數量的唯一鍵,鍵可以是字符串(String)類型或標記(Symbol,ES6新增的基本數據類型)類型,每個鍵對應一個值,值可以是任意類型的任意值。對于對象內的屬性,JavaScript提供了一個屬性描述器接口 PropertyDescriptor ,大部分開發者并不需要直接使用它,但是很多框架和類庫內部實現使用了它,如avalon.js,Vue.js,本篇介紹屬性描述器及相關應用。
定義對象屬性
在介紹對象屬性描述之前,先介紹一下如何定義對象屬性。最常用的方式就是使用如下方式:
var a = {
name: 'jh'
};
// or
var b = {};
b.name = 'jh';
// or
var c = {};
var key = 'name';
c[key] = 'jh';
本文使用字面量方式創建對象,但是JavaScript還提供其他方式,如,new Object(),Object.create(),了解更多請查看 對象初始化 。
Object.defineProperty()
上面通常使用的方式不能實現對屬性描述器的操作,我們需要使用 defineProperty() 方法,該方法為一個對象定義新屬性或修改一個已定義屬性,接受三個參數 Object.defineProperty(obj, prop, descriptor) ,返回值為操作后的對象:
- obj, 待操作對象
- 屬性名
- 操作屬性的屬性描述對象
var x = {};
Object.defineProperty(x, 'count', {});
console.log(x); // Object {count: undefined}
由于傳入一個空的屬性描述對象,所以輸出對象屬性值為undefined,當使用 defineProperty() 方法操作屬性時,描述對象默認值為:
- value: undefined
- set: undefined
- get: undefined
- writable: false
- enumerable: false,
- configurable: false
不使用該方法定義屬性,則屬性默認描述為:
- value: undefined
- set: undefined
- get: undefined
- writable: true
- enumerable: true,
- configurable: true
默認值均可被明確參數值設置覆蓋。
當然還支持批量定義對象屬性及描述對象,使用“Object.defineProperties()`方法,如:
var x = {};
Object.defineProperties(x, {
count: {
value: 0
},
name: {
value: 'jh'
}
});
console.log(x); // Object {count: 0, name: 'jh'}
讀取屬性描述對象
JavaScript支持我們讀取某對象屬性的描述對象,使用 Object.getOwnPropertyDescriptor(obj, prop) 方法:
var x = {
name: 'jh'
};
Object.defineProperty(x, 'count', {});
Object.getOwnPropertyDescriptor(x, 'count');
Object.getOwnPropertyDescriptor(x, 'name');
// Object {value: undefined, writable: false, enumerable: false, configurable: false}
// Object {value: "jh", writable: true, enumerable: true, configurable: true}
該實例也印證了上面介紹的以不同方式定義屬性時,其默認屬性描述對象是不同的。
屬性描述對象
PropertyDescriptor API提供了六大實例屬性以描述對象屬性,包括:configurable, enumerable, get, set, value, writable.
value
指定對象屬性值:
var x = {};
Object.defineProperty(x, 'count', {
value: 0
});
console.log(x); // Object {count: 0}
writable
指定對象屬性是否可變:
var x = {};
Object.defineProperty(x, 'count', {
value: 0
});
console.log(x); // Object {count: 0}
x.count = 1; // 靜默失敗,不會報錯
console.log(x); // Object {count: 0}
使用 defineProperty() 方法時,默認有 writable: false , 需要顯示設置 writable: true 。
存取器函數(getter/setter)
對象屬性可以設置存取器函數,使用 get 聲明存取器getter函數, set 聲明存取器setter函數;若存在存取器函數,則在訪問或設置該屬性時,將調用對應的存取器函數:
get
讀取該屬性值時調用該函數并將該函數返回值賦值給屬性值;
var x = {};
Object.defineProperty(x, 'count', {
get: function() {
console.log('讀取count屬性 +1');
return 0;
}
});
console.log(x); // Object {count: 0}
x.count = 1;
// '讀取count屬性 +1'
console.log(x.count); // 0
set
當設置函數值時調用該函數,該函數接收設置的屬性值作參數:
var x = {};
Object.defineProperty(x, 'count', {
set: function(val) {
this.count = val;
}
});
console.log(x);
x.count = 1;
執行上訴代碼,會發現報錯,執行棧溢出:

上述代碼在設置 count 屬性時,會調用 set 方法,而在該方法內為 count 屬性賦值會再次觸發 set 方法,所以這樣是行不通的,JavaScript使用另一種方式,通常存取器函數得同時聲明,代碼如下:
var x = {};
Object.defineProperty(x, 'count', {
get: function() {
return this._count;
},
set: function(val) {
console.log('設置count屬性 +1');
this._count = val;
}
});
console.log(x); // Object {count: undefined}
x.count = 1;
// '設置count屬性 +1'
console.log(x.count); 1
事實上,在使用 defineProperty() 方法設置屬性時,通常需要在對象內部維護一個新內部變量(以下劃線 _ 開頭,表示不希望被外部訪問),作為存取器函數的中介。
注:當設置了存取器描述時,不能設置 value 和 writable 描述。
我們發現,設置屬性存取器函數后,我們可以實現對該屬性的實時監控,這在實踐中很有用武之地,后文會印證這一點。
enumerable
指定對象內某屬性是否可枚舉,即使用 for in 操作是否可遍歷:
var x = {
name: 'jh'
};
Object.defineProperty(x, 'count', {
value: 0
});
for (var key in x) {
console.log(key + ' is ' + x[key]);
}
// name is jh
上面無法遍歷 count 屬性,因為使用 defineProperty() 方法時,默認有 enumerable: false ,需要顯示聲明該描述:
var x = {
name: 'jh'
};
Object.defineProperty(x, 'count', {
value: 0,
enumerable: true
});
for (var key in x) {
console.log(key + ' is ' + x[key]);
}
// name is jh
// count is 0
x.propertyIsEnumerable('count'); // true
configurable
該值指定對象屬性描述是否可變:
var x = {};
Object.defineProperty(x, 'count', {
value: 0,
writable: false
});
Object.defineProperty(x, 'count', {
value: 0,
writable: true
});
執行上述代碼會報錯,因為使用 defineProperty() 方法時默認是 configurable: false ,輸出如圖:

修改如下,即可:
var x = {};
Object.defineProperty(x, 'count', {
value: 0,
writable: false,
configurable: true
});
x.count = 1;
console.log(x.count); // 0
Object.defineProperty(x, 'count', {
writable: true
});
x.count = 1;
console.log(x.count); // 1
屬性描述與視圖模型綁定
介紹完屬性描述對象,我們來看看其在現代JavaScript框架和類庫上的應用。目前有很多框架和類庫實現數據和DOM視圖的單向甚至雙向綁定,如React,angular.js,avalon.js,,Vue.js等,使用它們很容易做到對數據變更進行響應式更新DOM視圖,甚至視圖和模型可以實現雙向綁定,同步更新。當然這些框架、類庫內部實現原理主要分為三大陣營。本文以Vue.js為例,Vue.js是當下比較流行的一個響應式的視圖層類庫,其內部實現響應式原理就是本文介紹的屬性描述在技術中的具體應用。
可以點擊此處,查看一個原生JavaScript實現的簡易數據視圖單向綁定實例,在該實例中,點擊按鈕可以實現計數自增,在輸入框輸入內容會同步更新到展示DOM,甚至在控制臺改變 data 對象屬性值,DOM會響應更新,如圖:

數據視圖單向綁定
現有如下代碼:
var data = {};
var contentEl = document.querySelector('.content');
Object.defineProperty(data, 'text', {
writable: true,
configurable: true,
enumerable: true,
get: function() {
return contentEl.innerHTML;
},
set: function(val) {
contentEl.innerHTML = val;
}
});
很容易看出,當我們設置data對象的 text 屬性時,會將該值設置為視圖DOM元素的內容,而訪問該屬性值時,返回的是視圖DOM元素的內容,這就簡單的實現了數據到視圖的單向綁定,即數據變更,視圖也會更新。
以上僅是針對一個元素的數據視圖綁定,但稍微有經驗的開發者便可以根據以上思路,進行封裝,很容易的實現一個簡易的數據到視圖單向綁定的工具類。
抽象封裝
接下來對以上實例進行簡單抽象封裝, 點擊查看完整實例代碼 。
首先聲明數據結構:
window.data = {
title: '數據視圖單向綁定',
content: '使用屬性描述器實現數據視圖綁定',
count: 0
};
var attr = 'data-on'; // 約定好的語法,聲明DOM綁定對象屬性
然后封裝函數批量處理對象,遍歷對象屬性,設置描述對象同時為屬性注冊變更時的回調:
// 為對象中每一個屬性設置描述對象,尤其是存取器函數
function defineDescriptors(obj) {
for (var key in obj) {
// 遍歷屬性
defineDescriptor(obj, key, obj[key]);
}
// 為特定屬性設置描述對象
function defineDescriptor(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
var value = val;
return value;
},
set: function(newVal) {
if (newVal !== val) {
// 值發生變更才執行
val = newVal;
Observer.emit(key, newVal); // 觸發更新DOM
}
}
});
Observer.subscribe(key); // 為該屬性注冊回調
}
}
管理事件
以發布訂閱模式管理屬性變更事件及回調:
// 使用發布/訂閱模式,集中管理監控和觸發回調事件
var Observer = {
watchers: {},
subscribe: function(key) {
var el = document.querySelector('[' + attr + '="'+ key + '"]');
// demo
var cb = function react(val) {
el.innerHTML = val;
}
if (this.watchers[key]) {
this.watchers[key].push(cb);
} else {
this.watchers[key] = [].concat(cb);
}
},
emit: function(key, val) {
var len = this.watchers[key] && this.watchers[key].length;
if (len && len > 0) {
for(var i = 0; i < len; i++) {
this.watchers[key][i](val);
}
}
}
};
初始化實例
最后初始化實例:
// 初始化demo
function init() {
defineDescriptors(data); // 處理數據對象
var eles = document.querySelectorAll('[' + attr + ']');
// 初始遍歷DOM展示數據
// 其實可以將該操作放到屬性描述對象的get方法內,則在初始化時只需要對屬性遍歷訪問即可
for (var i = 0, len = eles.length; i < len; i++) {
eles[i].innerHTML = data[eles[i].getAttribute(attr)];
}
// 輔助測試實例
document.querySelector('.add').addEventListener('click', function(e) {
data.count += 1;
});
}
init();
html代碼參考如下:
<h2 class="title" data-on="title"></h2>
<div class="content" data-on="content"></div>
<div class="count" data-on="count"></div>
<div>
請輸入內容:
<input type="text" class="content-input" placeholder="請輸入內容">
</div>
<button class="add" onclick="">加1</button>
Vue.js的響應式原理
上一節實現了一個簡單的數據視圖單向綁定實例,現在對Vue.js的響應式單向綁定進行簡要分析,主要需要理解其如何追蹤數據變更。
依賴追蹤
Vue.js支持我們通過 data 參數傳遞一個JavaScript對象做為組件數據,然后Vue.js將遍歷此對象屬性,使用 Object.defineProperty 方法設置描述對象,通過存取器函數可以追蹤該屬性的變更,本質原理和上一節實例差不多,但是不同的是,Vue.js創建了一層 Watcher 層,在組件渲染的過程中把屬性記錄為依賴,之后當依賴項的 setter 被調用時,會通知 Watcher 重新計算,從而使它關聯的組件得以更新,如下圖:

組件掛載時,實例化 watcher 實例,并把該實例傳遞給依賴管理類,組件渲染時,使用對象觀察接口遍歷傳入的data對象,為每個屬性創建一個依賴管理實例并設置屬性描述對象,在存取器函數get函數中,依賴管理實例添加(記錄)該屬性為一個依賴,然后當該依賴變更時,觸發set函數,在該函數內通知依賴管理實例,依賴管理實例分發該變更給其內存儲的所有 watcher 實例, watcher 實例重新計算,更新組件。
因此可以總結說Vue.js的響應式原理是 依賴追蹤 ,通過一個觀察對象,為每個屬性,設置存取器函數并注冊一個依賴管理實例 dep , dep 內為每個組件實例維護一個 watcher 實例,在屬性變更時,通過setter通知 dep 實例, dep 實例分發該變更給每一個 watcher 實例, watcher 實例各自計算更新組件實例,即 watcher 追蹤 dep 添加的依賴, Object.defineProperty() 方法提供這種追蹤的技術支持, dep 實例維護這種追蹤關系。
源碼簡單分析
接下來對Vue.js源碼進行簡單分析,從對JavaScript對象和屬性的處理開始:
觀察對象(Observer)
首先,Vue.js也提供了一個抽象接口觀察對象,為對象屬性設置存儲器函數,收集屬性依賴然后分發依賴更新:
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep(); // 管理對象依賴
this.vmCount = 0;
def(value, '__ob__', this); // 緩存處理的對象,標記該對象已處理
if (Array.isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
} else {
this.walk(value);
}
};
上面代碼關注兩個節點, this.observeArray(value) 和 this.walk(value); :
-
若為對象,則調用 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], obj[keys[i]]); } };可以看到,最終設置屬性描述對象是通過調用 defineReactive$$1() 方法。
-
若value為對象數組,則需要額外處理,調用 observeArray() 方法對每一個對象均產生一個 Observer 實例,遍歷監聽該對象屬性:
Observer.prototype.observeArray = function observeArray (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } };核心是為每個數組項調用 observe 函數:
function observe(value, asRootData) { if (!isObject(value)) { return // 只需要處理對象 } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; // 處理過的則直接讀取緩存 } else if ( observerState.shouldConvert && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) { ob = new Observer(value); // 處理該對象 } if (asRootData && ob) { ob.vmCount++; } return ob }調用 ob = new Observer(value); 后就回到第一種情況的結果:調用 defineReactive$$1() 方法生成響應式屬性。
生成響應式屬性
源碼如下:
function defineReactive$$1 (obj,key,val,customSetter) {
var dep = new Dep(); // 管理屬性依賴
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// 之前已經設置了的get/set需要合并調用
var getter = property && property.get;
var setter = property && property.set;
var childOb = observe(val); // 屬性值也可能是對象,需要遞歸觀察處理
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) { // 管理依賴對象存在指向的watcher實例
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 ("development" !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal); // 更新屬性值
} else {
val = newVal; // 更新屬性值
}
childOb = observe(newVal); // 每次值變更時需要重新觀察,因為可能值為對象
dep.notify(); // 發布更新事件
}
});
}
該方法使用 Object.defineProperty() 方法設置屬性描述對象,邏輯集中在屬性存取器函數內:
- get: 返回屬性值,如果 watcher 存在,則遞歸記錄依賴;
- set: 屬性值發生變更時,更新屬性值,并調用 dep.notify() 方法發布更新事件;
管理依賴
Vue.js需要管理對象的依賴,在屬性更新時通知 watcher 更新組件,進而更新視圖,Vue.js管理依賴接口采用發布訂閱模式實現,源碼如下:
var uid$1 = 0;
var Dep = function Dep () {
this.id = uid$1++; // 依賴管理實例id
this.subs = []; // 訂閱該依賴管理實例的watcher實例數組
};
Dep.prototype.depend = function depend () { // 添加依賴
if (Dep.target) {
Dep.target.addDep(this); // 調用watcher實例方法訂閱此依賴管理實例
}
};
Dep.target = null; // watcher實例
var targetStack = []; // 維護watcher實例棧
function pushTarget (_target) {
if (Dep.target) { targetStack.push(Dep.target); }
Dep.target = _target; // 初始化Dep指向的watcher實例
}
function popTarget () {
Dep.target = targetStack.pop();
}
訂閱
如之前,生成響應式屬性為屬性設置存取器函數時,get函數內調用 dep.depend() 方法添加依賴,該方法內調用 Dep.target.addDep(this); ,即調用指向的 watcher 實例的 addDep 方法,訂閱此依賴管理實例:
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) { // 是否已訂閱
this.newDepIds.add(id); // watcher實例維護的依賴管理實例id集合
this.newDeps.push(dep); // watcher實例維護的依賴管理實例數組
if (!this.depIds.has(id)) { // watcher實例維護的依賴管理實例id集合
// 調用傳遞過來的依賴管理實例方法,添加此watcher實例為訂閱者
dep.addSub(this);
}
}
};
watcher 實例可能同時追蹤多個屬性(即訂閱多個依賴管理實例),所以需要維護一個數組,存儲多個訂閱的依賴管理實例,同時記錄每一個實例的id,便于判斷是否已訂閱,而后調用依賴管理實例的 addSub 方法:
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub); // 實現watcher到依賴管理實例的訂閱關系
};
該方法只是簡單的在訂閱數組內添加一個訂閱該依賴管理實例的 watcher 實例。
發布
屬性變更時,在屬性的存取器set函數內調用了 dep.notify() 方法,發布此屬性變更:
Dep.prototype.notify = function notify () {
// 復制訂閱者數組
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update(); // 分發變更
}
};
觸發更新
前面提到,Vue.js中由 watcher 層追蹤依賴變更,發生變更時,通知組件更新:
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) { // 同步
this.run();
} else { // 異步
queueWatcher(this); // 最后也是調用run()方法
}
};
調用 run 方法,通知組件更新:
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get(); // 獲取新屬性值
if (value !== this.value || // 若值
isObject(value) || this.deep) {
var oldValue = this.value; // 緩存舊值
this.value = value; // 設置新值
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};
調用 this.get() 方法,實際上,后面會看到在該方法內處理了屬性值的更新與組件的更新,這里判斷當屬性變更時調用初始化時傳給實例的 cb 回調函數,并且回調函數接受屬性新舊值兩個參數,此回調通常是對于 watch 聲明的監聽屬性才會存在,否則默認為空函數。
追蹤依賴接口實例化
每一個響應式屬性都是由一個 Watcher 實例追蹤其變更,而針對不同屬性(data, computed, watch),Vue.js進行了一些差異處理,如下是接口主要邏輯:
var Watcher = function Watcher (vm,expOrFn,cb,options) {
this.cb = cb;
...
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
this.value = this.lazy
? undefined
: this.get();
};
在初始化 Watcher 實例時,會解析 expOrFn 參數(表達式或者函數)成拓展getter this.getter ,然后調用 this.get() 方法,返回值作為 this.value 值:
Watcher.prototype.get = function get () {
pushTarget(this); // 入棧watcher實例
var value;
var vm = this.vm;
if (this.user) {
try {
value = this.getter.call(vm, vm); // 通過this.getter獲取新值
} catch (e) {
handleError(e, vm, ("getter for watcher \"" +
(this.expression) + "\""));
}
} else {
value = this.getter.call(vm, vm); // 通過this.getter獲取新值
}
if (this.deep) { // 深度遞歸遍歷對象追蹤依賴
traverse(value);
}
popTarget(); // 出棧watcher實例
this.cleanupDeps(); // 清空緩存依賴
return value // 返回新值
};
這里需要注意的是對于 data 屬性,而非 computed 屬性或 watch 屬性,而言,其 watcher 實例的 this.getter 通常就是 updateComponent 函數,即渲染更新組件, get 方法返回undefined,而對于 computed 計算屬性而言,會傳入對應指定函數給 this.getter ,其返回值就是此 get 方法返回值。
data普通屬性
Vue.jsdata屬性是一個對象,需要調用對象觀察接口 new Observer(value) :
function observe (value, asRootData) {
if (!isObject(value)) {
return
}
var ob;
ob = new Observer(value); // 對象觀察實例
return ob;
}
// 初始處理data屬性
function initData (vm) {
// 調用observe函數
observe(data, true /* asRootData */);
}
計算屬性
Vue.js對計算屬性處理是有差異的,它是一個變量,可以直接調用 Watcher 接口,把其屬性指定的計算規則傳遞為,屬性的拓展 getter ,即:
// 初始處理computed計算屬性
function initComputed (vm, computed) {
for (var key in computed) {
var userDef = computed[key]; // 對應的計算規則
// 傳遞給watcher實例的this.getter -- 拓展getter
var getter = typeof userDef === 'function' ?
userDef : userDef.get;
watchers[key] = new Watcher(vm,
getter, noop, computedWatcherOptions);
}
}
watch屬性
而對于watch屬性又有不同,該屬性是變量或表達式,而且與計算屬性不同的是,它需要指定一個變更事件發生后的回調函數:
function initWatch (vm, watch) {
for (var key in watch) {
var handler = watch[key];
createWatcher(vm, key, handler[i]); // 傳遞回調
}
}
function createWatcher (vm, key, handler) {
vm.$watch(key, handler, options); // 回調
}
Vue.prototype.$watch = function (expOrFn, cb, options) {
// 實例化watcher,并傳遞回調
var watcher = new Watcher(vm, expOrFn, cb, options);
}
初始化Watcher與依賴管理接口的連接
無論哪種屬性最后都是由 watcher 接口實現追蹤依賴,而且組件在掛載時,即會初始化一次 Watcher 實例,綁定到 Dep.target ,也就是將 Watcher 和 Dep 建立連接,如此在組件渲染時才能對屬性依賴進行追蹤:
function mountComponent (vm, el, hydrating) {
...
updateComponent = function () {
vm._update(vm._render(), hydrating);
...
};
...
vm._watcher = new Watcher(vm, updateComponent, noop);
...
}
如上,傳遞 updateComponent 方法給 watcher 實例,該方法內觸發組件實例的 vm._render() 渲染方法,觸發組件更新,此 mountComponent() 方法會在 $mount() 掛載組件公開方法中調用:
// public mount method
Vue$3.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
總結
到此為止,對于JavaScript屬性描述器接口的介紹及其應用,還有其在Vue.js中的響應式實踐原理基本闡述完了,這次總結從原理到應用,再到實踐剖析,花費比較多精力,但是收獲是成正比的,不僅對JavaScript基礎有更深的理解,還更熟悉了Vue.js響應式的設計原理,對其源碼熟悉度也有較大提升,之后在工作和學習過程中,會進行更多的總結分享。
參考
來自: 從JavaScript屬性描述器剖析Vue.js響應式視圖