Vue2源码解析☞ 4 ☞ 搞懂Vue中的三种Watcher

3,369 阅读14分钟

雨露同行风雨同舟.jpg

活着,最有意义的事情,就是不遗余力地提升自己的认知,拓展自己的认知边界。

引言

Vue中,会遇到三种类型的watcher实例,他们分别为

  • render watcher:用于vue组件渲染
  • watch watcher:被监听的属性值变化后,执行对应的回调函数
  • computed watcher:用于控制计算属性是否需要重新计算

vue组件渲染,计算属性和属性监听都会创建watcher实例,那么

  • watcher是如何初始化的?
  • 用户编写的computed选项,watch选项是如何初始化的?
  • 不同的watcher实例有何区别?
  • 不同的watcher是如何收集依赖的?
  • 不同的watcher是如何实现更新的?
  • 源码对于我们编写代码有哪些启示?

希望下面的解读,能帮你解读上面的疑惑,旅途愉快!!!

测试案例

<!DOCTYPE html>
<html>
<head>
    <script src="../../dist/vue.js"></script>
</head>
<body>
    <div id="demo">
        <h1>鞋袜搭配</h1>
        <div>{{sock}}</div>
        <div @click="change">change</div>
    </div>
    <script>
        // 创建实例
        const app = new Vue({
            el: '#demo',
            data: { 
              sock: '袜子',
              shoes: '帆布鞋',
              stillLove: true,
              feelHurt: true,
                life: {
                  light: 'can not bear',
                  chinese: '不能承受的生命之轻',
                  author: ''
              }
            },
            computed: {
                missing: {
                    get: {
                      feelLive(){
                        return this.stillLove || this.feelHurt;
                      },
                      cache: false
                    }
                },
                dressup(){
                  return this.sock + ',' + this.shoes
                }
            },
            methods:{
              change(){
                this.sock.name = '蓝色图案的袜子'
              },
              changeAuthor(){
                  this.life.author = '米兰昆德拉'
              }
            },
            watch: {
              life: [
                  {
                      handler: function(newVal, oldVal){
                          console.log(newVal)
                      },
                      deep: true,
                      sync: true,
                      immediate: true,
                  },
                  {
                      handler: function(newVal, oldVal){
                          console.log(newVal)
                      }
                  },
                  function(newVal, oldVal){
                      console.log(newVal)
                  }
              ]
          }
        });
    </script>
</body>
</html>

源码解析

watcher实例化

构造函数参数

    vm: Component,//vue实例
    expOrFn: string | Function,//字面意思:表达式或函数,在不同的watcher中有不同的处理逻辑
    cb: Function,//回调函数
    options?: ?Object,//控制选项
    isRenderWatcher?: boolean
  • isRenderWatcher:是否是render watcher,也就是渲染watcher(用于触发组件渲染,在响应式机制中有详细描述)

处理options

不同类型的watcheroptions是不同的(见具体watcher的实例化分析)

    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
    }
  • deep:被监听属性的回调函数,响应内部属性的变化。
  • user:标识用户代码,值为true时,出现异常时会有错误提示。
  • lazy:用于computed watcher,值为true时,不会执行run方法。
  • sync:用于watch watcher,值为true时,同步执行run方法,执行更新。
  • before:用于触发beforeUpdate钩子

挂载回调函数

    this.cb = cb //回调函数

挂载特定场景下使用的属性

    this.id = ++uid // uid for batching 在batch过程中区分不同的watcher
    this.active = true //定义watcher的激活状态
    this.dirty = this.lazy // for lazy watchers

依赖收集相关属性

响应式机制章节中,依赖收集阶段的addDep方法中使用这些属性。

    this.deps = [] //执行cleanupDeps后,将newDeps赋值给deps
    this.newDeps = [] //用于收集更新后当前组件实例依赖的deps
    this.depIds = new Set() //执行cleanupDeps后,将newDepIds赋值给depIds
    this.newDepIds = new Set() //防止重复收集依赖

处理expOrFn参数

    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn) //解析path字符串:例如,obj.key.getter
    }

this.getter会在后面的get方法中调用。

watchervalue属性赋值

    this.value = this.lazy
      ? undefined
      : this.get()

响应式机制章节中,对render watcherget方法触发渲染的过程有详细介绍。

get方法的源码如下:

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)//建立watcher实例与dep实例的关联,对于三种watcher都适用
    } catch (e) {
      //略
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {//仅对watch watcher实例适用
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  • pushTarget:将当前的watcher实例赋值给Dep.target
  • popTarget:将Dep.target的值还原为之前的watcer实例;
  • cleanupDeps:执行到此时,表明当前watcher的依赖收集完毕,需要将newDepIdsnewDeps的值分别赋给depIdsdeps,并且将newDepIdsnewDeps的值清空,等待下一次页面更新时,重新收集依赖;

如何理解watch watcher的deep属性 当watch watcher的options中deep属性为true时,会执行traverse函数,源码如下:

export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}
  • 如果val既不是数组,也不是对象,则直接返回;
  • 如果val是冻结的对象,直接返回;
  • 如果val是虚拟dom对象,直接返回;
  • 如果valobserve了,可以通过对应depid属性,防止重复建立watcherdep之间的关联;
  • 如果val是数组,其的元素是对象或数组,则递归地执行_traverse,那么通过val[i]访问时,会触发依赖收集,建立当前watch watcher实例和对应dep实例的关联;如果val的元素是简单值,则不需要建立关联,通过指定的7中方法即可触发valreactiveGetter;
  • 如果val是对象,访问val[keys[i]]时,会建立watch watcher实例与对应dep实例的关联;

上面的描述很抽象,举个例子吧:

    data(){
        return {
            foot: {
                haveScar: true,
                touched: ''
            }
        }
    },
    methods: {
        touch(){
            this.foot.touched = '疼'
        }
    },
    watch: {
        'foot': {
            handler(newVal, oldVal){
                console.log(newVal)
            },
            deep: true,
        }
    }
  • 首先,在initWatch时,会为foot创建一个watch watcher实例,由于deep的值为true,所以会为foottouched属性对应的dep实例和watch watcher实例建立关联;
  • touched属性的值变化后,会执行dep.notify,然后通知所有关联的watcher实例去更新;
  • 因此,当foottouched属性变化后,foothandler也会执行,deep的意义也正在于此;

watcher实例执行更新

通常一个属性的值变化后,会触发对应的reactiveSetter函数,执行dep.notify函数,通知dep.subs中的所有watcher实例执行update

watcher的update方法

  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  • 场景一:lazy属性为true,将dirty的值恢复为true,表示watcher监听的值变化了,但并不会立即执行,也不会加入到异步更新队列。
  • 场景二:sync属性为true,立即执行watcher实例的run方法。
  • 场景三:大部分情况下,会将该watcher实例加入到异步更新队列中,然后依次执行nextTicktimerFuncflushCallbacksflushSchedulerQueue(这些方法将会在后续的异步更新机制章节中详细说明),然后执行watcherrun方法。

watcher的run方法

  run () {
    if (this.active) {
      const value = this.get()//获取watcher的value修改后的值
      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) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
  • active:表示watcher实例是否处于激活状态;
  • watcher的类型是render watcher时,get方法中的this.getter(vm,vm)本质上就是执行updateComponent,返回值永远是undefined,所以不会执行后面的逻辑;
  • watcher的类型是watch watcher时,根据user属性的设置,有分别的执行回调函数;
  • computed watcher实例不会执行run方法;

computed watchers

computed选项的初始化

computed选项的初始化,发生在beforeCreate钩子之后,created钩子之前。

那么Vue是如何初始化用户编写的coputed代码,请看下面的源码:

const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  
  for (const key in computed) {
    const userDef = computed[key] //获取用户定义的代码
    // 用户编写代码的两种形式:1、函数 2、定义取值器
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    
    if (!isSSR) {//非服务端渲染
      // create internal watcher for the computed property. 
      // 为每个computed选项创建一个内部watcher,内部watcher是相对于vue实例而言的
      // 一个vue实例对应一个render watcher,所以也可以理解为是相对于render watcher而言的
      //创建watcher实例,并将key和watchers(computedWatchers)建立映射
      watchers[key] = new Watcher( 
        vm,
        getter || noop,
        noop,
        computedWatcherOptions //对所有的computed watcher是相同的(见第一行代码)
      )
    }

    // 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)
      } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(`The computed property "${key}" is already defined as a method.`, vm)
      }
    }
  }
}
  • computed选项是一个对象,会为每一个key创建一个watcher实例.
  • 用户编写在computed中的代码(userDef),就是watcher实例化过程中的expOrFn
  • defineComputed:定义计算属性,详细逻辑(见下文)
  • 对于所有的computed watcheroptions中仅传入了lazy:true,不会立即调用get方法,value的默认值是undefined

defineComputed:定义计算属性

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering() //客户端渲染:缓存watcher实例
  if (typeof userDef === 'function') { //userDef是函数
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else { //userDef是对象,且拥有取值器get方法
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)//定义属性
}
  • 通常情况下,用户编写的userDef是函数,服务端渲染不缓存watcher实例,客户端渲染缓存watcher实例。
  • 通过对源码的分析,若想自行控制一个计算属性是否缓存时,将userDef写成对象形式,并定义cache属性,通过cache的值来决定一个计算属性是否缓存对应的watcher实例。
  • 最后,将计算属性挂载到vm上(可以通过this访问)

createComputedGetter:创建计算属性的getter(缓存)

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 //返回计算属性的值
    }
  }
}
  • _computedWatchers属性上保存了所有计算属性的computed watcher实例。
  • 初始化阶段,只是将computedGetter函数赋值给对应key的取值器。computedGetter函数的执行细节,见下文。

createGetterInvoker:创建getter触发器

function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}
  • fn:是用户编写的userDef
  • thiscomputedGetter被赋值给计算属性key的取值器,并被挂载到vm实例上,所以此处的thisvm
  • 通过源码可知,此computedGetter中并没有使用缓存的computed watcher实例,而是每次访问时都将userDef重新执行一遍,无论计算属性依赖的变量是否变化。

computed watcher的值是何时计算的?

userDef代码中,打一个断点,下面我们看看执行userDef时的调用堆栈:

computedGetter.png

  • 从userDef开始往前回溯,一直到Vue._render(可参考响应式机制章节的依赖收集)
  • (匿名):渲染函数,执行过程中访问计算属性
  • computedGetter:计算属性的取值器,从vm的_computedWatchers中取出对应计算属性的watcher实例,初次渲染时,由于watcher实例化时,dirtylazy属性被赋值为true,所以一定会执行watcher.evaluate()方法。
  • evaluate:在此方法中,做了两件事,执行watcher实例的get方法,将dirty属性赋值为false
  • get:主要执行了getter方法,这个方法是由构造函数的expOrFn参数决定的,也就是用户编写的userDef
  • dressup:要执行的userDef,除了会返回计算属性的值,在访问其他属性的时候,还会将computed watcher实例与data中的关联属性对应的dep实例建立关联。

computed watcher的依赖收集

  if (watcher.dirty) {
    watcher.evaluate()
  }
  if (Dep.target) {
    watcher.depend()
  }
  return watcher.value
  • 在执行watcher.evaluate()方法后,对计算属性进行了重新计算;
  • 建立render watcher实例与data中关联属性对应的dep实例之间的关系。(如果模板template中有直接访问data中的属性,那么在data初始化的过程中,将会建立render watcher实例与dep实例之间的关联;如果模板template中没有直接访问,而是通过计算属性间接访问,那么在此补充建立render watcher实例与dep实例之间的关联。建立关联时,会通过dep实例的id属性,防止重复)
  • 最后,返回计算属性的值;

计算属性值的更新

当计算属性依赖的属性key发生变化后,key对应的dep会通知关联的watcher实例执行update,其中包括computed watcher实例,render watcher实例等。

由于computed watcher实例的lazy属性值为true,因此执行实例方法update时,会将watcher实例的dirty属性置为true,不会调用run方法。

render watcher实例执行updaterun方法后,会重新渲染页面,在执行渲染函数时,会访问计算属性。

由于createComputedGetter方法中为计算属性定义了取值器(computedGetter),所以访问计算属性时,如果computed watcher实例的dirty属性为true,将会调用evaluate方法,重新计算计算属性的值。

watch watcher

在初始化用户编写的watch选项时,依次执行如下函数:

  • Vue._init
  • initState
  • initWatch

initWatch:初始化watch选项

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key] //从watch选项中取出key对应的处理函数
    if (Array.isArray(handler)) {//handler可以是数组
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
  • 在初始化阶段,Vue会为watch选项的每一个key的每一个处理函数执行createWatcher
  • 源码告诉我们:可以为key定义一个或多个handler

createWatcher:调用$watch

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)
}
  • 如果handler是一个普通对象,意味着我们在写代码时要将处理函数挂载到handler属性上;
  • 如果handler是字符串,意味着我们可以将options写在datamethods等选项中;
  • 最后调用vue的实例方法$watch

$watch的逻辑

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {//将handler从cb中解析出来
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true //标识:用户编写
    //创建watch watcher实例,会挂载回调函数,并挂载value属性,即expOrFn属性对应的值
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {//如果用户想立即执行
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      //执行回调函数,如果报错,则执行错误处理
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
  • 用户可以在代码中动态的调用$watch,也可以通过编写watch选项间接调用$watch
  • 只要cb是普通对象,就需要执行createWatcher函数,从中去解析handler,也就是真正的处理函数;
  • watcher实例化时,computed watcherrender watcher传入的cb都是noop,也就是什么都不执行,只有watch watcher传入的cb是用户编写的处理函数;由于lazy的值是false,所以会执行watcherget方法,调用this.getter(vm, vm),其实就是获取被watch的属性expOrFn的值,在此过程中会收集依赖,建立watch watcher实例与dep实例的关联(下面有详细说明)。
  • 如果用户编写的options中的immediate属性是true,则在expOrFn的值第一次变化时,就执行回调函数,回调函数中的oldValueundefined
  • 执行invokeWithErrorHandling,出现异常时,在控制台打印info
  • 最后,返回unwatchFn,用于销毁watcher实例,本质上是将watcher实例从vm._watchers中删除,从depsubs中删除,解除对watcher实例的引用。(适用场景:动态调用$watch)

依赖收集

在dep.js文件中的depend方法中打一个断点,下面看一下调用堆栈:

watch_watcher.png

  • Vue.$watch:调用Vue的原型方法$watch
  • Watcher:创建watcher实例
  • get:在watcher实例化过程中,lazy属性为false时,执行get方法
  • (匿名):(注意)此处不是渲染函数,而是在实例化过程中,赋值给getter属性的parsePath函数(下面会展示parsePath的源码)
  • proxyGetter:调用parsePath函数中访问了data选项中的属性,data中的属性通过proxyGetter被代理并挂载到vm
  • reactiveGetterdata选项中的属性,在initData阶段定义的数据劫持
  • dependdep实例的方法

parsePath的逻辑

export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
  • 在实例化过程中,调用parsePath,传入了expOrFn(对于watch watcher来说,就是watch选项中被监听的属性),并将返回的函数赋值给watcher实例的getter属性。
  • watcherget方法中,调用了this.getter(vm, vm),第一个vm是绑定函数上下文中的this,第二个vm就是函数的实参,形参是obj,通过segments数组,将递归转换成了循环,最终返回的就是被监听属性对应的值。
  • 在执行obj = obj[segments[i]]代码时,由于访问了data选项中的属性,所以触发了相应属性的取值器,建立watch watcher实例与dep实例的关联。

触发watch wacherhandler

watch watcher实例为什么不能设置lazy为true 如果给watchoptions设置lazytrue,那么watch watcher实例的handler将不会执行,被监听的keyhandler之间并没有建立关联。

computed watcher之所以可以设置lazytrue,是因为在初始化时,为计算属性定义了computedGetter函数,当页面重新渲染时,执行渲染函数,访问计算属性,computedGetter会执行,因此会获取计算属性的最新结果。

sync为false时的调用堆栈如下:

触发watcher的handler.png

syncfalse时,会将watch watcher实例添加到异步更新队列中,依次执行了queueWatchernextTicktimerFuncflushCallbacksflushSchedulerQueuerun等方法,异步更新机制将在后续的文章中详细说明。

sync为true时的调用堆栈如下:

sync.png

sync属性为true时,在update方法中,会直接调用run方法,进而执行watch watcher实例的回调函数,也就是用户编写的handler

render watcher

Initial Render:初次渲染

关于render watcher的详细内容,在响应式机制章节的初始化render watcher阶段中已做详细说明,

Rerender:重新渲染

render watcherwatch watcher的异步更新流程大致相似,区别在于:

  • watch watcher可以通过sync属性控制同步或异步执行run方法,而render watcher只能是异步执行的;
  • watch watcher是通过执行watcher.get方法获取修改的值,通过回调函数执行用户逻辑,而render watcher是通过watcher.get触发渲染的。

编码小贴士

computed选项

userDef(计算属性的值)是函数:

    computed: {
        timeToSleep(){
            return this.workIsDone && this.messageIsSend
        }
    }

userDef是对象:

    computed: {
        missing: {
            get(){
              return this.stillLove || this.feelHurt;
            },
            cache: false
        },
    }

当设置cache属性为false时,computed watcher实例将不会保存在vm._watchers中,只要访问计算属性都将执行一遍userDef

watch 选项

示例:

    data(){
        return {
            life: {
                light: 'can not bear',
                chinese: '不能承受的生命之轻',
                author: ''
            }
        }
    },
    methods: {
        changeAuthor(){
            this.life.author = '米兰昆德拉'
        }
    },
    watch: {
        life: [
            {
                handler: function(newVal, oldVal){
                    console.log(newVal)
                },
                deep: true,
                sync: true,
                immediate: true,
            },
            {
                handler: function(newVal, oldVal){
                    console.log(newVal)
                }
            },
            function(newVal, oldVal){
                console.log(newVal)
            }
        ]
    }
  • options的三种形式:数组,对象,函数
  • options是数组时,每一个handler可以是对象,也可以是函数,当handler是对象时,内部需要显示定义handler属性,用来标识回调函数。
  • 当配置deeptrue时,被监听的属性的回调函数可以响应内部属性的变化。
  • 当配置immediatetrue时,在初始化时就会执行一次回调函数。
  • 当配置synctrue时,watcher实例执行update方法时,会同步执行run方法,进而执行回调函数,而不会进入异步更新队列。

结束语

山水.png

千山万水何惧怕,拨开云雾见红霞

—— 祝君好梦 ——