【Vue.js】计算属性 computed

29 阅读2分钟

1. 什么是 computed

computed (计算属性):是 Vue 提出的缓存计算渲染逻辑的优秀解决方案。

2. computed 的语法使用

export default {
  name: 'Comp',
  
  data() {
    return {
      a: 1,
      b: 2
    };
  },
  
  computed: {
    foo() {
      return this.a + this.b;
    },
    
    fooFull: {
      get() {
        return this.a + this.b;
      },
      set(newValue) {
        // ... dosomething
      }
    },
  }
};

3. computed 的特点

  1. computed 在依赖没有发生变化时,computed 会返回之前缓存的运算结果;一旦依赖发生变化,computed 会重新计算并返回运算结果。
  2. 多次调用同一个 computed,computed 只会计算一次!
  3. computed 主要是运用于模板渲染的,而视图的渲染一般是有纯度要求(尽可能少的参杂副作用)
  4. computed 一般是用的都是它的 getter。当然在一些特殊情况下,我们也会去同时地设置 computed 的 getter 和 setter

4. 自己实现一个计算属性的功能

var Vue = (function (doc) {
  function isString(tar) {
    return Object.prototype.toString.call(tar) === '[object String]';
  }
  function isObject(tar) {
    return Object.prototype.toString.call(tar) === '[object Object]';
  }
  function isFunction(tar) {
    return Object.prototype.toString.call(tar) === '[object Function]';
  }

  /**
   * k
   * value: 函数执行的结果
   * getter
   * dep: []
   */
  var computedData = {};

  var replaceVar = /\{\{(.+?)\}\}/g;

  function initData(vm) {
    for (var key in vm.$data) {
      ;(function (k) {
        Object.defineProperty(vm, k, {
          get: function () {
            return vm.$data[k];
          },
          set: function (newVal) {
            vm.$data[k] = newVal;
            setComputedData(vm, k);
            update(vm);

            return true;
          }
        });
      })(key);
    }
  }

  function initComputed(vm, computed) {
    for (var key in computed) {
      ;(function (k) {
        try {
          var computedOption = computed[k],
            computedGetter = isFunction(computedOption)
              ? computed[k]
              : computedOption.getter,
            computedSetter = isFunction(computedOption)
              ? null
              : computedOption.setter;

          computedData[k] = {
            value: computedGetter.call(vm),
            __dep__: collectDeps(computedGetter),
            __getter__: computedGetter,
          };

          Object.defineProperty(vm, k, {
            get: function () {
              return computedData[k].value;
            },
            set: function (newValue) {
              computedData[k].value = newValue;
            },
          });
        } catch (e) {
          throw new Error(e);
        }
      })(key);
    }
  }

  function render(vm, template) {
    var oContainer = doc.createElement('div'),
      _el = vm.$el;

    oContainer.innerHTML = getTotalTpl(vm, template);

    if (!_el.innerHTML) {
      _el.appendChild(oContainer);
    }
  }

  function getTotalTpl(vm, template) {
    return template.replace(replaceVar, function (node, key) {
      var k = key.trim();

      return vm[k] || '';
    });
  }

  function update(vm) {
    var template = vm.$options.template,
      _el = vm.$el;

    console.log('update!');

    vm.$el.innerHTML = getTotalTpl(vm, template);
  }

  function collectDeps(fn) {
    var matches = fn.toString().match(/this\.(.+?)/g);

    if (matches && matches.length) {
      return [].slice.call(matches);
    }

    return [];
  }

  function setComputedData(vm, dataKey) {
    for (var computedKey in computedData) {
      var computedItem = computedData[computedKey],
        deps = computedItem.__dep__.map(function (key) { return key.split('this.')[1]; });

      if (deps.includes(dataKey)) {
        computedItem.value = computedItem.__getter__.call(vm);
      }
    }
  }

  function Vue(options) {
    options = options || {};
    this.$options = options;
    this.$el = doc.querySelector(options.el);
    this.$data = isFunction(options.data)
      ? options.data()
      : isObject(options.data)
        ? options.data
        : {};

    this.init(options);
  }

  Vue.prototype.init = function (options) {
    var vm = this,
      template = isString(options.template) ? options.template : '',
      computed = isObject(options.computed) ? options.computed : {};

    initData(vm);
    initComputed(vm, computed);
    render(vm, template);
  }

  return Vue;
})(document);

5. 文档建议:

计算属性函数不应该有副作用!

  • 计算属性的计算函数应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举例来说,不要在计算函数中做异步请求或者更改 DOM!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此计算函数的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用监听器根据其他响应式状态的变更来创建副作用。

避免直接修改计算属性值!

  • 从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。