Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue源码-计算和监听属性 #5

Open
wozien opened this issue Aug 18, 2020 · 0 comments
Open

Vue源码-计算和监听属性 #5

wozien opened this issue Aug 18, 2020 · 0 comments
Labels

Comments

@wozien
Copy link
Owner

wozien commented Aug 18, 2020

在Vue的每个组件都有一个渲染watcher,它会被模版用到的数据作为依赖收集,在状态发生变化时,会通知该watcher,从而使组件重新执行renderpatch,最后渲染最新的视图。组件中除了渲染watcher之外,还有计算属性computed和监听属性watch,它们在Vue内部也是watcher的一种。

计算属性

Vue的计算属性常用在模版,并且它的值由响应对象的属性计算而来。来看下面例子:

// App.vue
<template>
  <div class="app">
    <p>{{ fullName }}</p>
    <button @click="handleClick">click</button>
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
    return {
      firstName: 'wozien',
      secondName: 'zhang'
    };
  },
  computed: {
    fullName() {
      return this.firstName + '-' + this.secondName;
    }
  },
  methods: {
    handleClick() {
      this.firstName = 'wozien1';
    }
  }
};
</script>

上面的例子其实可以用方法实现,但是计算属性和方法的区别就是具有缓存的机制,而方法在每次render的时候都会重新执行一遍。现在我们通过源码来看看Vue是怎么设计计算属性的。

计算属性的初始化是在src/core/instance/state.js中的initComputed函数:

const computedWatcherOptions = { lazy: true }

function initComputed(vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // 新建一个计算watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // 组件的computed会在新建组件构造器是挂载到原型对象上
    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._computedWatchers存储实例的所有计算watcher,然后遍历计算属性,把计算函数作为getter新建对应的计算watcher。这里和渲染watcher的一个区别就是它有一个配置项{ lazy: true }, 在构造函数会走下面逻辑:

this.dirty = this.lazy 

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

很显然,新建一个计算watcher不会马上执行get方法。接下来,判断如果计算属性的key不存在实例上,会调用defineComputed方法定义计算属性,否则判断是否和data或者props重名。

对于组件的计算属性,调用defineComputed方法是在新建组件构造器是定义的,它把计算属性挂载到构造器的原型对象。这样做的目的是防止每次新建组件实例都去重新定义一遍。它定义在Vue.extend()中:

Vue.extend = function (extendOptions: Object): Function {
  // ...
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // ...
}

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

现在来看下defineComputed方法的定义:

// 用Object.defineProperty定义计算属性
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

这个方法主要就是把计算属性定义在target上,并且通过createComputedGetter方法设置getter,对于setter用户可以自定义否则为空函数noop。现在我们看下createComputedGetter是怎么定义的:

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        // 计算属性获取最新值
        watcher.evaluate()
      }
      if (Dep.target) {
        // 让计算属性的关联的属性收集渲染watcher
        watcher.depend()
      }
      return watcher.value
    }
  }
}

这个方法返回了计算属性的getter函数。当我们正在执行渲染watcherget方法时,对于组件模版访问到的计算属性,就会触发这个getter

首先,通过实例上_computedWatchers获取到计算watcher。如果watcher.dirtytrue,就是执行evaluate方法:

evaluate () {
  this.value = this.get()
  this.dirty = false
}

所以,计算watcher的求值会在模版render的时候去调用,然后把this.dirty设置为false,所以下面再次访问计算属性会直接返回之前保存的值。那么什么时候会还原成true呢?那当然是计算属性依赖的状态数据发生改变时,接下来我们会分析到。

另外一个注意的是,在执行计算watcherget方法时,也就是执行计算函数。比如我们例子中的:

fullName() {
  return this.firstName + '-' + this.secondName;
}

在执行这个函数时,用到的firstName和secondName状态会触发getter,然后把fullName的计算watcher作为依赖进行收集。在计算属性求完值后,会执行下面逻辑:

if (Dep.target) {
  // 让计算属性的关联的属性收集渲染watcher
  watcher.depend()
}

// watcher.js
depend () {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

这端代码的意思就是让计算属性依赖的状态收集当前的Dep.target。很明显,当前的Dep.target就是组件的渲染watcher。所以我们的例子的firstName和secondName都有两个依赖,分别是计算watcher和渲染watcher。这样我们的组件就渲染结束,接下来就看看当计算属性的依赖状态发生改变时Vue的处理。

比如我们例子点击后会触发this.firstName = 'wozien1',然后会执行firstName状态的setter通知依赖,调用依赖的update方法:

// watcher.js
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    // 计算属性watcher的更新把dirty设为true
    // 在执行渲染watcher的时候获取最新的值
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

对于第一个依赖是计算watcher,它的lazytrue,所以只是简单的把dirty重新设置为true就结束了。然后到了渲染watcher,它会执行queueWatcher(this)在下个tick中执行run方法:

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)
      }
    }
  }
}

执行run方法组件就会重新render,这个时候计算属性由于dirty已经重置为true,所以会执行evaluate获取最新的值来渲染视图。因此,我们的状态改变导致计算属性变化进而更新视图的流程就结束了。你会发现,如果我们的状态设置成和之前一样的值,这个时候不会触发状态setter,也就是计算watcherdirty还是为false,所以计算属性还是缓存了之前的值。

说白了就是,如果模版中用到了计算属性,那么计算属性依赖的状态的改变必然会引起模版的变化,所以把渲染watcher收集进状态的dep即可。然后在触发渲染watcher要把计算watcher的缓存标记dirty设置为true,获取计算属性最新值。

监听属性

对于监听属性的入口是定义src/core/instance/state.js中的initWatch方法:

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)
    }
  }
}

这个方法主要遍历实例的watch配置调用createWatcher方法。因为Vue允许我们为一个watch添加多个handler,所以要处理handler是数组的情况。

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)
}

首先处理下watch的配置是一个对象的情况,获取对象的handler最后调用vm.$watch方法。这个方法是stateMixins挂载到原型上的:

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) {
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  return function unwatchFn () {
    watcher.teardown()
  }
}

这个方法为实例的监听属性新建一个watcher。如果expOrFn是一个函数的话,它会作为watchergetter。否则会执行下面的代码:

// watcher.js
if (typeof expOrFn === 'function') {
  this.getter = expOrFn
} else {
// 字符串情况
  this.getter = parsePath(expOrFn)
  if (!this.getter) {
    this.getter = noop
    process.env.NODE_ENV !== 'production' && warn(
      `Failed watching path: "${expOrFn}" ` +
      'Watcher only accepts simple dot-delimited paths. ' +
      'For full control, use a function instead.',
      vm
    )
  }
}

parsePath方法是获取字符串形式的对象属性,并且返回一个获取的函数:

const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
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
  }
}

这个方法就是把字符串按.切割,然后循环就可以访问到最终监听的属性。这样两种方式的监听属性的getter都可以拿到,在执行getter后就会把该watcher作为依赖被监听的属性收集。要注意的是,如果监听属性是一个函数,则这个函数里面访问的属性都会收集这个watcher。然后在状态发生改变时,就会通知这个watcher执行update,最后在下个tick中执行对应的回调函数。

接下来对于配置了immediatetrue的属性,马上执行回调函数。最后返回一个可以卸载这个watcher的函数,卸载watcher主要是调用teardown方法:

teardown () {
  if (this.active) {
    // remove self from vm's watcher list
    // this is a somewhat expensive operation so we skip it
    // if the vm is being destroyed.
    if (!this.vm._isBeingDestroyed) {
      remove(this.vm._watchers, this)
    }
    let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)
    }
    this.active = false
  }
}

这个方法首先把自己从实例中移除,然后循环从订阅的Dep中移除。因为我们的watcher回调函数是在下一tick执行的,也就是异步执行的。如果要求回调函数在当前的执行栈中执行,可以设置监听属性的synctrue,然后update方法最直接调用run方法,而不是丢进watcher队列:

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    // 计算属性watcher的更新把dirty设为true
    // 在执行渲染watcher的时候获取最新的值
    this.dirty = true
  } else if (this.sync) {
    // 同步执行
    this.run()
  } else {
    queueWatcher(this)
  }
}

另外一种情况,如果我们监听的是一个对象,并且我们想要对象的子属性发生变化也要触发回调函数。我们可以设置deeptrue

watch: {
  obj: {
    handler: function(){},
    deep: true
  }
}

现在来看下Vue对deep处理。在执行watcherget时候有这样一段逻辑:

if (this.deep) {
  traverse(value)
}

traverse方法主要是循环访问value的属性,触发属性的getter,因为当前Dep.target指向这个监听watcher,所以value的属性也会收集这个watcher,从而子属性改变时就会触发监听的回调函数。

const seenObjects = new Set()

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
  }
  // 对响应式数据deep才有效
  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)
  }
}

总结

现在,我们对Vue的所有类型的watcher都分析完了,其实它们的背后逻辑都是一样的。就是把watcher自身作为依赖收集进状态的Dep,在状态发生改变时,执行回调函数或重新渲染组件。

@wozien wozien added the vue label Sep 15, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant