您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“Vue響應式流程及原理是什么”,內容詳細,步驟清晰,細節處理妥當,希望這篇“Vue響應式流程及原理是什么”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
執行init操作。包括且不限制initLifecycle
、initState
等
執行mount。進行元素掛載
compiler步驟在runtime-only版本中沒有。
compiler步驟對template屬性進行編譯,生成render函數。
一般在項目中是在.vue
文件開發,通過vue-loader處理生成render函數。
執行render。生成vnode
<div id="app">{{ message }}</div>
render (h) { return h('div', { attrs: { id: 'app' }, }, this.message) }
render例子,如下
對應手寫的render函數
patch。新舊vnode經過diff后,渲染到真實dom上
執行$mount
。
實際執行mountComponent
這里會實例化一個Watcher
Watcher中會執行get
方法,觸發updateComponent
執行updateComponent
。執行vm._update(vm._render(), hydrating)
執行vm.render()
。
render其實調用createElment
(h
函數)
根據tag的不同,生成組件、原生VNode并返回
執行vm.update()
。createElm()
到 createChildren()
遞歸調用
將VNode轉化為真實的dom,并且最終渲染到頁面
這里以如下代碼案例講解更加清晰~沒錯,就是這么熟悉!就是一個初始化的Vue項目
// mian.js import Vue from 'vue' import App from './App.vue' new Vue({ render: h => h(App), }).$mount('#app')
// App.vue <template> <div id="app"> <p>{{ msg }}</p> </div> </template> <script> export default { name: 'App', data () { return { msg: 'hello world' } } } </script>
主要講解組件跟普通元素的不同之處,主要有2點:
如何生成VNode——創建組件VNodecreateComponent
如何patch——組件new Vue到patch流程createComponent
$vnode:占位符vnode。最終渲染vnode掛載的地方。所有的組件通過遞歸調用createComponent直至不再存在組件VNode,最終都會轉化成普通的dom。
{ tag: 'vue-component-1-App', componentInstance: {組件實例}, componentOptions: {Ctor, ..., } }
_vnode:渲染vnode。
{ tag: 'div', { "attrs": { "id": "app" } }, // 對應占位符vnode: $vnode parent: { tag: 'vue-component-1-App', componentInstance: {組件實例}, componentOptions: {Ctor, ..., } }, children: [ // 對應p標簽 { tag: 'p', // 對應p標簽內的文本節點{{ msg }} children: [{ text: 'hello world' }] }, { // 如果還有組件VNode其實也是一樣的 tag: 'vue-component-2-xxx' } ] }
(注意:這一步對應上圖render流程的紫色塊的展開!!!)
區分普通元素VNode
普通VNode:tag是html的保留標簽,如tag: 'div'
組件VNode:tag是以vue-component
開頭,如tag: 'vue-component-1-App'
(注意:這一步對應上圖patch流程的紫色塊的展開!!!)
相信你看完細粒度的Vue組件化過程可能已經暈頭轉向了,這里會用一個簡化版的流程圖進行回顧,加深理解
案例代碼
// 案例 export default { name: 'App', data () { return { msg: 'hello world', arr = [1, 2, 3] } } }
這里會從Observer、Dep、Watcher三個對象進行講解,分 object
、array
兩種依賴收集方式。
一定要注意!數組
的依賴收集 跟 對象的屬性
是不一樣的。對象屬性經過深度遍歷后,最終就是以一個基本類型的數據為單位收集依賴,但是數組仍然是一個引用類型。
如果這里不懂,先想一個問題: 我們用 this.msg = 'xxx'
能觸發 setter
派發更新,但是我們修改數組并不是用 this.arr = xxx
,而是用 this.arr.push(xxx)
等修改數組的方法。很顯然,這時候并不是通過觸發 arr
的 setter
去派發更新的。那是怎么做的呢?先帶著這個問題繼續往下看吧!
三個核心對象:Observer
(藍)、Dep
(綠)、Watcher
(紫)
依賴收集準備階段——Observer、Dep的實例化
// 以下是initData調用的方法講解,排列遵循調用順序 function observe (value, asRootData) { if (!isObject(value)) return // 非對象則不處理 // 實例化Observer對象 var ob; ob = new Observer(value); return ob } function Observer (value) { this.value = value; // 保存當前的data this.dep = new Dep(); // 實例化dep,數組進行依賴收集的dep(對應案例中的arr) def(value, '__ob__', this); if (Array.isArray(value)) { if (hasProto) { // 這里會改寫數組原型。__proto__指向重寫數組方法的對象 protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } } // 遍歷數組元素,執行對每一項調用observe,也就是說數組中有對象會轉成響應式對象 Observer.prototype.observeArray = function observeArray (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } } // 遍歷對象的全部屬性,調用defineReactive Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); // 如案例代碼,這里的 keys = ['msg', 'arr'] for (var i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]); } }
function defineReactive (obj, key, val) { // 產生一個閉包dep var dep = new Dep(); // 如果val是object類型,遞歸調用observe,案例代碼中的arr會走這個邏輯 var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { get: function reactiveGetter () { // 求value的值 var value = getter ? getter.call(obj) : val; if (Dep.target) { // Dep.target就是當前的Watcher // 這里是閉包dep dep.depend(); if (childOb) { // 案例代碼中arr會走到這個邏輯 childOb.dep.depend(); // 這里是Observer里的dep,數組arr在此依賴收集 if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { // 下文派發更新里進行講解 } }); }
注意 對象 、 數組 的不同處理方式。這里以 核心代碼 + 圖 進行講解
接下來核心分析 defineReactive
做了什么。注意 childOb
,這是數組進行依賴收集的地方(也就是為什么我們 this.arr.push(4)
能找到 Watcher
進行派發更新)
依賴收集觸發階段——Wather實例化、訪問數據、觸發依賴收集
// new Wathcer核心 function Watcher (vm, expOrFn, cb, options, isRenderWatcher) { if (typeof expOrFn === 'function') { // 渲染watcher中,這里傳入的expOrFn是updateComponent = vm.update(vm.render()) // this.getter等價于vm.update(vm.render()) this.getter = expOrFn; } else { ... } // 這里進行判斷,lazy為true時(計算屬性)則什么都不執行,否則執行get this.value = this.lazy ? undefined : this.get(); // 本次為渲染Watcher,執行get,繼續往下看~ } // Watcher的get方法 Watcher.prototype.get = function get () { // 這里很關鍵,pushTarget就是把當前的Wather賦值給“Dep.target” pushTarget(this); var value; var vm = this.vm; try { // 1. 這里調用getter,也就是執行vm.update(vm.render()) // 2. 執行vm.render函數就會訪問到響應式數據,觸發get進行依賴收集 // 3. 此時的Dep.target為當前的渲染Watcher,數據就可以理所應當的把Watcher加入自己的subs中 // 4. 所以此時,Watcher就能監測到數據變化,實現響應式 value = this.getter.call(vm, vm); } catch (e) { ... } finally { popTarget(); /* * cleanupDeps是個優化操作,會移除Watcher對本次render沒被使用的數據的觀測 * 效果:處于v-if為false中的響應式數據改變不會觸發Watcher的update * 感興趣的可以自己去debugger調試,這里就不展開了 */ this.cleanupDeps(); } return value }
Dep.target相關講解
targetStack:棧結構,用來保存Watcher
pushTarget:往targetStack
中push
當前的Watcher
(排在前一個Watcher的后面),并把Dep.target
賦值給當前Watcher
popTarget:先把targetStack
最后一個元素彈出(.pop),再把Dep.target
賦值給最后一個Watcher
(也就是還原了前一個Watcher)
通過上述實現,vue保證了全局唯一的Watcher
,準確賦值在Dep.target
中
細節太多繞暈了?來個整體流程,從宏觀角度再過一遍(computed部分可看完彩蛋后再回來重溫一下)
派發更新區分對象屬性、數組方法進行講解
如果想要深入了解組件的異步更新,戳這里,了解Vue組件異步更新之nextTick。本文只針對派發更新流程,不會對異步更新DOM進行展開講解~
這里可以先想一下,以下操作會發生什么?
this.msg = 'new val'
this.arr.push(4)
是的,毫無疑問都會先觸發他們之中的get
,那再觸發什么呢?我們接下來看
對象屬性修改觸發set,派發更新。this.msg = 'new val'
... Object.defineProperty (obj, key, { get () {...}, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; // 判斷新值相比舊值是否已經改變 if (newVal === value || (newVal !== newVal && value !== value)) { return } // 如果新值是引用類型,則將其轉化為響應式 childOb = !shallow && observe(newVal); // 這里通知dep的所有watcher進行更新 dep.notify(); } } ...
數組調用方法。this.arr.push(4)
// 數組方法改寫是在 Observer 方法中 function Observer () { if (hasProto) { // 用案例講解,也就是this.arr.__proto__ = arrayMethods protoAugment(value, arrayMethods); } } // 以下是數組方法重寫的實現 var arrayProto = Array.prototype; // 保存真實數組的原型 var arrayMethods = Object.create(arrayProto); // 以真數組為原型創建對象 // 可以看成:arrayMethods.__proto__ = Array.prototype var methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; // 一個裝飾器模型,重寫7個數組方法 methodsToPatch.forEach(function (method) { // 保存原生的數組方法 var original = arrayProto[method]; // 劫持arrayMethods對象中的數組方法 def(arrayMethods, method, function mutator () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; var result = original.apply(this, args); var ob = this.__ob__; // 當我門調用this.arr.push(),這里就能到數組對象的ob實例 var inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { ob.observeArray(inserted); } // 由于數組對象在new Observer中實例化了一個dep,并通過childOb邏輯收集了依賴,這里就能在ob實例中拿到dep屬性 ob.dep.notify(); return result }); })
這里可以聯合數組的依賴收集再看一遍,你就恍然大悟了。為什么 對象的屬性 、數組 的依賴收集方式不一樣
整個new Vue階段、到依賴收集、派發更新的全部流程就到這里結束了。可以縱觀流程圖看出,Vue應用就是一個個Vue組件組成的,雖然整個組件化、響應式流程很多,但核心的路徑一旦走通,你就會恍然大悟。
案例代碼
<template> <div id="app"> {{ name }} </div> </template> <script> export default { name: 'App', computed: { name () { return this.firstName + this.secondName } }, data () { return { firstName: 'jing', secondName: 'boran' } } } </script>
我們先看流程圖。圖有點大~大家可以放大看看,每個核心步驟都附有文字說明
根據案例概括一下,加深理解
// 訪問computed時觸發get的核心代碼 function createComputedGetter (key) { return function computedGetter () { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { // dirty第一次為true watcher.evaluate(); // 這里是對computed進行求值,對computed watcher執行依賴收集 } if (Dep.target) { watcher.depend(); // 這里是對渲染Watcher進行依賴收集 } return watcher.value } } }
computed中的name
其實就是一個computed Watcher,這個Watcher在init
階段生成
當App組件render的階段,render函數會訪問到模版中的{{ name }}
,則會觸發computed的求值,也就是執行上面代碼computedGetter()
。執行watcher.evaluate()
。也就是執行wathcer.get
。上文依賴收集的第3點:依賴收集觸發階段有對get方法進行講解,忘了的可以上去回顧一下執行watcher.depend()
Watcher.prototype.depend = function depend () { var i = this.deps.length; while (i--) { // 也就是調用Dep.depend => Watcher.addDep => dep.addSub this.deps[i].depend(); } }
// this.firstName和this.secondName的dep.subs dep.subs: [name的computed watcher, App組件的渲染Watcher]
代碼中判斷watcher.dirty
標志是什么?有什么用?
只有computed的值發生改變(也就是其依賴的數據改變),watcher.dirty
才會被設為true
只有watcher.dirty
為true
才會對computed進行 求值 或 重新求值
總結:也就是組件每次render,如果computed的值沒改變,直接返回value值(是不需要重新計算的),這也是computed的一個特點
首先pushTarget
把Dep.target
從App組件的渲染Watcher改為name的computed Watcher
其次執行cb:function() { return this.firstName + this.secondName }
執行cb的過程中,必然會訪問到firstName
、secondName
,這時候就是我們熟悉的依賴收集階段了。firstName、secondName都會把name這個computed watcher收集到自己的dep.subs[]
中
最后popTarget
把name的computed Watcher彈出棧,并恢復Dep.target
為當前App組件的渲染Watcher
遍歷computed watcher的deps。其實就是firstName、secondName實例的Dep
dep.depend
也就是調用watcher.addDep
(把Dep收集進watcher.deps中),再由watcher.appDep調用dep.addSub
(把Watcher收集進dep.subs中)
這樣一來,就完成了firstName、secondName對App組件的渲染watcher進行收集
結果如下。響應式數據中會存在兩個Watcher
至于為什么響應式數據要收集2個watcher?下文computed派發更新會講解
講到這里,我以自己的理解講解下文章開頭引言的問題:為什么Watcher、Dep多對多且相互收集? 這可能也是大家閱讀Vue源碼中一直存在的一個疑惑(包括我自己剛開始讀也是這樣)
對的,當然是為了computed中的響應式數據收集渲染Watcher啦!!!
還有!!! 還記得前文中依賴收集的第3點——依賴收集觸發階段的代碼講解中我寫了很多注釋的cleanupDeps
嗎?
// 此時flag為true,也就是說msg2沒有渲染在頁面中 <div v-if="flag">{{ msg1 }}</div> <div v-else>{{ msg2 }}</div> <button @click=() => { this.msg2 = 'change' }>changeMsg2</button>
function cleanupDeps () { var i = this.deps.length; while (i--) { // 這里對watcher所觀測的響應式數據的dep進行遍歷 // 對的,這樣一來,是不是watcher中的deps就發揮作用了呢? var dep = this.deps[i]; if (!this.newDepIds.has(dep.id)) { // 這里對當前渲染中沒有訪問到的響應式數據進行依賴移除 dep.removeSub(this); } } ... }
cleanupDeps
的作用就是清除掉當前沒有使用到的響應式數據。怎么清除?我們往下看
首先看個案例回答個問題,代碼如下。當flag為true時,msg2
并沒有渲染在頁面中,那么此時我們點擊按鈕修改msg2
的值會不會、或者應不應該觸發這個組件的重新渲染呢?
答案肯定是不會、不應該。所以:cleanupDeps
就是為此而存在的
那cleanupDeps
是怎么工作的呢?接著看下面代碼
到此,你是否已經懂得了watcher中為什么要收集自己觀測的響應式數據對應的dep呢?
派發相對來說比較簡單了~跟響應式的派發更新基本一致,繼續以案例來講解吧!
當我們修改firstName會發生什么?this.firstName = 'change'
首先觸發firstName的set,最終會調用dep.notify()
。firstName的dep.subs中有2個watcher,分別執行對應watcher的notify
Watcher.prototype.update = function update () { if (this.lazy) { this.dirty = true; // computed會走到這里,然后就結束了 } else if (this.sync) { this.run(); } else { queueWatcher(this); // 渲染watcher會走到這里 } }
computed watcher:將dirty屬性置為true。
渲染watcher會執行派發更新流程(如本文響應式流程——2.派發更新一致)
nextTick階段執行flushSchedulerQueue
,則會執行watcher.run()
watcher.run會執行watcher.get方法,也就是重新執行render、update的流程
執行render又會訪問到name的computed,從而又會執行computedGetter
此時的watcher.dirty在本步驟3已經置為true,又會執行watcher.evaluate()
進行computed的求值,執行watcher.depend()
......后續的流程就是派發更新的流程了~
user Watcher的依賴收集相比computed會簡單一點,這里不會贅述太多,只說核心區別,還有watch的常用配置immediate
、deep
、sync
user Watcher在init階段會執行一次watcher.get()
,在這里會訪問我們watch的響應式數據,從而進行依賴收集。回顧下computed,computed在這個階段什么也沒做。
// 沒錯,又是這段熟悉的代碼 this.value = this.lazy ? undefined : this.get(); // user Watcher和渲染 Watcher都在new Watcher階段執行get()
如果userWatcher設置的immediate: true
,則會在new Watcher后主動觸發一次cb的執行
Vue.prototype.$watch = function (expOrFn, cb, options) { ... var watcher = new Watcher(vm, expOrFn, cb, options); if (options.immediate) { // immediate則會執行我們傳入的callback try { cb.call(vm, watcher.value); } catch (error) { } } return function unwatchFn () { watcher.teardown(); } };
deep
邏輯很簡單,大概講下:深度遍歷這個對象,訪問到該對象的所有屬性,以此來觸發所有屬性的getter。這樣,所有屬性都會把當前的user Watcher收集到自己的dep中。因此,深層的屬性值修改(觸發set派發更新能通知到user Watcher),watch自然就能監測到數據改變~感興趣的同學可以自己去看看源碼中traverse
的實現。
sync
。當前tick執行,以此能先于渲染Wathcer執行。不設置同步的watcher都會放到nextTick中執行。
Watcher.prototype.update = function update () { if (this.lazy) { this.dirty = true; // 計算屬性 } else if (this.sync) { this.run(); // 同步的user Wathcer } else { queueWatcher(this); // 普通user Watcher和渲染Watcher } }
讀到這里,這篇“Vue響應式流程及原理是什么”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。