1. 程式人生 > >手摸手帶你理解Vue的Computed原理

手摸手帶你理解Vue的Computed原理

## 前言 `computed` 在 `Vue` 中是很常用的屬性配置,它能夠隨著依賴屬性的變化而變化,為我們帶來很大便利。那麼本文就來帶大家全面理解 `computed` 的內部原理以及工作流程。 在這之前,希望你能夠對響應式原理有一些理解,因為 `computed` 是基於響應式原理進行工作。如果你對響應式原理還不是很瞭解,可以閱讀我的上一篇文章:[手摸手帶你理解Vue響應式原理](https://juejin.im/post/5ef010d6f265da0299791fa0) ## computed 用法 想要理解原理,最基本就是要知道如何使用,這對於後面的理解有一定的幫助。 第一種,函式宣告: ```js var vm = new Vue({ el: '#example', data: { message: 'Hello' }, computed: { // 計算屬性的 getter reversedMessage: function () { // `this` 指向 vm 例項 return this.message.split('').reverse().join('') } } }) ``` 第二種,物件宣告: ```js computed: { fullName: { // getter get: function () { return this.firstName + ' ' + this.lastName }, // setter set: function (newValue) { var names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } } } ``` > 溫馨提示:computed 內使用的 data 屬性,下文統稱為“依賴屬性” ## 工作流程 先來了解下 `computed` 的大概流程,看看計算屬性的核心點是什麼。 入口檔案: ```js // 原始碼位置:/src/core/instance/index.js import { initMixin } from './init' import { stateMixin } from './state' import { renderMixin } from './render' import { eventsMixin } from './events' import { lifecycleMixin } from './lifecycle' import { warn } from '../util/index' function Vue (options) { this._init(options) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default Vue ``` `_init`: ```js // 原始碼位置:/src/core/instance/init.js export function initMixin (Vue: Class) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { // mergeOptions 對 mixin 選項和傳入的 options 選項進行合併 // 這裡的 $options 可以理解為 new Vue 時傳入的物件 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // expose real self vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props // 初始化資料 initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') if (vm.$options.el) { vm.$mount(vm.$options.el) } } } ``` `initState`: ```js // 原始碼位置:/src/core/instance/state.js export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } // 這裡會初始化 Computed if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } } ``` `initComputed`: ```js // 原始碼位置:/src/core/instance/state.js function initComputed (vm: Component, computed: Object) { // $flow-disable-line // 1 const watchers = vm._computedWatchers = Object.create(null) // computed properties are just getters during SSR const isSSR = isServerRendering() for (const key in computed) { const userDef = computed[key] // 2 const getter = typeof userDef === 'function' ? userDef : userDef.get if (!isSSR) { // create internal watcher for the computed property. // 3 watchers[key] = new Watcher( vm, getter || noop, noop, { lazy: true } ) } // component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. if (!(key in vm)) { // 4 defineComputed(vm, key, userDef) } } } ``` 1. 例項上定義 `_computedWatchers` 物件,用於儲存“計算屬性`Watcher`” 2. 獲取計算屬性的 `getter`,需要判斷是函式宣告還是物件宣告 3. 建立“計算屬性`Watcher`”,`getter` 作為引數傳入,它會在依賴屬性更新時進行呼叫,並對計算屬性重新取值。需要注意 `Watcher` 的 `lazy` 配置,這是實現快取的標識 4. `defineComputed` 對計算屬性進行資料劫持 `defineComputed`: ```js // 原始碼位置:/src/core/instance/state.js const noop = function() {} // 1 const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } export function defineComputed ( target: any, key: string, userDef: Object | Function ) { // 判斷是否為服務端渲染 const shouldCache = !isServerRendering() if (typeof userDef === 'function') { // 2 sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef) sharedPropertyDefinition.set = noop } else { // 3 sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop sharedPropertyDefinition.set = userDef.set || noop } // 4 Object.defineProperty(target, key, sharedPropertyDefinition) } ``` 1. `sharedPropertyDefinition` 是計算屬性初始的屬性描述物件 2. 計算屬性使用函式宣告時,設定屬性描述物件的 `get` 和 `set` 3. 計算屬性使用物件宣告時,設定屬性描述物件的 `get` 和 `set` 4. 對計算屬性進行資料劫持,`sharedPropertyDefinition` 作為第三個給引數傳入 客戶端渲染使用 `createComputedGetter` 建立 `get`,服務端渲染使用 `createGetterInvoker` 建立 `get`。它們兩者有很大的不同,服務端渲染不會對計算屬性快取,而是直接求值: ```js function createGetterInvoker(fn) { return function computedGetter () { return fn.call(this, this) } } ``` 但我們平常更多的是討論客戶端渲染,下面看看 `createComputedGetter` 的實現。 `createComputedGetter`: ```js // 原始碼位置:/src/core/instance/state.js function createComputedGetter (key) { return function computedGetter () { // 1 const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 2 if (watcher.dirty) { watcher.evaluate() } // 3 if (Dep.target) { watcher.depend() } // 4 return watcher.value } } } ``` 這裡就是計算屬性的實現核心,`computedGetter` 也就是計算屬性進行資料劫持時觸發的 `get`。 1. 在上面的 `initComputed` 函式中,“計算屬性`Watcher`”就儲存在例項的`_computedWatchers`上,這裡取出對應的“計算屬性`Watcher`” 2. `watcher.dirty` 是實現計算屬性快取的觸發點,`watcher.evaluate` 對計算屬性重新求值 3. 依賴屬性收集“渲染`Watcher`” 4. 計算屬性求值後會將值儲存在 `value` 中,`get` 返回計算屬性的值 ## 計算屬性快取及更新 ### 快取 下面我們來將 `createComputedGetter` 拆分,分析它們單獨的工作流程。這是快取的觸發點: ```js if (watcher.dirty) { watcher.evaluate() } ``` 接下來看看 `Watcher` 相關實現: ```js export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array; newDeps: Array; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true // dirty 初始值等同於 lazy this.dirty = this.lazy // for lazy watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } this.value = this.lazy ? undefined : this.get() } } ``` 還記得建立“計算屬性`Watcher`”,配置的 `lazy` 為 true。`dirty` 的初始值等同於 `lazy`。所以在初始化頁面渲染,對計算屬性取值時,會執行一次 `watcher.evaluate`。 ```js evaluate() { this.value = this.get() this.dirty = false } ``` 求值後將值賦給 `this.value`,上面 `createComputedGetter` 內的 ` watcher.value` 就是在這裡更新。接著 `dirty` 置為 false,如果依賴屬性沒有變化,下一次取值時,是不會執行 `watcher.evaluate` 的, 而是直接就返回 `watcher.value`,這樣就實現了快取機制。 ### 更新 依賴屬性在更新時,會呼叫 `dep.notify`: ```js notify() { this.subs.forEach(watcher => watcher.update()) } ``` 然後執行 `watcher.update`: ```js update() { if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } ``` 由於“計算屬性`Watcher`”的 `lazy` 為 true,這裡 `dirty` 會置為 true。等到頁面渲染對計算屬性取值時,執行 `watcher.evaluate` 重新求值,計算屬性隨之更新。 ## 依賴屬性收集依賴 ### 收集計算屬性Watcher 初始化時,頁面渲染會將“渲染`Watcher`”入棧,並掛載到`Dep.target` 在頁面渲染過程中遇到計算屬性,因此執行 `watcher.evaluate` 的邏輯,內部呼叫 `this.get`: ```js 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 { popTarget() this.cleanupDeps() } return value } ``` ```js Dep.target = null let stack = [] // 儲存 watcher 的棧 export function pushTarget(watcher) { stack.push(watcher) Dep.target = watcher } export function popTarget(){ stack.pop() Dep.target = stack[stack.length - 1] } ``` `pushTarget` 輪到“計算屬性`Watcher`”入棧,並掛載到`Dep.target`,此時棧中為 [渲染Watcher, 計算屬性Watcher] `this.getter` 對計算屬性求值,在獲取依賴屬性時,觸發依賴屬性的 資料劫持`get`,執行 `dep.depend` 收集依賴(“計算屬性`Watcher`”) ### 收集渲染Watcher `this.getter` 求值完成後`popTragte`,“計算屬性`Watcher`”出棧,`Dep.target` 設定為“渲染`Watcher`”,此時的 `Dep.target` 是“渲染`Watcher`” ```js if (Dep.target) { watcher.depend() } ``` `watcher.depend` 收集依賴: ```js depend() { let i = this.deps.length while (i--) { this.deps[i].depend() } } ``` `deps` 記憶體儲的是依賴屬性的 `dep`,這一步是依賴屬性收集依賴(“渲染`Watcher`”) 經過上面兩次收集依賴後,依賴屬性的 `subs` 儲存兩個 `Watcher`,[計算屬性Watcher,渲染Watcher] ### 為什麼依賴屬性要收集渲染Watcher 我在初次閱讀原始碼時,很奇怪的是依賴屬性收集到“計算屬性`Watcher`”不就好了嗎?為什麼依賴屬性還要收集“渲染`Watcher`”? 第一種場景:模板裡同時用到依賴屬性和計算屬性 ```
export default { data(){ return { msg: 'hello' } }, computed:{ msg1(){ return this.msg + ' world' } } } ``` 模板有用到依賴屬性,在頁面渲染對依賴屬性取值時,依賴屬性就儲存了“渲染`Watcher`”,所以 `watcher.depend` 這步是屬於重複收集的,但 `watcher` 內部會去重。 這也是我為什麼會產生疑問的點,`Vue` 作為一個優秀的框架,這麼做肯定有它的道理。於是我想到了另一個場景能合理解釋 `watcher.depend` 的作用。 第二種場景:模板內只用到計算屬性 ```
export default { data(){ return { msg: 'hello' } }, computed:{ msg1(){ return this.msg + ' world' } } } ``` 模板上沒有使用到依賴屬性,頁面渲染時,那麼依賴屬性是不會收集 “渲染`Watcher`”的。此時依賴屬性裡只會有“計算屬性`Watcher`”,當依賴屬性被修改,只會觸發“計算屬性`Watcher`”的 `update`。而計算屬性的 `update` 裡僅僅是將 `dirty` 設定為 true,並沒有立刻求值,那麼計算屬性也不會被更新。 所以需要收集“渲染`Watcher`”,在執行完“計算屬性`Watcher`”後,再執行“渲染`Watcher`”。頁面渲染對計算屬性取值,執行 `watcher.evaluate` 才會重新計算求值,頁面計算屬性更新。 ## 總結 計算屬性原理和響應式原理都是大同小異的,同樣的是使用資料劫持以及依賴收集,不同的是計算屬性有做快取優化,只有在依賴屬性變化時才會重新求值,其它情況都是直接返回快取值。服務端不對計算屬性快取。 計算屬性更新的前提需要“渲染`Watcher`”的配合,因此依賴屬性的 `subs` 中至少會儲存兩個 `Wat