vue2 不同类型Watcher对比

2,019 阅读3分钟

在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方法,分为两种情况:

  1. 传入的key为 函数类型,getter设置为此函数;
  2. 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,并将回调函数cbthis指向当前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
}
  1. 将传入的el赋值给实例的$el
  2. 如果options中未定义render函数,就将render赋值为一个空的虚拟节点
  3. 调用beforeMount钩子
  4. 定义updateComponent 方法作为实例化render watcher的getter方法
  5. 实例化render watcher,第二个参数传入updateComponent 方法,cb为noop,options包含before( )方法, isRenderWatch为true(区别于其他两种watcher的参数)。
  6. 如果实例的$vnode不存在,将实例的_isMounted赋值为true,并调用mounted钩子
  7. 最后,返回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类型的参数,实例化和更新时完成各自的职责。