在vue项目实践中,你是否理解了数据驱动视图的原理呢?这篇文章从源码的角度,介绍和总结了vue2中3种watcher的区别,以及各自的工作方式,旨在阐明数据变化之后,视图是怎样触发更新的。
一、user watcher
依然从new Vue ( ) 说起:
在src\core\instance\index.js中定义了
initMixin( Vue ),stateMin( Vue ) 方法分别在Vue的原型链上添加_init和$watch方法,并将Vue导出。
new Vue实例便会调用_init方法,首先创建vue实例vm,下面将流程进一步简化下:
_init( ) --> initState( vm ) --> initWatch( vm, vm.$options.watch ) --> createWatcher( vm, key, handler ) --> vm.$watch( key, handler) --> new Watcher( vm, key, handler)
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)
}
}
}
src\core\observer\watcher.js中包含了Watcher类的定义,先看构造函数声明:
constructor Watcher(
vm: any,
expOrFn: string | Function,
cb: Function,
options?: Object,
isRenderWatcher?: boolean): Watcher
其中,vm为传入的Vue实例,expOrFn 为string或者Function类型的key( 因为自定义watcher有几种不同的形式 ),cb 为用户自定义的回调函数,其他参数为空。 接着根据传入的参数进行Watcher的实例化工作(构造函数实现):
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
if (!option) { // $watch中option = {user: true}, 所以此if语句不会执行
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
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
// 返回值不存在时,this.getter 设置为noop,并报错提示
}
this.value = this.lazy ? undefined : this.get()
设置Warcher实例的getter方法,分为两种情况:
- 传入的key为 函数类型,getter设置为此函数;
- key的类型为其他(自定义watcher中为string类型), 调用parsePath方法,返回包含key的闭包函数;
不管key是什么类型,在不报错的前提下, getter属性最终为一个函数。
value属性是不同类型watcher的区别之一, 敲黑板!!!
自定义Watcher,lazy属性取默认值false,接下的话,就会通过三元表达式执行get方法,下面详细看下get方法的执行过程
function 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
}
pushTarget( this )将当前watcher实例推入targetStack的顶端。
调用getter方法,参数为vue实例vm,返回vm中data属性里定义的key属性的值。访问key属性的getter方法,因为vue实例化已经做了getter拦截,所以会key属性的依赖收集, 将当前watcher实例添加到订阅者Dep中(这部分内容可以参考 Vue2响应式原理 的介绍)。
当data中的key属性发生变化时,setter方法中调用dep.notify( ),执行watcher实例的update( ) --> queueWatcher( ) --> run( ) --> cb
function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
function run () {
const value = this.get()
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)
}
}
get( )获取到newValue,若不等于oldValue,并将回调函数cb的this指向当前vue实例并调用,完成用户指定的操作。
二、computed
如果定义了计算属性,在initWatch之前会先执行initComputed( vm, vm.$options.computed)。不考虑ssr的情况,代码主要逻辑如下:
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions)
}
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)
}
}
}
}
遍历vm, vm.$options.computed对象,userDefinition为每一个key属性对应的值。getter方法定义为userDef或者key属性的get方法。定义一个watchers对象,存储key和watch的对应关系。
没有定义computed中的userDef方法,或者userDef不是函数类型,也没有get方法(这里一般指不是响应式对象)时,开发环境下报错。当key是data或者props中的属性时,开发环境下也会报错。
watcher实例化时传入function类型的getter,cb为noop,computedWatcherOptions中lazy为true(lazy属性是区别于其他两种watcher的)。
this.dirty = this.lazy // for lazy watchers
this.getter = expOrFn
this.value = this.lazy ? undefined : this.get() // lazy 为true,所以不会执行get方法,value的值为undefined
各个key对应的方法被watcher实例化之后,调用defineComputed( vm, key, userDef)方法,其中的主要步骤是
Object.defineProperty(vm, key, sharedPropertyDefinition)
拦截key的get和set。其中sharedPropertyDefinition的get被定义为createComputedGetter(key),set当userDef为function时,为noop,否则为userDef.set || noop。
下面看一下createComputedGetter (key)方法的实现:
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
其中this._computedWatchers即为上文的watchers对象,保存着key和计算属性方法的关系。
当视图中用到key属性时,get方法调用到createComputedGetter (key)方法。因为computed watcher的dirty属性初始化为true,所以执行evaluted( )方法。
function evaluate () {
this.value = this.get()
this.dirty = false
}
因为key是响应式的,所以watcher的get方法让UseDef中依赖的data属性的dep收集了此watcher依赖,然后将dirty赋值为false,最后返回userDef的结果value。
此后,当依赖的data属性发生变化,通知watcher调用update方法进行更新。( 再次敲黑板!!!)
function update () {
if (this.lazy) {
this.dirty = true
}
// 省略其他代码
}
由于lazy为true,所以只会将dirty赋值为true。等到再次调用到计算属性时,才会执行 createComputedGetter (key)调用 watcher 的 evaluate 方法计算value。
这样做的好处是什么呢?
其中之一就是减少渲染消耗,比如,某一个被依赖的data属性频繁变化,如果没有dirty的逻辑,就应该去更新视图了。data属性每变化一次,就要更新一次视图,会造成一定的性能损耗。computed watcher会等到视图需要更新时一次将value更新到最新的值。
computed vs methods
我们可以使用 methods 来替代 computed,效果上两个都是一样的,但是 computed 是基于它的依赖缓存,只有相关依赖发生改变时才会重新取值。而使用 methods ,在重新渲染的时候,函数总会重新调用执行。
三、render watcher
initMixin( Vue )方法的最后执行
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
$mount方法在不同的目标平台的runtime中都返回mountComponent方法,mountComponent方法在src\core\instance\lifecycle.js文件中被定义,主要代码如下:
function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
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
}
- 将传入的el赋值给实例的$el
- 如果options中未定义render函数,就将render赋值为一个空的虚拟节点
- 调用beforeMount钩子
- 定义updateComponent 方法作为实例化render watcher的getter方法
- 实例化render watcher,第二个参数传入updateComponent 方法,cb为noop,options包含before( )方法, isRenderWatch为true(区别于其他两种watcher的参数)。
- 如果实例的$vnode不存在,将实例的_isMounted赋值为true,并调用mounted钩子
- 最后,返回vue实例对象
下面我们再深入到watcher的构造函数中,看一下render watcher的初始化过程。
if (isRenderWatcher) {
vm._watcher = this
}
this.before = options.before
以上是render watcher独有的配置。before方法在watcher被通知更新时在run方法之前调用,因为其中会调用beforeUpdate,所以才会在视图更新时执行到我们beforeUpdate钩子中的逻辑。
视图之所以能够渲染出来,就是updateComponent 方法中vm._update(vm._render(), hydrating)的作用。
lifecycleMixin(Vue)中在Vue原型在定义了_update方法,renderMixin(Vue)中在Vue原型在定义了_render方法。render watcher实例化时,执行vm._update(vm._render(), true)。_render方法调用传入的options.render(可以是新建 Vue 实例时传入的 render() 方法,也可以由 Vue 的 compiler 模块根据传入的 template 自动生成)生成vdom树,_update调用Vue原型上的__patch__转化为真实Dom,完成视图的渲染。
render函数将template转化为dom的过程中,将render watcher收集到各属性的依赖中,属性变化触发render watcher更新,继续执行updateComponent方法,完成视图的更新(详细过程参考vue2 vdom和diff算法)。
四、总结
- user watcher
options中的user置为true,实例化时传入string类型的key,回调函数cb。参数key必须是data中的属性,初始化时返回data 中key属性的值(未在data中定义,开发环境下会提示响应的错误信息),属性发生变化时,调用回调函数。
用法:
var watchFn = function(newValue3, oldValue3){
console.log(newValue3, oldValue3)
}
var watch = {
first(newValue1, oldValue1){
console.log(newValue1, oldValue1)
},
second: {
handler(newValue2, oldValue2){
console.log(newValue2, oldValue2)
},
immediate: true
deep: true
},
'third.msg': watchFn
}
配置immediate属性为true,实例化watcher之后,$watch中会立即执行一次回调函数
- computed watcher
watcher实例化时传入function类型的getter,cb为noop,computedWatcherOptions中lazy为true。key是data或者props中的属性名时,开发环境下报错提示。 当依赖的属性变化时,只是将dirty赋值为true。等到计算属性被引用时,才取value的值,并将dirty赋值为false。
- render watcher
watcher实例化时传入function类型的getter,cb为noop,options中包含before( )函数。
user watcher实例化时返回data属性中的数据,数据发生变化后调用回调函数。 computed watcher和render watcher没有回调函数,在watcher的get方法中执行传入的第二个function类型的参数,实例化和更新时完成各自的职责。