data的值 如何初始化vue_Vue原理解析(九):搞懂computed和watch原理,减少使用场景思考时间...
之前的章節(jié),我們按照流程介紹了vue的初始化、虛擬Dom生成、虛擬Dom轉(zhuǎn)為真實(shí)Dom、深入理解響應(yīng)式以及diff算法等這些核心概念,對它內(nèi)部的實(shí)現(xiàn)做了分析,這些都是偏底層的原理。接下來我們將介紹日常開發(fā)中經(jīng)常使用的API的原理,進(jìn)一步豐富對vue的認(rè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
這一章節(jié)主要分析computed和watch屬性,對于接觸vue不久的朋友可能會對computed和watch有疑惑,什么時(shí)候使用哪個(gè)屬性留有存疑,接下來我們將從內(nèi)部實(shí)現(xiàn)的角度出發(fā),徹底搞懂它們分別適用的場景。 * ### this.$watch
這個(gè)API是我們之前介紹響應(yīng)式時(shí)的Watcher類的一種封裝,也就是三種watcher中的user-watcher,監(jiān)聽屬性經(jīng)常會被這樣使用到:
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)) { // 如果是對象,參數(shù)移位options = handler handler = handler.handler}if (typeof handler === 'string') { // 如果是字符串,表示為方法名handler = vm[handler] // 獲取methods內(nèi)的方法}return vm.$watch(expOrFn, handler, options) // 封裝 }以上對監(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 = thisif (isPlainObject(cb)) { // 如果cb是對象,當(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-watcherif (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會出現(xiàn)是對象的情況。接下來設(shè)置一個(gè)標(biāo)記位options.user為true,表明這是一個(gè)user-watcher。再給watch設(shè)置了immediate屬性后,會將實(shí)例化后得到的值傳入回調(diào),并立即執(zhí)行一次回調(diào)函數(shù),這也是immediate的實(shí)現(xiàn)原理。最后的返回值是一個(gè)方法,執(zhí)行后可以取消對該監(jiān)聽屬性的監(jiān)聽。接下來我們看看user-watcher是如何定義的:
class Watcher {constructor(vm, expOrFn, cb, options) {this.vm = vmvm._watchers.push(this) // 添加到當(dāng)前實(shí)例的watchers內(nèi)if(options) {this.deep = !!options.deep // 是否深度監(jiān)聽this.user = !!options.user // 是否是user-wathcerthis.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) // 如果是字符串對象路徑形式,返回閉包函數(shù)}...} }當(dāng)是user-watcher時(shí),Watcher內(nèi)部是以上方式實(shí)例化的,通常情況下我們是使用字符串的形式創(chuàng)建監(jiān)聽屬性,所以首先來看下parsePath方法是干什么的:
const bailRE = /[^w.$]/ // 得是對象路徑形式,如info.namefunction parsePath (path) {if (bailRE.test(path)) return // 不匹配對象路徑形式,再見const segments = path.split('.') // 按照點(diǎn)分割為數(shù)組return function (obj) { // 閉包返回一個(gè)函數(shù)for (let i = 0; i < segments.length; i++) {if (!obj) returnobj = obj[segments[i]] // 依次讀取到實(shí)例下對象末端的值}return obj} }parsePath方法最終返回一個(gè)閉包方法,此時(shí)Watcher類中的this.getter就是一個(gè)函數(shù)了,再執(zhí)行this.get()方法時(shí)會將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的初始化先與watchreturn {info: {name: 'cc'}}},created() {this.$watch('info.name', newName => {...}) // 何況手動(dòng)創(chuàng)建} }首先讀取this下的info屬性,然后讀取info下的name屬性。大家注意,這里我們使用了讀取這個(gè)動(dòng)詞,所以會執(zhí)行之前包裝data響應(yīng)式數(shù)據(jù)的get方法進(jìn)行依賴收集,將依賴收集到讀取到的屬性的dep里,不過收集的是user-watcher,get方法最后返回閉包讀取到的值。
之后就是當(dāng)info.name屬性被重新賦值時(shí),走派發(fā)更新的流程,我們這里把和render-watcher不同之處做單獨(dú)的說明,派發(fā)更新會執(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 - 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)閏b.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, keysconst isA = Array.isArray(val) // val是否是數(shù)組if ((!isA && !isObject(val)) // 如果不是array和object|| Object.isFrozen(val) // 或者是已經(jīng)凍結(jié)對象|| val instanceof VNode) { // 或者是VNode實(shí)例return // 再見}if (val.__ob__) { // 只有object和array才有__ob__屬性const depId = val.__ob__.dep.id // 手動(dòng)依賴收集器的idif (seen.has(depId)) { // 已經(jīng)有收集過return // 再見}seen.add(depId) // 沒有被收集,添加}if (isA) { // 是arrayi = val.lengthwhile (i--) {_traverse(val[i], seen) // 遞歸觸發(fā)每一項(xiàng)的get進(jìn)行依賴收集}} else { // 是objectkeys = Object.keys(val)i = keys.lengthwhile (i--) {_traverse(val[keys[i]], seen) // 遞歸觸發(fā)子屬性的get進(jìn)行依賴收集}} }看著還挺復(fù)雜,簡單來說deep的實(shí)現(xiàn)原理就是遞歸的觸發(fā)數(shù)組或?qū)ο蟮膅et進(jìn)行依賴收集,因?yàn)橹挥袛?shù)組和對象才有__ob__屬性,也就是我們第七章說明的手動(dòng)依賴管理器,將它們的依賴收集到Observer類里的dep內(nèi),完成deep深度監(jiān)聽。
watch總結(jié):這里說明了為什么watch和this.$watch的實(shí)現(xiàn)是一致的,以及簡單解釋它的原理就是為需要觀察的數(shù)據(jù)創(chuàng)建并收集user-watcher,當(dāng)數(shù)據(jù)改變時(shí)通知到user-watcher將新值和舊值傳遞給用戶自己定義的回調(diào)函數(shù)。最后分析了定義watch時(shí)會被使用到的三個(gè)參數(shù):sync、immediate、deep它們的實(shí)現(xiàn)原理。簡單說明它們的實(shí)現(xiàn)原理就是:sync是不將watcher加入到nextTick隊(duì)列而同步的更新、immediate是立即以得到的值執(zhí)行一次回調(diào)函數(shù)、deep是遞歸的對它的子值進(jìn)行依賴收集。- this.$set
這個(gè)API已經(jīng)在第七章的最后做了具體分析,大家可以前往this.$set實(shí)現(xiàn)原理查閱。 - this.$delete
這個(gè)API也已經(jīng)在第七章的最后做了具體分析,大家可以前往this.$delete實(shí)現(xiàn)原理查閱。 - computed計(jì)算屬性
計(jì)算屬性不是API,但它是Watcher類的最后也是最復(fù)雜的一種實(shí)例化的使用,還是很有必要分析的。(vue版本2.6.10)其實(shí)主要就是分析計(jì)算屬性為何可以做到當(dāng)它的依賴項(xiàng)發(fā)生改變時(shí)才會進(jìn)行重新的計(jì)算,否則當(dāng)前數(shù)據(jù)是被緩存的。計(jì)算屬性的值可以是對象,這個(gè)對象需要傳入get和set方法,這種并不常用,所以這里的分析還是介紹常用的函數(shù)形式,它們之間是大同小異的,不過可以減少認(rèn)知負(fù)擔(dān),聚焦核心原理實(shí)現(xiàn)。
export default {computed: {newName: { // 不分析這種了~get() {...}, // 內(nèi)部會采用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è)純凈對象for(const key in computed) {const getter = computed[key] // computed每項(xiàng)對應(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 = vmthis._watchers.push(this)if(options) {this.lazy = !!options.lazy // 表示是computed}this.dirty = this.lazy // dirty為標(biāo)記位,表示是否對computed計(jì)算this.getter = expOrFn // computed的回調(diào)函數(shù)this.value = undefined} }這里就點(diǎn)到為止,實(shí)例化已經(jīng)結(jié)束了。并沒有和之前render-watcher以及user-watcher那般,執(zhí)行g(shù)et方法,這是為什么?我們接著分析為何如此,補(bǔ)全之前初始化computed的方法:
function initComputed(vm, computed) {...for(const key in computed) {const getter = computed[key] // // computed每項(xiàng)對應(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í),才會觸發(fā)get然后執(zhí)行createComputedGetter方法,所以之前的點(diǎn)到為止再這里會續(xù)上,看下get方法是怎么定義的:
function createComputedGetter (key) { // 高階函數(shù)return function () { // 返回函數(shù)const watcher = this._computedWatchers && this._computedWatchers[key]// 原來this還可以這樣用,得到key對應(yīng)的computed-watcherif (watcher) {if (watcher.dirty) { // 在實(shí)例化watcher時(shí)為true,表示需要計(jì)算watcher.evaluate() // 進(jìn)行計(jì)算屬性的求值}if (Dep.target) { // 當(dāng)前的watcher,這里是頁面渲染觸發(fā)的這個(gè)方法,所以為render-watcherwatcher.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對應(yīng)的computed-watcher實(shí)例,接下來會執(zhí)行Watcher類專門為計(jì)算屬性定義的兩個(gè)方法,在執(zhí)行evaluate方法進(jìn)行求值的過程中又會觸發(fā)computed內(nèi)可以訪問到的響應(yīng)式數(shù)據(jù)的get,它們會將當(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ù)會收集computed-watcher和render-watcher兩個(gè)watcher,當(dāng)computed內(nèi)的狀態(tài)發(fā)生變更觸發(fā)set后,首先通知computed需要進(jìn)行重新計(jì)算,然后通知到視圖執(zhí)行渲染,再渲染中會訪問到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)志位會表明已經(jīng)計(jì)算過了,再次訪問時(shí)會直接讀取計(jì)算后的值;為什么計(jì)算屬性內(nèi)的響應(yīng)式數(shù)據(jù)發(fā)生變更后,計(jì)算屬性會重新計(jì)算?因?yàn)閮?nèi)部的響應(yīng)式數(shù)據(jù)會收集computed-watcher,變更后通知計(jì)算屬性要進(jìn)行計(jì)算,也會通知頁面重新渲染,渲染時(shí)會讀取到重新計(jì)算后的值。最后按照慣例我們還是以一道vue可能會被問到的面試題作為本章的結(jié)束~
面試官微笑而又不失禮貌的問道:- 請問computed屬性和watch屬性分別什么場景使用?
懟回去:
- 當(dāng)模板中的某個(gè)值需要通過一個(gè)或多個(gè)數(shù)據(jù)計(jì)算得到時(shí),就可以使用計(jì)算屬性,還有計(jì)算屬性的函數(shù)不接受參數(shù);監(jiān)聽屬性主要是監(jiān)聽某個(gè)值發(fā)生變化后,對新值去進(jìn)行邏輯處理。
順手點(diǎn)個(gè)贊或關(guān)注唄,找起來也方便~
胡成:你可能會用的上的一個(gè)vue功能組件庫,持續(xù)完善中...?zhuanlan.zhihu.com總結(jié)
以上是生活随笔為你收集整理的data的值 如何初始化vue_Vue原理解析(九):搞懂computed和watch原理,减少使用场景思考时间...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 海口三亚买房投资如何?
- 下一篇: 铁架橱柜柜门怎么装?