菜鸟初探Vue源码(十)-- 计算属性和侦听属性

在开发过程当中,咱们对这两个属性已经很是熟悉了:computedwatch。可是究其实现原理,或者说两者到底有何区别,以及何时使用计算属性,何时使用侦听属性,相信有很多朋友们仍存在疑惑。下面就一块儿来探讨一下:react

computed

关于计算属性的使用,见以下代码。express

export default {
    data() {
        return {
            msg : {a : 1},
            count : 1
        }
    }
    methods : {
        changeA() {
            this.count++;
        }
    }
    computed: {
        newMsg() {
            if (this.count < 3) return this.count;
            return 5;
        }
    }   
}
复制代码

在初始化时(initState),判断若是opts.computed存在(用户定义了computed属性),执行initComputed数组

const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  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.
      // self-notes : it won't evaluate value immediately
      watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
    }
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else {
      // throw a warn(The computed property id already defined in data / prop
    }
  }
}
复制代码

initComputed中,首先获取用户定义的computed(能够是函数,也能够是对象)。若是不是服务端渲染,就调用new Watcher()实例化一个 Watcher(咱们称之为computed Watcher),实例化的具体过程咱们稍后讨论。紧接着,判断若是当前 vm 对象上找不到computed对象的 key 值,调用defineComputed,而该函数就完成了将 key 值变成了 vm 的属性。函数

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
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
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
复制代码

除此以外,在该函数中还作了另外一件更重要的事情,设置了computed的 getter 和 setter,将createComputedGetter函数的返回值做为 getter,而 setter 并不经常使用,能够暂且不关注。oop

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
    }
  }
}
复制代码

该函数返回一个函数,函数中进行了computed值的计算和依赖收集。那何时会触发这个 getter 呢?一样是在执行render时,会访问到咱们定义的相关数据。以上就是计算属性初始化的大体过程。ui

可是,若是顺着以上思路走,在调试源码时会发现一个问题,在走到if(!(key in vm))时,条件是不成立的,这样就没法执行defineComputed,还谈何初始化。那这是为何呢?其实,查找源码会发现,在Vue.extend(也就是处理子组件的构造器时)中已经对计算属性作了一些处理。下面一块儿来看一下。this

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])
  }
}
复制代码

所以总体逻辑是这样的。在最外层组件的initState时,判断opts.computedundefined,继续走到Vue.extend(生成子组件的构造器),此时Sub.options.computed是存在的,调用initComputed(函数内调用defineComputed),设置计算属性的 getter 为createComputedGetter函数的返回值(是一个函数)。lua

当再次走到子组件的initState时,此时opts.computed是存在的,调用initComputed()(实例化computed Watcher,值为undefined),此时if(!(key in vm))条件不成立,计算属性完成的初始化。spa

因为设置了 getter,在render过程当中访问到计算属性时,就会调用其 getter,在该函数中调用watcher.evaluate()计算出计算属性的值【调用watcher.get() -> watcher.getter()(即用户定义的计算属性的 getter,在过程当中会访问到计算属性依赖的其余数据(即执行数据的 getter,同时进行依赖收集),将订阅该数据变化的computed Watcher存入new Dep().subs中)】,最后调用watcher.depend()(再次依赖收集,将render Watcher存入new Dep().subs中)。此时就完成了计算属性的初次计算以及依赖收集。当咱们更改数据时,就进入了派发更新的过程。prototype

set: function reactiveSetter(newVal) {
    var value = getter ? getter.call(obj) : val;
    /* eslint-disable no-self-compare */
    if (newVal === value || (newVal !== newVal && value !== value)) {
        return
    }
    // #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();
}
复制代码

当咱们更改计算属性依赖的数据时,会触发它的 setter,若是值发生变化,会调用dep.notify()。在该函数中,会遍历依赖数组分别执行updatesubs中有两个 Watcher(computed Watcherrender Watcher)),当执行到render Watcherupdate时,调用queueWatcher(在过程当中从新计算计算属性的值,从新渲染页面)。

notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
}
Watcher.prototype.update = function update() {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
}
复制代码

以上就是关于计算属性的内容,接下来看侦听属性。

watch

关于侦听属性的使用,见以下代码。

export default {
  data() {
    return {
      msg: { a: 1 },
      count: 1,
      nested: {
        a: {
          b: 1
        }
      }
    };
  },
  methods: {
    change() {
      this.count++;
      this.nested.a.b++;
    }
  },
  computed: {
    newMsg() {
      if (this.count < 4) {
        return this.count;
      }
      return 5;
    }
  },
  watch: {
    newMsg(newVal) {
      console.log("newMsg : " + newVal);
    },
    count: {
      immediate: true,
      handler(newVal) {
        console.log("count : " + newVal);
      }
    },
    nested: {
      deep: true,
      sync: true,
      handler(newVal) {
        console.log("nested : " + newVal.a.b);
      }
    }
  }
}
复制代码

同计算属性同样,在initState中,判断若是opts.watch存在,则调用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)
    }
  }
}

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

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()
    }
}
复制代码

初始的流程也很是简单,大体通过了initWatch -> createWatcher -> vm.$watch,在vm.$watch中,建立了user Watcher,且该函数返回一个函数,能够用来销毁user Watcher。接下来一块儿探讨user Watcher的建立过程。

// new Watcher(vm, expOrFn, cb, options)
export default class Watcher {
    constructor(vm, exporFn, cb, options){
        this.deep = !!options.deep
        this.user = !!options.user
        this.sync = !!options.sync
        this.getter = parsePath(exporFn)
        this.value = this.get()
    }
    get () {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
            value = this.getter.call(vm, vm)
        } finally {
            if (this.deep) {
                traverse(value)
            }
            popTarget()
            this.cleanupDeps()
        }
        return value
    }
}

export function parsePath (path: string): any {
    const segments = path.split('.')
    return function (obj) {
        for (let i = 0; i < segments.length; i++) {
            obj = obj[segments[i]]
        }
        return obj
    }
}
复制代码

user Watcher的建立过程当中,因为传入的getter是一个字符串,须要通过parsePath函数处理,该函数返回一个函数,赋值给watcher.getter。与计算属性不一样,侦听属性会当即调用watcher.get()进行求值(过程当中执行watcher.getter会访问到相关数据,触发数据的 getter,进行依赖收集)。user Watcher建立完毕后,判断options.immediate若是为 true,则当即执行用户定义的回调函数。 当数据发生更改时,调用dep.notify()进行派发更新,总体更新过程仍是相似以前,此处就再也不进行详细解释。 总结一下,计算属性的本质是computed Watcher,侦听属性的本质是user Watcher,那究竟什么时候使用计算属性,什么时候使用侦听属性呢?通常来讲,计算属性适合使用在模板渲染中,某个值是依赖了某些响应式对象而计算得来的;而侦听属性适用于观测某个值的变化去完成一段逻辑。

相关文章
相关标签/搜索