很多面试题都会遇到, Computed的实现原理, 其实在Vue中, 如果能 理解透三个概念:
observe,watcher和dep, 那么 对于理解响应式 很有帮助;
Computed 示例
<div id="app">{{ fulllName }}</div>
const vm = new Vue({
data: {
surName: '张',
lastName: '先森'
},
computed: {
fulllName () {
return this.surName + this.lastName
}
}
}).$mount('#app')
学过Vue的同学, 都知道 computed 实现计算属性, 可以简化复杂的逻辑, 就比如上面获取fulllName, 但是 还有一个 隐藏的技能就是,计算属性缓存, 当计算属性的响应式依赖surName 和 lastName 没有发生改变的时候, fulllName 不会重新计算
问:
computed和methods获取值的区别?答:
methods页面重新渲染的时候,会重新调用函数计算, 而computed只要响应式依赖没有发生改变, 尽管页面重新渲染, 也不会重新计算值
Computed源码阅读
定位文件: src/core/instance/state.js/initComputed
源码
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
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]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// 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)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
创建 _computedWatchers
在Vue挂载的时候, 会创建一个 Render Watcher(拓展阅读#1), 而对于 computed 会 创建 Computed Watcher,故initComputed会在当前Vue实例上新建一个 _computedWatchers, 用于存储所有的 Computed Watcher
const watchers = vm._computedWatchers = Object.create(null)
解析 计算属性
就是 遍历所有的 computed属性, 对于实现计算属性的 getter方式有两种
- 函数
{
fullName: function() {
return this.surName + this.lastName
}
}
- 包含get属性的对象
{
fullName: {
get: function() {
return this.surName + this.lastName
}
}
}
所以在源码中对两种情况进行解析:
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
新建 Computed Watcher
对当前的计算属性, 新建一个 Watcher 实例
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions // { lazy: true }
)
因为设置了, computedWatcherOptions: { lazy: true } 所以此时此刻的计算属性还没有值
// src/core/observer/watcher.js Watcher
this.value = this.lazy
? undefined
: this.get()
defineComputed
当 computed的属性,不存在当前Vue实例中,则调用 defineComputed方法, 其实就是 将 computed 的属性,变成 Object.defineProperty
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
createComputedGetter
在 defineComputed 对 计算属性的获取方式 进行封装, 当调用 计算属性的时候, 就会执行 返回的computedGetter方法
// createComputedGetter
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
- 首先判断 当前计算属性的
watcher是否存在, 如果存在则继续;- 因为 计算属性在初始化
Watcher时, 传入了{lazy: true}, 并且在Watcher构造函数中 有这样一行代码 :this.dirty = options.lazy, 所以这里面就会执行到watcher.evaluate(), 注意此时的Dep.target是当前的Computed Watcher, 也就是, 会将当前的Computed Watcher追加到 响应式依赖的Dep中,在本文中是surName和lastName的Dep中, 这样 当 响应式依赖发生改变的时候, 计算属性会重新计算evaluate () { this.value = this.get() this.dirty = false } get () { pushTarget(this) // 当前的实例赋值给 Dep.target 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 { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() // 释放掉当前的的 wacther this.cleanupDeps() } return value }
- 此时的
Dep.target是Render Watcher,当Computed Watcher的依赖响应式发生改变的时候, 不仅仅改变Computed Watcher的值, 也要通知Render Watcher去进行操作
至此, computed的实现原理分析完毕, 希望能够抛砖引玉,在您学习Vue源码的路上贡献一点力量