一篇文章帶你徹底搞懂VUE響應(yīng)式原理
首先上圖,下面這張圖,即為MVVM響應(yīng)式原理的整個(gè)過程圖,我們本篇都是圍繞著這張圖進(jìn)行分析,所以這張圖是重中之重。
響應(yīng)式原理圖

一臉懵逼?沒關(guān)系,接下來我們將通過創(chuàng)建一個(gè)簡單的MVVM響應(yīng)系統(tǒng)來一步步了解這個(gè)上圖中的全過程。全文分為兩大塊,首先介紹實(shí)例模板的編譯過程,然后詳細(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ù)對象data。然后我們要開始解析模板,即解析我們所綁定的DOM
class miniVue {
constructor(options) {
this.$el = options.el
? ? ? ?this.$data = options.data
? ? ? ?this.$options = options
? }
? ?if(this.$el) {
// 解析模板 to Compile
? }
}
這里我們來創(chuàng)建一個(gè)compile類來進(jìn)行解析模板的操作
創(chuàng)建compile類
Compile類是用來解析模板的,所以肯定要傳入要解析的DOM。拿到DOM后直接操作這個(gè)DOM會導(dǎo)致頁面頻繁的回流和重繪,所以我們把這個(gè)DOM放到一個(gè)文檔碎片中,然后操作這個(gè)文檔碎片。操作這個(gè)文檔碎片的過程中我們需要獲取到數(shù)據(jù)對象data中的屬性來填充一些節(jié)點(diǎn)的內(nèi)容,所以我們還需要傳入實(shí)例對象。最后將操作好的文檔碎片追加到原本的DOM上。
class Compile {
? ?constructor(el, vm) {
? ? ? ?// 判斷的原因是因?yàn)閭魅氲膃l有可能是DOM,也有可能是選擇器例如‘#app'
this.el = this.isElementNode(el) ? el : document.querySelector(el)
? ? ? ?this.vm = vm
? ? ? ?// 新建文檔碎片存儲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ù),然后把文檔碎片通過參數(shù)傳入進(jìn)來。
操作文檔碎片我們又可以分為兩步。因?yàn)獒槍?strong>文本節(jié)點(diǎn)和元素節(jié)點(diǎn),我們需要進(jìn)行不同的操作,所以我們在遍歷所有節(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
? ?// 新建文檔碎片存儲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指令,分離出來vue指令的名稱和值(注意:@開的頭的指令需要額外處理)。然后還有很重要的一步,那就是去掉這些指令(這些指令updater是不認(rèn)的)
compileElement(node) {
const attrs = node.attributes
? ?// 遍歷節(jié)點(diǎn)上的全部屬性
? [...attrs].forEach(({name, value}) => {
? ? ? ?// 分類看指令以什么開頭
? if(this.headWithV(name)) {
// 以v開頭
? ? ? ? ? ?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)) {
// 以@開投
? ? ? ? ? ?const [, event] = name.split("@")
? ? ? ? ? ?// 和上面一樣,但是指令名字是確定的,為“on” 因?yàn)锧是v-on的語法糖
? ? ? ? ? ?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)類似,只不過文本節(jié)點(diǎn)的信息存儲在節(jié)點(diǎn)的textContent里面,主要用來替換mustache語法,(雙大括號插值)需要通過正則識別額外處理。如果是正常的文本節(jié)點(diǎn),則不進(jìn)行處理(原模原樣展示即可)。
compileText(node) {
? ?const content = node.textContent
? ?if(!/{{(.+?)}}/.test(content)) return
? ?// 識別到是mustache語法 處理方法其實(shí)和v-text一樣
? ?handleNode["text"](node, content,this.vm)
}
操作fragment
前面鋪墊了這么多,終于到了操作文檔碎片這一步了。按照上面的思路,handleNode應(yīng)該是一個(gè)對象,里面有多個(gè)屬性對應(yīng)不同的指令的處理方法。
// node--操作的node節(jié)點(diǎn) exp--指令的值(或者是mustache語法內(nèi)部插入的內(nèi)容) vm--vm實(shí)例 event--事件名稱
const handleNode = {
? ?// v-html
? ?html(node, exp, vm) {
? // 去vm實(shí)例中找到這個(gè)表達(dá)式所對應(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)聽器
? ? ? ?const listener = vm.$options.methods && vm.$options.methods[exp] // 獲取監(jiān)聽器的回調(diào)
? ? ? ?// 綁定監(jiān)聽器,注意回調(diào)綁定使用bind把this指向vm實(shí)例,false代表事件冒泡時(shí)觸發(fā)監(jiān)聽器
? ? ? ?node.addEventListener(event, listener.bind(this), false)
},
? ?// v-text
? ?text(node, exp, vm) {
? ? ? ?// v-text是最復(fù)雜的,需要考慮兩種情況,一種是通過v-text指令操作node,另一種則是通過mustache語法操作node,需分類
? ? ? ?let value
? ? ? ?if(exp.indexOf("{{") !== -1) {
? ? ? ? ? ?// mustache語法操作node
? ? ? ? ? ?// 捕捉到所有的mustache語法,將其整個(gè)替換為vm實(shí)例中屬性對應(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ù)對象里面獲取值
_get(vm, exp) {
? ?const segments = exp.split('.')
? ?// 這里使用reduce是為了獲取嵌套對象內(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)在我們還沒有涉及到響應(yīng)式這三個(gè)字。下面我們開始介紹本篇的核心,即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ù)劫持
思路很簡單,其實(shí)就是從最外層的data層開始遍歷屬性,通過Object.defineProperty給這些屬性都添加上getter和setter,需要注意對象的嵌套,所以需要使用遞歸來為嵌套的屬性添加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í)說白了,就是數(shù)據(jù)的依賴,data中的某個(gè)屬性,可能在DOM中好幾個(gè)地方進(jìn)行了使用,那DOM中使用到該屬性的地方就都會產(chǎn)生一個(gè)對于該屬性的依賴,也就是watcher。當(dāng)該屬性的值發(fā)生了變化,那么就可以通知watcher來使得頁面中使用到這個(gè)屬性的地方進(jìn)行視圖更新。為每個(gè)屬性綁定watcher的過程其實(shí)就是訂閱,反過來,當(dāng)屬性的值發(fā)生了變化,通知所有watcher的過程就是發(fā)布。
下面我們來將依賴抽象化,即實(shí)現(xiàn)watcher
class Watcher {
// data--最外層數(shù)據(jù)對象 exp--表達(dá)式 cb--數(shù)據(jù)更新后需要執(zhí)行的回調(diào)
// 通過data和exp可以獲取watcher所依賴屬性的具體值
constructor(data, exp, cb) {
this.data = data
this.exp = exp
this.cb = cb
// 每次初始化watcher實(shí)例時(shí),對依賴屬性進(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ù)對象里面獲取值 其實(shí)上面已經(jīng)定義過一個(gè)了,功能是一樣的,這里重復(fù)定義加深一下印象,也方便閱讀
function _get(obj, exp) {
const segments = exp.split('.')
// 這里使用reduce是為了獲取嵌套對象內(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)收集起來,所以我們該如何收集依賴呢?
首先我們先想第一個(gè)問題,一個(gè)屬性會有一個(gè)或者好多個(gè)watcher,我們應(yīng)該如何保存這些watcher呢,這個(gè)我們很容易想到,我們可以專門拿一個(gè)數(shù)組保存一個(gè)屬性的全部watcher,我們把這個(gè)數(shù)組命名為dep(dependency)。
第二個(gè)問題,我們應(yīng)該什么時(shí)候進(jìn)行收集watcher的操作呢。還記得我們上面提到的訂閱嗎,我們每次初始化watcher時(shí),會為該watcher訂閱屬性,訂閱的過程中我們會首先獲取這個(gè)屬性的值,這時(shí)就可以發(fā)揮數(shù)據(jù)劫持的作用了,獲取這個(gè)屬性值的時(shí)候,我們就會進(jìn)到這個(gè)屬性的getter方法中,所以我們可以在這個(gè)時(shí)候完成收集watcher的操作。
第三個(gè)問題,我們說watcher的作用其實(shí)就是監(jiān)聽到訂閱屬性的變化(即監(jiān)聽發(fā)布),監(jiān)聽到變化后執(zhí)行其update方法,即執(zhí)行更新回調(diào),來更新視圖。那么我們怎樣才能讓watcher監(jiān)聽到“發(fā)布”呢,這時(shí)我們又需要用到數(shù)據(jù)劫持,即在setter中通知這個(gè)屬性所有的watcher。
function defineReactive(data, key, value) {
// 新建用于存儲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)在我覺得我有必要理一下這個(gè)依賴收集的全過程。首先頁面初次渲染的時(shí)候,會遇到我們在data中定義的屬性(注意:此時(shí)屬性上面已經(jīng)定義好getter和setter了),遇到屬性后會初始化一個(gè)watcher實(shí)例,在此過程中watcher實(shí)例會獲取這個(gè)屬性的值,于是會進(jìn)入到這個(gè)屬性的getter中,于是我們通過數(shù)據(jù)劫持來收集這個(gè)watcher。那么又出現(xiàn)了一個(gè)問題,我們此時(shí)在getter中,如何獲取到初始化的watcher實(shí)例呢,也就是dep.push的時(shí)候,其實(shí)我們是沒有辦法直接拿到這個(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) {
// 新建用于存儲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) {
// 新建用于存儲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è)場景,我們在訪問到data中的一個(gè)屬性a后,實(shí)例化了一個(gè)watcher1,此時(shí)在實(shí)例化這個(gè)watcher1的過程中,會把window.target設(shè)置為watcher1,之后我們在沒有實(shí)例化其他watcher的情況下直接去訪問其他的屬性,例如屬性b,那么屬性b中的getter會直接把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í)添加一層過濾
addSub(watcher) {
if(watcher) {
this.subs.push(watcher)
}
}依賴的update方法
上面我們在watcher的update方法中更新了值并且執(zhí)行了數(shù)據(jù)更新后的回調(diào),為了讓豐富回調(diào)中的操作,我們可以將回調(diào)的this指向我們的最外層數(shù)據(jù)對象,這樣在回調(diào)中就可以通過this任意獲取數(shù)據(jù)對象中的其他屬性,并且將更新之前的舊值和新值一起傳入到update里面
update() {
const oldValue = this.value // 獲取舊值
this.value = parsePath(this.data, this.expression) // 獲取新值
this.cb.call(this.data, this.value, oldValue)
}
需要注意的一個(gè)地方
下面是watcher中獲取所依賴屬性值的方法,這里需要說明一下,對于存在對象嵌套的情況,每一層屬性的依賴數(shù)組中都會添加這個(gè)watcher,想不明白的話可以看一下下面的注解。
// 根據(jù)表達(dá)式去數(shù)據(jù)對象里面獲取值
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í),肯定會去訪問這兩個(gè)屬性的值,于是會進(jìn)入到這兩個(gè)屬性的getter里面
所以這個(gè)watcher不僅僅會被添加到最內(nèi)層屬性的getter中,中間每一層屬性的getter中都會有這個(gè)watcher
即如果data[a]的值發(fā)生了變化,也會通知這個(gè)watcher去更新視圖
*/
segments.reduce((pre, key) => {
return pre[key]
}, obj)
}雙劍合璧
怎樣將上面的編譯和響應(yīng)式整合到一起形成一個(gè)完整的具有響應(yīng)式的miniVue類呢。其實(shí)很簡單,從我們最上面那張圖就可以看出來??偨Y(jié)一下就兩點(diǎn),在我們通過各種指令操作node節(jié)點(diǎn)的時(shí)候,同時(shí)初始化watcher,另一點(diǎn)即為初始化watcher時(shí)指定的回調(diào)內(nèi)部需要執(zhí)行updater里面對應(yīng)的方法來更新視圖
兩點(diǎn)分別對應(yīng)下圖的這兩根線:

這樣是不是就清晰多了。至此”雙劍合璧“完成,下面貼一下合璧后的代碼(只放需要合成的部分,這樣更清晰一點(diǎn))
// node--操作的node節(jié)點(diǎn) exp--指令的值(或者是mustache語法內(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只針對屬性 v-on這里不會生成watcher(方法名也沒什么好監(jiān)聽的,一般也不會操作方法名讓方法名發(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語法操作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;
? }
}最后的最后,修改一下我們最開始定義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)該還是會感覺到些許混亂。下面允許我為大家概括一下整體的流程。建議結(jié)合我們最上方的中心圖。
- 1.初始化
minivue實(shí)例 執(zhí)行其構(gòu)造函數(shù),首先對實(shí)例的數(shù)據(jù)對象data中全部屬性添加數(shù)據(jù)劫持功能(getterandsetter) - 2.開始編譯實(shí)例綁定的模板。
- 3.首先編譯做準(zhǔn)備,創(chuàng)建compile類,拿到模板的整個(gè)
DOM對象,遍歷其子節(jié)點(diǎn),獲取到每個(gè)子節(jié)點(diǎn)上的信息,這些信息中凡是引用過vm實(shí)例data中的屬性的,一律都新增一個(gè)watcher實(shí)例 - 4.初始化
watcher實(shí)例的時(shí)候,會訪問這個(gè)屬性,然后進(jìn)入這個(gè)屬性的getter中,在getter中,將這個(gè)watcher添加到這個(gè)屬性的Dep類中 - 5.最后更新
node,至此初始化編譯完成 - 6.當(dāng)data中某一個(gè)屬性的值發(fā)生變化,會進(jìn)入這個(gè)屬性的
setter中,setter會通知該屬性的Dep類 - 7.
Dep類會通知存儲的所有相關(guān)watcher進(jìn)行更新,于是這些watcher分別執(zhí)行自己update中的回調(diào)?;卣{(diào)即會更新node。
到此這篇關(guān)于一篇文章帶你徹底搞懂VUE響應(yīng)式原理的文章就介紹到這了,更多相關(guān) VUE響應(yīng)式原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何使用electron將vue項(xiàng)目打包成.exe文件(保姆級教程)
本文給大家介紹如何使用electron將vue項(xiàng)目打包成.exe文件,大家要注意一下vue2項(xiàng)目,使用的vue-element-admin框架,用electron打包成.exe文件,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2024-03-03
Vue3使用vant檢索組件van-search遇到的問題小結(jié)
當(dāng)清空按鈕與檢索按鈕同時(shí)居右時(shí),點(diǎn)擊clear清空按鈕事件時(shí)會同時(shí)觸發(fā)click-right-icon事件,這個(gè)時(shí)候容易觸發(fā)一系列問題,小編小編給大家分享Vue3使用vant檢索組件van-search遇到的問題小結(jié),感興趣的朋友一起看看吧2024-02-02
Vue無法訪問.env.development定義的變量值問題及解決
這篇文章主要介紹了Vue無法訪問.env.development定義的變量值問題及解決,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01
vue與iframe頁面數(shù)據(jù)互相通信的實(shí)現(xiàn)示例
這篇文章主要給大家介紹了vue與iframe頁面數(shù)據(jù)互相通信的實(shí)現(xiàn)示例,文中通過代碼示例給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-12-12
Vue + better-scroll 實(shí)現(xiàn)移動端字母索引導(dǎo)航功能
better-scroll 是一款重點(diǎn)解決移動端(已支持 PC)各種滾動場景需求的插件。這篇文章主要介紹了Vue + better-scroll 實(shí)現(xiàn)移動端字母索引導(dǎo)航功能,需要的朋友可以參考下2018-05-05
vue-router鉤子函數(shù)實(shí)現(xiàn)路由守衛(wèi)
這篇文章主要介紹了vue-router鉤子函數(shù)實(shí)現(xiàn)路由守衛(wèi),對vue感興趣的同學(xué),可以參考下2021-04-04

