前言
隨著前后端分離成為Web開發(fā)的常態(tài)疫向,Mvvm框架越來越普及咳蔚。讓前端開發(fā)從關(guān)注Dom,變?yōu)殛P(guān)注數(shù)據(jù)搔驼,提高了開發(fā)效率谈火,降低了學(xué)習(xí)成本。同時(shí)也能有效避免低級的Dom操作錯(cuò)誤舌涨。
在享受Mvvm框架帶來的便利的同時(shí)糯耍,我們也會對它的具體實(shí)現(xiàn)產(chǎn)生興趣。筆者認(rèn)為Mvvm框架重要的有兩個(gè)部分
數(shù)據(jù)變化的捕獲囊嘉,通知與響應(yīng)
vDom對通知產(chǎn)生響應(yīng)温技,并對Dom進(jìn)行相應(yīng)的操作
今天我們先來看一下變化的捕獲,通知與響應(yīng)扭粱,分為下面四個(gè)部分
數(shù)據(jù)變化的捕獲
監(jiān)聽器的創(chuàng)建
數(shù)據(jù)變化與監(jiān)聽器的關(guān)聯(lián)
變化的響應(yīng)
一舵鳞,數(shù)據(jù)變化的捕獲
日常項(xiàng)目中,我們常用的與數(shù)據(jù)變化相關(guān)的琢蛤,有以下三個(gè):
Data: 包括定義數(shù)據(jù)模型設(shè)置的初始值蜓堕,Prop傳遞的值
Watch:監(jiān)聽某一個(gè)值的變化進(jìn)行后續(xù)業(yè)務(wù)處理
Computed:頁面展示的值是多個(gè)值的組合變化
除了上述三個(gè),其實(shí)vue框架本身博其,還有一個(gè)組件層面的變化套才,比如路由變化會重新渲染組件。雖然都是變化慕淡,但這四者既有聯(lián)系也有區(qū)別背伴,關(guān)系如下圖
數(shù)據(jù)變化會觸發(fā)監(jiān)聽器,會觸發(fā)組件渲染,而組件渲染的時(shí)候傻寂,會重新計(jì)算屬性息尺。那么該如何監(jiān)聽數(shù)據(jù)變化呢?
監(jiān)聽數(shù)據(jù)變化
JavaScript中監(jiān)聽數(shù)據(jù)變化API:Getter和Setter崎逃,先看一個(gè)簡單的示例:
var user = {}
var name;
Object.defineProperty(user, 'current', {
get: function(){
console.log('獲取名稱')
return name
},
set:function(val){
console.log('設(shè)置名稱')
name = val
}
})
user.current = '張三';
console.log(user.current);
控制臺輸出:
設(shè)置名稱
獲取名稱
張三
API getter和setter就是數(shù)據(jù)劫持的基礎(chǔ)掷倔,通過這個(gè)例子我們看到,在設(shè)置數(shù)據(jù)或獲取數(shù)據(jù)的時(shí)候个绍,我們都可以加入自己的處理邏輯勒葱。從而達(dá)到我們監(jiān)聽數(shù)據(jù)變化的目的。接下來我們再對比一下vue的代碼實(shí)現(xiàn)巴柿。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const 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) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && 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()
}
})
同樣的凛虽,Vue也是通過這種方式劫持?jǐn)?shù)據(jù),然后攔截到變化后广恢,去通知訂閱者凯旋。Vue捕獲變化并發(fā)送通知的流程圖如下(放大查看):
當(dāng)監(jiān)聽到數(shù)據(jù)變化時(shí),我們通過Watcher來發(fā)送通知
-
當(dāng)獲取數(shù)據(jù)的時(shí)候钉迷,我們把Watcher加入到通知列表
什么是Watcher呢至非?
二,監(jiān)聽器的創(chuàng)建
Watcher的分類
Watcher什么時(shí)候創(chuàng)建的呢糠聪?先看下面這段熟悉的代碼:
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>’
})
上面這塊代碼簡單來說就是創(chuàng)建了一個(gè)Vue的實(shí)例荒椭。聲明了一個(gè)組件App,渲染綁定的節(jié)點(diǎn)是#app舰蟆,還有路由趣惠。
Vue實(shí)例化的處理流程(本文無關(guān)的部分略過)。
和變化相關(guān)的有三個(gè)方法:
InitRender階段,綁定組件的渲染方法
InitState階段,創(chuàng)建數(shù)據(jù)模型,監(jiān)聽器,計(jì)算屬性的Watcher
$mount階段,創(chuàng)建組件Watcher
我們分別看一下三種監(jiān)聽器的創(chuàng)建
監(jiān)聽器Watcher
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
注意這一行:const watcher = new Watcher(vm, expOrFn, cb, options)
$watch方法也是Vue對外提供的API
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
},
watch: {
firstName: function (val) {
this.fullName = val + ' ' + this.lastName
},
lastName: function (val) {
this.fullName = this.firstName + ' ' + val
}
}
})
我們結(jié)合Vue官網(wǎng)watch例子來看new Wacher的參數(shù):
vm:Vue實(shí)例本身
expOrFn:firstName
cb:對應(yīng)的函數(shù)
option:額外的參數(shù)身害,從上面我們也看到味悄,有個(gè)immediate屬性,如果為true就先調(diào)用一次
計(jì)算屬性Watcher
計(jì)算屬性也是和上面類似塌鸯,但有個(gè)重要的參數(shù)侍瑟,lazy為true。這就代表著丙猬,在創(chuàng)建的時(shí)候丢习,并不會立即執(zhí)行。
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
……略
for (const key in computed) {
……略
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
}
……略
}
……略
}
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
而調(diào)用的時(shí)機(jī)是在渲染組件的時(shí)候觸發(fā)淮悼,然后watcher.evaluate()
組件Watcher
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
………略
let updateComponent
if (process.env.NODE_ENV !== ‘production’ && config.performance && mark) {
updateComponent = () => {
………略
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
………略
return vm
}
當(dāng)組件發(fā)生變化的時(shí)候就會觸發(fā)vm._update(vm._render(), hydrating),在創(chuàng)建的時(shí)候也會執(zhí)行一次揽思,進(jìn)行第一次渲染袜腥。具體見下圖:
Init方法中,會進(jìn)行各種Watcher的創(chuàng)建
$mount中會創(chuàng)建組件Watcher并執(zhí)行
組件Watcher觸發(fā)渲染
渲染過程發(fā)現(xiàn)有子組件,對子組件再走一遍上面的流程
注意:上面我們說了三種Watcher的創(chuàng)建,計(jì)算屬性的Watcher不會立即執(zhí)行,而其他兩個(gè)都會立即執(zhí)行一次羹令。
三鲤屡,數(shù)據(jù)變化與監(jiān)聽器的關(guān)聯(lián)
到目前為止,我們解決了變化的監(jiān)聽福侈,以及觀察者的創(chuàng)建酒来,那么兩者又是如何聯(lián)系起來的呢?
再來看一下數(shù)據(jù)劫持的getter方法肪凛,我們發(fā)現(xiàn)只有在Dep.target(Watcher)存在的時(shí)候才建立關(guān)聯(lián)
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
再看一下Watcher類的get方法(這個(gè)就是一個(gè)普通的方法名稱,不要和getter混淆)
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
pushTarget(this):將當(dāng)前watcher壓入棧堰汉,同時(shí)將this賦值給Dep.target
popTarget():出棧
哪這個(gè)棧又是個(gè)什么東東呢?
Watcher Stack
組件是按樹形的結(jié)構(gòu)遞歸解析伟墙。如果不考慮出棧的情況翘鸭,那么整個(gè)棧的情況如圖所示:
而在實(shí)際過程中當(dāng)關(guān)聯(lián)設(shè)置結(jié)束后,會進(jìn)行出棧操作戳葵。整個(gè)解析過程按照從根節(jié)點(diǎn)到子節(jié)點(diǎn)就乓,也就是監(jiān)聽先壓入棧,然后解析的時(shí)候發(fā)現(xiàn)棧里有監(jiān)聽拱烁,就會綁定生蚁。
是不是有點(diǎn)亂?沒關(guān)系戏自,我們再捋一遍邦投。
new Watcher的時(shí)候調(diào)用其內(nèi)部get方法,在這個(gè)方法中會將當(dāng)前監(jiān)聽壓入棧浦妄,并賦值給target尼摹。
繼續(xù)向下執(zhí)行,解析組件時(shí)第一次必然獲取數(shù)據(jù)剂娄,這個(gè)時(shí)候就會觸發(fā)數(shù)據(jù)劫持的getter蠢涝,在getter里判斷當(dāng)前target是否有值,有值就把當(dāng)前數(shù)據(jù)和Watcher進(jìn)行關(guān)聯(lián)阅懦,沒有就忽略繼續(xù)向下
出棧并清空target
結(jié)合上面的文字和二,再具體看一下這三個(gè)Watcher關(guān)聯(lián)的流程
組件Watcher關(guān)聯(lián)
監(jiān)聽器Watcher關(guān)聯(lián)
組件Watcher和監(jiān)聽器Watcher的區(qū)別,是組件Watcher要進(jìn)行渲染耳胎。這當(dāng)然也比較好理解惯吕,監(jiān)聽的目的,歸根結(jié)底是要渲染到頁面用戶才能看到變化怕午。比如vue-router废登,就是利用組件Watcher進(jìn)行的觸發(fā)。
計(jì)算屬性Watcher關(guān)聯(lián)
計(jì)算屬性和前面兩個(gè)不同郁惜,它在創(chuàng)建watcher的時(shí)候堡距,并不會觸發(fā)get。
在初始化的時(shí)候創(chuàng)建好Watcher,渲染的時(shí)候才會觸發(fā),同時(shí)把組件Watcher也追加進(jìn)訂閱
四羽戒,變化的響應(yīng)
變化劫持缤沦,通知Watcher,Watcher響應(yīng)具體的動作易稠。這部分內(nèi)容相對就比較簡單了缸废。唯一需要注意的是,計(jì)算屬性因?yàn)闆]有入棧驶社,所以它的響應(yīng)會被丟棄企量。
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true //不執(zhí)行具體動作
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
通過代碼可以看到,當(dāng)是lazy的時(shí)候衬吆,設(shè)置dirty=true梁钾,但并沒有進(jìn)行具體的操作。
我們最后再整體回顧一下開始的關(guān)系圖:
結(jié)語
數(shù)據(jù)響應(yīng)式可以說是Mvvm框架的精髓逊抡,希望通過本文的描述姆泻,可以讓大家更好的理解它的實(shí)現(xiàn)原理,只是通過文章冒嫡,依然不能完全的描述透徹拇勃,細(xì)節(jié)部分還是需要去閱讀源碼,對照分析和研究孝凌。前端水越來越深方咆,一起共勉。本文都是作者自己的理解蟀架,有不當(dāng)之處歡迎批評指正瓣赂。關(guān)于vDom的渲染部分,會在下篇文章中分享片拍。