Vue響應式系統原理並實現一個雙向繫結
這一章就著重講兩個點:
響應式系統如何收集依賴
-
響應式系統如何更新檢視
我們知道通過Object.defineProperty
做了資料劫持,當資料改變的時候,get
方法收集依賴,進而set
方法呼叫dep.notify
方法去通知Watcher
呼叫本身update
方法去更新檢視。那麼我們拋開其他問題,就討論get
,notify
,update
等方法,直接上程式碼:
get( )
get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }
我們知道Dep.target
在建立Watcher
的時候是null
,並且它只是起到一個標記的作用,當我們建立Watcher
例項的時候,我們的Dep.target
就會被賦值到Watcher
例項,進而放入target
棧中,我們這裡呼叫的是pushTarget
函式:
// 將watcher例項賦值給Dep.target,用於依賴收集。同時將該例項存入target棧中 export function pushTarget (_target: ?Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target }
那我們繼續執行到if (Dep.target)
語句的時候就會呼叫Dep.depend
函式:
// 將自身加入到全域性的watcher中
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
那下面的childOb
是啥東西呢?
let childOb = !shallow && observe(val)
我們通過這個變數判斷當前屬性下面是否還有ob
屬性,如果有的話繼續呼叫Dep.depend
函式,沒有的話則不處理。
我們還需要處理當前傳入的value
型別,是陣列屬性的話則會呼叫dependArray
// 收集陣列依賴
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
那麼收集依賴部分
到這裡就完了現在進行下一步觸發更新
了
set( )
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
// 判斷NaN的情況
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
我們看到了下面的 set
函式觸發了dep.notify()
方法
notify( )
// 通知所有訂閱者
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
在notify
裡面我們就做了一件事情,遍歷subs
數組裡面的所有Watcher
,逐一呼叫update
方法,也就是我們說的通知所有的訂閱者Watcher
呼叫自身update
方法 update( )
update () {
if (this.lazy) {
// 計算屬性會進來這段程式碼塊
// 這裡將dirty賦值為true
// 也不會馬上去讀取值
// 當render-watcher的update被觸發時
// 重新渲染頁面,計算屬性會重新讀值
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
那麼update方法實現了什麼呢?lazy
,dirty
,sync
又是啥?
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
// 這裡將lazy的值賦值給了dirty
// 就是說例項化的時候dirty = lazy = true
this.dirty = this.lazy // for lazy watchers
那是控制計算屬性的,當render—watcher
的方法update
被呼叫的時候,this.dirty
會變為true
會重新計算computed
值,渲染檢視,我們這裡不敘述。
那麼我們直接看queueWatcher()
函式:
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
我們可以看到一個更新佇列,更新佇列指向:
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
我們的callback
呼叫updated
鉤子
講到這裡就有點超綱
了,咱們初始化渲染
會呼叫一個initRender
函式建立dom
,還有上面所述的nextTick
,後期都會講,那麼瞭解了更新機制
,下一章我們就來實現一個讓面試官都驚呆了
的雙向繫結
我們對Vue
的響應式系統
有一定的瞭解,並且知道它是如何實現資料更新檢視
,檢視改變資料的
,那麼有這樣的基礎,我們來手寫一個MVVM
,以便面試的時候,吊打面試官(此為笑談
,不足論,嘿嘿)。
那麼先丟擲一張在座的各位再也熟悉不過的圖:
1、當我們new MVVM
之後有兩步操作,Observer
,Compile
,我們知道Observer
是做資料劫持,Compile
是解析指令,那麼問題來了:
-
Observer
為什麼要做資料劫持? -
Compile
為什麼要做解析指令?
帶著這兩個問題,我們回顧一下往期內容: - 什麼是
資料響應式
? -
資料響應式原理
是什麼? - 資料響應式是如何
實現
的?
參考 Vue面試題詳細解答
資料響應式
就是資料雙向繫結,就是把Model
繫結到View
,當我們用JavaScript
程式碼更新Model
時,View
就會自動更新;如果使用者更新了View
,那麼Model
資料也被自動更新了,這種情況就是雙向繫結。
資料響應式原理
-
Vue
實現資料響應式原理就是通過Object.defineProperty()
這個方法重新定義了物件獲取屬性值get
設定屬性值set
的操作來實現的 -
Vue3.0
中是通過ECMAScript6
中的proxy
物件代理來實現的。
那麼本章節就是來實現資料響應式
的。
那麼回答前面的兩個問題,為什麼要劫持資料
?為什麼要解析指令
?
- 只有劫持到資料,才能對資料做到監聽,以便於資料更改能夠及時做到更新檢視。
-
Vue
中自定義了N
多指令,只有解析它,我們JavaScript
才能認識它,並執行它。
諸如此類問題我們不再複述,下面開始實現資料響應式。
寫一個demo
之前,我們應當整理好思路:
1. 首先實現整體的一個架構(包括MVVM類或者VUE類、Watcher類), /這裡用到一個訂閱釋出者設計模式。
2. 然後實現MVVM中的由M到V,把模型裡面的資料繫結到檢視。
3. 最後實現V-M, 當文字框輸入文字的時候,由文字事件觸發更新模型中的資料
4. 同時也更新相對應的檢視。
//html程式碼
<div id="app">
<h1>MVVM雙向繫結</h1>
<div>
<div v-text="myText"></div>
<div v-text="myBox"></div>
<input type="text" v-model="myText" />
<input type="text" v-model="myBox" />
</div>
</div>
我們建立了兩個div
與input
實現input
框資料關聯,說白了也就是相同的資料來源,那我們的資料來源在哪呢?
//資料來源data
const app = new Vue({
el: "#app",
data: {
myText: "大吉大利!今晚吃雞!",
myBox: "我是一個盒子!",
},
});
可見我們需要一個Vue
類,也就是一個釋出者,那麼直接上程式碼:
//Vue類(釋出者)
class Vue{
}
釋出者有了,我們還需要有訂閱者:
//Watcher類(訂閱者)
class Watcher{
}
可見兩者都有了,那麼我們該怎麼實現呢?
- 獲取data資料
- 獲取元素物件
- 構造一個存放訂閱者的物件
class Vue {
constructor(optios) {
this.$data = optios.data; //獲取資料
this.$el = document.querySelector(optios.el); //獲取元素物件
this._directive = {}; // 存放訂閱者
}
}
那麼我們說了,我們需要劫持資料
,解析指令
,那麼我們得構造兩個方法。
class Vue {
constructor(optios) {
this.$data = optios.data; //獲取資料
this.$el = document.querySelector(optios.el); //獲取元素物件
this._directive = {}; // 存放訂閱者
this.Observer(this.$data);
this.Compile(this.$el);
}
//劫持資料
Observer(data) {
Object.defineProperty(this.$data, key, {
get: function(){},
set: function(){}
},
});
}
//解析指令 //檢視 --- >物件 -- >指令
Compile(el) {
}
}
一個是劫持資料
,一個是解析元素指令
,劫持到的屬性要根據屬性分配容器,噹噹前容器不存在該屬性的時候,我們便需要把他新增到訂閱器物件裡面,等待通知更新。
for (let key in data) {
this._directive[key] = [];
let val =data[key];
let watch = this._directive[key];
}
那麼解析指令,首先必須要遞迴
當前節點,是否還有子節點,是否有v-text
指令,v-model
指令。
let nodes = el.children;
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i];
//遞迴 查詢所有當前物件子類是否再含有子類
if (node.children.length) {
this.Compile(nodes[i]);
}
//判斷是否含有V-text指令
if (node.hasAttribute("v-text")) {
let attrVal = node.getAttribute("v-text");
this._directive[attrVal].push(
new Watcher(node, this, "innerHTML", attrVal)
);
}
//判斷是否含有V-model指令
if (node.hasAttribute("v-model")) {
let attrVal = node.getAttribute("v-model");
this._directive[attrVal].push(
new Watcher(node, this, "value", attrVal)
);
node.addEventListener("input", () => {
//賦值到模型
this.$data[attrVal] = node.value;
// console.log(this.$data);
});
}
}
那麼我們觸發更新時候需要收集依賴,我們直接吧收集到的依賴return
出去
Object.defineProperty(this.$data, key, {
get: function(){
return val;
}
}
那麼我們訂閱者長什麼樣呢?我們訂閱者,接收當前元素資訊,MVVM物件,標識,屬性。並且需要構造一個更新方法update
class Watcher {
constructor(el, vm, exp, attr) {
this.el = el;
this.vm = vm;
this.exp = exp;
this.attr = attr;
this.update();
}
//更新檢視
update() {
this.el[this.exp] = this.vm.$data[this.attr];
//div.innerHTML/value = this.Vue.$data["myText/myBox"]
}
}
到這裡已經快完成了,那麼我們收集了依賴就要去,通知watcher去更新檢視啊,那麼來了:
Object.defineProperty(this.$data, key, {
get: function(){
return val;
},
set: function(newVal){
if(newVal !== val){
val = newVal;
watch.forEach(element => {
element.update();
});
}
},
});
做到這裡,你就可以實現一個數據響應式了。
我們已經掌握了響應式原理,那我們開始著手Vue的另一個核心概念元件系統
了