9

手写Vue源码(九) - 计算属性

 3 years ago
source link: https://zhuanlan.zhihu.com/p/345208305
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

手写Vue源码(九) - 计算属性

Enjoy what you are doing!

官网对计算属性的介绍在这里:传送门

计算属性是Vue中很常用的一个配置项,我们先用一个简单的例子来讲解它的功能:

<div id="app">
  {{fullName}}
</div>
<script>
  const vm = new Vue({
    data () {
      return {
        firstName: 'Foo',
        lastName: 'Bar'
      };
    },
    computed: {
      fullName () {
        return this.firstName + this.lastName;
      }
    }
  });
</script>

在例子中,计算属性中定义的fullName函数,会最终处理为vm.fullNamegetter函数。所以vm.fullName = this.firstName + this.lastName = 'FooBar'

计算属性有以下特点:

  • 计算属性可以简化模板中的表达式,用户可以书写更加简洁易读的template
  • Vue为计算属性提供了缓存功能,只有当它依赖的属性(例子中的this.firstNamethis.lastName)发生变化时,才会重新执行属性对应的getter函数,否则会将之前计算好的值返回。

正是由于computed的缓存功能,使得用户在使用时会优先考虑它,而不是使用watchmethods属性。

在了解了计算属性的用法后,我们通过代码来一步步实现computed,并让它完成上边的例子。

初始化计算属性

初始化computed的逻辑会书写在scr/state.js中:

function initState (vm) {
  const options = vm.$options;
  // some code ...
  if (options.computed) {
    initComputed(vm);
  }
}

initComputed中,可以通过vm.$options.computed拿到所有定义的计算属性。对于每个计算属性,需要对其做如下处理:

  • 实例化计算属性对应的Watcher
  • 取到计算属性的key,通过Object.definePropertyvm实例添加key属性,并设置它的get/set方法
function initComputed (vm) {
  const { computed } = vm.$options;
  // 将计算属性watcher存储到vm._computedWatchers属性中,之后方法直接通过实例vm来获取
  const watchers = vm._computedWatchers = {};
  for (const key in computed) {
    if (computed.hasOwnProperty(key)) {
      const userDef = computed[key];
      // 计算属性key的值有可能是对象,在对象中会设置它的get set 方法
      const getter = typeof userDef === 'function' ? userDef : userDef.get;
      // 为每一个计算属性创建一个watcher
      watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true });
      // 将计算属性的key添加到实例vm上
      defineComputed(vm, key, userDef);
    }
  }
}

计算属性也可以传入set方法,用于设置值时处理的逻辑,此时计算属性的value是一个对象:

new Vue({
    // ...
    computed: {
      fullName: {
        // getter
        get: function () {
          return this.firstName + ' ' + this.lastName
        },
        // setter
        set: function (newValue) {
          var names = newValue.split(' ')
          this.firstName = names[0]
          this.lastName = names[names.length - 1]
        }
      }
    }
  }
  //...  
)

defineComputed函数中,我们会根据计算属性的类型来确定是否为其定义set方法:

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
};

function defineComputed (target, key, userDef) {
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = createComputedGetter(key);
  } else {
    sharedPropertyDefinition.get = createComputedGetter(key);
    // 如果是对象,用户会传入set方法
    sharedPropertyDefinition.set = userDef.set;
  }
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

// 创建Object.defineProperty的get函数
function createComputedGetter (key) {
  return function () {
    // 通过之前保存的_computedWatchers来取到对应的计算属性watcher
    const watcher = this._computedWatchers[key];
    if (watcher.dirty) {
      // 只有在dirty为true的时候才会重新执行计算属性
      watcher.evaluate();
      if (Dep.target) {
        // 此时,如果栈中有渲染watcher,会为当前计算属性watcher中收集的所有dep再收集渲染watcher
        // 在watcher收集的dep对应的属性(this.firstName,this.lastName)更新后,通知视图更新,从而更新页面中的计算属性
        watcher.depend();
      }
    }
    return watcher.value;
  };
}

在对计算属性取值时,首先会调用它在vm.fullName上定义的get方法,也就是上边的createComputedGetter执行后返回的函数。在函数内部,只有当watcher.dirtytrue 时,才会执行watcher.evaluate

下面我们先看下Watcher中关于计算属性的代码:

import { popTarget, pushTarget } from './dep';
import { nextTick } from '../shared/next-tick';
import { traverse } from './traverse';

let id = 0;

class Watcher {
  constructor (vm, exprOrFn, cb, options = {}) {
    // some code ...
    // 设置dirty的初始值为false
    this.lazy = options.lazy;
    this.dirty = this.lazy;
    if (typeof exprOrFn === 'function') {
      this.getter = this.exprOrFn;
    }
    // some code ...
    // 初始化时计算属性的getter不会执行,用到的时候才会执行
    this.value = this.lazy ? undefined : this.get();
  }

  // 执行传入的getter函数进行求值,将其赋值给this.value
  // 求值完毕后,将dirty置为false,下次将不会再重新执行求值函数
  evaluate () {
    this.value = this.get();
    this.dirty = false;
  }

  // 为watcher中的dep,再收集渲染watcher
  depend () {
    this.deps.forEach(dep => dep.depend());
  }

  get () {
    pushTarget(this);
    const value = this.getter.call(this.vm);
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    return value;
  }

  update () {
    if (this.lazy) { // 依赖的值更新后,只需要将this.dirty设置为true
      // 之后获取计算属性的值时会再次执行evaluate来执行this.get()方法
      this.dirty = true;
    } else {
      queueWatcher(this);
    }
  }

  // some code ...
}

watcher.evaluate中的逻辑便是执行我们在定义计算属性时传入的回调函数(getter),将其返回值赋值给watcher.value,并在取值完毕后,将watcher.dirty置为false 。这样再次取值时便直接将watcher.value返回即可,而不用再执行回调函数进行重复计算。

当计算属性的依赖属性(this.firstNamethis.lastName)发生变化后,我们要更新视图,让计算属性重新执行getter函数获取到最新值。所以代码中判断Dep.target(此时为渲染watcher) 是否存在,如果存在会为依赖属性收集对应的渲染watcher。这样在依赖属性更新时,便会通过渲染watcher来通知视图更新,获取到最新的计算属性。

依赖属性更新

以文章开始时的demo为例,首次执行时的逻辑如下图:

用文字来描述:

  • 初始化计算属性,为vm添加fullName属性,并设置其get方法
  • 首次渲染页面,stack中存储了渲染watcher。由于页面中用到了fullName属性,所以在渲染时会触发fullNameget方法
  • fullName执行get会通过依赖属性firstNamelastName来求值,computed watcher会进入stack
  • 此时又会触发firstNamelastNameget方法,收集computed watcher
  • fullName求值方法执行完成,computed watcher出栈,Dep.target为渲染watcher
  • 此时为fullName对应的computed watcher中的dep(也就是firstNamelastName对应的dep)收集渲染watcher
  • 完成fullName的取值过程,此时firstNamelastNamedep中分别收集的watcher[computed watcher, render watcher]

假设我们更新了依赖,会通知收集的watcher进行更新:

vm.firstName = 'F'

firstName属性更新后,会触发其对应的set方法,执行dep中收集的computed watcherrender watcher

  • computed watcher: 将this.dirty设置为truefullName之后取值时需要重新执行用户传入的getter函数
  • render watcher: 通知视图更新,获取fullName的最新值

到这里我们实现的computed属性便能正常工作了!

文章的源代码在这里:传送门

本文从一个简单的计算属性例子开始,一步步实现了计算属性。并且针对这个例子,详细分析了页面渲染时的整个代码执行逻辑。希望小伙伴们在读完本文后,能够从源码的角度,分析自己代码中对应计算属性相关代码的执行流程,体会一下Vuecomputed属性到底帮我们做了些什么。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK