面试官-你真的懂computed原理?(源码解读)

要理解 computed 的工作原理,只需要理解下面4个特性

- 特性1:computed默认不执行(因为 lazy 的原因,在新建watcher实例的时候,会将 watcher.value 赋值为 undefined,而不会立马进行计算。)

- 特性2:取值的时候,computed里面的方法会被执行

- 特性3:computed是惰性的,computed依赖的其它属性发生变化时,computed不会立即重新计算,要等到获取computed的值,也就是求值的时候才会重新计算(依靠watcher中的lazy属性判断,如果lazy是true,则不执行函数)

- 特性4:computed是缓存的,如果computed依赖的其它属性没有发生变化,即使重新调用,也不会重新计算(依靠watcher中的dirty属性判断,如果dirty是true,则重新计算,否则不计算,直接取watcher内的缓存value)

用法

Vue中computed典型的用法有如下两种:

// 方式1:
computed: {
    fullName() {
        return this.firstName + this.lastName
    }
}
// 方式2:
computed: {
    fullName: {
        get() {
            console.log('ooo')
            return this.firstName + this.lastName
        },
        set() {
            console.log("set full name")
        }
    }
}

computed源码实现

vue在初始化的时候,如果发现传入的属性是一个computed,则对其进行初始化处理

export function initState(vm: Component) {
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)

  // Composition API
  initSetup(vm)

  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    const ob = observe((vm._data = {}))
    ob && ob.vmCount++
  }
  if (opts.computed) initComputed(vm, opts.computed) // 初始化
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

可以看出初始化的优先级是 props->setup->methods->data->computed->watch

initComputed函数

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

  for (const key in computed) {
    const userDef = computed[key]
    // 对 computed 对象遍历,如果定义的是函数则直接取函数,不是则取对象内的get方法
    const getter = isFunction(userDef) ? userDef : userDef.get
    if (__DEV__ && getter == null) {
      warn(`Getter is missing for computed property "${key}".`, vm)
    }

    // 定义一个computed-wacther
    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        { lazy: true }
    )

    // 将key定义在vm上
    defineComputed(vm, key, userDef)
  }
}

  • computed是一个对象,首先要对其进行遍历。基于其用法的两种不同形式(函数和对象),会对其进行判断。对 computed 对象遍历,如果定义的是函数则直接取函数,不是则取对象内的get方法。

  • 在这里需要将每个computed的属性生成的watcher维护一个watchers,并且放在vm实例上。这样做的用处是,在创建computed getter的时候,可以顺利的获取到它的dirty属性。而dirty=lazy=true。

  • 然后将计算后的属性,定义到vm上

defineComputed方法

function defineComputed(vm, key, userDef) {
    const shouldCache = !isServerRendering()
    if (isFunction(userDef)) {
      //判断是否为缓存,如果不是每一次取值都会走get,如果发现是脏的,就重新获取,如果不是脏的,就不走get
      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 createGetterInvoker(fn) {
  return function computedGetter() {
    return fn.call(this, this)
  }
}

在这个方法中,维护了一个sharedPropertyDefinition对象,用来存储defineProperty的第三个参数。这个对象中的get方法是一个自定义的get方法。即createComputedGetter(key)

之所以要用自定义的,是因为computed取值的时候,是有缓存的,如果没有变化,则不计算

从这里可以看出,computed依赖值发生变化的时候,是调用createComputedGetter的方法的。

createComputedGetter

一旦computed依赖的值发生变化,就会立刻进入这个方法。

function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 重点这段
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        if (__DEV__ && Dep.target.onTrack) {
          Dep.target.onTrack({
            effect: Dep.target,
            target: this,
            type: TrackOpTypes.GET,
            key
          })
        }
        watcher.depend()
      }
      return watcher.value
    }
  }
}

在这个方法中,根据每一个watcher实例的dirty属性来判断是否执行计算方法。并返回计算过后的值。因为初始化的时候 dirty = true ,初始化调用 watcher 的evaluate 方法,其实这里可以看出 computed 的缓存就是通过dirty属性来判断,缓存数据存储在watcher的value属性。

watcher.get(重点:依赖数据watcher和computed的watcher互相绑定)

  evaluate() {
    this.value = this.get()
    this.dirty = false
  }
get() {
    pushTarget(this)
    // Dep.target = target 通过Dep保存当前 computed-watcher(Dep.target = target)并调用我们传入的函数
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e: any) {
      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
  }
export function pushTarget(target?: DepTarget | null) {
  targetStack.push(target)
  Dep.target = target
}

Dep.target = target,通过Dep保存当前 computed-watcher(Dep.target = target),所以当前的全局Dep.target保存的是 computed-watcher。当调用 this.getter.call(vm, vm) 会触发依赖项内部的属性的get方法。例如我们使用的是 fullName() { return this.firstName + this.lastName },当计算属性调用fullName 函数,触发 this.firstName 和 this.lastName的 get 方法(Observer类内)。

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
      }
      return isRef(value) && !shallow ? value.value : value
    }
  })

  depend(info?: DebuggerEventExtraInfo) {
    if (Dep.target) {
      // 调用 computed-watcher 内的 addDep 方法
      Dep.target.addDep(this)
    }
  }

因为get方法是瞬时同步的,会拦截访问提前执行,执行dep.depend(),而 Dep.target = computed-watcher,会执行 watcher 的 addDep 方法。并且将 this(依赖数据的watcher)带到computed-watcher。

  addDep(dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

实参 dep 是依赖数据的watcher,这时将computed的watcher添加到 data的watcher内 ,这样就实现data依赖 收集到 依赖computed的watcher,从而 data 变化时,会同时通知 computed 和 computed的地方。

需要注意的是,在computed的依赖属性的Dep上,收集了两个watcher:

  • computed的watcher
  • 渲染watcher

这两个watcher都会被执行。

猜你喜欢

转载自blog.csdn.net/qq_38261819/article/details/128465775