<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
Observer
、Dep
、Watcher
三大物件傻傻分不清?dep
、watcher
互調造成混亂?本文主要分為1. 元件化;2. 響應式原理;3. 彩蛋(computed和watch)進行講解。本文偵錯原始碼的vue版本是v2.6.14。整篇將採用原始碼講解 + 流程圖的方式詳細還原整個Vue響應式原理的全過程。你可以瞭解到Dep.target
、pushTarget
、popTarget
;響應式中的三大Watcher;Dep
、Wathcer
多對多的,互相收集的關係。
這篇是進階的 Vue 響應式原始碼解析,文章比較長,內容比較深,大家可以先mark後看。看不懂的不要強行看,可以先看看其他作者的偏簡單一點的原始碼解析文章,然後好好消化。等過段時間再回來看這篇,相信你由淺入深後再看本文,一定會有意想不到的收穫~
在講解整個響應式原理之前,先介紹一下Vue中另一個比較核心的概念——元件化,個人認為這也是學習響應式的前置核心。搞懂元件化,響應式學習如虎添翼!
initLifecycle
、initState
等.vue
檔案開發,通過vue-loader處理生成render函數。執行render。生成vnode
<div id="app">{{ message }}</div>
render (h) { return h('div', { attrs: { id: 'app' }, }, this.message) }
$mount
。
mountComponent
get
方法,觸發updateComponent
updateComponent
。執行vm._update(vm._render(), hydrating)
vm.render()
。createElment
(h
函數)vm.update()
。createElm()
到 createChildren()
遞迴呼叫這裡以如下程式碼案例講解更加清晰~沒錯,就是這麼熟悉!就是一個初始化的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' } ] }
tag: 'div'
vue-component
開頭,如tag: 'vue-component-1-App'
相信你看完細粒度的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 }
Watcher
targetStack
中push
當前的Watcher
(排在前一個Watcher的後面),並把Dep.target
賦值給當前Watcher
targetStack
最後一個元素彈出(.pop),再把Dep.target
賦值給最後一個Watcher
(也就是還原了前一個Watcher)全域性唯一的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 Watcherfunction() { return this.firstName + this.secondName }
firstName
、secondName
,這時候就是我們熟悉的依賴收集階段了。firstName、secondName都會把name這個computed watcher收集到自己的dep.subs[]
中popTarget
把name的computed Watcher彈出棧,並恢復Dep.target
為當前App元件的渲染Watcherdep.depend
也就是呼叫watcher.addDep
(把Dep收集進watcher.deps中),再由watcher.appDep呼叫dep.addSub
(把Watcher收集進dep.subs中)講到這裡,我以自己的理解講解下文章開頭引言的問題:為什麼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
的作用就是清除掉當前沒有使用到的響應式資料。怎麼清除?我們往下看msg2
並沒有渲染在頁面中,那麼此時我們點選按鈕修改msg2
的值會不會、或者應不應該觸發這個元件的重新渲染呢?cleanupDeps
就是為此而存在的cleanupDeps
是怎麼工作的呢?接著看下面程式碼派發相對來說比較簡單了~跟響應式的派發更新基本一致,繼續以案例來講解吧!
當我們修改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的原始碼其實是比較好上手的,整體程式碼流程非常的清晰。但是想要深入某一塊邏輯,最好結合流程圖加debugger方式親自上手實踐。畢竟真正搞懂一門框架的原始碼並非易事,我也是通過不斷debugger偵錯,一遍遍走核心流程,才能較好的學習理解vue的實現原理~
寫在最後,這篇文章也算是自己的一個知識沉澱吧,畢竟很早之前就學習過Vue的原始碼了,但是也一直沒做筆記。現在回顧一下,發現很多都有點忘了,但是缺乏一個快速記憶、回顧的筆記。如果要直接硬磕原始碼重新記憶,還是比較費時費力的~作為知識分享,希望可以幫助到想學習原始碼,想要進階的你,大家彼此共勉,一同進步!
以上就是圖解Vue 響應式原理的詳細內容,更多關於Vue 響應式的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45