学习笔记,持续补充
1. 使用示例 (官网)
var vm = new Vue({
data: { a: 1 },
computed: {
// 计算属性的 getter
// 仅读取
aDouble: function () {
// `this` 指向 vm 实例
return this.a * 2
},
// 读取和设置
aPlus: {
get: function () {
return this.a + 1
},
set: function (v) {
this.a = v - 1
}
}
}
})
vm.aPlus // => 2
vm.aPlus = 3
vm.a // => 2
vm.aDouble // => 4
计算属性的结果会被缓存,除非依赖的响应式 property 变化才会重新计算。注意,如果某个依赖 (比如非响应式 property) 在该实例范畴之外,则计算属性是不会被更新的。
这是官网给的有关计算属性的说明,至于计算属性到底是个什么,为什么会被缓存呢?我们继续往下看
2. 源码解析(2.5.17beta版,多计算,少渲染)
这里结合一个场景例子,直观的分析一下计算属性的实现
var vm = new Vue({
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
1. initComputed()
首先,在vue初始化过程中,在 initState ( ) 中会对computed计算属性进行初始化处理,判断如果定义了计算属性,会执行
initComputed()方法,如下
if (opts.computed) initComputed(vm, opts.computed)
看一下这个 initComputed()方法,主要分为四步
- 创建一个空对象,用来保存watcher
- 对computed计算属性遍历,拿到定义的每一个计算属性
- 为每一个计算属性创建一个watcher
- 对遍历出的每一个计算属性做判断,如果不是vm上的属性,执行defineComputed ( )方法,如果是vm上的属性,判断当前计算属性名是不是已经在data和props定义了
可以看一下下面代码及相关注释
const computedWatcherOptions = { computed: true } // 实例化Watcher时的配置项
function initComputed (vm: Component, computed: Object) {
// 第一步. 创建空对象 watchers,vm._computedWatchers
const watchers = vm._computedWatchers = Object.create(null)
// 判断是否是服务端渲染(SSR),这里我们分析的是浏览器环境
const isSSR = isServerRendering()
// 第二步. 对computed计算属性遍历
for (const key in computed) {
const userDef = computed[key] // 拿到定义的每一个计算属性的value
// 判断上边拿到的计算属的value,是否是function,如果是function,直接赋值给getter,不是的话将计算属性的get赋值给getter(看下官网计算属性定义的方式,---->示例)
const getter = typeof userDef === 'function' ? userDef : userDef.get
// 如果计算属性没定义getter,给出警告
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// 第三步. 为每一个getter创建一个watcher(可以称为computed watcher,在下面对实例化computed watcher进行了分析)
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. 组件定义的计算属性已经在组件原型上定义了
// 第四步. 判断key,如果不是vm的属性,执行defineComputed( )做响应式处理
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
// 如果key已经在data,或者props中定义了,给出警告
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)
}
}
}
}
下面我们分析一下以上四步中所做的一些处理
2. defineComputed()
这个方法,他的目的就是利用Object.defineProperty()给每一个计算属性添加getter和setter,使之响应式
// 定义一个对象,当作Object.defineProperty()的参数
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop, // 空函数
set: noop
}
export function defineComputed (
target: any,
key: string, // 定义的计算属性名
userDef: Object | Function // 计算属性所做的处理
) {
// 下面的操作就是给定义的这个 sharedPropertyDefinition 对象添加get和set处理函数
const shouldCache = !isServerRendering() // 非服务器端渲染
if (typeof userDef === 'function') { // 是函数
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else { // 是对象
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.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对计算属性进行处理
Object.defineProperty(target, key, sharedPropertyDefinition)
}
这里我们先忽略计算属性对set函数的处理,在平时的开发场景中,计算属性有 setter 的情况比较少,主要分析一下getter, 上面对于get函数的定义,在我们平常使用的浏览器环境中会进入createComputedGetter ( ),我们来看一下这个函数做了什么,继续往下
3. createComputedGetter()
最终getter对应的是createComputedGetter()的返回值,如下
function createComputedGetter (key) {
return function computedGetter () {
// 这里会去获取this._computedWatchers,这个是上面在initComputed中定义在vm 上的对象,可以通过this访问到,这个对象保存的是每一个计算属性的watcher,下面定义的这个 watcher,就是取到的每个计算属性的watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 调用watcher.depend(),返回watcher.evaluate(),这是什么??看下面 4
watcher.depend()
return watcher.evaluate()
}
}
}
对于返回的这个函数,如果会调用depend ( )和evaluate ( )方法,这俩方法定义在Watcher这个类中,如下
4. 计算属性的new Watcher()
Watcher是定义的一个类,下面我直接把定义Watcher的代码拿了过来,有点多,大家可以看下,和计算属性相关的,我会随后拿出来分析
/* @flow */
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError
} from '../util/index'
import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'
import type { SimpleSet } from '../util/index'
let uid = 0
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
computed: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
dep: Dep;
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.computed = !!options.computed
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.computed = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.computed // for computed watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
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 {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}
getAndInvoke (cb: Function) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
/**
* Evaluate and return the value of the watcher.
* This only gets called for computed property watchers.
*/
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
/**
* Depend on this watcher. Only for computed property watchers.
*/
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
在上面执行initComputed( )的时候,在我们所说的第三步中,遍历实例化创建了计算属性的watcher (computed watcher)
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions // const computedWatcherOptions = { computed: true }
)
简化一下new Watcher,我们看一下
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
if (this.computed) { // 判断this.computed
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get() // 调用get()求值
}
}
这里可以看到,计算属性的watcher并不会立即求值,而是判断了this.computed,同时实例化了一个dep实例。
在vue渲染数据到页面这一过程中,如果访问到定义的计算属性时,就会触发计算属性的getter( getter对应的是 上边所说的createComputedGetter( )函数的返回值 ),拿到计算属性对应的watcher,然后执行Watcher中的depend 方法,并且返回evaluate方法处理后的值
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
depend 方法
/**
* Depend on this watcher. Only for computed property watchers.
*/
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
注意,这时候的 Dep.target 是渲染 watcher ,所以 this.dep.depend( ) 相当于渲染 watcher 订阅了这个 computed watcher 的变化。
然后再执行watcher.evaluate ( ) 求值
evaluate( )这个函数 首先判断this.dirty,如果为true,会调用get ( ) 求值,get ( )也定义在Watcher这个类中,他的核心就是
value = this.getter.call(vm, vm) ,实际上就是调用计算属性中定义的getter函数,求值, 并把this.dirty设置为false,最后返回所求的值
/**
* Evaluate and return the value of the watcher.
* This only gets called for computed property watchers.
*/
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
这里需要特别注意的是,由于 this.firstName
和 this.lastName
都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep
添加到当前正在计算的 watcher
中,这个时候 Dep.target
就是这个 computed watcher
。
5. 修改计算属性所依赖的数据
一旦我们对计算属性所依赖的数据进行修改, 会触发setter过程,通知所有订阅他变化的watcher更新( dep.notify ( ) )
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
如上,遍历循环,并执行watcher的update ( )方法
update () {
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
对于计算属性的watcher,他实际上有两种模式,lazy和active,如果this.dep.subs.length === 0说明没有人去订阅这个计算属性watcher的变化,把this.dirty设为true,使只有当下次再访问这个计算属性的时候才会重新求值。
在我们所举例的场景下,渲染 watcher 订阅了这个 computed watcher 的变化,那么它会执行:
this.getAndInvoke(() => {
this.dep.notify()
})
getAndInvoke (cb: Function) {
const value = this.get() // 会重新计算
// 新旧值会进行比较,如果变化了 ,才会执行传入的回调函数
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
getAndInvoke 函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher重新渲染
这里对计算属性的新旧值会进行比较,这样保证了,当计算属性依赖的值发生变化时,他会被重新计算,但不一定会重新渲染,只有计算出来的新值相对于旧值改变了,才会触发渲染watcher重新渲染,本质上这是vue的一种优化,多计算,少更新
3. 源码解析(2.6.13版本,多渲染,少计算)
在watch的update方法中,没有getAndInvoke这一方法,不会比较计算属性计算出的新旧值,不管相等否,都会触发updateComponent去update更新渲染
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
看之前版本(2.5.17beta)
Watcher.prototype.update = function update () {
var this$1 = this;
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true;
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(function () {
this$1.dep.notify();
});
}
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
看一下getAndInvoke ( )方法
Watcher.prototype.getAndInvoke = function getAndInvoke (cb) {
var value = this.get();
// 做了判断,如果计算出的新值和旧值不一样 的话,才会继续执行,如果一样,就不会继续,,不会再次渲染
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value;
this.value = value;
this.dirty = false;
if (this.user) {
try {
cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
cb.call(this.vm, value, oldValue);
}
}
};