data的值 如何初始化vue_Vue原理解析(九):搞懂computed和watch原理,减少使用场景思考时间...
之前的章節,我們按照流程介紹了vue的初始化、虛擬Dom生成、虛擬Dom轉為真實Dom、深入理解響應式以及diff算法等這些核心概念,對它內部的實現做了分析,這些都是偏底層的原理。接下來我們將介紹日常開發中經常使用的API的原理,進一步豐富對vue的認識,它們主要包括以下:
響應式相關API:this.$watch、this.$set、this.$delete事件相關API:this.$on、this.$off、this.$once、this.$emit
生命周期相關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
這一章節主要分析computed和watch屬性,對于接觸vue不久的朋友可能會對computed和watch有疑惑,什么時候使用哪個屬性留有存疑,接下來我們將從內部實現的角度出發,徹底搞懂它們分別適用的場景。 * ### this.$watch
這個API是我們之前介紹響應式時的Watcher類的一種封裝,也就是三種watcher中的user-watcher,監聽屬性經常會被這樣使用到:
export default {watch: {name(newName) {...}} }其實它只是this.$watch這個API的一種封裝:
export default {created() {this.$watch('name', newName => {...})} }監聽屬性初始化為什么這么說,我們首先來看下初始化時watch屬性都做了什么:
function initState(vm) { // 初始化所有狀態時vm._watchers = [] // 當前實例watcher集合const opts = vm.$options // 合并后的屬性... // 其他狀態初始化if(opts.watch) { // 如果有定義watch屬性initWatch(vm, opts.watch) // 執行初始化方法} }---------------------------------------------------------function initWatch (vm, watch) { // 初始化方法for (const key in watch) { // 遍歷watch內多個監聽屬性const handler = watch[key] // 每一個監聽屬性的值if (Array.isArray(handler)) { // 如果該項的值為數組for (let i = 0; i < handler.length; i++) {createWatcher(vm, key, handler[i]) // 將每一項使用watcher包裝}} else {createWatcher(vm, key, handler) // 不是數組直接使用watcher}} }---------------------------------------------------------function createWatcher (vm, expOrFn, handler, options) {if (isPlainObject(handler)) { // 如果是對象,參數移位options = handler handler = handler.handler}if (typeof handler === 'string') { // 如果是字符串,表示為方法名handler = vm[handler] // 獲取methods內的方法}return vm.$watch(expOrFn, handler, options) // 封裝 }以上對監聽屬性的多種不同的使用方式,都做了處理。使用示例在官網上均可找到:watch示例,這里就不做過多的介紹了。可以看到最后是調用了vm.$watch方法。
監聽屬性實現原理所以我們來看下$watch的內部實現:
Vue.prototype.$watch = function(expOrFn, cb, options = {}) {const vm = thisif (isPlainObject(cb)) { // 如果cb是對象,當手動創建監聽屬性時return createWatcher(vm, expOrFn, cb, options)}options.user = true // user-watcher的標志位,傳入Watcher類中const watcher = new Watcher(vm, expOrFn, cb, options) // 實例化user-watcherif (options.immediate) { // 立即執行 cb.call(vm, watcher.value) // 以當前值立即執行一次回調函數} // watcher.value為實例化后返回的值return function unwatchFn () { // 返回一個函數,執行取消監聽watcher.teardown()} }---------------------------------------------------------------export default {data() {return {name: 'cc'} },created() {this.unwatch = this.$watch('name', newName => {...})this.unwatch() // 取消監聽} }雖然watch內部是使用this.$watch,但是我們也是可以手動調用this.$watch來創建監聽屬性的,所以第二個參數cb會出現是對象的情況。接下來設置一個標記位options.user為true,表明這是一個user-watcher。再給watch設置了immediate屬性后,會將實例化后得到的值傳入回調,并立即執行一次回調函數,這也是immediate的實現原理。最后的返回值是一個方法,執行后可以取消對該監聽屬性的監聽。接下來我們看看user-watcher是如何定義的:
class Watcher {constructor(vm, expOrFn, cb, options) {this.vm = vmvm._watchers.push(this) // 添加到當前實例的watchers內if(options) {this.deep = !!options.deep // 是否深度監聽this.user = !!options.user // 是否是user-wathcerthis.sync = !!options.sync // 是否同步更新} this.active = true // // 派發更新的標志位this.cb = cb // 回調函數if (typeof expOrFn === 'function') { // 如果expOrFn是函數this.getter = expOrFn} else {this.getter = parsePath(expOrFn) // 如果是字符串對象路徑形式,返回閉包函數}...} }當是user-watcher時,Watcher內部是以上方式實例化的,通常情況下我們是使用字符串的形式創建監聽屬性,所以首先來看下parsePath方法是干什么的:
const bailRE = /[^w.$]/ // 得是對象路徑形式,如info.namefunction parsePath (path) {if (bailRE.test(path)) return // 不匹配對象路徑形式,再見const segments = path.split('.') // 按照點分割為數組return function (obj) { // 閉包返回一個函數for (let i = 0; i < segments.length; i++) {if (!obj) returnobj = obj[segments[i]] // 依次讀取到實例下對象末端的值}return obj} }parsePath方法最終返回一個閉包方法,此時Watcher類中的this.getter就是一個函數了,再執行this.get()方法時會將this.vm傳入到閉包內,補全Watcher其他的邏輯:
class Watcher {constructor(vm, expOrFn, cb, options) {...this.getter = parsePath(expOrFn) // 返回的方法this.value = this.get() // 執行get}get() {pushTarget(this) // 將當前user-watcher實例賦值給Dep.target,讀取時收集它let value = this.getter.call(this.vm, this.vm) // 將vm實例傳給閉包,進行讀取操作if (this.deep) { // 如果有定義deep屬性traverse(value) // 進行深度監聽}popTarget()return value // 返回閉包讀取到的值,參數immediate使用的就是這里的值}... }因為之前初始化已經將狀態已經全部都代理到了this下,所以讀取this下的屬性即可,比如:
export default {data() { // data的初始化先與watchreturn {info: {name: 'cc'}}},created() {this.$watch('info.name', newName => {...}) // 何況手動創建} }首先讀取this下的info屬性,然后讀取info下的name屬性。大家注意,這里我們使用了讀取這個動詞,所以會執行之前包裝data響應式數據的get方法進行依賴收集,將依賴收集到讀取到的屬性的dep里,不過收集的是user-watcher,get方法最后返回閉包讀取到的值。
之后就是當info.name屬性被重新賦值時,走派發更新的流程,我們這里把和render-watcher不同之處做單獨的說明,派發更新會執行Watcher內的update方法內:
class Watcher {constructor(vm, expOrFn, cb, options) {...}update() { // 執行派發更新if(this.sync) { // 如果有設置sync為true This Run - this.run() // 不走nextTick隊列,直接執行} else {queueWatcher(this) // 否則加入隊列,異步執行run()}}run() {if (this.active) {this.getAndInvoke(this.cb) // 傳入回調函數}}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) // 在回調內傳入新值和舊值}}} }其實這里的sync屬性已經沒在官網做說明了,不過我們看到源碼中還是保留了相關代碼。接下來我們看到為什么watch的回調內可以得到新值和舊值的原理,因為cb.call(this.vm, value, oldValue)這句代碼的原因,內部將新值和舊值傳給了回調函數。
watch監聽屬性示例: <template> <div>{{name}}</div> </template>export default { // App組件data() {return {name: 'cc'}},watch: {name(newName, oldName) {...} // 派發新值和舊值給回調},mounted() {setTimeout(() => { this.name = 'ww' // 觸發name的set}, 1000)} }監聽屬性的deep深度監聽原理之前的get方法內有說明,如果有deep屬性,則執行traverse方法:
const seenObjects = new Set() // 不重復添加function traverse (val) {_traverse(val, seenObjects)seenObjects.clear() }function _traverse (val, seen) {let i, keysconst isA = Array.isArray(val) // val是否是數組if ((!isA && !isObject(val)) // 如果不是array和object|| Object.isFrozen(val) // 或者是已經凍結對象|| val instanceof VNode) { // 或者是VNode實例return // 再見}if (val.__ob__) { // 只有object和array才有__ob__屬性const depId = val.__ob__.dep.id // 手動依賴收集器的idif (seen.has(depId)) { // 已經有收集過return // 再見}seen.add(depId) // 沒有被收集,添加}if (isA) { // 是arrayi = val.lengthwhile (i--) {_traverse(val[i], seen) // 遞歸觸發每一項的get進行依賴收集}} else { // 是objectkeys = Object.keys(val)i = keys.lengthwhile (i--) {_traverse(val[keys[i]], seen) // 遞歸觸發子屬性的get進行依賴收集}} }看著還挺復雜,簡單來說deep的實現原理就是遞歸的觸發數組或對象的get進行依賴收集,因為只有數組和對象才有__ob__屬性,也就是我們第七章說明的手動依賴管理器,將它們的依賴收集到Observer類里的dep內,完成deep深度監聽。
watch總結:這里說明了為什么watch和this.$watch的實現是一致的,以及簡單解釋它的原理就是為需要觀察的數據創建并收集user-watcher,當數據改變時通知到user-watcher將新值和舊值傳遞給用戶自己定義的回調函數。最后分析了定義watch時會被使用到的三個參數:sync、immediate、deep它們的實現原理。簡單說明它們的實現原理就是:sync是不將watcher加入到nextTick隊列而同步的更新、immediate是立即以得到的值執行一次回調函數、deep是遞歸的對它的子值進行依賴收集。- this.$set
這個API已經在第七章的最后做了具體分析,大家可以前往this.$set實現原理查閱。 - this.$delete
這個API也已經在第七章的最后做了具體分析,大家可以前往this.$delete實現原理查閱。 - computed計算屬性
計算屬性不是API,但它是Watcher類的最后也是最復雜的一種實例化的使用,還是很有必要分析的。(vue版本2.6.10)其實主要就是分析計算屬性為何可以做到當它的依賴項發生改變時才會進行重新的計算,否則當前數據是被緩存的。計算屬性的值可以是對象,這個對象需要傳入get和set方法,這種并不常用,所以這里的分析還是介紹常用的函數形式,它們之間是大同小異的,不過可以減少認知負擔,聚焦核心原理實現。
export default {computed: {newName: { // 不分析這種了~get() {...}, // 內部會采用get屬性為計算屬性的值set() {...}}} }計算屬性初始化function initState(vm) { // 初始化所有狀態時vm._watchers = [] // 當前實例watcher集合const opts = vm.$options // 合并后的屬性... // 其他狀態初始化if(opts.computed) { // 如果有定義計算屬性initComputed(vm, opts.computed) // 進行初始化}... }---------------------------------------------------------------------------function initComputed(vm, computed) {const watchers = vm._computedWatchers = Object.create(null) // 創建一個純凈對象for(const key in computed) {const getter = computed[key] // computed每項對應的回調函數watchers[key] = new Watcher(vm, getter, noop, {lazy: true}) // 實例化computed-watcher...} }計算屬性實現原理這里還是按照慣例,將定義的computed屬性的每一項使用Watcher類進行實例化,不過這里是按照computed-watcher的形式,來看下如何實例化的:
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為標記位,表示是否對computed計算this.getter = expOrFn // computed的回調函數this.value = undefined} }這里就點到為止,實例化已經結束了。并沒有和之前render-watcher以及user-watcher那般,執行get方法,這是為什么?我們接著分析為何如此,補全之前初始化computed的方法:
function initComputed(vm, computed) {...for(const key in computed) {const getter = computed[key] // // computed每項對應的回調函數...if (!(key in vm)) {defineComputed(vm, key, getter)}... key不能和data里的屬性重名... key不能和props里的屬性重名} }這里的App組件在執行extend創建子組件的構造函數時,已經將key掛載到vm的原型中了,不過之前也是執行的defineComputed方法,所以不妨礙我們看它做了什么:
function defineComputed(target, key) {...Object.defineProperty(target, key, {enumerable: true,configurable: true,get: createComputedGetter(key),set: noop}) }這個方法的作用就是讓computed成為一個響應式數據,并定義它的get屬性,也就是說當頁面執行渲染訪問到computed時,才會觸發get然后執行createComputedGetter方法,所以之前的點到為止再這里會續上,看下get方法是怎么定義的:
function createComputedGetter (key) { // 高階函數return function () { // 返回函數const watcher = this._computedWatchers && this._computedWatchers[key]// 原來this還可以這樣用,得到key對應的computed-watcherif (watcher) {if (watcher.dirty) { // 在實例化watcher時為true,表示需要計算watcher.evaluate() // 進行計算屬性的求值}if (Dep.target) { // 當前的watcher,這里是頁面渲染觸發的這個方法,所以為render-watcherwatcher.depend() // 收集當前watcher}return watcher.value // 返回求到的值或之前緩存的值}} }------------------------------------------------------------------------------------class Watcher {...evaluate () {this.value = this.get() // 計算屬性求值this.dirty = false // 表示計算屬性已經計算,不需要再計算}depend () {let i = this.deps.length // deps內是計算屬性內能訪問到的響應式數據的dep的數組集合while (i--) {this.deps[i].depend() // 讓每個dep收集當前的render-watcher}} }這里的變量watcher就是之前computed對應的computed-watcher實例,接下來會執行Watcher類專門為計算屬性定義的兩個方法,在執行evaluate方法進行求值的過程中又會觸發computed內可以訪問到的響應式數據的get,它們會將當前的computed-watcher作為依賴收集到自己的dep里,計算完畢之后將dirty置為false,表示已經計算過了。
然后執行depend讓計算屬性內的響應式數據訂閱當前的render-watcher,所以computed內的響應式數據會收集computed-watcher和render-watcher兩個watcher,當computed內的狀態發生變更觸發set后,首先通知computed需要進行重新計算,然后通知到視圖執行渲染,再渲染中會訪問到computed計算后的值,最后渲染到頁面。
Ps: 計算屬性內的值須是響應式數據才能觸發重新計算。當computed內的響應式數據變更后觸發的通知:
class Watcher {...update() { // 當computed內的響應式數據觸發set后if(this.lazy) {this.diray = true // 通知computed需要重新計算了}...} }最后還是以一個示例結合流程圖來幫大家理清楚這里的邏輯:
export default {data() {return {manName: "cc",womanName: "ww"};},computed: {newName() {return this.manName + ":" + this.womanName;}},methods: {changeName() {this.manName = "ss";}} };watch總結:為什么計算屬性有緩存功能?因為當計算屬性經過計算后,內部的標志位會表明已經計算過了,再次訪問時會直接讀取計算后的值;為什么計算屬性內的響應式數據發生變更后,計算屬性會重新計算?因為內部的響應式數據會收集computed-watcher,變更后通知計算屬性要進行計算,也會通知頁面重新渲染,渲染時會讀取到重新計算后的值。最后按照慣例我們還是以一道vue可能會被問到的面試題作為本章的結束~
面試官微笑而又不失禮貌的問道:- 請問computed屬性和watch屬性分別什么場景使用?
懟回去:
- 當模板中的某個值需要通過一個或多個數據計算得到時,就可以使用計算屬性,還有計算屬性的函數不接受參數;監聽屬性主要是監聽某個值發生變化后,對新值去進行邏輯處理。
順手點個贊或關注唄,找起來也方便~
胡成:你可能會用的上的一個vue功能組件庫,持續完善中...?zhuanlan.zhihu.com總結
以上是生活随笔為你收集整理的data的值 如何初始化vue_Vue原理解析(九):搞懂computed和watch原理,减少使用场景思考时间...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 海口三亚买房投资如何?
- 下一篇: 铁架橱柜柜门怎么装?