響應式系統及實現
響應式原理
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)
}
}
//通知所有訂閱者
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: [],
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)
}
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
命令來測試下專案吧。