數據響應式原理也是老生常談了,什麼是數據響應式呢?
數據響應式,從官方定義來講,將Model綁定到View,當用代碼更新Model時,View會自動更新。
數據響應式強調數據驅動DOM生成,而不是直接操做DOM。
而經常和數據響應式混爲一談的數據雙向綁定,則特指v-model
,該指令實現了若是用戶更新了View,Model也會隨之更新。結合響應式原理,則造成了雙向綁定,即雙向綁定 = 單向綁定 + UI事件監聽
。
本文分爲四個部分:javascript
Vue經過訂閱發佈者模式來實現,經過三個類Observer
、Dep
、Watcher
來實現,主要關注每一個類的功能和類之間的關係。
html
Observer
與Dep
是一對一的關係,Dep
與Watcher
是多對多的關係。
Observer
類:數據的
觀察者(我的理解上是一個代理髮布者),當初始化數據時,遍歷數據全部屬性,爲每個屬性設置一個調度中心(
Dep
實例對象),經過
Object.defineProperty
把屬性轉爲
getter/setter
,注入相關的
Dep
調度方法。
Watcher
類:數據的
訂閱者,當接收到調度中心
Dep
的更新通知時,
Watcher
實例執行回調cb,更新視圖。
Dep
類:
調度中心,做爲發佈者和訂閱者之間的消息傳遞樞紐。當
Observer
類觸發
getter
時,
Dep
收集依賴的
Watcher
對象。當
Observer
類觸發**
setter
**時,
Observer
將數據更新信息發送給
Dep
,
Dep
通知訂閱者
Watcher
更新。
這一步驟主要是肯定好整個流程框架,參照流程圖能夠肯定:vue
class Vue{
constructor(options) {
// 1. 初始化數據
this.options = options
this.$data = options.data
this.$el = document.querySelector(options.el)
this._directive = [] // 收集依賴的容器
this.observer() // 2. 數據監測
this.compiler() // 3. 編譯模板,收集依賴
}
// TODO: 數據劫持,下發更新
observer() {}
// TODO: 判斷指令,收集依賴
compiler() {}
}
// 訂閱者,主要更新視圖
class Watcher {
constructor() {
this.update()
}
// TODO: 更新視圖
update() {}
}
var vm = new Vue({
el: '#app',
data: {
myText: '一開始,只是平平無奇的text',
myModel: '普普統統的model',
}
})
複製代碼
在框架上補充具體的邏輯:java
data
的屬性監聽的,也就是當data[prop]
更新時,只有訂閱prop
屬性的訂閱者會有更新操做,其餘訂閱者不會收到更新。因此訂閱者容器是一個屬性對應一個列表。Object.defineProperty
重寫get
/set
(Vue3經過ES6的Proxy實現),對數據屬性進行劫持監聽。當更改數據屬性時,下發更新。
爲何用Proxy 替代 Object.defineProperty ?
Object.defineProperty
只能劫持對象的屬性,所以咱們須要對每一個對象的每一個屬性進行遍歷。經過遞歸以及遍歷data對象來實現對數據的監控的,若是屬性值也是對象那麼須要深度遍歷。
Proxy相比較與Object.defineProperty
,優勢以下:
1. 能夠劫持整個對象,並返回一個新對象。顯然能夠大幅度提高性能。
2. z多種劫持操做node
v-model
,進行依賴收集操做<body>
<div id="app">
<h2>響應式原理實現</h2>
<div v-text="myText"></div>
<div v-text="myModel"></div>
<input v-model="myModel" />
</div>
</body>
<script> class Vue{ constructor(options) { // 1. 初始化數據 this.options = options this.$data = options.data this.$el = document.querySelector(options.el) this._directive = {} // 收集依賴的容器 // 2. 數據監測 this.observer(this.$data) // 3. 編譯模板 this.compiler(this.$el) } observer(data) { for (var key in data) { this._directive[key] = [] // 初始化訂閱者容器 let val = data[key] let watchers = this._directive[key] Object.defineProperty(this.$data, key, { get: function() { return val }, set: function(newVal) { // 更新數值,下發更新 val = newVal watchers.forEach(watcher => watcher.update()) }, }) } // es6 // const handler = { // set: function(data, prop, val) { // }, // } // data = new Proxy(data, handler) } compiler(el) { let nodes = el.children for(let i=0; i<nodes.length; i++) { let node = nodes[i] // 若是有子元素,遞歸調用 if (node.children.length > 0) this.compiler(node) // 判斷指令,收集依賴 if(node.hasAttribute("v-model")) { let attrVal = node.getAttribute("v-model") this._directive[attrVal].push(new Watcher(node, this, attrVal, 'value')) } if(node.hasAttribute("v-text")) { let attrVal = node.getAttribute("v-text") this._directive[attrVal].push(new Watcher(node, this, attrVal, 'innerText')) } } } } // 訂閱者,主要更新數據 class Watcher { // el: 訂閱節點 // vm: vue實例 // exp: 訂閱的data屬性值 // attr: 不一樣訂閱者更新視圖時,修改的屬性不一樣 constructor(el, vm, exp, attr) { this.el = el this.vm = vm this.exp = exp this.attr = attr this.update() } // 更新 update() { this.el[this.attr] = this.vm.$data[this.exp] } } var vm = new Vue({ el: '#app', data: { myText: '一開始,只是平平無奇的text', myModel: '普普統統的model', }, }) setTimeout(function(){ console.log(vm.$data) vm.$data["myText"] = '3秒後,text更新了' }, 3000) </script>
複製代碼
對v-model
指令,編譯時加入事件監聽。react
// ...
if(node.hasAttribute("v-model")) {
let attrVal = node.getAttribute("v-model")
this._directive[attrVal].push(new Watcher(node, this, attrVal, 'value'))
// V -> M: 加入事件監聽
node.addEventListener("input", (function () {
return function() {
this.$data[attrVal] = node.value
}
})().bind(this))
}
複製代碼
const input = document.getElementById('input')
const span = document.getElementById('span')
const obj = {
text: '文本文本文本'
}
const handler = {
set: function(target, prop, val) {
target[prop] = val
span.innerText = val
input.value = val
}
}
const myText = new Proxy(obj, handler);
input.addEventListener('keyup', function(e){
// 賦值觸發了set,初始化視圖
// myText代理了text屬性,當text改變觸發set
myText.text = e.target.value;
})
複製代碼
vuex
的狀態管理來得便利。Object.defineProperty
實現數據綁定時,直接添加對象屬性obj[prop]
,該屬性並不是響應式,即沒法經過數據驅動視圖。對於數組對象也有相似的限制,直接經過索引修改數組也不會驅動視圖更新。爲了成爲響應式屬性,須要經過Vue.set
來設置。(Vue3.0使用Proxy實現,屬性的添加和刪除、數組索引和長度的變動均可以被監聽,並能夠支持 Map、Set、WeakMap 和 WeakSet,該問題獲得解決)