06 | 【阅读Vue2源码】computed是如何实现的?

153 阅读7分钟

前言

在看computed实现的源码之前,我是一直有些疑问的:

  • computed是有缓存的,那么缓存是怎么实现的呢?
  • 在computed里定义的计算属性是一个函数,怎么我们取值的时候,就变成直接取属性了(this.xxx)?

在看完源码之后,找到了答案。

先简单概括下computed的实现原理:

computed底层也是由Watcher类实现的,初始化时也会new一个Watcher,但是不取value值,接着使用Object.defineProperty把开发者在computed中定义的函数名定义到Vue/Vue组件的实例上作为属性,当取计算属性的值时,触发对应的属性描述符的getter函数,调用watcher执行回调函数,拿到最新的值。

思维导图

computed的全链路图:

分析源码

示例代码

分析源码前,我们先写一个示例代码,便于分析

// index.html
<section id="app">
  <div>count:{{ count }}</div>
  <div>countComputed:{{ countComputed }}</div>
  <button @click="plus">+1</button>
</section>

<script src="../../dist/vue.js"></script>
<script src="app.js"></script>

// app.js
const app = new Vue({
  name: 'SimpleDemoAPI_Computed',
  data() {
    return {
      count: 0,
    }
  },
  computed: {
    countComputed() {
      return this.count * 2;
    }
  },
  created() {
    console.log('alan->countComputed', this.countComputed);
  },
  methods: {
    plus() {
      this.count += 1;
    }
  }
})

computed源码

源码预览,源码对应的位置:src\core\instance\state.js

// src\core\instance\state.js

// ...

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) // 定义computed,挂getter函数,将computed的方法名挂载到vm上,提供computedGettergetter函数
    } 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)
      } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(`The computed property "${key}" is already defined as a method.`, 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
  }
  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
      )
    }
  }
  // 把computed的属性,通过Object.defineProperty的方式定义到Vue/Vue组件实例上,属性描述符为sharedPropertyDefinition
  // 其中sharedPropertyDefinition的get就是createComputedGetter返回的一个取值函数computedGetter
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

// 创建函数computed取值函数
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        // 执行computed中的函数,把返回值保存到value中
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      // 返回computed中函数的返回值
      return watcher.value
    }
  }
}

代码逻辑解析:

  1. 首先是new Vue时初始化,调用this._init()

  2. _init()调用initState()initState()调用initComputed()初始化computed

  3. 重点看initComputed(),创建watchers用于收集watcher,然后遍历组件中定义的computed对象

  4. 接着把用户定义的computed的函数保存到userDef和getter变量中

  5. 然后new一个Watcher,以用户定义的computed函数作为表达式,在第2个参数传给Watcher

    1. Watcher实例化时,标记computed对应的watcher的lazy为true(在外层定义的computedWatcherOptions = { lazy: true }
    2. 把用户定义的computed函数赋值给watcher的getter
    3. watcher的value置为undefined
  6. initComputed()调用defineComputed(vm, key, userDef),主要是调用createComputedGetter(key)sharedPropertyDefinition.get赋值

  7. createComputedGetter(key)返回一个函数computedGetter

  8. 然后使用Object.defineProperty把computed的属性,通过Object.defineProperty的方式定义到Vue/Vue组件实例上,属性描述符为sharedPropertyDefinition,其中sharedPropertyDefinition的get就是createComputedGetter返回的一个取值函数computedGetter

到这里,前面我的疑问也有了答案:

  • Q:在computed里定义的计算属性是一个函数,怎么我们取值的时候,就变成直接取属性了(this.xxx)?
  • A:通过Object.defineProperty把computed的函数名绑定到Vue/Vue组件的实例中,所以可以再组件中通过this来访问计算属性
  1. 当我们取countComputed的值时,触发Object.defineProperty定义的getter,执行computedGetter函数

computedGetter里调用watcher.evaluate()evaluate()调用自己的get()get()执行自己的getter(),这个getter就是用户定义computed的函数,如当前示例代码中的countComputed

执行完函数,取到最新的值。

到这里,第1个疑问也有了答案:

Q:computed是有缓存的,那么缓存是怎么实现的呢?

A:初始化时watcher标记为lazy,不立即计算值,value赋值为undefined,每次访问的时候都执行函数取到最新值

结合调用链路图,理解会更好一点

computed与watch的区别

经过源码分析,可以得出结论:

相同点:

  • 都是使用了Watcher类创建了实例

不同点:

  • computed的watcher标记为lazy
  • computed初始化时不取值,值为undefined,后面触发取值时才追踪依赖;watch的watcher初始化时,会取一次值,立即追踪依赖
  • computed通过Object.defineProperty把属性定义到组件实例上
  • computed的取值是通过watcher同步调用getter,getter函数就是用户定义的computed的函数;watch是异步调用cb回调函数

动手实现computed

本次computed的实现,基于上一篇文章的代码来实现的

05 | 【阅读Vue2源码】watch实现原理

首先,在MiniVue函数中新增initComputed方法和defineComputed方法,初始化时调用initComputed

function MiniVue(options = {}) {
	// ...

  // 初始化computed
  if(options.computed) initComputed(vm, options.computed);

	// ...
  
  function initComputed(vm, computed = {}) {
    // 给vm新增_computedWatchers属性,收集computed的watchers
    const watchers = vm._computedWatchers = Object.create(null);
    for (const key in computed) {
      const userDef = computed[key];
      const getter = userDef;
      const noop = () => {};
      // new 一个Watcher,第2个参数为我们定义的computed的函数,传入lazy标记
      watchers[key] = new MiniWatcher(vm, getter, noop, {lazy: true});
      // 定义给vm绑上computed属性
      defineComputed(vm, key);
    }
  }

  function defineComputed(vm, key) {
    // 定义getter函数
    const computedGetter = () => {
      const watcher = vm._computedWatchers[key]
      watcher.evaluate()
      return watcher.value;
    }
    // 定义属性描述符
    const descriptor = {
      get: computedGetter, // 传入getter函数
      set: () => {}
    }
    // 定义computed的属性到vm上
    Object.defineProperty(vm, key, descriptor);
  }
}

加上了初始化computed的函数,然后我们还有改造一下Watcher类,增加对computed的支持

class MiniWatcher {
	// ...

  constructor(vm, expOrFn, cb, options = {}) {
    this.vm = vm;
    this.cb = cb;
    this.expression = expOrFn;
    // 对getter做区分处理
    if(typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parseExpression(this.expression, vm, this);
    }
    this.user = options.user;
    // 初始化lazy
    this.lazy = !!options.lazy;
    // 增加对computed的处理
    this.value = this.lazy ? undefined :this.get();
  }

  get() {
    const value = this.getter.call(vm);
    return value;
  }

  // 新增computed用的计算值的函数
  evaluate() {
    this.value = this.get();
  }

  // ...
}

最后在改造一下组件代码,新增computed选项

const vm = new MiniVue({
  data: {
    count: 0
  },
  computed: {
    countComputed() {
      return this.count;
    }
  }
})

const btn = document.getElementById('btnPlus');
const res = document.getElementById('res');
btn.onclick = () => {
  vm.data.count = vm.data.count + 1;
  const count = vm.data.count
  console.log('alan->count', count);
  console.log('alan->countComputed', vm.countComputed);
  res.innerHTML = count;
}

测试效果:

完整代码

mini-computed.html

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Mini Computed</title>
</head>

<body>
  <section id="mini-vue-app">
    <button id="btnPlus">+1</button>
    <h1 id="res"></h1>
  </section>

  <script src="./mini-computed.js"></script>
</body>

</html>

mini-computed.js


class MiniWatcher {
  vm = null; // 当前vue/vue组件实例
  cb = () => {}; // 回调函数
  getter = () => {}; // 取值函数
  expression = ''; // watch的键名
  user = false; // 是否是用户定义的watch
  value; // 当前观察的属性的值

  constructor(vm, expOrFn, cb, options = {}) {
    this.vm = vm;
    this.cb = cb;
    this.expression = expOrFn;
    // 对getter做区分处理
    if(typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parseExpression(this.expression, vm, this);
    }
    this.user = options.user;
    // 初始化lazy
    this.lazy = !!options.lazy;
    // 增加对computed的处理
    this.value = this.lazy ? undefined :this.get();
  }

  get() {
    const value = this.getter.call(vm);
    return value;
  }

  update() {
    nextTick(() => {
      this.run();
    })
  }

  run() {
    // 获取新值和旧值
    const newValue = this.get();
    const oldValue = this.value;
    this.value = newValue;
    this.cb.call(this.vm, newValue, oldValue);
  }

  // 新增computed用的计算值的函数
  evaluate() {
    this.value = this.get();
  }
}

class MiniDep {
  static target = null;
  subs = [];

  depend(sub) {
    if(sub && !this.subs.includes(sub)) {
      this.subs.push(sub);
    }
  }

  notify() {
    this.subs.forEach(sub => {
      sub && sub.update();
    })
  }
}

// 解析表达式,返回一个函数
function parseExpression(key, vm, watcher) {
  return () => {
    MiniDep.target = watcher;
    // 取值,触发getter,取值前先把watcher实例放到target中
    const value = vm.data[key];
    // 取完值后,清空Dep.target
    MiniDep.target = null;
    return value;
  }
}

function nextTick(cb) {
  return Promise.resolve().then(cb);
}

function MiniVue(options = {}) {
  const vm = this;
  this.vm = this;
  this.data = options.data;
  this.watch = options.watch;
  this.deps = new Set();

  initData(vm, this.data); // 初始化data
  initWatch(this.watch); // 初始化watch

  // 初始化computed
  if(options.computed) initComputed(vm, options.computed);

  function observe(data) {
    for (const key in data) {
      defineReactive(data, key);
    }
  }

  function defineReactive(data, key) {
    const dep = new MiniDep();
    vm.deps.add(dep);
    const clonedData = JSON.parse(JSON.stringify(data));
    Object.defineProperty(data, key, {
      get: function reactiveGetter() {
        // console.log('alan->', 'get', clonedData[key]);
        dep.depend(MiniDep.target);
        return clonedData[key];
      },
      set: function reactiveSetter(value) {
        // console.log('alan->', 'set', key, value);
        dep.notify();
        clonedData[key] = value;
        return value;
      }
    });
  }
  
  function initData(vm, data = {}) {
    for (const key in data) {
      Object.defineProperty(vm, key, {
        configurable: true,
        enumerable: true,
        get() {
          return vm['data'][key];
        },
        set(val) {
          vm['data'][key] = val;
        }
      })
      observe(vm.data);
    }
  }

  function initWatch(watch = {}) {
    for (const key in watch) {
      new MiniWatcher(vm, key, watch[key], {user: true}); // user = true,标记这是用户定义的watch
    }
  }

  function initComputed(vm, computed = {}) {
    // 给vm新增_computedWatchers属性,收集computed的watchers
    const watchers = vm._computedWatchers = Object.create(null);
    for (const key in computed) {
      const userDef = computed[key];
      const getter = userDef;
      const noop = () => {};
      // new 一个Watcher,第2个参数为我们定义的computed的函数,传入lazy标记
      watchers[key] = new MiniWatcher(vm, getter, noop, {lazy: true});
      // 定义给vm绑上computed属性
      defineComputed(vm, key);
    }
  }

  function defineComputed(vm, key) {
    // 定义getter函数
    const computedGetter = () => {
      const watcher = vm._computedWatchers[key]
      watcher.evaluate()
      return watcher.value;
    }
    // 定义属性描述符
    const descriptor = {
      get: computedGetter, // 传入getter函数
      set: () => {}
    }
    // 定义computed的属性到vm上
    Object.defineProperty(vm, key, descriptor);
  }
}

const vm = new MiniVue({
  data: {
    count: 0
  },
  computed: {
    countComputed() {
      return this.count;
    }
  }
})

console.log('alan->', vm);

const btn = document.getElementById('btnPlus');
const res = document.getElementById('res');
btn.onclick = () => {
  vm.data.count = vm.data.count + 1;
  const count = vm.data.count;
  console.log('alan->countComputed', vm.countComputed);
  res.innerHTML = count;
}

总结

computed其实也是由Watcher实现的,但是和watch又有一些不同点。computed通过Object.defineProperty把属性定义到组件实例上,并且getter函数里调用watcher去取值,追踪依赖,再返回值,以实现计算属性的效果。