前端微信支付
前言
computed在vue中是很常用的屬性配置,它能夠隨著依賴屬性的變化而變化,為我們帶來很大便利。那麼本文就來帶大家全面理解computed的內部原理以及工作流程。
在這之前,希望你能夠對響應式原理有一些理解,因為computed是基於響應式原理進行工作。如果你對響應式原理還不是很瞭解,可以閱讀我的上一篇文章:手摸手帶你理解Vue響應式原理
computed 用法
想要理解原理,最基本就是要知道如何使用,這對於後面的理解有一定的幫助。
第一種,函式宣告:
Copyvar vm = new vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 計算屬性的 getter
reversedMessage: function () {
// `this` 指向 vm 例項
return this.message.split('').reverse().join('')
}
}
})
第二種,物件宣告:
Copycomputed: {
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的大概流程,看看計算屬性的核心點是什麼。
入口檔案:
Copy// 原始碼位置:/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:
Copy// 原始碼位置:/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
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:
Copy// 原始碼位置:/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:
Copy// 原始碼位置:/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)
}
}
}
- 例項上定義_computedWatchers物件,用於儲存“計算屬性Watcher”
- 獲取計算屬性的getter,需要判斷是函式宣告還是物件宣告
- 建立“計算屬性Watcher”,getter作為引數傳入,它會在依賴屬性更新時進行呼叫,並對計算屬性重新取值。需要注意Watcher的lazy配置,這是實現快取的標識
- defineComputed對計算屬性進行資料劫持
defineComputed:
Copy// 原始碼位置:/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)
}
- sharedPropertyDefinition是計算屬性初始的屬性描述物件
- 計算屬性使用函式宣告時,設定屬性描述物件的get和set
- 計算屬性使用物件宣告時,設定屬性描述物件的get和set
- 對計算屬性進行資料劫持,sharedPropertyDefinition作為第三個給引數傳入
客戶端渲染使用createComputedGetter建立get,服務端渲染使用createGetterInvoker建立get。它們兩者有很大的不同,服務端渲染不會對計算屬性快取,而是直接求值:
Copyfunction createGetterInvoker(fn) {
return function computedGetter () {
return fn.call(this, this)
}
}
但我們平常更多的是討論客戶端渲染,下面看看createComputedGetter的實現。
createComputedGetter:
Copy// 原始碼位置:/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。
- 在上面的initComputed函式中,“計算屬性Watcher”就儲存在例項的_computedWatchers上,這裡取出對應的“計算屬性Watcher”
- watcher.dirty是實現計算屬性快取的觸發點,watcher.evaluate對計算屬性重新求值
- 依賴屬性收集“渲染Watcher”
- 計算屬性求值後會將值儲存在value中,get返回計算屬性的值
計算屬性快取及更新
快取
下面我們來將createComputedGetter拆分,分析它們單獨的工作流程。這是快取的觸發點:
Copyif (watcher.dirty) {
watcher.evaluate()
}
接下來看看Watcher相關實現:
Copyexport 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<Dep>;
newDeps: Array<Dep>;
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。
Copyevaluate() {
this.value = this.get()
this.dirty = false
}
求值後將值賦給this.value,上面createComputedGetter內的watcher.value就是在這裡更新。接著dirty置為 false,如果依賴屬性沒有變化,下一次取值時,是不會執行watcher.evaluate的, 而是直接就返回watcher.value,這樣就實現了快取機制。
更新
依賴屬性在更新時,會呼叫dep.notify:
Copynotify() {
this.subs.forEach(watcher => watcher.update())
}
然後執行watcher.update:
Copyupdate() {
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:
Copyget () {
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
}
Copy
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”
Copyif (Dep.target) {
watcher.depend()
}
watcher.depend收集依賴:
Copydepend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
deps記憶體儲的是依賴屬性的dep,這一步是依賴屬性收集依賴(“渲染Watcher”)
經過上面兩次收集依賴後,依賴屬性的subs儲存兩個Watcher,[計算屬性Watcher,渲染Watcher]
為什麼依賴屬性要收集渲染Watcher
我在初次閱讀原始碼時,很奇怪的是依賴屬性收集到“計算屬性Watcher”不就好了嗎?為什麼依賴屬性還要收集“渲染Watcher”?
第一種場景:模板裡同時用到依賴屬性和計算屬性
Copy<template>
<div>{{msg}} {{msg1}}</div>
</template>
export default {
data(){
return {
msg: 'hello'
}
},
computed:{
msg1(){
return this.msg + ' world'
}
}
}
模板有用到依賴屬性,在頁面渲染對依賴屬性取值時,依賴屬性就儲存了“渲染Watcher”,所以watcher.depend這步是屬於重複收集的,但watcher內部會去重。
這也是我為什麼會產生疑問的點,Vue作為一個優秀的框架,這麼做肯定有它的道理。於是我想到了另一個場景能合理解釋watcher.depend的作用。
第二種場景:模板內只用到計算屬性
Copy<template>
<div>{{msg1}}</div>
</template>
export default {
data(){
return {
msg: 'hello'
}
},
computed:{
msg1(){
return this.msg + ' world'
}
}
}
模板上沒有使用到依賴屬性,頁面渲染時,那麼依賴屬性是不會收集 “渲染Watcher”的。此時依賴屬性裡只會有“計算屬性Watcher”,當依賴屬性被修改,只會觸發“計算屬性Watcher”的update。而計算屬性的update裡僅僅是將dirty設定為 true,並沒有立刻求值,那麼計算屬性也不會被更新。
所以需要收集“渲染Watcher”,在執行完“計算屬性Watcher”後,再執行“渲染Watcher”。頁面渲染對計算屬性取值,執行watcher.evaluate才會重新計算求值,頁面計算屬性更新。
秒收目錄站https://www.tomove.com.cn
總結
計算屬性原理和響應式原理都是大同小異的,同樣的是使用資料劫持以及依賴收集,不同的是計算屬性有做快取優化,只有在依賴屬性變化時才會重新求值,其它情況都是直接返回快取值。服務端不對計算屬性快取。
計算屬性更新的前提需要“渲染Watcher”的配合,因此依賴屬性的subs中至少會儲存兩個Watcher。