1.為什麼我們把屬性定義在data、props、methods等引數裡,卻能通過this物件直接訪問呢。
原理:
因為vue內部做了代理。假如我們用this去訪問某個屬性,vue會自動去data,props,methods等引數物件裡面去查詢。所以我們開發時會發現,props裡面定義過的屬性,data不能再定義了,會丟擲警告。methods也一樣。
用過Vue都知道,Vue本身是一個建構函式,所以我們的用法是直接new Vue()。下面我們用程式碼模擬一下Vue內部的代理
(部分程式碼來源:vue專案下 src/core/instance/state.js)
// 定義一個空函式
function noop() {}
// 定義一個公用的屬性描述物件
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
/**
* 定義代理函式
* @target 當前物件
* @sourceKey 傳入的是來源,也就是代理物件的名稱
* @key 要訪問的屬性
*/
function proxy(target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter() {
// 示例:如果你在data中訪問this.name,那麼此時返回的是 this['_data']['name']
// target[key] => target[source][key]
return target[sourceKey][key];
}
sharedPropertyDefinition.set = function proxySetter(val) {
target[sourceKey][key] = val;
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
// 建構函式
function MyVue(options) {
this._data = options.data || {};
this._props = options.props || {};
this._methods = options.methods || {};
this.init(options);
}
MyVue.prototype.init = function(options) {
initData(this, options.data);
initProps(this, options.props);
iniMethods(this, options.methods);
}
// 相關方法
function initData(vm, dataObj) {
Object.keys(dataObj).forEach(key => proxy(vm, '_data', key));
}
function initProps(vm, propsObj) {
Object.keys(propsObj).forEach(key => proxy(vm, '_props', key));
}
function iniMethods(vm, methodsObj) {
Object.keys(methodsObj).forEach(key => proxy(vm, '_methods', key));
}
複製程式碼
這裡的程式碼主要是示例,並沒有判斷屬性是否重複。
測試程式碼:
let myVm = new MyVue({
data: {
name: 'JK',
age: 25
},
props: {
sex: 'man'
},
methods: {
about() {
console.log(`my Name is ${this.name}, age is ${this.age}, sex is ${this.sex}`);
}
}
});
myVm.name // 'JK'
myVm.age // 25
myVm.sex // 'man'
myVm.about() // my Name is JK, age is 25, sex is man
myVm.age = 24;
複製程式碼
具體Vue內部的處理是比較複雜的,會判斷很多邊界情況。例如data返回一個函式時需要單獨處理,例如props傳入具有default和type屬性的物件等等。
2. 如何實現一個簡易的資料響應式系統
Vue的資料響應式實現是依賴 Object.defineProperty
這個api的,這也是它不支援IE8且無法hack的原因。
據說Vue3.0改用了ES6 的 ```Proxy``,並使用TypeScript編寫。很是期待。
vue改變data之後做了什麼? 如果要說完整的一套流程,那是很多的,涉及到 watcher,render 渲染函式,VNode,Dom diff 等等。
響應式系統本身是基於觀察者模式的,也可以說是釋出/訂閱模式。 釋出/訂閱模式,就好比是你去找中介租房子。而觀察者模式呢,就好比你直接去城中村找房東租房子。 釋出/訂閱模式比觀察者模式多了個排程中心(中介)。
我這裡只是先說一下怎麼收集依賴,修改了值是怎麼通知的思路。
(部分程式碼來源:vue專案下 src/core/observer/)
丟擲任何其他的因素,我們先實現一個響應式的雛形
// 假如有一個物件是 data
let data = {
x: 1,
y: 2
}
// 我們把這個物件變成響應式的
for(const key in data) {
Object.defineProperty(data, key, {
get() {
console.log(`我獲取了data的${key}`);
return data[key]
},
set(val) {
console.log(`我設定了data的${key}為${val}`);
data[key] = val;
}
})
}
複製程式碼
把這個程式碼扔到瀏覽器裡,然後獲取一下data.x
,會發現,啊哦,怎麼瀏覽器一直在輸出,為什麼?
因為我在 get
中 return data[key]
,相當於又訪問了一次 data[key]
, 會一直觸發 get
方法的,造成死迴圈。所以我們等會把程式碼優化下。
接下來,我們在 get
裡收集依賴,set
裡觸發響應
怎麼收集依賴,怎麼觸發響應? 熟悉觀察者模式的同學應該能馬上想到,維護一個陣列,每次觸發 get 都把對應的函式push到這個陣列,每次 set
時將對應的函式觸發。是不是很像我們自定義一個事件系統,當然Vue內部肯定不會這麼簡單。
// 定義一個 watch 函式,作用是拿到改變某個值時對應的處理函式
// Target 是全域性變數, 用於儲存對應的函式
let Target = null
function $watch (exp, fn) {
// 將 Target 的值設定為 fn
Target = fn;
// 讀取欄位值,觸發 get 函式
data[exp];
}
// dep 在 get 和 set 被閉包引用,不會被回收
// 每一個 key 都有一個屬於自己的 dep
for(const key in data) {
const dep = [];
// 優化死迴圈
let val = data[key];
Object.defineProperty(data, key, {
get() {
console.log(`我獲取了data的${key}`);
// 收集依賴
dep.push(Target);
return val;
},
set(newVal) {
console.log(`我設定了data的${key}為${newVal}`);
if (val === newVal) {
return ;
}
val = newVal;
// 觸發依賴
dep.forEach(fn => fn());
}
})
}
// 監聽資料變化
$watch('x', () => console.log('x被修改')); // 輸出 '我獲取了data的x'
data.x = 3; // 輸出 '我設定了data的x為3', x被修改
複製程式碼
響應式是做好了,但眼尖的同學可能會發現,$watch 函式裡,竟然寫了一個固定的 data[exp]
,這裡的 data
是我們上一段程式碼定義的變數,在開發中,肯定不可能是固定的呀。所以再優化下, 傳入一個渲染函式,渲染函式內部觸發屬性的 get
。
全部程式碼:
let data = {
x: 1,
y: 2
}
// Target 是全域性變數, 用於儲存對應的函式
let Target = null
function $watch (exp, fn) {
// 將 Target 的值設定為 fn
Target = fn;
// 如果 exp 是函式,直接執行該函式
if (typeof exp === 'function') {
exp();
return;
}
// 讀取欄位值,觸發 get 函式
data[exp];
}
// dep 在 get 和 set 被閉包引用,不會被回收
// 每一個 key 都有一個屬於自己的 dep
for(const key in data) {
const dep = [];
// 優化死迴圈
let val = data[key];
Object.defineProperty(data, key, {
get() {
console.log(`我獲取了data的${key}`);
// 收集依賴
dep.push(Target);
return val;
},
set(newVal) {
console.log(`我設定了data的${key}為${newVal}`);
if (val === newVal) {
return ;
}
val = newVal;
// 觸發依賴
dep.forEach(fn => fn());
}
})
}
// 測試程式碼
function render () {
return document.write(`x:${data.x}; y:${data.y}`)
}
$watch(render, render);
複製程式碼
實際上Vue內部的處理是不會這麼簡單的,例如對陣列和物件的區別處理,物件的深度遍歷等,我們這裡都還沒考慮。
還有好多問題要學習:
如何避免重複收集依賴,如何根據template模板的解析並生成渲染函式,AST的實現,v-on,v-bind,v-for等指令的內部解析。
用vue時,push,slice等api改變data時可以觸發資料響應,而直接改資料的下標或length卻不會觸發呢, Vue.$set 內部做了什麼操作,
修改完資料後,內部怎麼觸發渲染對應的dom節點。
參考