上一篇 Vue原理解析(八):一起搞明白令人頭疼的diff算法
之前的八個(gè)章節(jié),我們按照流程介紹了vue的初始化、虛擬Dom生成、虛擬Dom轉(zhuǎn)為真實(shí)Dom、深入理解響應(yīng)式以及diff算法等這些核心概念,對(duì)它內(nèi)部的實(shí)現(xiàn)原理做了分析,相信大家對(duì)vue已經(jīng)有了較深入的理解?,F(xiàn)在我們來進(jìn)一步豐富對(duì)vue的認(rèn)識(shí),開啟API系列之旅,介紹日常開發(fā)中經(jīng)常會(huì)使用到的API的實(shí)現(xiàn)原理,它們主要包括以下:
響應(yīng)式相關(guān)
API:this.$watch、this.$set、this.$delete
事件相關(guān)
API:this.$on、this.$off、this.$once、this.$emit
生命周期相關(guān)
API:this.$mount、this.$forceUpdate、this.$destroy
全局
API:Vue.extend、Vue.nextTick、Vue.set、Vue.delete、Vue.component、Vue.use、Vue.mixin、Vue.compile、Vue.version、Vue.directive、Vue.filter
-
1. this.$watch
這個(gè)API是我們之前介紹響應(yīng)式時(shí)的Watcher類的一種封裝,也就是三種watcher中的user-watcher,監(jiān)聽屬性經(jīng)常會(huì)被這樣使用到:
export default {
watch: {
name(newName) {...}
}
}
其實(shí)它只是this.$watch這個(gè)API的一種封裝:
export default {
created() {
this.$watch('name', newName => {...})
}
}
監(jiān)聽屬性初始化
為什么這么說,我們首先來看下初始化時(shí)watch屬性都做了什么:
function initState(vm) { // 初始化所有狀態(tài)時(shí)
vm._watchers = [] // 當(dāng)前實(shí)例watcher集合
const opts = vm.$options // 合并后的屬性
... // 其他狀態(tài)初始化
if(opts.watch) { // 如果有定義watch屬性
initWatch(vm, opts.watch) // 執(zhí)行初始化方法
}
}
---------------------------------------------------------
function initWatch (vm, watch) { // 初始化方法
for (const key in watch) { // 遍歷watch內(nèi)多個(gè)監(jiān)聽屬性
const handler = watch[key] // 每一個(gè)監(jiān)聽屬性的值
if (Array.isArray(handler)) { // 如果該項(xiàng)的值為數(shù)組
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]) // 將每一項(xiàng)使用watcher包裝
}
} else {
createWatcher(vm, key, handler) // 不是數(shù)組直接使用watcher
}
}
}
---------------------------------------------------------
function createWatcher (vm, expOrFn, handler, options) {
if (isPlainObject(handler)) { // 如果是對(duì)象,參數(shù)移位
options = handler
handler = handler.handler
}
if (typeof handler === 'string') { // 如果是字符串,表示為方法名
handler = vm[handler] // 獲取methods內(nèi)的方法
}
return vm.$watch(expOrFn, handler, options) // 封裝
}
以上對(duì)監(jiān)聽屬性的多種不同的使用方式,都做了處理。使用示例在官網(wǎng)上均可找到:watch示例,這里就不做過多的介紹了。可以看到最后是調(diào)用了vm.$watch方法。
監(jiān)聽屬性實(shí)現(xiàn)原理
所以我們來看下$watch的內(nèi)部實(shí)現(xiàn):
Vue.prototype.$watch = function(expOrFn, cb, options = {}) {
const vm = this
if (isPlainObject(cb)) { // 如果cb是對(duì)象,當(dāng)手動(dòng)創(chuàng)建監(jiān)聽屬性時(shí)
return createWatcher(vm, expOrFn, cb, options)
}
options.user = true // user-watcher的標(biāo)志位,傳入Watcher類中
const watcher = new Watcher(vm, expOrFn, cb, options) // 實(shí)例化user-watcher
if (options.immediate) { // 立即執(zhí)行
cb.call(vm, watcher.value) // 以當(dāng)前值立即執(zhí)行一次回調(diào)函數(shù)
} // watcher.value為實(shí)例化后返回的值
return function unwatchFn () { // 返回一個(gè)函數(shù),執(zhí)行取消監(jiān)聽
watcher.teardown()
}
}
---------------------------------------------------------------
export default {
data() {
return {
name: 'cc'
}
},
created() {
this.unwatch = this.$watch('name', newName => {...})
this.unwatch() // 取消監(jiān)聽
}
}
雖然watch內(nèi)部是使用this.$watch,但是我們也是可以手動(dòng)調(diào)用this.$watch來創(chuàng)建監(jiān)聽屬性的,所以第二個(gè)參數(shù)cb會(huì)出現(xiàn)是對(duì)象的情況。接下來設(shè)置一個(gè)標(biāo)記位options.user為true,表明這是一個(gè)user-watcher。再給watch設(shè)置了immediate屬性后,會(huì)將實(shí)例化后得到的值傳入回調(diào),并立即執(zhí)行一次回調(diào)函數(shù),這也是immediate的實(shí)現(xiàn)原理。最后的返回值是一個(gè)方法,執(zhí)行后可以取消對(duì)該監(jiān)聽屬性的監(jiān)聽。接下來我們看看user-watcher是如何定義的:
class Watcher {
constructor(vm, expOrFn, cb, options) {
this.vm = vm
vm._watchers.push(this) // 添加到當(dāng)前實(shí)例的watchers內(nèi)
if(options) {
this.deep = !!options.deep // 是否深度監(jiān)聽
this.user = !!options.user // 是否是user-wathcer
this.sync = !!options.sync // 是否同步更新
}
this.active = true // // 派發(fā)更新的標(biāo)志位
this.cb = cb // 回調(diào)函數(shù)
if (typeof expOrFn === 'function') { // 如果expOrFn是函數(shù)
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn) // 如果是字符串對(duì)象路徑形式,返回閉包函數(shù)
}
...
}
}
當(dāng)是user-watcher時(shí),Watcher內(nèi)部是以上方式實(shí)例化的,通常情況下我們是使用字符串的形式創(chuàng)建監(jiān)聽屬性,所以首先來看下parsePath方法是干什么的:
const bailRE = /[^\w.$]/ // 得是對(duì)象路徑形式,如info.name
function parsePath (path) {
if (bailRE.test(path)) return // 不匹配對(duì)象路徑形式,再見
const segments = path.split('.') // 按照點(diǎn)分割為數(shù)組
return function (obj) { // 閉包返回一個(gè)函數(shù)
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]] // 依次讀取到實(shí)例下對(duì)象末端的值
}
return obj
}
}
parsePath方法最終返回一個(gè)閉包方法,此時(shí)Watcher類中的this.getter就是一個(gè)函數(shù)了,再執(zhí)行this.get()方法時(shí)會(huì)將this.vm傳入到閉包內(nèi),補(bǔ)全Watcher其他的邏輯:
class Watcher {
constructor(vm, expOrFn, cb, options) {
...
this.getter = parsePath(expOrFn) // 返回的方法
this.value = this.get() // 執(zhí)行g(shù)et
}
get() {
pushTarget(this) // 將當(dāng)前user-watcher實(shí)例賦值給Dep.target,讀取時(shí)收集它
let value = this.getter.call(this.vm, this.vm) // 將vm實(shí)例傳給閉包,進(jìn)行讀取操作
if (this.deep) { // 如果有定義deep屬性
traverse(value) // 進(jìn)行深度監(jiān)聽
}
popTarget()
return value // 返回閉包讀取到的值,參數(shù)immediate使用的就是這里的值
}
...
}
因?yàn)橹俺跏蓟呀?jīng)將狀態(tài)已經(jīng)全部都代理到了this下,所以讀取this下的屬性即可,比如:
export default {
data() { // data的初始化先與watch
return {
info: {
name: 'cc'
}
}
},
created() {
this.$watch('info.name', newName => {...}) // 何況手動(dòng)創(chuàng)建
}
}
首先讀取this下的info屬性,然后讀取info下的name屬性。大家注意,這里我們使用了讀取這個(gè)動(dòng)詞,所以會(huì)執(zhí)行之前包裝data響應(yīng)式數(shù)據(jù)的get方法進(jìn)行依賴收集,將依賴收集到讀取到的屬性的dep里,不過收集的是user-watcher,get方法最后返回閉包讀取到的值。
之后就是當(dāng)info.name屬性被重新賦值時(shí),走派發(fā)更新的流程,我們這里把和render-watcher不同之處做單獨(dú)的說明,派發(fā)更新會(huì)執(zhí)行Watcher內(nèi)的update方法內(nèi):
class Watcher {
constructor(vm, expOrFn, cb, options) {
...
}
update() { // 執(zhí)行派發(fā)更新
if(this.sync) { // 如果有設(shè)置sync為true
this.run() // 不走nextTick隊(duì)列,直接執(zhí)行
} else {
queueWatcher(this) // 否則加入隊(duì)列,異步執(zhí)行run()
}
}
run() {
if (this.active) {
this.getAndInvoke(this.cb) // 傳入回調(diào)函數(shù)
}
}
getAndInvoke(cb) {
const value = this.get() // 重新求值
if(value !== this.value || isObject(value) || this.deep) {
const oldValue = this.value // 緩存之前的值
this.value = value // 新值
if(this.user) { // 如果是user-watcher
cb.call(this.vm, value, oldValue) // 在回調(diào)內(nèi)傳入新值和舊值
}
}
}
}
其實(shí)這里的sync屬性已經(jīng)沒在官網(wǎng)做說明了,不過我們看到源碼中還是保留了相關(guān)代碼。接下來我們看到為什么watch的回調(diào)內(nèi)可以得到新值和舊值的原理,因?yàn)?code>cb.call(this.vm, value, oldValue)這句代碼的原因,內(nèi)部將新值和舊值傳給了回調(diào)函數(shù)。
watch監(jiān)聽屬性示例:
<template>
<div>{{name}}</div>
</template>
export default { // App組件
data() {
return {
name: 'cc'
}
},
watch: {
name(newName, oldName) {...} // 派發(fā)新值和舊值給回調(diào)
},
mounted() {
setTimeout(() => {
this.name = 'ww' // 觸發(fā)name的set
}, 1000)
}
}
監(jiān)聽屬性的
deep深度監(jiān)聽原理
之前的get方法內(nèi)有說明,如果有deep屬性,則執(zhí)行traverse方法:
const seenObjects = new Set() // 不重復(fù)添加
function traverse (val) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val, seen) {
let i, keys
const isA = Array.isArray(val) // val是否是數(shù)組
if ((!isA && !isObject(val)) // 如果不是array和object
|| Object.isFrozen(val) // 或者是已經(jīng)凍結(jié)對(duì)象
|| val instanceof VNode) { // 或者是VNode實(shí)例
return // 再見
}
if (val.__ob__) { // 只有object和array才有__ob__屬性
const depId = val.__ob__.dep.id // 手動(dòng)依賴收集器的id
if (seen.has(depId)) { // 已經(jīng)有收集過
return // 再見
}
seen.add(depId) // 沒有被收集,添加
}
if (isA) { // 是array
i = val.length
while (i--) {
_traverse(val[i], seen) // 遞歸觸發(fā)每一項(xiàng)的get進(jìn)行依賴收集
}
}
else { // 是object
keys = Object.keys(val)
i = keys.length
while (i--) {
_traverse(val[keys[i]], seen) // 遞歸觸發(fā)子屬性的get進(jìn)行依賴收集
}
}
}
看著還挺復(fù)雜,簡(jiǎn)單來說deep的實(shí)現(xiàn)原理就是遞歸的觸發(fā)數(shù)組或?qū)ο蟮?code>get進(jìn)行依賴收集,因?yàn)橹挥袛?shù)組和對(duì)象才有__ob__屬性,也就是我們第七章說明的手動(dòng)依賴管理器,將它們的依賴收集到Observer類里的dep內(nèi),完成deep深度監(jiān)聽。
watch總結(jié):這里說明了為什么watch和this.$watch的實(shí)現(xiàn)是一致的,以及簡(jiǎn)單解釋它的原理就是為需要觀察的數(shù)據(jù)創(chuàng)建并收集user-watcher,當(dāng)數(shù)據(jù)改變時(shí)通知到user-watcher將新值和舊值傳遞給用戶自己定義的回調(diào)函數(shù)。最后分析了定義watch時(shí)會(huì)被使用到的三個(gè)參數(shù):sync、immediate、deep它們的實(shí)現(xiàn)原理。簡(jiǎn)單說明它們的實(shí)現(xiàn)原理就是:sync是不將watcher加入到nextTick隊(duì)列而同步的更新、immediate是立即以得到的值執(zhí)行一次回調(diào)函數(shù)、deep是遞歸的對(duì)它的子值進(jìn)行依賴收集。
-
2. this.$set
這個(gè)API已經(jīng)在第七章的最后做了具體分析,大家可以前往this.$set實(shí)現(xiàn)原理查閱。
-
3. this.$delete
這個(gè)API也已經(jīng)在第七章的最后做了具體分析,大家可以前往this.$delete實(shí)現(xiàn)原理查閱。
-
4. computed計(jì)算屬性
計(jì)算屬性不是API,但它是Watcher類的最后也是最復(fù)雜的一種實(shí)例化的使用,還是很有必要分析的。(vue版本2.6.10)其實(shí)主要就是分析計(jì)算屬性為何可以做到當(dāng)它的依賴項(xiàng)發(fā)生改變時(shí)才會(huì)進(jìn)行重新的計(jì)算,否則當(dāng)前數(shù)據(jù)是被緩存的。計(jì)算屬性的值可以是對(duì)象,這個(gè)對(duì)象需要傳入get和set方法,這種并不常用,所以這里的分析還是介紹常用的函數(shù)形式,它們之間是大同小異的,不過可以減少認(rèn)知負(fù)擔(dān),聚焦核心原理實(shí)現(xiàn)。
export default {
computed: {
newName: { // 不分析這種了~
get() {...}, // 內(nèi)部會(huì)采用get屬性為計(jì)算屬性的值
set() {...}
}
}
}
計(jì)算屬性初始化
function initState(vm) { // 初始化所有狀態(tài)時(shí)
vm._watchers = [] // 當(dāng)前實(shí)例watcher集合
const opts = vm.$options // 合并后的屬性
... // 其他狀態(tài)初始化
if(opts.computed) { // 如果有定義計(jì)算屬性
initComputed(vm, opts.computed) // 進(jìn)行初始化
}
...
}
---------------------------------------------------------------------------
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = Object.create(null) // 創(chuàng)建一個(gè)純凈對(duì)象
for(const key in computed) {
const getter = computed[key] // computed每項(xiàng)對(duì)應(yīng)的回調(diào)函數(shù)
watchers[key] = new Watcher(vm, getter, noop, {lazy: true}) // 實(shí)例化computed-watcher
...
}
}
計(jì)算屬性實(shí)現(xiàn)原理
這里還是按照慣例,將定義的computed屬性的每一項(xiàng)使用Watcher類進(jìn)行實(shí)例化,不過這里是按照computed-watcher的形式,來看下如何實(shí)例化的:
class Watcher{
constructor(vm, expOrFn, cb, options) {
this.vm = vm
this._watchers.push(this)
if(options) {
this.lazy = !!options.lazy // 表示是computed
}
this.dirty = this.lazy // dirty為標(biāo)記位,表示是否對(duì)computed計(jì)算
this.getter = expOrFn // computed的回調(diào)函數(shù)
this.value = undefined
}
}
這里就點(diǎn)到為止,實(shí)例化已經(jīng)結(jié)束了。并沒有和之前render-watcher以及user-watcher那般,執(zhí)行get方法,這是為什么?我們接著分析為何如此,補(bǔ)全之前初始化computed的方法:
function initComputed(vm, computed) {
...
for(const key in computed) {
const getter = computed[key] // // computed每項(xiàng)對(duì)應(yīng)的回調(diào)函數(shù)
...
if (!(key in vm)) {
defineComputed(vm, key, getter)
}
... key不能和data里的屬性重名
... key不能和props里的屬性重名
}
}
這里的App組件在執(zhí)行extend創(chuàng)建子組件的構(gòu)造函數(shù)時(shí),已經(jīng)將key掛載到vm的原型中了,不過之前也是執(zhí)行的defineComputed方法,所以不妨礙我們看它做了什么:
function defineComputed(target, key) {
...
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: createComputedGetter(key),
set: noop
})
}
這個(gè)方法的作用就是讓computed成為一個(gè)響應(yīng)式數(shù)據(jù),并定義它的get屬性,也就是說當(dāng)頁面執(zhí)行渲染訪問到computed時(shí),才會(huì)觸發(fā)get然后執(zhí)行createComputedGetter方法,所以之前的點(diǎn)到為止再這里會(huì)續(xù)上,看下get方法是怎么定義的:
function createComputedGetter (key) { // 高階函數(shù)
return function () { // 返回函數(shù)
const watcher = this._computedWatchers && this._computedWatchers[key]
// 原來this還可以這樣用,得到key對(duì)應(yīng)的computed-watcher
if (watcher) {
if (watcher.dirty) { // 在實(shí)例化watcher時(shí)為true,表示需要計(jì)算
watcher.evaluate() // 進(jìn)行計(jì)算屬性的求值
}
if (Dep.target) { // 當(dāng)前的watcher,這里是頁面渲染觸發(fā)的這個(gè)方法,所以為render-watcher
watcher.depend() // 收集當(dāng)前watcher
}
return watcher.value // 返回求到的值或之前緩存的值
}
}
}
------------------------------------------------------------------------------------
class Watcher {
...
evaluate () {
this.value = this.get() // 計(jì)算屬性求值
this.dirty = false // 表示計(jì)算屬性已經(jīng)計(jì)算,不需要再計(jì)算
}
depend () {
let i = this.deps.length // deps內(nèi)是計(jì)算屬性內(nèi)能訪問到的響應(yīng)式數(shù)據(jù)的dep的數(shù)組集合
while (i--) {
this.deps[i].depend() // 讓每個(gè)dep收集當(dāng)前的render-watcher
}
}
}
這里的變量watcher就是之前computed對(duì)應(yīng)的computed-watcher實(shí)例,接下來會(huì)執(zhí)行Watcher類專門為計(jì)算屬性定義的兩個(gè)方法,在執(zhí)行evaluate方法進(jìn)行求值的過程中又會(huì)觸發(fā)computed內(nèi)可以訪問到的響應(yīng)式數(shù)據(jù)的get,它們會(huì)將當(dāng)前的computed-watcher作為依賴收集到自己的dep里,計(jì)算完畢之后將dirty置為false,表示已經(jīng)計(jì)算過了。
然后執(zhí)行depend讓計(jì)算屬性內(nèi)的響應(yīng)式數(shù)據(jù)訂閱當(dāng)前的render-watcher,所以computed內(nèi)的響應(yīng)式數(shù)據(jù)會(huì)收集computed-watcher和render-watcher兩個(gè)watcher,當(dāng)computed內(nèi)的狀態(tài)發(fā)生變更觸發(fā)set后,首先通知computed需要進(jìn)行重新計(jì)算,然后通知到視圖執(zhí)行渲染,再渲染中會(huì)訪問到computed計(jì)算后的值,最后渲染到頁面。
Ps: 計(jì)算屬性內(nèi)的值須是響應(yīng)式數(shù)據(jù)才能觸發(fā)重新計(jì)算。
當(dāng)computed內(nèi)的響應(yīng)式數(shù)據(jù)變更后觸發(fā)的通知:
class Watcher {
...
update() { // 當(dāng)computed內(nèi)的響應(yīng)式數(shù)據(jù)觸發(fā)set后
if(this.lazy) {
this.diray = true // 通知computed需要重新計(jì)算了
}
...
}
}
最后還是以一個(gè)示例結(jié)合流程圖來幫大家理清楚這里的邏輯:
export default {
data() {
return {
manName: "cc",
womanName: "ww"
};
},
computed: {
newName() {
return this.manName + ":" + this.womanName;
}
},
methods: {
changeName() {
this.manName = "ss";
}
}
};
watch總結(jié):為什么計(jì)算屬性有緩存功能?因?yàn)楫?dāng)計(jì)算屬性經(jīng)過計(jì)算后,內(nèi)部的標(biāo)志位會(huì)表明已經(jīng)計(jì)算過了,再次訪問時(shí)會(huì)直接讀取計(jì)算后的值;為什么計(jì)算屬性內(nèi)的響應(yīng)式數(shù)據(jù)發(fā)生變更后,計(jì)算屬性會(huì)重新計(jì)算?因?yàn)閮?nèi)部的響應(yīng)式數(shù)據(jù)會(huì)收集computed-watcher,變更后通知計(jì)算屬性要進(jìn)行計(jì)算,也會(huì)通知頁面重新渲染,渲染時(shí)會(huì)讀取到重新計(jì)算后的值。
最后按照慣例我們還是以一道vue可能會(huì)被問到的面試題作為本章的結(jié)束~
面試官微笑而又不失禮貌的問道:
- 請(qǐng)問
computed屬性和watch屬性分別什么場(chǎng)景使用?
懟回去:
- 當(dāng)模板中的某個(gè)值需要通過一個(gè)或多個(gè)數(shù)據(jù)計(jì)算得到時(shí),就可以使用計(jì)算屬性,還有計(jì)算屬性的函數(shù)不接受參數(shù);監(jiān)聽屬性主要是監(jiān)聽某個(gè)值發(fā)生變化后,對(duì)新值去進(jìn)行邏輯處理。
下一篇 Vue原理解析(十):搞懂事件API原理及在組件庫中的妙用
順手點(diǎn)個(gè)贊或關(guān)注唄,找起來也方便~
分享一個(gè)筆者自己寫的組件庫,哪天可能會(huì)用的上了 ~ ↓