一篇文章帶你徹底搞懂VUE響應(yīng)式原理
首先上圖,下面這張圖,即為MVVM
響應(yīng)式原理的整個過程圖,我們本篇都是圍繞著這張圖進(jìn)行分析,所以這張圖是重中之重。
響應(yīng)式原理圖
一臉懵逼?沒關(guān)系,接下來我們將通過創(chuàng)建一個簡單的MVVM
響應(yīng)系統(tǒng)來一步步了解這個上圖中的全過程。全文分為兩大塊,首先介紹實(shí)例模板的編譯過程,然后詳細(xì)介紹響應(yīng)式,這里先介紹編譯是為了給介紹響應(yīng)式奠定基礎(chǔ)。
編譯
我們把我們創(chuàng)建的這個微型響應(yīng)系統(tǒng)命名為miniVue
,我們按照平常使用Vue的模式,首先創(chuàng)建一個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ù)這個實(shí)例,我們可以創(chuàng)建出miniVue
的類,這個類中我們肯定要保存該實(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)建一個compile
類來進(jìn)行解析模板的操作
創(chuàng)建compile類
Compile
類是用來解析模板的,所以肯定要傳入要解析的DOM
。拿到DOM
后直接操作這個DOM
會導(dǎo)致頁面頻繁的回流和重繪,所以我們把這個DOM
放到一個文檔碎片中,然后操作這個文檔碎片。操作這個文檔碎片的過程中我們需要獲取到數(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
操作保存好的文檔碎片,我們可以專門定義一個函數(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)上的信息主要就是這個元素節(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)、整個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)該是一個對象,里面有多個屬性對應(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í)例中找到這個表達(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代表事件冒泡時觸發(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語法,將其整個替換為vm實(shí)例中屬性對應(yīng)的值 ? ? ? ? ? ?// 拿我們最初初始化實(shí)例的一個數(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)式這三個字。下面我們開始介紹本篇的核心,即vue是如何實(shí)現(xiàn)響應(yīng)式的。
響應(yīng)式
數(shù)據(jù)劫持
關(guān)鍵點(diǎn):Object.defineProperty(具體用法參考MDN)
主要目的:為data中每個屬性添加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ù)劫持 在這個地方進(jìn)行相關(guān)操作 return value ? ? ? } ? ? ? ?set(newVal) { if(newVal == value) return ? ? ? value = newVal ? ? ? // 為新數(shù)據(jù)添加getter和setter ? ? ? observe(newVal) ? ? ? // 數(shù)據(jù)劫持 在這個地方進(jìn)行相關(guān)操作 ? } ? }) }
收集依賴
依賴其實(shí)說白了,就是數(shù)據(jù)的依賴,data中的某個屬性,可能在DOM
中好幾個地方進(jìn)行了使用,那DOM
中使用到該屬性的地方就都會產(chǎn)生一個對于該屬性的依賴,也就是watcher
。當(dāng)該屬性的值發(fā)生了變化,那么就可以通知watcher
來使得頁面中使用到這個屬性的地方進(jìn)行視圖更新。為每個屬性綁定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í)例時,對依賴屬性進(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)定義過一個了,功能是一樣的,這里重復(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) }
依賴我們大概清楚了,但是我們上面講,需要把一個屬性全部的依賴(watcher)收集起來,所以我們該如何收集依賴呢?
首先我們先想第一個問題,一個屬性會有一個或者好多個watcher
,我們應(yīng)該如何保存這些watcher
呢,這個我們很容易想到,我們可以專門拿一個數(shù)組保存一個屬性的全部watcher
,我們把這個數(shù)組命名為dep
(dependency)。
第二個問題,我們應(yīng)該什么時候進(jìn)行收集watcher
的操作呢。還記得我們上面提到的訂閱嗎,我們每次初始化watcher
時,會為該watcher
訂閱屬性,訂閱的過程中我們會首先獲取這個屬性的值,這時就可以發(fā)揮數(shù)據(jù)劫持的作用了,獲取這個屬性值的時候,我們就會進(jìn)到這個屬性的getter
方法中,所以我們可以在這個時候完成收集watcher
的操作。
第三個問題,我們說watcher
的作用其實(shí)就是監(jiān)聽到訂閱屬性的變化(即監(jiān)聽發(fā)布),監(jiān)聽到變化后執(zhí)行其update
方法,即執(zhí)行更新回調(diào),來更新視圖。那么我們怎樣才能讓watcher
監(jiān)聽到“發(fā)布”呢,這時我們又需要用到數(shù)據(jù)劫持,即在setter
中通知這個屬性所有的watcher
。
function defineReactive(data, key, value) { // 新建用于存儲watcher的數(shù)據(jù) const dep = [] // 遞歸子屬性 observe(value) Object.defineProperty(data, key, { get() { // 數(shù)據(jù)劫持 在這個地方進(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ù)劫持 在這個地方進(jìn)行相關(guān)操作 dep.notify() // 通知依賴 } }) }
現(xiàn)在我覺得我有必要理一下這個依賴收集的全過程。首先頁面初次渲染的時候,會遇到我們在data中定義的屬性(注意:此時屬性上面已經(jīng)定義好getter和setter了),遇到屬性后會初始化一個watcher
實(shí)例,在此過程中watcher
實(shí)例會獲取這個屬性的值,于是會進(jìn)入到這個屬性的getter
中,于是我們通過數(shù)據(jù)劫持來收集這個watcher
。那么又出現(xiàn)了一個問題,我們此時在getter
中,如何獲取到初始化的watcher
實(shí)例呢,也就是dep.push
的時候,其實(shí)我們是沒有辦法直接拿到這個watcher
的。因此,我們需要在初始化watcher
的時候,把這個watcher
放到全局,比如window.target
。
subscribe() { // 獲取依賴屬性的值 window.target = this // 這里的this即為此時初始化的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ù)組抽象為一個類
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用完清空
下面有一個場景,我們在訪問到data中的一個屬性a
后,實(shí)例化了一個watcher1
,此時在實(shí)例化這個watcher1
的過程中,會把window.target
設(shè)置為watcher1
,之后我們在沒有實(shí)例化其他watcher
的情況下直接去訪問其他的屬性,例如屬性b
,那么屬性b
中的getter
會直接把watcher1
推入到它的依賴數(shù)組中。這樣是不合理的,所以我們每次將watcher
推入到依賴數(shù)組中后,要將這個watcher
從全局中收回。(window.target這里改成Dep.target了,其實(shí)都是一樣的)
subscribe() { Dep.target = this // 這里的this即為此時初始化的watcher實(shí)例 const value = _get(this.data, this.exp) Dep.target = null // 清空暴露在全局中的watcher return value } // 同時在收集依賴時添加一層過濾 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) }
需要注意的一個地方
下面是watcher
中獲取所依賴屬性值的方法,這里需要說明一下,對于存在對象嵌套的情況,每一層屬性的依賴數(shù)組中都會添加這個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]時,肯定會去訪問這兩個屬性的值,于是會進(jìn)入到這兩個屬性的getter里面 所以這個watcher不僅僅會被添加到最內(nèi)層屬性的getter中,中間每一層屬性的getter中都會有這個watcher 即如果data[a]的值發(fā)生了變化,也會通知這個watcher去更新視圖 */ segments.reduce((pre, key) => { return pre[key] }, obj) }
雙劍合璧
怎樣將上面的編譯和響應(yīng)式整合到一起形成一個完整的具有響應(yīng)式的miniVue
類呢。其實(shí)很簡單,從我們最上面那張圖就可以看出來??偨Y(jié)一下就兩點(diǎn),在我們通過各種指令操作node節(jié)點(diǎn)的時候,同時初始化watcher,另一點(diǎn)即為初始化watcher時指定的回調(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); ? }) ? ? ? ?// 這里是編譯的時候更新視圖 ? ? ? ?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ù)劫持功能(getter
andsetter
) - 2.開始編譯實(shí)例綁定的模板。
- 3.首先編譯做準(zhǔn)備,創(chuàng)建compile類,拿到模板的整個
DOM
對象,遍歷其子節(jié)點(diǎn),獲取到每個子節(jié)點(diǎn)上的信息,這些信息中凡是引用過vm實(shí)例data中的屬性的,一律都新增一個watcher
實(shí)例 - 4.初始化
watcher
實(shí)例的時候,會訪問這個屬性,然后進(jìn)入這個屬性的getter
中,在getter
中,將這個watcher
添加到這個屬性的Dep
類中 - 5.最后更新
node
,至此初始化編譯完成 - 6.當(dāng)data中某一個屬性的值發(fā)生變化,會進(jìn)入這個屬性的
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-03Vue3使用vant檢索組件van-search遇到的問題小結(jié)
當(dāng)清空按鈕與檢索按鈕同時居右時,點(diǎn)擊clear清空按鈕事件時會同時觸發(fā)click-right-icon事件,這個時候容易觸發(fā)一系列問題,小編小編給大家分享Vue3使用vant檢索組件van-search遇到的問題小結(jié),感興趣的朋友一起看看吧2024-02-02Vue無法訪問.env.development定義的變量值問題及解決
這篇文章主要介紹了Vue無法訪問.env.development定義的變量值問題及解決,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01vue與iframe頁面數(shù)據(jù)互相通信的實(shí)現(xiàn)示例
這篇文章主要給大家介紹了vue與iframe頁面數(shù)據(jù)互相通信的實(shí)現(xiàn)示例,文中通過代碼示例給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-12-12Vue + better-scroll 實(shí)現(xiàn)移動端字母索引導(dǎo)航功能
better-scroll 是一款重點(diǎn)解決移動端(已支持 PC)各種滾動場景需求的插件。這篇文章主要介紹了Vue + better-scroll 實(shí)現(xiàn)移動端字母索引導(dǎo)航功能,需要的朋友可以參考下2018-05-05vue-router鉤子函數(shù)實(shí)現(xiàn)路由守衛(wèi)
這篇文章主要介紹了vue-router鉤子函數(shù)實(shí)現(xiàn)路由守衛(wèi),對vue感興趣的同學(xué),可以參考下2021-04-04