深入浅出Vue源码 - Computed和Watch
本篇文章将针对Watcher
的三种形态(计算属性 watcher (computed watcher)
、侦听器 watcher(user watcher)
、渲染 watcher (render watcher)
)进行源码分析,从而理解Computed
和Watch
的本质区别。
1.计算属性 watcher (computed watcher)
当我们使用computed
属性时,代码通常是这样书写的
computed: {
isComputed() {
return this.count + 1
},
isGetterComp: {
get() {
return this.count + 1
}
}
}
之后Vue
进行初始化的时候就会通过initComputed
去解析我们所写的computed
里面属性
src/core/instance/state.js
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 (!isSSR) {
// 创建 Watcher 实例
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{ lazy: true }
)
}
...
// 挂载到 vm 实例上
defineComputed(vm, key, userDef)
...
}
}
- 初始化时会根据输入方式的不同获取
computed
中的getter
方法,如果是函数,则直接将该函数赋值给getter
方法;否则则取出计算属性的get
函数,然后赋值给getter
方法 - 如果不是服务器渲染,则创建
computed Watcher
,直接通过new Watcher
的形式创建一个监听器Watcher
- 最后执行
definedComputed
方法,将计算属性挂载在vm
实例上
接下来继续进入Watcher
方法中生成实例
src/core/observer/watcher.js
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
...
vm._watchers.push(this)
this.lazy = !!options.lazy
this.value = this.lazy
? undefined
: this.get()
}
update () {
// 更新时,转换dirty
if (this.lazy) {
this.dirty = true
}
...
}
...
evaluate () {
// computed 执行更新后,将dirty重新赋值
this.value = this.get()
this.dirty = false
}
...
}
// src/core/instance/state.js
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 初始化时直接执行,依赖未变更时,不执行
if (watcher.dirty) {
watcher.evaluate()
}
...
return watcher.value
}
}
}
Watcher
初始化的时候,会对this.dirty
进行赋值,这个值就是传过来的{ lazy: true }
- 页面初始化渲染时,获取计算属性的值,会调用属性中
getter
方法,从而执行Watcher.evaluate
方法 - 执行
evaluate
方法后,会将dirty
值更新为false
- 所以我们下次再次使用这个
computed
属性的时候,由于dirty
的原因,便不会执行watcher.evaluate
,而是直接返回值 - 当计算属性依赖的值发生变化时,
Watcher.update
方法会将this.dirty
重新变为true
,所以当依赖发生变化时,Watcher
会再次执行evaluate
方法,从而对数据进行更新
2.侦听器 watcher(user watcher)
user watcher
是我们书写在watch
对象上的监听器,用于监听某个值,当数据发生变化时,执行watch
中的自定义回调, 处理业务相关逻辑。关于use watcher
的初始化是在initWatch
方法中进行的。
src/core/instance/state.js
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)
}
}
}
- 初始化解析时,
initWatch
会遍历对象,拿到属性值 - 因为属性值可能为数组,所以做了判断处理参考官网watch用法,最终会循环获取每一个监听的值
- 最终调用
createWatcher
生成watcher
实例
src/core/instance/state.js
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)
}
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
...
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
...
}
createWatcher
中则是通过执行vm.$watch
方法进行创建watcher
实例- 创建
user watcher
时,会将options
中的user
置为true
new Watcher
的时候就会执行this.get
,其内部会先访问监听的属性,从而触发getter
进行收集依赖- 当数据发生变化时,
setter
通知依赖更新,最终通过nextTick
函数执行了flushSchedulerQueue
,而在此函数中会调用watcher.run()
完成依赖更新,最终执行this.cb
函数
3.渲染 watcher (render watcher)
渲染watcher
顾名思义,是用来渲染DOM
的,所以渲染watcher
是在组件初始化完成之后,执行$mount
时生成的,接下来根据源码继续分析。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
$mount
挂载的过程其实就是执行了mountComponent
src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
...
callHook(vm, 'beforeMount')
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
...
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
...
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
- 这里通过
new Watcher
进行初始化,初始化时传入了updateComponent
,此函数会在执行this.get()
方法时执行。 updateComponent
函数执行中会调用vm._update(vm._render(), hydrating)
,此_update
函数则是更新DOM的关键- 从这里也可以看出,组组件初始化挂载阶段才会生成
render watcher
,并且组件内只有一个render watcher
至此,Watcher
三种形态的创建以及更新过程全部梳理完成。文中有不正确的地方还请指正。
4. 总结
通过阅读源码,我们知道其实computed
和watch
的本质都是通过 new watcher
实现的,主要区别在于使用场景,computed
是懒执行;而user watcher
则是观察Vue
组件中的属性,当属性更新时作出相应的操作,执行传入的回调函数;至于render watcher
则是专门用于组件渲染的监听器,并且每个组件只有一个render watcher
。
转载自:https://juejin.cn/post/7205141492265418813