Cosen95 / fe_interview

字节、阿里、美团、滴滴、腾讯等大厂高级前端面试题整理
238 stars 25 forks source link

你知道Vue中的computed是怎么实现的吗? #113

Open Cosen95 opened 4 years ago

Cosen95 commented 4 years ago

这里先给一个结论:计算属性computed的本质是 computed Watcher,其具有缓存。

一张图了解下computed的实现:

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 (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

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

initComputed 函数拿到 computed 对象然后遍历每一个计算属性。判断如果不是服务端渲染就会给计算属性创建一个 computed Watcher 实例赋值给watchers[key](对应就是vm._computedWatchers[key])。然后遍历每一个计算属性调用 defineComputed 方法,将组件原型,计算属性和对应的值传入。

// src/core/instance/state.js
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);
}

首先定义了 shouldCache 表示是否需要缓存值。接着对 userDef 是函数或者对象分别处理。这里有一个 sharedPropertyDefinition ,我们来看它的定义:

// src/core/instance/state.js
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop,
};

sharedPropertyDefinition其实就是一个属性描述符。

回到 defineComputed 函数。如果 userDef 是函数的话,就会定义 getter 为调用 createComputedGetter(key) 的返回值。

因为 shouldCachetrue

userDef 是对象的话,非服务端渲染并且没有指定 cachefalse 的话,getter 也是调用 createComputedGetter(key) 的返回值,setter 则为 userDef.set 或者为空。

所以 defineComputed 函数的作用就是定义 gettersetter ,并且在最后调用 Object.defineProperty 给计算属性添加 getter/setter ,当我们访问计算属性时就会触发这个 getter

对于计算属性的 setter 来说,实际上是很少用到的,除非我们在使用 computed 的时候指定了 set 函数。

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

我们知道访问计算属性时才会触发这个 getter,对应就是computedGetter函数被执行。

computedGetter 函数首先通过 this._computedWatchers[key] 拿到前面实例化组件时创建的 computed Watcher 并赋值给 watcher

new Watcher时传入的第四个参数computedWatcherOptionslazytrue,对应就是watcher的构造函数中的dirtytrue。在computedGetter中,如果dirtyfalse(即依赖的值没有发生变化),就不会重新求值。相当于computed被缓存了。

接着有两个 if 判断,首先调用 evaluate 函数:

/**
 * Evaluate the value of the watcher.
 * This only gets called for lazy watchers.
 */
evaluate () {
  this.value = this.get()
  this.dirty = false
}

首先调用 this.get() 将它的返回值赋值给 this.value ,来看 get 函数:

// src/core/observer/watcher.js
/**
 * 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 函数第一步是调用 pushTargetcomputed Watcher 传入:

// src/core/observer/dep.js
export function pushTarget(target: ?Watcher) {
  targetStack.push(target);
  Dep.target = target;
}

可以看到 computed Watcher 被 push 到 targetStack 同时将 Dep.target 置为 computed Watcher 。而 Dep.target 原来的值是渲染 Watcher ,因为正处于渲染阶段。回到 get 函数,接着就调用了 this.getter

回到 evaluate 函数:

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

执行完get函数,将dirty置为false

回到computedGetter函数,接着往下进入另一个if判断,执行了depend函数:

// src/core/observer/watcher.js
/**
 * Depend on all deps collected by this watcher.
 */
depend () {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

这里的逻辑就是让 Dep.target 也就是渲染 Watcher 订阅了 this.dep 也就是前面实例化 computed Watcher 时候创建的 dep 实例,渲染 Watcher 就被保存到 this.depsubs 中。

在执行完 evaluatedepend 函数后,computedGetter 函数最后将 evaluate 的返回值返回出去,也就是计算属性最终计算出来的值,这样页面就渲染出来了。

a572251465 commented 2 years ago

大佬,computed的缓存特征以及惰性特征没有讲哦 查找 https://www.yuque.com/lihaohao-f0jle/apohhm/dwkp57