DMQ / mvvm

剖析vue实现原理,自己动手实现mvvm
5.21k stars 1.26k forks source link

想知道computed实现原理 #51

Open Been101 opened 4 years ago

Been101 commented 4 years ago

大佬, 能解答一下,为什么 Object.defineProperty 劫持一下 computed 中的属性,就可以实现 computed 的功能呢,

computed: {
   fullname () {
       return this.firstname + this.lastname
   }
}

firstname 或 lastname 更新怎么触发的 fullname, 这一块不是很明白, 为什么 lastname 更新时 set 方法里的 dep.notify(); 这个 dep 是 跟 fullname 相关的。

大佬方便微信或其他聊天工具联系一下吗

JasonXiang2014 commented 3 years ago
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]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    //...省略

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

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

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

具体源码的位置是在 vue/src/core/instance/state.js 源码大致分析: 初始化的时候 vm 劫持了computed里面的key,而具体的赋值是在compile之后生成vnode之前 触发了computedGetter回调方法, 这里面会执行 watcher.evaluate()

 evaluate () {
    this.value = this.get()
    this.dirty = false
  }
  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
   //..省略
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
     //..省略
    }
    return value
  }

其中this.getter就是computed对应key的回调函数,执行了 return this.firstname + this.lastname 然后生成vnode, 因为是第一次渲染所以不涉及diff过程,之后页面就渲染完并展示fullname。

当 lastname 更新时,触发dep.notify(),此时就进入了update过程,update过程其实就是将初始化得到语法树转换成虚拟dom的过程,这个过程会读取fullname的value, 又会重新触发computedGetter过程,而这个过程会依次读取this.firstname 和 this.lastname的value 并返回最终的结果,得到结果然后生成新的虚拟dom,再进入diff流程,最终更新视图。

这里再插一句: computed 会对应一个内部watcher, data 对应一个更新watcher,data每一个key对应一个dep,如果key对应的value是object,会递归defineReactive,也就是说每一层obj的key都会对应一个dep。当更改data中的某一项数据时,会触发dep.notify, 继而触发dep依赖的所有watcher, 这里只说computed + data的情况,这时其实有两个watcher,data对应的watcher是和真实更新虚拟dom相关的,computed的更新受益与虚拟dom的更新。

简单理解,有问题欢迎沟通交流。