computed
用法
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
computed选项的属性值可以是一个函数,该函数默认为取值器getter,用于仅读取数据;还可以是一个对象,对象里面有取值器getter和存值器setter,用于读取和设置数据
源码分析
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
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.
// 则创建一个watcher实例,并将当前循环到的的属性名作为键,
// 创建的watcher实例作为值存入watchers对象中。
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
// 实例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)
}
}
}
}
● 首先会在实例上创建一个_computedWatchers对象,这个对象会存放每个computed 类watcher
● 遍历computed对象上的每个属性,如果该属性是一个函数,则默认取值器getter是这个函数,如果该属性是一个对象,则取值器getter是对象的getter属性,如果没有取值器的话,会警告
● 对每一个computed key设置一个watcher,将setter传进去,。。。。
● 设置完之后,判断实例上有没有key这个属性,没有的话就设置在实例上,使用的是defineComputed函数
defineComputed函数
该函数接受3个参数,分别是:target、key和userDef。其作用是为target上定义一个属性key,并且属性key的getter和setter根据userDef的值来设置。下面我们就来看一下该函数的具体逻辑。
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (target,key,userDef) {
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(target, key, sharedPropertyDefinition)
}
该函数的主要作用就是给computed属性设置一个具有缓存功能的setter函数,在createComputedGetter函数里面设置,但是在服务端渲染的情况下不需要缓存???
createComputedGetter函数
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
}
}
这里面会返回一个函数,这个函数就是该计算属性值的getter,在这个函数中,会先拿出对应的watcher出来,在执行depend和evalute方法,也就是说,computed的缓存的奥秘就在watcher的这两个方法中
接着,我们在来回顾下watcher类的定义,了解computed的缓存
watcher类
export default class Watcher {
constructor (vm,expOrFn,cb,options,isRenderWatcher) {
if (options) {
// ...
this.computed = !!options.computed
// ...
} else {
// ...
}
this.dirty = this.computed // for computed watchers
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
if (this.computed) {
this.value = undefined
this.dep = new Dep()
}
}
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
// 收集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)) {
dep.addSub(this)
}
}
}
/**
* Depend on this watcher. Only for computed property watchers.
*/
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
update () {
if (this.computed) {
if (this.dep.subs.length === 0) {
this.dirty = true
} else {
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)
}
}
}
}
● 对于watcher类来说,有专门针对computed 类型的wathcer的一些方法
● 会通过this.computed属性判断是不是一个computed类型的watcher
● 如果是的话,会创建一个deps数组来收集dep,同时,会设置一个this.dirty属性来判断是否使用缓存数据,如果dirty为false(表示数据不脏)的话,取缓存,dirty为true的话不取缓存
可以看到,在实例化Watcher类的时候,第四个参数传入了一个对象computedWatcherOptions = { computed: true },该对象中的computed属性标志着这个watcher实例是计算属性的watcher实例,即Watcher类中的this.computed属性,同时类中还定义了this.dirty属性用于标志计算属性的返回值是否有变化,计算属性的缓存就是通过这个属性来判断的,每当计算属性依赖的数据发生变化时,会将this.dirty属性设置为true,这样下一次读取计算属性时,会重新计算结果返回,否则直接返回之前的计算结果。
当调用watcher.depend()方法时,会将读取计算属性的那个watcher添加到计算属性的watcher实例的依赖列表中,当计算属性中用到的数据发生变化时,计算属性的watcher实例就会执行watcher.update()方法,在update方法中会判断当前的watcher是不是计算属性的watcher,如果是则调用getAndInvoke去对比计算属性的返回值是否发生了变化,如果真的发生变化,则执行回调,通知那些读取计算属性的watcher重新执行渲染逻辑。
当调用watcher.evaluate()方法时,会先判断this.dirty是否为true,如果为true,则表明计算属性所依赖的数据发生了变化,则调用this.get()重新获取计算结果最后返回;如果为false,则直接返回之前的计算结果。
watch
用法
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)
},
// methods选项中的方法名
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) { /* ... */ },
}
],
// 侦听表达式
'e.f': function (val, oldVal) { /* ... */ }
}
})
vm.a = 2 // => new: 2, old: 1
● 可以看出,watch对象的属性表示的是侦听的属性,这个属性也可以是侦听表达式
● 每个属性值可以直接是一个函数,函数参数是newVal和oldVal,当属性变化的时候会执行该函数
● 也可以是一个字符串,表示的是methods中的方法名
● 也可以是一个对象,配置一些属性,handler表示的是数据发生变化时要执行的函数
源码分析
function initWatch (vm, watch) {
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函数
function createWatcher (
vm: Component, // 当前实例
expOrFn: string | Function, // 被侦听的属性表达式
handler: any, // watch选项中每一项的值
options?: Object // 用于传递给vm.$watch的选项对象
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 这种写法对应handler是method中的方法
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
其实就是调用了vm.$watch这个方法,将监听的属性,回调函数作为参数
vm.$watch函数
Vue.prototype.$watch = function (expOrFn,cb,options) {
const vm: Component = this
if (isPlainObject(cb)) {
// 从用户合起来传入的对象中把回调函数cb和参数options剥离出来
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 用户手动调动该方法创建出来的
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
// 立即执行回调
cb.call(vm, watcher.value)
}
return function unwatchFn () {
// 取消观察,就是将所依赖的dep中将自己删除
watcher.teardown()
}
}
computed和watch的区别
computed的缓存
computed的缓存说的是只有计算属性的依赖发生变化的时候,计算属性才会去重新计算它的值,也就是说,如果页面重新渲染而所依赖的数据没有变化的时候,计算属性不会重新计算,而方法则会重新执行
这对一些需要处理很多数据的计算属性来说,可以提高性能
对于computed:
● 它支持缓存,只有当依赖的数据发生变化,才会重新计算
● 不支持异步,当computed中有异步操作时,无法监听数据的变化。因为计算属性一般是通过return返回计算属性值的,所以异步的时候不会返回正确的值
● computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data声明过,或者父组件传递过来的props中的数据进行计算的。
● 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用computed
● 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。
对于watch:
● 不支持缓存,数据变化时,会触发相应的操作
● 支持异步监听,也就是可以再处理函数中使用异步操作
● 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
● 当一个属性发生变化时,就需要执行相应的操作
● 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
○ immediate:组件加载立即触发回调函数
○ deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,加上deep的话,数组内元素的变化或者对象属性值的变化,都能狗侦测的到。
当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。
参考链接:
https://vue-js.com/learn-vue/instanceMethods/data.html#_1-vm-watch
https://www.yuque.com/cuggz/interview/hswu8g#d72e59d5f3d78b8cf5d8038e0e12803e