原理:javascript
由於vue內部作了代理。假如咱們用this去訪問某個屬性,vue會自動去data,props,methods等參數對象裏面去查找。因此咱們開發時會發現,props裏面定義過的屬性,data不能再定義了,會拋出警告。methods也同樣。vue
用過Vue都知道,Vue自己是一個構造函數,因此咱們的用法是直接new Vue()。下面咱們用代碼模擬一下Vue內部的代理java
(部分代碼來源:vue項目下 src/core/instance/state.js)api
// 定義一個空函數
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屬性的對象等等。閉包
Vue的數據響應式實現是依賴 Object.defineProperty
這個api的,這也是它不支持IE8且沒法hack的緣由。dom
聽說Vue3.0改用了ES6 的 ```Proxy``,並使用TypeScript編寫。非常期待。函數
vue改變data以後作了什麼? 若是要說完整的一套流程,那是不少的,涉及到 watcher,render 渲染函數,VNode,Dom diff 等等。oop
響應式系統自己是基於觀察者模式的,也能夠說是發佈/訂閱模式。 發佈/訂閱模式,就比如是你去找中介租房子。而觀察者模式呢,就比如你直接去城中村找房東租房子。 發佈/訂閱模式比觀察者模式多了個調度中心(中介)。
我這裏只是先說一下怎麼收集依賴,修改了值是怎麼通知的思路。
(部分代碼來源: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節點。
參考