Vue之watch、Computed源码解读
1.Watch
watch 用法
watch是Vue中一个监听数据变化的一个方法
监听基本数据类型
{{ msg }}
data () {
return {
msg: '1231'
}
},
watch: {
msg (val, oldVal) {
console.log(val, oldVal) // 不好 1231
}
},
methods: {
changeMsg () {
this.msg = this.msg === '哈哈哈哈' ? '好' : '不好'
}
}
watch 可以接收两个参数,一个是变化后的数据,一个是变化之前的数据。
监听对象
{{objectData.msg}}
data () {
return {
objectData: {
msg: '对象'
}
}
},
watch: {
objectData: {
handler (val, oldVal) {
console.log(val.msg, oldVal.msg) // 对象改变了 对象改变了
},
deep: true,
immediate: true
}
},
methods: {
changObject () {
this.objectData.msg = '对象改变了'
}
}
在监听对象变化的时候,加上deep这个属性即可深度监听对象数据;如果你想在页面进来时就执行watch方法,加上immediate即可。值得注意的是,设置了immediate属性的watch的执行顺序实在created 生命周期之前。
- deep 设置为 true 用于监听对象内部值的变化
- immediate 设置为 true 将立即以表达式的当前值触发回调
watch接收参数为数组
watch监听的属性不去设置一个方法而是接收一个数组的话,可以向当前监听的属性传递多个方法。
{{name}}
data () {
return {
name: '修改前'
}
},
watch: {
name: [
{
handler: function (val, oldVal) {
console.log(val, oldVal) // 修改前 修改后
},
immediate: true
},
function (val, oldVal) {
console.log(val, oldVal, 2) // 修改前 修改后
}
]
},
methods: {
changeName () {
this.name = '修改后'
}
}
数组中可以接收不同形式的参数,可以是方法,也可以是一个对象。具体的书写方式和普通的watch没什么不同。
初始化watch
initState
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // 如果有 props ,初始化 props
if (opts.methods) initMethods(vm, opts.methods) // 如果有 methods ,初始化 methods 里面的方法
if (opts.data) { // 如果有 data 的话,初始化,data;否则响应一个空对象
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed) // 如果有 computed ,初始化 computed
if (opts.watch && opts.watch !== nativeWatch) { // 如果有 watch ,初始化 watch
initWatch(vm, opts.watch)
}
}
首先在initState初始化watch 如果有watch这个属性,就将watch传入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)
}
}
}
这个函数主要就是初始化watch,我们可以看到initWatch会遍历watch,然后判断每个值是否是数组。如果是数组就遍历这个数组,创建多个回调函数,这块也就解释了上边watch监听的数据可以接收数组为参数原因;如果不是数组的话,就直接创建回调函数。
createWatcher
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
createWatcher 中会判断handler是否是对象 ,如果是对象将handler挂载到options这个属性。再将对象的handler属性提取出来;如果handler是一个字符串的话,会才Vue实例找到这个方法赋值给handler。从这里我们可以看出来。watch还可以支持字符串的写法。执行vue实例上的$watch方法
$watch
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
// 获取 Vue 实例 this
const vm: Component = this
if (isPlainObject(cb)) {
// 判断如果 cb 是对象执行 createWatcher
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 标记为用户 watcher
options.user = true
// 创建用户 watcher 对象
const watcher = new Watcher(vm, expOrFn, cb, options)
// 判断 immediate 如果为 true
if (options.immediate) {
// 立即执行一次 cb 回调,并且把当前值传入
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
// 返回取消监听的方法
return function unwatchFn () {
watcher.teardown()
}
}
watch去调用。在这里不再过多赘述。
$watch 会创建一个Watcher对象,这块也是涉及响应式原理,在watch中改变的数据可以进行数据的响应式变化。同时也会判断是否有immediate这个属性,如果有的话,就直接调用回调
Watcher
export 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;
newDeps: Array;
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
this.dirty = this.lazy // for lazy 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 {
// expOrFn 是字符串的时候,例如 watch: { 'person.name': function... }
// parsePath('person.name') 返回一个函数获取 person.name 的值
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
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
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
/*获得getter的值并且重新进行依赖收集*/
get () {
/*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
/*
执行了getter操作,看似执行了渲染操作,其实是执行了依赖收集。
在将Dep.target设置为自身观察者实例以后,执行getter操作。
譬如说现在的的data中可能有a、b、c三个数据,getter渲染需要依赖a跟c,
那么在执行getter的时候就会触发a跟c两个数据的getter函数,
在getter函数中即可判断Dep.target是否存在然后完成依赖收集,
将该观察者对象放入闭包中的Dep的subs中去。
*/
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
/*如果存在deep,则触发每个深层对象的依赖,追踪其变化*/
if (this.deep) {
/*递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系*/
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
... 其他方法
}
2.Computed
computed基本用法
{{msg}}
// 25
data () {
return {
conut1: 12,
conut2: 13
}
},
computed: {
msg () {
return this.conut1 + this.conut2
}
}
Vue中我们不需要在template里面直接计算{{this.conut1 + this.conut2}},因为在模板中放入太多声明式的逻辑会让模板本身过重,尤其当在页面中使用大量的复杂的逻辑表达式处理数据时,回对页面的可维护性造成很大的影响/ 而computed的涉及初衷也正是用于解决此类问题
computed和watch的差异
1.computed是计算一个新的属性,并将该属性挂载到vm(Vue实例)上,而watch是监听已经存在且已挂载到vm上的数据,所以用watch同样可以监听computed计算属性的变化(其它还有data、props)
2.computed的本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问computed属性,才会计算新的值,而watch则是当数据发生变化便会调用执行函数
3.从使用场景上说,computed适用一个数据被多个数据影响,而watch适用一个数据影响多个数据
原理分析
当你把一个普通的JavaScript对象传给Vue实例的data选项时,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setter,这些getter/setter对用户来说是不可见的,但是在内部它们让Vue追踪依赖,在属性被访问和修改时通知变化,每个组件都有相应的watcher实例对象,他会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
Vue 响应系统,其核心有三点:observe
、watcher
、dep
:
-
observe
:遍历data
中的属性,使用 Object.defineProperty 的get/set
方法对其进行数据劫持; -
dep
:每个属性拥有自己的消息订阅器dep
,用于存放所有订阅了该属性的观察者对象; -
watcher
:观察者(对象),通过dep
实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应。
初始化computed
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函数(其前后也分别初始化了initData和initWatch)并传入两个参数vm实例和opt.compuetd开发者定义的computed选项,转到initComputed函数:
const computedWatcherOptions = { computed: true }
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) {
// 获取计算属性的定义userDef和getter求值函数
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)
}
}
}
}
-
获取计算属性的定义
userDef
和getter
求值函数const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get
定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加set和get方法的对象形式,所以这里首先获取计算属性的定义userDef,再根据userDef的类型获取相应的getter求值函数。
2.计算属性的观察者 `watcher` 和消息订阅器 `dep`
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
这里的watcher也就是vm._computedWatcher对象的引用,存放了每个计算属性的观察者watcher实例,,Watcher
构造函数在实例化时传入了 4 个参数:vm
实例、getter
求值函数、noop
空函数、computedWatcherOptions
常量对象(在这里提供给 Watcher
一个标识 {computed:true}
项,表明这是一个计算属性而不是非计算属性的观察者。
watcher
中实例化了 dep
并向 dep.subs
中添加了订阅者,dep
通过 notify
遍历了 dep.subs
通知每个 watcher
更新。
3.defineComputed
定义计算属性
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)
}
}
因为 computed
属性是直接挂载到实例对象中的,所以在定义之前需要判断对象中是否已经存在重名的属性,defineComputed
传入了三个参数:vm
实例、计算属性的 key
以及 userDef
计算属性的定义(对象或函数)。 然后继续找到 defineComputed
定义处:
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
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)
}
在这段代码的最后调用了原生 Object.defineProperty
方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition
,初始化为:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
随后根据 Object.defineProperty
前面的代码可以看到 sharedPropertyDefinition
的 get/set
方法在经过 userDef
和 shouldCache
等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinition
的 get
函数也就是 createComputedGetter(key)
的结果,我们找到 createComputedGetter
函数调用结果并最终改写 sharedPropertyDefinition
大致呈现如下:
sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
},
set: userDef.set || noop
}
当计算属性被调用时便会执行 get
访问函数,从而关联上观察者对象 watcher
然后执行 wather.depend()
收集依赖和 watcher.evaluate()
计算求值。
computed总结
1.在组件初始化的时候,computed和data会分别建立各自的响应系统,observer遍历data中每个属性设置get/set数据拦截
2.初始化computed会调用initComputed函数
1.注册一个watcher实例,并在内实例化一个Dep消息订阅器用作后续手机依赖(比如渲染函数的watcher或者其他观察该计算属性变化的watcher)
2.调用计算属性时会触发其objecet.defineProperty的get访问器函数
3.调用watcher.depend()方法向自身的消息订阅器dep的subs中添加其他属性的watcher
4.调用watcher的evaluate方法(进而调用watcher)的get方法让自身成为其他watcher的消息订阅器的订阅者,首先将watcher赋给Dep.target,然后执行getter求值函数,当访问求值函数里面的属性(比如来自
data
、props
或其他computed
)时,会同样触发它们的get
访问器函数从而将该计算属性的watcher
添加到求值函数中属性的watcher
的消息订阅器dep
中,当这些操作完成,最后关闭Dep.target
赋为null
并返回求值函数结果。5.当某个属性发生变化,触发
set
拦截函数,然后调用自身消息订阅器dep
的notify
方法,遍历当前dep
中保存着所有订阅者wathcer
的subs
数组,并逐个调用watcher
的update
方法,完成响应更新。