04-Vue的响应式机制浅谈-Watcher

颜志业
2023-12-01

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

那我们着重说一下_renderWatcher。

这个对象是在什么时候实例化的?答案是在挂载的时候,即调用 m o u n t , 具 体 的 说 实 在 m o u n t C o m p o n e n t 方 法 里 面 创 建 的 。 mount,具体的说实在mountComponent方法里面创建的。 mountmountComponentmount会调用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别的用法

我发现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源码的深入,不断的优化调整的。

如果你觉得有用,请点赞:)

 类似资料: