Vue響應(yīng)式原理的示例詳解
Vue 最獨(dú)特的特性之一,是非侵入式的響應(yīng)系統(tǒng)。數(shù)據(jù)模型僅僅是普通的 JavaScript 對象。而當(dāng)你修改它們時(shí),視圖會(huì)進(jìn)行更新。聊到 Vue 響應(yīng)式實(shí)現(xiàn)原理,眾多開發(fā)者都知道實(shí)現(xiàn)的關(guān)鍵在于利用 Object.defineProperty , 但具體又是如何實(shí)現(xiàn)的呢,今天我們來一探究竟。
為了通俗易懂,我們還是從一個(gè)小的示例開始:
<body> <div id="app"> {{ message }} </div> <script> var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } }) </script> </body>
我們已經(jīng)成功創(chuàng)建了第一個(gè) Vue 應(yīng)用!看起來這跟渲染一個(gè)字符串模板非常類似,但是 Vue 在背后做了大量工作?,F(xiàn)在數(shù)據(jù)和 DOM 已經(jīng)被建立了關(guān)聯(lián),所有東西都是響應(yīng)式的。我們要怎么確認(rèn)呢?打開你的瀏覽器的 JavaScript 控制臺(tái) (就在這個(gè)頁面打開),并修改 app.message的值,你將看到上例相應(yīng)地更新。修改數(shù)據(jù)便會(huì)自動(dòng)更新,Vue 是如何做到的呢?
通過 Vue 構(gòu)造函數(shù)創(chuàng)建一個(gè)實(shí)例時(shí),會(huì)有執(zhí)行一個(gè)初始化的操作:
function Vue (options) { this._init(options); }
這個(gè) _init初始化函數(shù)內(nèi)部會(huì)初始化生命周期、事件、渲染函數(shù)、狀態(tài)等等:
initLifecycle(vm); initEvents(vm); initRender(vm); callHook(vm, 'beforeCreate'); initInjections(vm); initState(vm); initProvide(vm); callHook(vm, 'created');
因?yàn)楸疚牡闹黝}是響應(yīng)式原理,因此我們只關(guān)注 initState(vm) 即可。它的關(guān)鍵調(diào)用步驟如下:
function initState (vm) { initData(vm); } function initData(vm) { // data就是我們創(chuàng)建 Vue實(shí)例傳入的 {message: 'Hello Vue!'} observe(data, true /* asRootData */); } function observe (value, asRootData) { ob = new Observer(value); } var Observer = function Observer (value) { this.walk(value); } Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { // 實(shí)現(xiàn)響應(yīng)式關(guān)鍵函數(shù) defineReactive$$1(obj, keys[i]); } }; }
我們來總結(jié)一下上面 initState(vm)流程。初始化狀態(tài)的時(shí)候會(huì)對應(yīng)用的數(shù)據(jù)進(jìn)行檢測,即創(chuàng)建一個(gè) Observer 實(shí)例,其構(gòu)造函數(shù)內(nèi)部會(huì)執(zhí)行原型上的 walk方法。walk方法的主要作用便是 遍歷數(shù)據(jù)的所有屬性,并把每個(gè)屬性轉(zhuǎn)換成響應(yīng)式,而這轉(zhuǎn)換的工作主要由 defineReactive$$1 函數(shù)完成。
function defineReactive$$1(obj, key, val) { var dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter(newVal) { var value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (customSetter) { customSetter(); } // #7981: for accessor properties without setter if (getter && !setter) { return } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); } }); }
defineReactive$$1函數(shù)內(nèi)部使用Object.defineProperty 來監(jiān)測數(shù)據(jù)的變化。每當(dāng)從 obj 的 key 中讀取數(shù)據(jù)時(shí),get 函數(shù)被觸發(fā);每當(dāng)往 obj 的 key 中設(shè)置數(shù)據(jù)時(shí),set 函數(shù)被觸發(fā)。我們說修改數(shù)據(jù)觸發(fā) set 函數(shù),那么 set 函數(shù)是如何更新視圖的呢?拿本文開頭示例分析:
<div id="app"> {{ message }} </div>
該模板使用了數(shù)據(jù) message, 當(dāng) message 的值發(fā)生改變的時(shí)候,應(yīng)用中所有使用到 message 的視圖都能觸發(fā)更新。在 Vue 的內(nèi)部實(shí)現(xiàn)中,先是收集依賴,即把用到數(shù)據(jù) message 的地方收集起來,然后等數(shù)據(jù)發(fā)生改變的時(shí)候,把之前收集的依賴全部觸發(fā)一遍就可以了。也就是說我們在上述的 get 函數(shù)中收集依賴,在 set 函數(shù)中觸發(fā)視圖更新。那接下來的重點(diǎn)就是分析 get 函數(shù)和 set 函數(shù)了。先看 get 函數(shù),其關(guān)鍵調(diào)用如下:
get: function reactiveGetter () { if (Dep.target) { dep.depend(); } } Dep.prototype.depend = function depend () { if (Dep.target) { Dep.target.addDep(this); } }; Watcher.prototype.addDep = function addDep (dep) { dep.addSub(this); } Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub); }; 其中 Dep 構(gòu)造函數(shù)如下: var Dep = function Dep () { this.id = uid++; this.subs = []; };
上述代碼中Dep.target的值是一個(gè)Watcher實(shí)例,稍后我們再分析它是何時(shí)被賦值的。我們用一句話總結(jié) get 函數(shù)所做的工作:把當(dāng)前 Watcher 實(shí)例(也就是Dep.target)添加到 Dep 實(shí)例的 subs 數(shù)組中。在繼續(xù)分析 get 函數(shù)前,我們需要弄清楚 Dep.target 的值何時(shí)被賦值為 Watcher 實(shí)例,這里我們需要從 mountComponent這個(gè)函數(shù)開始分析:
function mountComponent (vm, el, hydrating) { updateComponent = function () { vm._update(vm._render(), hydrating); }; new Watcher(vm, updateComponent, noop, xxx); } // Wather構(gòu)造函數(shù)下 var Watcher = function Watcher (vm, expOrFn, cb) { if (typeof expOrFn === 'function') { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); } this.value = this.get(); } Watcher.prototype.get = function get () { pushTarget(this); value = this.getter.call(vm, vm); } function pushTarget (target) { targetStack.push(target); Dep.target = target; }
由上述代碼我們知道m(xù)ountComponent函數(shù)會(huì)創(chuàng)建一個(gè) Watcher 實(shí)例,在其構(gòu)造函數(shù)中最終會(huì)調(diào)用 pushTarget函數(shù),把當(dāng)前 Watcher 實(shí)例賦值給 Dep.target。另外我們注意到,創(chuàng)建 Watcher 實(shí)例這個(gè)動(dòng)作是發(fā)生在函數(shù)mountComponent內(nèi)部,也就是說 Watcher 實(shí)例是組件級(jí)別的粒度,而不是說任何用到數(shù)據(jù)的地方都新建一個(gè) Watcher 實(shí)例?,F(xiàn)在我們再來看看 set 函數(shù)的主要調(diào)用過程:
set: function reactiveSetter (newVal) { dep.notify(); } Dep.prototype.notify = function notify () { var subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } } Watcher.prototype.update = function update () { queueWatcher(this); } Watcher.prototype.update = function update () { // queue是一個(gè)全局?jǐn)?shù)組 queue.push(watcher); nextTick(flushSchedulerQueue); } // flushSchedulerQueue是一個(gè)全局函數(shù) function flushSchedulerQueue () { for (index = 0; index < queue.length; index++) { watcher = queue[index]; watcher.run(); } } Watcher.prototype.run = function run () { var value = this.get(); }
set 函數(shù)內(nèi)容有點(diǎn)長,但上述代碼都是精簡過的,應(yīng)該不難理解。當(dāng)改變應(yīng)用數(shù)據(jù)的時(shí)候,觸發(fā) set 函數(shù)執(zhí)行。它會(huì)調(diào)用 Dep 實(shí)例的 notify()方法,而 notify 方法又會(huì)把當(dāng)前 Dep 實(shí)例收集的所有 Watcher 實(shí)例的 update 方法調(diào)用一遍,以達(dá)到更新所有用到該數(shù)據(jù)的視圖部分。我們繼續(xù)看 Watcher 實(shí)例的 update 方法做了什么。update 方法會(huì)把當(dāng)前的 watcher 添加到數(shù)組 queue 中,然后把 queue 中每個(gè) watcher 的 run 方法執(zhí)行一遍。run 方法內(nèi)部會(huì)執(zhí)行 Wather 原型上的 get 方法,后續(xù)的調(diào)用在前文分析 mountComponent 函數(shù)中都有描述,在此就不再贅述??偨Y(jié)來說,最終 update 方法會(huì)觸發(fā) updateComponent函數(shù):
updateComponent = function () { vm._update(vm._render(), hydrating); }; Vue.prototype._update = function (vnode, hydrating) { vm.$el = vm.__patch__(prevVnode, vnode); }
這里我們注意到 _update 函數(shù)的第一個(gè)參數(shù)是 vnode 。vnode 顧名思義是虛擬節(jié)點(diǎn)的意思,它是一個(gè)普通對象,該對象的屬性上保存了生成 DOM 節(jié)點(diǎn)所需要數(shù)據(jù)。說到虛擬節(jié)點(diǎn)你是不是很容易就聯(lián)想到虛擬 DOM 了呢,沒錯(cuò) Vue 中也使用了虛擬 DOM。前文說到 Wather 是和組件相關(guān)的,組件內(nèi)部的更新就用虛擬 DOM 進(jìn)行對比和渲染。_update 函數(shù)內(nèi)部調(diào)用了 patch 函數(shù),通過該函數(shù)對比新舊兩個(gè) vnode 之間的不同,然后根據(jù)對比結(jié)果找出需要更新的節(jié)點(diǎn)進(jìn)行更新。
注:本文分析示例基于 Vue v2.6.14 版本。
到此這篇關(guān)于Vue響應(yīng)式原理的示例詳解的文章就介紹到這了,更多相關(guān)Vue響應(yīng)式原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue3+Element-Plus?實(shí)現(xiàn)點(diǎn)擊左側(cè)菜單時(shí)顯示不同內(nèi)容組件展示在Main區(qū)域功能
這篇文章主要介紹了Vue3+Element-Plus?實(shí)現(xiàn)點(diǎn)擊左側(cè)菜單時(shí)顯示不同內(nèi)容組件展示在Main區(qū)域功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-01-01vue2使用element-ui,el-table不顯示,用npm安裝方式
這篇文章主要介紹了vue2使用element-ui,el-table不顯示,用npm安裝方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07Element實(shí)現(xiàn)登錄+注冊的示例代碼
登錄注冊是最常用的網(wǎng)站功能,本文主要介紹了Element實(shí)現(xiàn)登錄+注冊的示例代碼,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09