實現一個簡單版本的vue及原始碼解析
Vue.js的響應式原理依賴於Object.defineProperty,尤大大在Vue.js文件中就已經提到過,這也是Vue.js不支援IE8 以及更低版本瀏覽器的原因。Vue通過設定物件屬性的 setter/getter 方法來監聽資料的變化,通過getter進行依賴收集,而每個setter方法就是一個觀察者,在資料變更的時候通知訂閱者更新檢視。
Let data to observable
首先假定一種最簡單的情況,不去考慮其他情況。在initData中會呼叫observe這個函式將Vue的資料設定成observable的。當_data資料發生改變的時候就會觸發set,對訂閱者進行回撥(在這裡是render)。
function observe(value, cb) { Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb)) } function defineReactive (obj, key, val, cb) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: ()=>{ /*....依賴收集等....*/ /*Github:https://github.com/answershuto*/ return val }, set:newVal=> { val = newVal; cb();/*訂閱者收到訊息的回撥*/ } }) }
為了操作方便,我們需要將 _data 上的資料代理到vm例項上.
function proxy (data) { const that = this; Object.keys(data).forEach(key => { Object.defineProperty(that, key, { configurable: true, enumerable: true, get: function proxyGetter () { return that._data[key]; }, set: function proxySetter (val) { that._data[key] = val; } }) }); }
依賴收集
依賴收集的原因
按照上面的方法進行繫結會出現一個問題——實際模板中未使用的資料被更改後也會進行重新渲染,而這樣無疑會消耗效能,因此需要依賴收集來保證只渲染實際模板中使用到的資料。
Dep
當對data上的物件進行修改值的時候會觸發它的setter,那麼取值的時候自然就會觸發getter事件,所以我們只要在最開始進行一次render,那麼所有被渲染所依賴的data中的資料就會被getter收集到Dep的subs中去。在對data中的資料進行修改的時候setter只會觸發Dep的subs的函式.
Dep.prototype.depend 方法是將觀察者Watcher例項賦值給全域性的Dep.target,然後觸發render操作只有被Dep.target標記過的才會進行依賴收集。有Dep.target的物件會將Watcher的例項push到subs中,在物件被修改觸發setter操作的時候dep會呼叫subs中的Watcher例項的update方法來重新獲取資料生成虛擬節點,再由服務端將虛擬節點渲染成真實DOM。
src/oberver/dep.js
var uid = 0; //dep建構函式 export default function Dep(argument) { this.id = uid++ this.subs = [] } //新增一個觀察者物件 Dep.prototype.addSub = function(sub) { this.subs.push(sub) } //移除一個觀察者物件 Dep.prototype.removeSub = function(sub) { remove(this.subs, sub) } //依賴收集 Dep.prototype.depend = function() { if(Dep.target) { Dep.target.addDep(this) }全棧學習交流;582735936 } //通知所有訂閱者 Dep.prototype.notify = function() { var subs = this.subs.slice() for(var i = 0, l = subs.length; i < l; i++){ subs[i].update() } } Dep.target = null function remove (arr, item) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } }
實現
上面講述響應式原理和依賴收集的原因,接下來就來簡單實現一下。開始正式程式設計前,照常先寫測試用例。
test/observer/observer.spec.js
import { Observer, observe } from "../../src/observer/index" import Dep from '../../src/observer/dep' describe('Observer test', function() { it('observing object prop change', function() { const obj = { a:1, b:{a:1}, c:NaN} observe(obj) // mock a watcher! const watcher = { deps: [],全棧學習交流;582735936 addDep (dep) { this.deps.push(dep) dep.addSub(this) }, update: jasmine.createSpy() } // observing primitive value Dep.target = watcher obj.a Dep.target = null expect(watcher.deps.length).toBe(1) // obj.a }); });
接下來正式實現資料繫結,其中 observe 的作用是返回一個 observer 例項,而 observer 則負責實現資料繫結.
src/observer/index.js
import { def, //new hasOwn, isObject } from '../util/index' export function Observer(value) { this.value = value this.dep = new Dep() this.walk(value) def(value, '__ob__', this) } export function observe (value){ if (!isObject(value)) { return } var ob if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else { ob = new Observer(value) }全棧學習交流;582735936 return ob } Observer.prototype.walk = function(obj) { var keys = Object.keys(obj) for (var i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } } export function defineReactive (obj, key, val) { var dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = val if (Dep.target) { dep.depend() } return value }, set: function reactiveSetter (newVal) { var value =val if (newVal === value || (newVal !== newVal && value !== value)) { return } val = newVal dep.notify() } }) }
在上面的程式碼中我們用到了一些工具函式,下面我們就把這些工具函式在單獨的檔案中實現,方便之後其他元件的呼叫。
src/util/index.js
const hasOwnProperty = Object.prototype.hasOwnProperty //必須對傳入的引數進行判斷,不然obj為null時會報錯 export function hasOwn(obj, key) { if (!isObject(obj) && !Array.isArray(obj)) { return } return hasOwnProperty.call(obj, key) } export function isObject(obj) { return obj !== null && typeof obj === 'object' } //給要觀察的物件的_ob_屬性存放Observer物件,標記已觀察 export function def(obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) } 全棧學習交流
上面已經簡單實現了資料繫結,接下來不妨使用 npm run test 命令來測試下專案吧。