core/observer/watcher.js
Watcher的注释很能说明它的意图
/**
* 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.
* /
它解析表达式,收集依赖,当表达式变化时触发回调。从这话中我们能知道,Watcher连接了表达式和回调。表达式的值变了,就回调。
表达式和回调分别对应Watcher中的getter和cb字段。getter是对表达式处理过的得到的函数,当然表达式如果本身是函数,就不用处理了。
Watcher的正常用法是getter变化,然后出发cb回调。不过对于组件实例的_renderWatcher却有点特殊。在没有看代码之前,我的想法一定是_renderWatcher监控getter中的内容变化,然后回调cb用来更新视图。结果却不是,_renderWatcher只设置了getter,却没设置cb(是noop)。那它的getter是啥?
竟然是更新视图的操作,就问你惊不惊喜意不意外。
那我们着重说一下_renderWatcher。
这个对象是在什么时候实例化的?答案是在挂载的时候,即调用 m o u n t , 具 体 的 说 实 在 m o u n t C o m p o n e n t 方 法 里 面 创 建 的 。 mount,具体的说实在mountComponent方法里面创建的。 mount,具体的说实在mountComponent方法里面创建的。mount会调用mountComponent(core/instance/lifecycle.js里面)。
/**
* 挂载
* @param {*} vm
* @param {*} el
* @param {*} hydrating 不知道这个字段是什么意思,英文翻译是水化合啥的
*/
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el // 挂载点
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 原来组件实例的渲染watcher是在这里创建的
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// 在Watcher构造函数里面已经调用了updateComponent
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
我把创建_renderWatcher的地方摘录出来
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
第二个参数就是getter,第三个参数就是cb。在new Watcher的时候,也就是Watcher的构造函数里面,最后会调用getter这个方法获取当前Watcher实例的value值。也就是说在上面代码执行结束后,updateComponent已经被调用了。
那我就有疑问了,Vue是怎么做到数据变化之后通知修改视图的,这个Watcher根本就没有回调cb呀(cb是noop)。
在创建上面Watcher实例的过程中,我们说过会调用getter,如下
...
this.value = this.lazy
? undefined
: 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
}
构造函数是通过调用get方法,而get方法内部调用getter的。我们能看到get方法的开头有一句pushTarget(this),这是关键,圈起来要考的!这句是把当前Watcher实例放到Dep.target上,你知道Dep.target是一个全局的对象,指向当前的Watcher实例就行了。怎么用呢?玄机都在this.getter.call(vm, vm)里了。
按照我们刚才说的,getter是updateComponent,它会调用_render,而_render是根据模板生成的,要渲染模板势必会访问vm的属性。在defineReactive的时候,给所有响应式的属性设置了get,在里面会有一段如下的代码:
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
我把get摘出来,看的清楚一些
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
这里会判断Dep.target,然后调用dep.depend(),这句话的意思就是把dep跟Dep.target关联上。然后再看set
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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
最后一句dep.notify会通知跟当前属性关联的Watcher实例进行更新。联系上面我们说的是不是就通知了_renderWatcher进行更新呢?我们刚才说了_renderWatcher的cb是空(noop),那你通知_renderWatcher更新有个鬼用?还得看一下Watcher是怎么处理dep.notify的。dep.notify会调用Watcher的update,而update最终又会调用run,我们直接看run吧,这期间还牵扯到Vue异步异步执行Watcher的机制,跟我们要了解的内容关系不大,因此直接跳到run方法
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
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
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
看const value = this.get();,它调用了get方法,还记得我们上面说的吗,get会调用getter,而_renderWatcher的getter就是updateComponent,这样就实现了更新视图的效果。
哎,终于说完了,也不知道能看明白的有多少人,慢慢增进自己的表达能力吧。
可能是我对Vue的源码还没有十分熟悉的原因,我总觉得这个Dep.target很绕。理解起来真的好难。
后来我发现计算属性也是这样来实现的,getter就是是计算属性的值,cb还是noop。计算属性也应该写一篇文章来说一下,传送门
我发现Watcher有两种使用方法,一种就是上面_renderWatcher,cb本身没有啥东西,全靠getter。还有一种就是我们最能理解的,getter变化了,cb被调用。现在来说一下这个应用——watch的处理
{
watch: {
name: function() {
// 干点啥吧
}
}
}
上面代码给vm添加了一个对name的监听,发现watch变化了,就会调用后面的函数。
Vue内部是这样做的
// core/instance/state.js的createWatcher方法里
vm.$watch(expOrFn, handler, options)
// $watch方法是这样实现的
const watcher = new Watcher(vm, expOrFn, cb, options)
说下原理。创建Watcher实例的时候会调用expOrFn(get->getter)。expOrFn一定会访问vm上的属性(成员),它已经使用defineReactive添加了get,在访问这个成员时就会调用Dep.depend()来把这个属性跟上面创建的Watcher实例绑定。有地方修改这个属性的时候,就会dep.notify来通知Watcher实例,这样就走到了我们在_renderWatcher一节中说的run方法,会调用cb,就是name对应的那个函数。
本文会随着我对Vue源码的深入,不断的优化调整的。
如果你觉得有用,请点赞:)