计算属性与侦听属性
计算属性
computed,通过监听某些响应式数据,从而实现动态返回一个变量,和react hooks中的effect有点类似,可以实现相同效果。
// vue中写法
export default{
computed:{
name(){
return firstName + lastName
}
}
}
// react hooks写法
const App = () => {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + lastName);
}, [firstName, lastName]);
const setname = (e, type) => {
const value = e.target.value;
if (type === 1) {
setFirstName(value);
} else {
setLastName(value);
}
};
return (
<div>
<input onChange={(e) => setname(e, 1)}></input>
<input onChange={(e) => setname(e)}></input>
<div>{fullName}</div>
</div>
);
};
简单对比之后,下面开始详细的分析,vue中的计算属性。
计算属性的调用方式
这部分逻辑在vue官网API上有,主要调用方式就是下面两种,可以在computed中定义一个函数,也可以使用一个设置了get和set的对象。
var vm = new Vue({
data: { a: 1 },
computed: {
// 仅读取
aDouble: function () {
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
初始化
计算属性的初始化在本系列文章第一篇initState方法中就有提到,这部分逻辑定义在state.js文件中:
// initState 方法调用
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 */)
}
// 注意,当传入的options中有计算属性时,则进行计算属性初始化
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
接下来,我们跳到initComputed中,查看相关的逻辑。
initComputed方法实现
相关代码
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
// 判断是不是ssr环境
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
// 尝试去拿到计算属性的每一个key值对应的getter函数,如果没有拿到,开发环境下就会报错
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.
// 给每一个计算属性创建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 值在组件中没有使用
if (!(key in vm)) {
defineComputed(vm, key, userDef)
// key值在组件中已经被使用,如data、props
} 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)
}
}
}
}
可以看到,上面流程中,首先去获取计算属性上的getter方法,之后实例化计算属性的watcher,最后判断vm上没有对应key值的computed时,调用defineComputed方法,下面来看一下defineComputed的实现。
defineComputed方法实现
调用逻辑:
// vm:组件实例、key:计算属性中的一个key值,userDef:计算属性对应的value,可能是一个对象,也可能是一个函数
defineComputed(vm, key, userDef)
相关代码:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
// ssr情况下不进行缓存
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
}
// 开发环境下,在计算属性定义的是函数的时候,如果调用set方法,就会抛出这个错误
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
)
}
}
// 转换为响应式的,挂载到vm实例上,组件内部就可以访问了
Object.defineProperty(target, key, sharedPropertyDefinition)
}
这一块逻辑还是比较清晰的,在设置getter时,有调用createComputedGetter这个函数,现在来查看一下它的定义:
createComputedGetter函数定义
调用方式:
// 传入参数实际上就是计算属性的key值
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
函数定义:
function createComputedGetter (key) {
return function computedGetter () {
// 这个watcher就是在initComputed方法中实例化的watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 调用watcher的depend方法,实际上也就是调用dep的depend方法,将watcher添加到deps中
watcher.depend()
return watcher.evaluate()
}
}
}
现在回到计算属性初始化watcher这一步
调用方式:
const computedWatcherOptions = { computed: true }
// 传入参数:组件实例,getter,回调传入空函数,以及上面这个变量
if (!isSSR) {
// create internal watcher for the computed property.
// 给每一个计算属性创建watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
watcher初始化构造函数接收参数:
// 其余部分省略
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
}
由于传入了options配置,所以就会重新赋值,最终实例化的watcher:
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.computed = !!options.computed;
this.sync = !!options.sync;
this.before = options.before;
}
vm: Component;
cb: loop;
id: number;
deep: false;
user: false;
computed: true;
sync: false;
before: undefined;
active: true;
之后走到和渲染watcher相同的那部分逻辑:
if (this.computed) {
this.value = undefined;
this.dep = new Dep();
} else {
this.value = this.get();
}
区别在于,渲染watcher立即求值,而computed watcher没有立即求值,同时实例化一个Dep实例。
执行流程
当页面中有引用计算属性时,就会走到先前watcher.depend()这一步,对计算属性进行求值:
// 执行
watcher.depend()
// 触发
/**
* Depend on this watcher. Only for computed property watchers.
*/
depend() {
if (this.dep && Dep.target) {
this.dep.depend();
}
}
因为这时在页面渲染过程中,Dep.target中存放的是渲染watcher,执行dep.depend函数:
depend () {
// 渲染watcher存在情况下
if (Dep.target) {
// 渲染watcher将当前计算属性watcher添加到dep中,为什么是计算属性watcher,注意先前的调用方式,watcher.depend
Dep.target.addDep(this)
}
}
addDep方法执行:
/**
* 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);
}
}
}
上面流程执行完毕后,渲染watcher会将computed watcher添加到自身依赖当中,计算属性改变,即会触发渲染watcher的更新逻辑,从而更新到组件。
watcher.evaluate()方法定义
在computed watcher被渲染watcher添加到依赖当中后,有进行一个求值返回的操作,方法就是这个函数:
相关代码:
/**
* 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.dirty为true的情况下,才会进行重新求值,并修改标志位,否则直接返回this.value。
这个dirty标志位作用就是计算属性的缓存效果实现:
计算属性缓存
计算属性初始化阶段,标记位为true
// 此时为true
this.dirty = this.computed; // for computed watchers
所以在获取计算属性的val时,可以走到读值的逻辑,之后再次修改标志位:
evaluate() {
// 条件成立,进行读取操作
if (this.dirty) {
this.value = this.get();
// 修改标志位,下次就直接返回,不进行读取操作
this.dirty = false;
}
return this.value;
}
这个标志位在更新等过程中也会被刷新,下面会提到这一点:
计算属性更新
计算属性中引用响应式对象,那就会调用其getter触发依赖收集逻辑,computed watcher就会被收集到响应式对象的dep中,响应式对象发生变化,也就会通知到computed watcher。
// 逻辑实现
// 响应式对象
get(){
// 触发依赖收集逻辑
dep.depend()
}
Dep{
depend(){
// 此时,Dep.target 就是 computed watcher,this就是dep实例,属于响应式对象
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
// computed watcher
// 收集对应的deo,保存到deps中,并进行记录,避免重复引用,同时在dep中添加
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)) {
// 响应式对象中添加computed watcher
dep.addSub(this);
}
}
}
当计算属性依赖的数据发生变化,dep就会执行notify方法,通知到所有引用到当前数据的watcher,循环遍历subs数组,调用watcher的update方法,computed watcher也就这样被通知到:
相关代码:
// 响应式对象 setter触发
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 通知当前的依赖项更新
dep.notify()
}
// 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()
}
}
在notify中会调用每一个依赖项的update方法,computed watcher也就是这样被通知到,需要进行更新的,下面开始计算属性的更新逻辑:
computed watcher 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.
// 这里的this.dep是只有计算属性才有的
// 计算属性的watcher有两种模式,lazy 和 activated 这两种,默认为lazy
// 当不存在依赖时,也就是没有被组件内部、其他函数引用时,修改标志位,只有在下一次访问这个值时,才会进行重新求值
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);
}
}
总结
计算属性本身就是一个computed watcher,说它有缓存效果,实际上就是因为它有两种模式:lazy和activated这两种,默认为layz。
当存在引用时,他就是activated,计算属性依赖的值更新就会触发它的重新计算,不存在引用时,只是修改标志位,只有当下一次有引用时,才进行计算。
缓存的体现
- 值的缓存,计算结果只计算一次,依赖项未改变,则不进行计算,通过标志位实现
- 计算的缓存,惰性计算,不存在引用则不进行计算
侦听属性
侦听属性调用方式
var vm = new Vue({
data: {
a: 1,
b: 2,
c: 3,
d: 4,
e: {
f: {
g: 5
}
}
},
watch: {
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
// 方法名
b: 'someMethod',
// 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
c: {
handler: function (val, oldVal) { /* ... */ },
deep: true
},
// 该回调将会在侦听开始之后被立即调用
d: {
handler: 'someMethod',
immediate: true
},
// 你可以传入回调数组,它们会被逐一调用
e: [
'handle1',
function handle2 (val, oldVal) { /* ... */ },
{
handler: function handle3 (val, oldVal) { /* ... */ },
/* ... */
}
],
// watch vm.e.f's value: {g: 5}
'e.f': function (val, oldVal) { /* ... */ }
}
})
vm.a = 2 // => new: 2, old: 1
初始化
侦听属性watch的初始化,和computed在同一个函数中定义:
export function initState (vm: Component) {
// ...省略
// 侦听属性初始化
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
可以看到,最后是调用initWatch方法,直接跳转initWatch方法实现:
initWatch方法定义
相关代码:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
// 回调数组的处理逻辑,之前调用方式演示有提到
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
这段代码执行完成后,调用的是createWatcher这个方法,跳转到它的实现:
createWatcher方法定义
调用方式:
// vue组件实例,watcher key值,watcher对应的value
createWatcher(vm, key, handler)
相关代码:
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
// 判断是不是对象类型,如果是,那说明传入的是一个{handler:func,deep?:boolean}的对象,实际的方法是对象的handler属性
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 如果是个str,那说明传入的是方法名,从组件实例上去取对应方法就行
if (typeof handler === 'string') {
handler = vm[handler]
}
// 返回一个方法
return vm.$watch(expOrFn, handler, options)
}
vm.$watch方法实现
调用方式:
// watch key值、回调函数、配置参数
vm.$watch(expOrFn, handler, options)
这个方法是在执行startMixin时定义的,跳转到实现:
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
// 如果传入的回调是个对象类型,返回一个watcher,跟之前区别就是将cb解构出来了
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 定义的是用户watch
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
// 如果设置了immediate 属性,则会直接调用回调函数,然后返回一个unwatchFn的方法进行解绑操作。
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
不同watcher的实现
在watcher的构造函数中,分别针对不同的watcher做了不同的处理,传入不同的参数,就可以生成不同的watcher,之前的这些步骤也就是为了满足Watcher这个类,对参数的要求:
if (options) {
// 用户定义时传入的配置参数
this.deep = !!options.deep;
// user watch
this.user = !!options.user;
this.computed = !!options.computed;
// 另外一个
this.sync = !!options.sync;
this.before = options.before;
} else {
// 如果没有传入,则赋默认值false
this.deep = this.user = this.computed = this.sync = false;
}
传入deep的情况
在传入depp为true时,get方法逻辑会有些不同:
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;
}
traverse方法实现
实际上就是对一个对象进行深度遍历,将每一个属性都变成getter和setter的形式,之后如果再次访问就可以进行相应的依赖收集逻辑。
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
userWatch
在用户自定义watch时,会默认传入一个user的选项,如果是用户自定义的watch,会默认添加一些错误处理,这部分逻辑处理如下:
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`);
} else {
throw e;
}
}
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);
}
sync watch
即传入一个sync true,表示是一个同步的,源码实现如下
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();
});
}
// 更新过程中,如果是同步的,则不添加进任务队列,直接执行run方法
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
computed watch
这部分在计算属性这里已经进行了详细说明,不再讨论。
总结
watcher实际上就是再构造函数内部进行了一系列判断,通过传入的参数不同,从而生成不同用途的watch,watch的种类有:
- 渲染watch
- 计算属性computed watch
- 深层 deep watch
- 同步 sync watch
- 用户 user watch
通过这些逻辑的判断,实现了watcher类的高度复用,$watch这个方法前面的逻辑也就是一系列判断,参数合并,最终传递正确的参数,生成相对应的watcher实例。
计算属性和侦听属性的区别
- 两个都是共用一个watcher所实例化出的对象,传入参数不同所以有着不同的输出结果。
- 计算属性有一个lazy惰性求值,同时通过一个标志位实现了缓存效果,而watch没有缓存。
- 计算属性有一个实例化的dep用来收集依赖,从而实现惰性求值,没有依赖项,即不进行求值计算。
- 计算属性更多的是凭借几个响应式数据生成新的数据,供其他函数、模板调用。
- watch则是监听数据变化,从而进行业务逻辑处理,需要注意这些区别。