vue響應式原理學習(一)
原理:
因為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節點。
參考
ofollow,noindex">Vue技術內幕 。