vue computed实现原理

vue computed实现原理

在 Vue.prototype._init 方法中的 initState 中有一个对于computed 的判断,如果有则执行 initComputed 方法初始化 computed。

function initMixin (Vue) {
  Vue.prototype._init = function (options) {
var vm = this;
// ...
     initState(vm);
// ...
   };
}
function initState (vm) {
  vm._watchers = [];
var opts = vm.$options;
// ...

  if (opts.computed) { initComputed(vm, opts.computed); }
// ...
}

initComputed 初始化:

var computedWatcherOptions = { lazy: true };
function initComputed (vm, computed) {
// $flow-disable-line
  var watchers = vm._computedWatchers = Object.create(null);
// computed properties are just getters during SSR
  var isSSR = isServerRendering(); // 判断是否是服务端渲染

  for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (getter == null) {
      warn(
        ("Getter is missing for computed property \"" + key + "\"."),
        vm
      );
    }
if (!isSSR) {
// create internal watcher for the computed property.
      // 对computed内部属性进行监听,传入lazy为true,这个值将用于缓存判断
      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内部属性进行加工改造
    } else {
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);
      }
    }
  }
}

defineComputed:重写 get 方法,然后进行数据劫持操作

var sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
};
function defineComputed (
  target,
  key,
  userDef
) {
var 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 (sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        ("Computed property \"" + key + "\" was assigned to but it has no setter."),
this
      );
    };
  }
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

createComputedGetter:如果 dirty 为 true,则重新获取新值

function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
// 重新计算最新值        watcher.evaluate();
      }
if (Dep.target) {
        watcher.depend();
      }
return watcher.value
    }
  }
}
function createGetterInvoker(fn) {
return function computedGetter () {
return fn.call(this, this)
  }
}

而 dirty 值的变动则依赖 data,props 等属性的更新:

// evaluate 方法则将dirty更改为false,并重新执行get方法缓存最新值
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.dirty = false;
};

在 Watcher 构造函数中,初始参数 lazy 将赋值给 dirty:

var Watcher = function Watcher (
  vm,
  expOrFn,
  cb,
  options,
  isRenderWatcher
) {
this.vm = vm;
if (isRenderWatcher) {
    vm._watcher = this;
  }
  vm._watchers.push(this);
// options
  if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
  } else {
this.deep = this.user = this.lazy = this.sync = false;
  }
this.cb = cb;
this.id = ++uid$2; // uid for batching
  this.active = true;
this.dirty = this.lazy; // for lazy watchers
  this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// parse expression for getter
  if (typeof expOrFn === 'function') {
this.getter = expOrFn;
  } else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
      warn(
"Failed watching path: \"" + expOrFn + "\" " +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      );
    }
  }
// value 值为第一次缓存
  this.value = this.lazy
? undefined
    : this.get();
};

当依赖的数据变动后,dirty 将会更改为 true:

Watcher.prototype.update = function update () {
/* istanbul ignore else */
  if (this.lazy) {
this.dirty = true;
  } else if (this.sync) {
this.run();
  } else {
    queueWatcher(this);
  }
};

Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
  var subs = this.subs.slice();
if (!config.async) {
// subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    subs.sort(function (a, b) { return a.id - b.id; });
  }
for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};
function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter,
  shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
  }
// cater for pre-defined getter/setters
  var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }
var childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
        dep.depend();
if (childOb) {
          childOb.dep.depend();
if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
return value
    },
    set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
return
      }
/* eslint-enable no-self-compare */
      if (customSetter) {
        customSetter();
      }
// #7981: for accessor properties without setter
      if (getter && !setter) { return }
if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });
}

defineReactive$$1 的引用:

// propsfunction initProps (vm, propsOptions) {
var propsData = vm.$options.propsData || {};
var props = vm._props = {};
// cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  var keys = vm.$options._propKeys = [];
var isRoot = !vm.$parent;
// root instance props should be converted
  if (!isRoot) {
    toggleObserving(false);
  }
var loop = function ( key ) {
    keys.push(key);
var value = validateProp(key, propsOptions, propsData, vm);
/* istanbul ignore else */
    {
var hyphenatedKey = hyphenate(key);
if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          ("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
          vm
        );
      }
      defineReactive$$1(props, key, value, function () {
if (!isRoot && !isUpdatingChildComponent) {
          warn(
"Avoid mutating a prop directly since the value will be " +
            "overwritten whenever the parent component re-renders. " +
            "Instead, use a data or computed property based on the prop's " +
            "value. Prop being mutated: \"" + key + "\"",
            vm
          );
        }
      });
    }
// static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, "_props", key);
    }
  };
for (var key in propsOptions) loop( key );
  toggleObserving(true);
}
// datafunction initData (vm) {
var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
if (!isPlainObject(data)) {
    data = {};
    warn(
'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    );
  }
// proxy data on instance
  var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
    {
if (methods && hasOwn(methods, key)) {
        warn(
          ("Method \"" + key + "\" has already been defined as a data property."),
          vm
        );
      }
    }
if (props && hasOwn(props, key)) {
      warn(
"The data property \"" + key + "\" is already declared as a prop. " +
        "Use prop default value instead.",
        vm
      );
    } else if (!isReserved(key)) {
      proxy(vm, "_data", key);
    }
  }
// observe data
  observe(data, true /* asRootData */);
}
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
  }
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }
if (asRootData && ob) {
    ob.vmCount++;
  }
return ob
}
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
  def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
this.observeArray(value);
  } else {
this.walk(value);
  }
};

Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i]);
  }
};

总结:在对 computed 的属性进行 watcher 的时候,传入一个 lazy 为 true 的参数,在 watcher 内部将 lazy 值赋值给 dirty 属性,在获取 computed 属性的时候,如果 dirty 为 true,则重新执行被 defineComputed 改写过的 get 方法,获取最新值,如果 dirty 为 false,则获取上一次 watcher 实例的 value 值,这里就实现了缓存。对于 dirty 值更改为 true 的时机,则是在 props 和 data 等被 defineProperty 劫持改写的 set 方法内,每当数据发生变化,则通过 defineReactive$$1 方法最后执行 update 方法更新 dirty 值。

vue computed实现原理的相似文章

30 道 Vue 面试题,内含详细讲解(涵盖入门到精通,自测 Vue 掌握程度)分析快速上手Vuex 到 手写简易 Vuex 分析前端抢饭碗系列之Vue项目中如何做单元测试分析复制excel内容到input框并改变其格式分析虚拟列表分析前端开发中的长列表分析vue双向绑定原理分析实现一个最精简的响应式系统来学习Vue的data、computed、watch源码分析