一篇文章帶你徹底搞懂VUE響應(yīng)式原理
首先上圖,下面這張圖,即為MVVM
響應(yīng)式原理的整個(gè)過(guò)程圖,我們本篇都是圍繞著這張圖進(jìn)行分析,所以這張圖是重中之重。
響應(yīng)式原理圖
一臉懵逼?沒(méi)關(guān)系,接下來(lái)我們將通過(guò)創(chuàng)建一個(gè)簡(jiǎn)單的MVVM
響應(yīng)系統(tǒng)來(lái)一步步了解這個(gè)上圖中的全過(guò)程。全文分為兩大塊,首先介紹實(shí)例模板的編譯過(guò)程,然后詳細(xì)介紹響應(yīng)式,這里先介紹編譯是為了給介紹響應(yīng)式奠定基礎(chǔ)。
編譯
我們把我們創(chuàng)建的這個(gè)微型響應(yīng)系統(tǒng)命名為miniVue
,我們按照平常使用Vue的模式,首先創(chuàng)建一個(gè)miniVue
的實(shí)例。
<scirpt> const vm = new miniVue({ ? ? ? ?el: '#app', ? ? ? ?data: { ? ? ? ? ? ?obj: { ? ? ? ? ? ? ? ?name: "miniVue", ? ? ? ? ? ? ? ?auth: 'xxx' ? ? ? ? ? }, ? ? ? ? ? ?msg: "this is miniVue", ? ? ? ? ? ?htmlStr: "<h3>this is htmlStr</h3>" ? ? ? }, ? ? ? ?methods: { ? ? ? ? ? ?handleClick() { ? ? ? ? ? ? ? ?console.log(this); ? ? ? ? ? } ? ? ? } ? }); </scirpt>
我們根據(jù)這個(gè)實(shí)例,我們可以創(chuàng)建出miniVue
的類,這個(gè)類中我們肯定要保存該實(shí)例所綁定的DOM
以及數(shù)據(jù)對(duì)象data
。然后我們要開(kāi)始解析模板,即解析我們所綁定的DOM
class miniVue { constructor(options) { this.$el = options.el ? ? ? ?this.$data = options.data ? ? ? ?this.$options = options ? } ? ?if(this.$el) { // 解析模板 to Compile ? } }
這里我們來(lái)創(chuàng)建一個(gè)compile
類來(lái)進(jìn)行解析模板的操作
創(chuàng)建compile類
Compile
類是用來(lái)解析模板的,所以肯定要傳入要解析的DOM
。拿到DOM
后直接操作這個(gè)DOM
會(huì)導(dǎo)致頁(yè)面頻繁的回流和重繪,所以我們把這個(gè)DOM
放到一個(gè)文檔碎片中,然后操作這個(gè)文檔碎片。操作這個(gè)文檔碎片的過(guò)程中我們需要獲取到數(shù)據(jù)對(duì)象data
中的屬性來(lái)填充一些節(jié)點(diǎn)的內(nèi)容,所以我們還需要傳入實(shí)例對(duì)象。最后將操作好的文檔碎片追加到原本的DOM
上。
class Compile { ? ?constructor(el, vm) { ? ? ? ?// 判斷的原因是因?yàn)閭魅氲膃l有可能是DOM,也有可能是選擇器例如‘#app' this.el = this.isElementNode(el) ? el : document.querySelector(el) ? ? ? ?this.vm = vm ? ? ? ?// 新建文檔碎片存儲(chǔ)DOM ? ? ? const fragment = this.toFragment(this.el) ? ? ? ?// 操作文檔碎片 to handle fragment ? ? ? ?// 將操作好的文檔碎片追加到原本的DOM上面 ? ? ? ?this.el.appendChild(fragment) ? } ? ?// 判斷是否為元素節(jié)點(diǎn) ? ?isElementNode(node) { ? ? ? ?return node.nodeType === 1 ? } ? ?// dom碎片化 ? ?toFragment(el) { ? ? ? ?const f = document.createDocumentFragment() f.appendChild(el.clone(true)) ? } } // 上面的miniVue實(shí)例相應(yīng)的改為 class miniVue { constructor(options) { this.$el = options.el ? ? ? ?this.$data = options.data ? ? ? ?this.$options = options ? } ? ?if(this.$el) { // 解析模板 to Compile ? ? ? ?new Compile(this.$el, this) // 這里的this就是miniVue實(shí)例 ? } }
操作fragment
操作保存好的文檔碎片,我們可以專門定義一個(gè)函數(shù),然后把文檔碎片通過(guò)參數(shù)傳入進(jìn)來(lái)。
操作文檔碎片我們又可以分為兩步。因?yàn)獒槍?duì)文本節(jié)點(diǎn)和元素節(jié)點(diǎn),我們需要進(jìn)行不同的操作,所以我們?cè)诒闅v所有節(jié)點(diǎn)后的第一步應(yīng)該先判斷它是元素節(jié)點(diǎn)還是文本節(jié)點(diǎn)。
handleFragment(fragment) { ? ?// 獲取文檔碎片的子節(jié)點(diǎn) const childNodes = fragment.childNodes ? ?// 遍歷所有子節(jié)點(diǎn) ? [...childNodes].forEach((child) => { if(this.isElementNode(child)) { ? ? ? ? ? ?// 元素節(jié)點(diǎn) ? ? ? ? ? ?this.compileElement(child) ? ? ? } else { ? ? ? ? ? ?// 文本節(jié)點(diǎn) ? ? ? ? ? ?this.compileText(child) ? ? ? } ? // 遞歸遍歷 ? if(child.childNodes && child.childNodes.length) { ? ? ? ? ? ?handleFragment(child) ? ? ? } ? }) } // 同樣的我們需要完善一下compile的構(gòu)造函數(shù) constructor(el, vm) { this.el = this.isElementNode(el) ? el : document.querySelector(el) ? ?this.vm = vm ? ?// 新建文檔碎片存儲(chǔ)DOM ? ?const fragment = this.toFragment(this.el) ? ?// 操作文檔碎片 to handle fragment ? ?this.handleFragment(fragment) ? ?// 將操作好的文檔碎片追加到原本的DOM上面 ? ?this.el.appendChild(fragment) }
獲取元素節(jié)點(diǎn)上的信息
元素節(jié)點(diǎn)上的信息主要就是這個(gè)元素節(jié)點(diǎn)上面的屬性,然后拿到綁定在節(jié)點(diǎn)上面的vue指令,分離出來(lái)vue指令的名稱和值(注意:@開(kāi)的頭的指令需要額外處理)。然后還有很重要的一步,那就是去掉這些指令(這些指令updater是不認(rèn)的)
compileElement(node) { const attrs = node.attributes ? ?// 遍歷節(jié)點(diǎn)上的全部屬性 ? [...attrs].forEach(({name, value}) => { ? ? ? ?// 分類看指令以什么開(kāi)頭 ? if(this.headWithV(name)) { // 以v開(kāi)頭 ? ? ? ? ? ?const [,directive] = name.split("-") //分離出具體指令 ? ? ? ? ? ?const [dir,event] = directive.split(":") // 考慮v-on的情況 例如v-on:click ? ? ? ? ? ?// 將指令的名稱、值、node節(jié)點(diǎn)、整個(gè)vm實(shí)例、事件名(如果有的話)一起傳給最后真正操作的node的函數(shù) ? ? ? ? ? ?handleNode[dir](node, value, this.vm, event) ? ? ? }else if(this.headWithoutV(name)) { // 以@開(kāi)投 ? ? ? ? ? ?const [, event] = name.split("@") ? ? ? ? ? ?// 和上面一樣,但是指令名字是確定的,為“on” 因?yàn)锧是v-on的語(yǔ)法糖 ? ? ? ? ? ?handleNode["on"](node, value, this.vm, event) ? ? ? } ? }) } ? headWithV(name) { return name.startsWith("v-"); } headWithoutV(name){ ? ?return name.startsWith("@"); }
獲取文本節(jié)點(diǎn)信息
文本節(jié)點(diǎn)和元素節(jié)點(diǎn)類似,只不過(guò)文本節(jié)點(diǎn)的信息存儲(chǔ)在節(jié)點(diǎn)的textContent
里面,主要用來(lái)替換mustache
語(yǔ)法,(雙大括號(hào)插值)需要通過(guò)正則識(shí)別額外處理。如果是正常的文本節(jié)點(diǎn),則不進(jìn)行處理(原模原樣展示即可)。
compileText(node) { ? ?const content = node.textContent ? ?if(!/{{(.+?)}}/.test(content)) return ? ?// 識(shí)別到是mustache語(yǔ)法 處理方法其實(shí)和v-text一樣 ? ?handleNode["text"](node, content,this.vm) }
操作fragment
前面鋪墊了這么多,終于到了操作文檔碎片這一步了。按照上面的思路,handleNode
應(yīng)該是一個(gè)對(duì)象,里面有多個(gè)屬性對(duì)應(yīng)不同的指令的處理方法。
// node--操作的node節(jié)點(diǎn) exp--指令的值(或者是mustache語(yǔ)法內(nèi)部插入的內(nèi)容) vm--vm實(shí)例 event--事件名稱 const handleNode = { ? ?// v-html ? ?html(node, exp, vm) { ? // 去vm實(shí)例中找到這個(gè)表達(dá)式所對(duì)應(yīng)的值 ? ? ? ?const value = this._get(vm, exp) ? ? ? ?// 更新node ? ? ? ?updater.htmlUpdater(node, value) }, ? ?// v-model ? ?model(node, exp, vm) { ? ? ? ?// 同html ? const value = this._get(vm, exp) ? ? ? ?updater.modelUpdater(node, value) }, ? ?// v-on ? ?on(node, exp, vm, event) { // v-on特殊一點(diǎn),我們需要為該node綁定事件監(jiān)聽(tīng)器 ? ? ? ?const listener = vm.$options.methods && vm.$options.methods[exp] // 獲取監(jiān)聽(tīng)器的回調(diào) ? ? ? ?// 綁定監(jiān)聽(tīng)器,注意回調(diào)綁定使用bind把this指向vm實(shí)例,false代表事件冒泡時(shí)觸發(fā)監(jiān)聽(tīng)器 ? ? ? ?node.addEventListener(event, listener.bind(this), false) }, ? ?// v-text ? ?text(node, exp, vm) { ? ? ? ?// v-text是最復(fù)雜的,需要考慮兩種情況,一種是通過(guò)v-text指令操作node,另一種則是通過(guò)mustache語(yǔ)法操作node,需分類 ? ? ? ?let value ? ? ? ?if(exp.indexOf("{{") !== -1) { ? ? ? ? ? ?// mustache語(yǔ)法操作node ? ? ? ? ? ?// 捕捉到所有的mustache語(yǔ)法,將其整個(gè)替換為vm實(shí)例中屬性對(duì)應(yīng)的值 ? ? ? ? ? ?// 拿我們最初初始化實(shí)例的一個(gè)數(shù)據(jù)舉例:{{obj.auth}} -- 'xxx' ? ? ? ? ? ?value = exp.replace(/{{(.+?)}}/g, this._get(vm, exp)) ? ? ? }else { ? ? ? ? ? ?// v-text操作node ? ? ? ? ? ?value = this._get(vm, exp) ? ? ? } ? ? ? ?// 更新node ? ? ? ?updater.textUpdater(node, value); ? }, } ? // 根據(jù)表達(dá)式去數(shù)據(jù)對(duì)象里面獲取值 _get(vm, exp) { ? ?const segments = exp.split('.') ? ?// 這里使用reduce是為了獲取嵌套對(duì)象內(nèi)部屬性的值,不熟悉的話去補(bǔ)一補(bǔ)reduce ? ?// 比如data.a.b.c,那么每次遍歷的值為data[a],data[a][b],最終結(jié)果是data[a][b][c] segments.reduce((pre, key) => { return pre[key] ? }, vm.$data) } // 更新node (終于到了更新node這一步) const updater = { ? ?textUpdater(node, value) { node.textContent = value; ? }, ? ?htmlUpdater(node, value) { node.innerHTML = value; ? }, ? ?modelUpdater(node, value){ node.value = value; ? } }
至此我們已經(jīng)實(shí)現(xiàn)了vue實(shí)例模板編譯,并更新了node,其實(shí)到現(xiàn)在我們還沒(méi)有涉及到響應(yīng)式這三個(gè)字。下面我們開(kāi)始介紹本篇的核心,即vue是如何實(shí)現(xiàn)響應(yīng)式的。
響應(yīng)式
數(shù)據(jù)劫持
關(guān)鍵點(diǎn):Object.defineProperty(具體用法參考MDN)
主要目的:為data中每個(gè)屬性添加getter
和setter
,然后在getter
和setter
中進(jìn)行數(shù)據(jù)劫持
思路很簡(jiǎn)單,其實(shí)就是從最外層的data層開(kāi)始遍歷屬性,通過(guò)Object.defineProperty
給這些屬性都添加上getter
和setter
,需要注意對(duì)象的嵌套,所以需要使用遞歸來(lái)為嵌套的屬性添加getter
和setter
function observe(data) { if(typeof data !== 'object') return ? ?Object.keys(data).forEach((key) => { ? ? ? ?defineReactive(data, key, data[key]) ? }) } function defineReactive(data, key, value) { ? ?// 遞歸子屬性 observe(value) ? ?Object.defineProperty(data, key, { get() { ? ? ? ? ? ?// 數(shù)據(jù)劫持 在這個(gè)地方進(jìn)行相關(guān)操作 return value ? ? ? } ? ? ? ?set(newVal) { if(newVal == value) return ? ? ? value = newVal ? ? ? // 為新數(shù)據(jù)添加getter和setter ? ? ? observe(newVal) ? ? ? // 數(shù)據(jù)劫持 在這個(gè)地方進(jìn)行相關(guān)操作 ? } ? }) }
收集依賴
依賴其實(shí)說(shuō)白了,就是數(shù)據(jù)的依賴,data中的某個(gè)屬性,可能在DOM
中好幾個(gè)地方進(jìn)行了使用,那DOM
中使用到該屬性的地方就都會(huì)產(chǎn)生一個(gè)對(duì)于該屬性的依賴,也就是watcher
。當(dāng)該屬性的值發(fā)生了變化,那么就可以通知watcher
來(lái)使得頁(yè)面中使用到這個(gè)屬性的地方進(jìn)行視圖更新。為每個(gè)屬性綁定watcher
的過(guò)程其實(shí)就是訂閱,反過(guò)來(lái),當(dāng)屬性的值發(fā)生了變化,通知所有watcher
的過(guò)程就是發(fā)布。
下面我們來(lái)將依賴抽象化,即實(shí)現(xiàn)watcher
class Watcher { // data--最外層數(shù)據(jù)對(duì)象 exp--表達(dá)式 cb--數(shù)據(jù)更新后需要執(zhí)行的回調(diào) // 通過(guò)data和exp可以獲取watcher所依賴屬性的具體值 constructor(data, exp, cb) { this.data = data this.exp = exp this.cb = cb // 每次初始化watcher實(shí)例時(shí),對(duì)依賴屬性進(jìn)行訂閱 this.value = this.subscribe() } // 訂閱 subscribe() { // 獲取依賴屬性的值 const value = _get(this.data, this.exp) return value } // 更新 update() { // 獲取新值 this.value = _get(this.data, this.exp) cb() } } // 根據(jù)表達(dá)式去數(shù)據(jù)對(duì)象里面獲取值 其實(shí)上面已經(jīng)定義過(guò)一個(gè)了,功能是一樣的,這里重復(fù)定義加深一下印象,也方便閱讀 function _get(obj, exp) { const segments = exp.split('.') // 這里使用reduce是為了獲取嵌套對(duì)象內(nèi)部屬性的值,不熟悉的話去補(bǔ)一補(bǔ)reduce // 比如data.a.b.c,那么每次遍歷的值為data[a],data[a][b],最終結(jié)果是data[a][b][c] segments.reduce((pre, key) => { return pre[key] }, obj) }
依賴我們大概清楚了,但是我們上面講,需要把一個(gè)屬性全部的依賴(watcher)收集起來(lái),所以我們?cè)撊绾?strong>收集依賴呢?
首先我們先想第一個(gè)問(wèn)題,一個(gè)屬性會(huì)有一個(gè)或者好多個(gè)watcher
,我們應(yīng)該如何保存這些watcher
呢,這個(gè)我們很容易想到,我們可以專門拿一個(gè)數(shù)組保存一個(gè)屬性的全部watcher
,我們把這個(gè)數(shù)組命名為dep
(dependency)。
第二個(gè)問(wèn)題,我們應(yīng)該什么時(shí)候進(jìn)行收集watcher
的操作呢。還記得我們上面提到的訂閱嗎,我們每次初始化watcher
時(shí),會(huì)為該watcher
訂閱屬性,訂閱的過(guò)程中我們會(huì)首先獲取這個(gè)屬性的值,這時(shí)就可以發(fā)揮數(shù)據(jù)劫持的作用了,獲取這個(gè)屬性值的時(shí)候,我們就會(huì)進(jìn)到這個(gè)屬性的getter
方法中,所以我們可以在這個(gè)時(shí)候完成收集watcher
的操作。
第三個(gè)問(wèn)題,我們說(shuō)watcher
的作用其實(shí)就是監(jiān)聽(tīng)到訂閱屬性的變化(即監(jiān)聽(tīng)發(fā)布),監(jiān)聽(tīng)到變化后執(zhí)行其update
方法,即執(zhí)行更新回調(diào),來(lái)更新視圖。那么我們?cè)鯓硬拍茏?code>watcher監(jiān)聽(tīng)到“發(fā)布”呢,這時(shí)我們又需要用到數(shù)據(jù)劫持,即在setter
中通知這個(gè)屬性所有的watcher
。
function defineReactive(data, key, value) { // 新建用于存儲(chǔ)watcher的數(shù)據(jù) const dep = [] // 遞歸子屬性 observe(value) Object.defineProperty(data, key, { get() { // 數(shù)據(jù)劫持 在這個(gè)地方進(jìn)行相關(guān)操作 dep.push(watcher) // 收集依賴 return value } set(newVal) { if(newVal == value) return value = newVal // 為新數(shù)據(jù)添加getter和setter observe(newVal) // 數(shù)據(jù)劫持 在這個(gè)地方進(jìn)行相關(guān)操作 dep.notify() // 通知依賴 } }) }
現(xiàn)在我覺(jué)得我有必要理一下這個(gè)依賴收集的全過(guò)程。首先頁(yè)面初次渲染的時(shí)候,會(huì)遇到我們?cè)赿ata中定義的屬性(注意:此時(shí)屬性上面已經(jīng)定義好getter和setter了),遇到屬性后會(huì)初始化一個(gè)watcher
實(shí)例,在此過(guò)程中watcher
實(shí)例會(huì)獲取這個(gè)屬性的值,于是會(huì)進(jìn)入到這個(gè)屬性的getter
中,于是我們通過(guò)數(shù)據(jù)劫持來(lái)收集這個(gè)watcher
。那么又出現(xiàn)了一個(gè)問(wèn)題,我們此時(shí)在getter
中,如何獲取到初始化的watcher
實(shí)例呢,也就是dep.push
的時(shí)候,其實(shí)我們是沒(méi)有辦法直接拿到這個(gè)watcher
的。因此,我們需要在初始化watcher
的時(shí)候,把這個(gè)watcher
放到全局,比如window.target
。
subscribe() { // 獲取依賴屬性的值 window.target = this // 這里的this即為此時(shí)初始化的watcher實(shí)例 const value = _get(this.data, this.exp) return value } function defineReactive(data, key, value) { // 新建用于存儲(chǔ)watcher的數(shù)據(jù) const dep = [] observe(value) Object.defineProperty(data, key, { get() { dep.push(window.target) // 改為window.target return value } set(newVal) { if(newVal == value) return value = newVal observe(newVal) dep.notify() } }) }
響應(yīng)式代碼完善
Dep類
我們可以講dep數(shù)組抽象為一個(gè)類
class Dep { constructor() { this.subs = [] } // 收集依賴 addSub(watcher) { this.subs.push(watcher) } // 通知依賴 notify() { [...this.subs].forEach((watcher) => { watcher.update() }) } }
defineReactive
也要做出相應(yīng)的調(diào)整
function defineReactive(data, key, value) { // 新建用于存儲(chǔ)watcher的數(shù)據(jù) const dep = new Dep() observe(value) Object.defineProperty(data, key, { get() { // 收集依賴 dep.addSub(window.target) return value } set(newVal) { if(newVal == value) return value = newVal observe(newVal) // 通知依賴 dep.notify() } }) }
全局watcher用完清空
下面有一個(gè)場(chǎng)景,我們?cè)谠L問(wèn)到data中的一個(gè)屬性a
后,實(shí)例化了一個(gè)watcher1
,此時(shí)在實(shí)例化這個(gè)watcher1
的過(guò)程中,會(huì)把window.target
設(shè)置為watcher1
,之后我們?cè)跊](méi)有實(shí)例化其他watcher
的情況下直接去訪問(wèn)其他的屬性,例如屬性b
,那么屬性b
中的getter
會(huì)直接把watcher1
推入到它的依賴數(shù)組中。這樣是不合理的,所以我們每次將watcher
推入到依賴數(shù)組中后,要將這個(gè)watcher
從全局中收回。(window.target這里改成Dep.target了,其實(shí)都是一樣的)
subscribe() { Dep.target = this // 這里的this即為此時(shí)初始化的watcher實(shí)例 const value = _get(this.data, this.exp) Dep.target = null // 清空暴露在全局中的watcher return value } // 同時(shí)在收集依賴時(shí)添加一層過(guò)濾 addSub(watcher) { if(watcher) { this.subs.push(watcher) } }
依賴的update方法
上面我們?cè)?code>watcher的update方法中更新了值并且執(zhí)行了數(shù)據(jù)更新后的回調(diào),為了讓豐富回調(diào)中的操作,我們可以將回調(diào)的this
指向我們的最外層數(shù)據(jù)對(duì)象,這樣在回調(diào)中就可以通過(guò)this任意獲取數(shù)據(jù)對(duì)象中的其他屬性,并且將更新之前的舊值和新值一起傳入到update里面
update() { const oldValue = this.value // 獲取舊值 this.value = parsePath(this.data, this.expression) // 獲取新值 this.cb.call(this.data, this.value, oldValue) }
需要注意的一個(gè)地方
下面是watcher
中獲取所依賴屬性值的方法,這里需要說(shuō)明一下,對(duì)于存在對(duì)象嵌套的情況,每一層屬性的依賴數(shù)組中都會(huì)添加這個(gè)watcher,想不明白的話可以看一下下面的注解。
// 根據(jù)表達(dá)式去數(shù)據(jù)對(duì)象里面獲取值 function _get(obj, exp) { const segments = exp.split('.') /* 比如data.a.b.c,那么每次遍歷的值為data[a],data[a][b],最終結(jié)果是data[a][b][c] 遍歷到data[a]、data[a][b]時(shí),肯定會(huì)去訪問(wèn)這兩個(gè)屬性的值,于是會(huì)進(jìn)入到這兩個(gè)屬性的getter里面 所以這個(gè)watcher不僅僅會(huì)被添加到最內(nèi)層屬性的getter中,中間每一層屬性的getter中都會(huì)有這個(gè)watcher 即如果data[a]的值發(fā)生了變化,也會(huì)通知這個(gè)watcher去更新視圖 */ segments.reduce((pre, key) => { return pre[key] }, obj) }
雙劍合璧
怎樣將上面的編譯和響應(yīng)式整合到一起形成一個(gè)完整的具有響應(yīng)式的miniVue
類呢。其實(shí)很簡(jiǎn)單,從我們最上面那張圖就可以看出來(lái)??偨Y(jié)一下就兩點(diǎn),在我們通過(guò)各種指令操作node節(jié)點(diǎn)的時(shí)候,同時(shí)初始化watcher,另一點(diǎn)即為初始化watcher時(shí)指定的回調(diào)內(nèi)部需要執(zhí)行updater里面對(duì)應(yīng)的方法來(lái)更新視圖
兩點(diǎn)分別對(duì)應(yīng)下圖的這兩根線:
這樣是不是就清晰多了。至此”雙劍合璧“完成,下面貼一下合璧后的代碼(只放需要合成的部分,這樣更清晰一點(diǎn))
// node--操作的node節(jié)點(diǎn) exp--指令的值(或者是mustache語(yǔ)法內(nèi)部插入的內(nèi)容) vm--vm實(shí)例 event--事件名稱 const handleNode = { ? ?// v-html ? ?html(node, exp, vm) { ? ? ? ?const value = this._get(vm, exp) ? ? ? ?// 新建watcher實(shí)例,并綁定更新回調(diào) ? ? ? ?new Watcher(vm, exp, (newVal, oldVal) => { ? ? ? ? ? ?// 這里是所依賴數(shù)據(jù)更新以后更新視圖 ? ? ? this.updater.htmlUpdater(node, newVal); ? }) ? ? ? ?// 這里是編譯的時(shí)候更新視圖 ? ? ? ?updater.htmlUpdater(node, value) }, ? ?// v-model ? ?model(node, exp, vm) { ? const value = this._get(vm, exp) ? ? ? ?// 新建watcher實(shí)例,并綁定更新回調(diào) ? ? ? ?new Watcher(vm, exp, (newVal, oldVal) => { ? ? this.updater.modelUpdater(node, newVal); ? }); ? ? ? ?updater.modelUpdater(node, value) }, ? ?// v-on ? ?on(node, exp, vm, event) { ? ? ? ?// watcher只針對(duì)屬性 v-on這里不會(huì)生成watcher(方法名也沒(méi)什么好監(jiān)聽(tīng)的,一般也不會(huì)操作方法名讓方法名發(fā)生變化) ? ? ? ?const listener = vm.$options.methods && vm.$options.methods[exp] ? ? ? ?node.addEventListener(event, listener.bind(this), false) }, ? ?// v-text ? ?text(node, exp, vm) { ? ? ? ?let value ? ? ? ?if(exp.indexOf("{{") !== -1) { ? ? ? ? ? ?// mustache語(yǔ)法操作node ? ? ? ? ? ?value = exp.replace(/{{(.+?)}}/g, this._get(vm, exp)) ? ? ? ? ? ? ? ? ? }else { ? ? ? ? ? ?// v-text操作node ? ? ? ? ? ?value = this._get(vm, exp) ? ? ? } ? ? ? ?// 新建watcher實(shí)例,并綁定更新回調(diào) ? ? ? ?new Watcher(vm, exp, (newVal, oldVal) => { ? ? this.updater.textUpdater(node, newVal); ? }); ? ? ? ?updater.textUpdater(node, value); ? }, } _get(vm, exp) { ? ?const segments = exp.split('.') segments.reduce((pre, key) => { return pre[key] ? }, vm.$data) } ? const updater = { ? ?textUpdater(node, value) { node.textContent = value; ? }, ? ?htmlUpdater(node, value) { node.innerHTML = value; ? }, ? ?modelUpdater(node, value){ node.value = value; ? } }
最后的最后,修改一下我們最開(kāi)始定義miniVue
類的構(gòu)造函數(shù)
class miniVue { constructor(options) { this.$el = options.el ? ? ? ?this.$data = options.data ? ? ? ?this.$options = options ? } ? ?if(this.$el) { ? ? ? ?// 添加數(shù)據(jù)劫持 this.observe() ? ? ? ?// 編譯 ? ? ? ?new Compile(this.$el, this); ? } }
大功告成。
總結(jié)
如果你是第一次閱讀本文,看到最后應(yīng)該還是會(huì)感覺(jué)到些許混亂。下面允許我為大家概括一下整體的流程。建議結(jié)合我們最上方的中心圖。
- 1.初始化
minivue
實(shí)例 執(zhí)行其構(gòu)造函數(shù),首先對(duì)實(shí)例的數(shù)據(jù)對(duì)象data中全部屬性添加數(shù)據(jù)劫持功能(getter
andsetter
) - 2.開(kāi)始編譯實(shí)例綁定的模板。
- 3.首先編譯做準(zhǔn)備,創(chuàng)建compile類,拿到模板的整個(gè)
DOM
對(duì)象,遍歷其子節(jié)點(diǎn),獲取到每個(gè)子節(jié)點(diǎn)上的信息,這些信息中凡是引用過(guò)vm實(shí)例data中的屬性的,一律都新增一個(gè)watcher
實(shí)例 - 4.初始化
watcher
實(shí)例的時(shí)候,會(huì)訪問(wèn)這個(gè)屬性,然后進(jìn)入這個(gè)屬性的getter
中,在getter
中,將這個(gè)watcher
添加到這個(gè)屬性的Dep
類中 - 5.最后更新
node
,至此初始化編譯完成 - 6.當(dāng)data中某一個(gè)屬性的值發(fā)生變化,會(huì)進(jìn)入這個(gè)屬性的
setter
中,setter
會(huì)通知該屬性的Dep
類 - 7.
Dep
類會(huì)通知存儲(chǔ)的所有相關(guān)watcher
進(jìn)行更新,于是這些watcher
分別執(zhí)行自己update
中的回調(diào)?;卣{(diào)即會(huì)更新node
。
到此這篇關(guān)于一篇文章帶你徹底搞懂VUE響應(yīng)式原理的文章就介紹到這了,更多相關(guān) VUE響應(yīng)式原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何使用electron將vue項(xiàng)目打包成.exe文件(保姆級(jí)教程)
本文給大家介紹如何使用electron將vue項(xiàng)目打包成.exe文件,大家要注意一下vue2項(xiàng)目,使用的vue-element-admin框架,用electron打包成.exe文件,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2024-03-03Vue3使用vant檢索組件van-search遇到的問(wèn)題小結(jié)
當(dāng)清空按鈕與檢索按鈕同時(shí)居右時(shí),點(diǎn)擊clear清空按鈕事件時(shí)會(huì)同時(shí)觸發(fā)click-right-icon事件,這個(gè)時(shí)候容易觸發(fā)一系列問(wèn)題,小編小編給大家分享Vue3使用vant檢索組件van-search遇到的問(wèn)題小結(jié),感興趣的朋友一起看看吧2024-02-02Vue無(wú)法訪問(wèn).env.development定義的變量值問(wèn)題及解決
這篇文章主要介紹了Vue無(wú)法訪問(wèn).env.development定義的變量值問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01vue與iframe頁(yè)面數(shù)據(jù)互相通信的實(shí)現(xiàn)示例
這篇文章主要給大家介紹了vue與iframe頁(yè)面數(shù)據(jù)互相通信的實(shí)現(xiàn)示例,文中通過(guò)代碼示例給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-12-12Vue + better-scroll 實(shí)現(xiàn)移動(dòng)端字母索引導(dǎo)航功能
better-scroll 是一款重點(diǎn)解決移動(dòng)端(已支持 PC)各種滾動(dòng)場(chǎng)景需求的插件。這篇文章主要介紹了Vue + better-scroll 實(shí)現(xiàn)移動(dòng)端字母索引導(dǎo)航功能,需要的朋友可以參考下2018-05-05vue使用自定義指令來(lái)控制頁(yè)面按鈕組的權(quán)限思想
這篇文章主要介紹了vue使用自定義指令來(lái)控制頁(yè)面按鈕組的權(quán)限思想,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08vue-router鉤子函數(shù)實(shí)現(xiàn)路由守衛(wèi)
這篇文章主要介紹了vue-router鉤子函數(shù)實(shí)現(xiàn)路由守衛(wèi),對(duì)vue感興趣的同學(xué),可以參考下2021-04-04