玩转Vuejs--数据侦听和计算属性实现原理

引言:

在 Vuejs 中用 watch 来侦听数据变化,computed 用来监听多个属性的变化并返回计算值,那么这两个特性是如何实现的呢?本文讲一下两者实现的具体方法以及一些使用经验,介绍过程中会使用到前面【核心原理】篇中的知识,建议先看透原理再看本文,可以达到互相印证加深理解的效果。

结论:

由前面的【Vue核心原理】篇中介绍的数据绑定可以了解到,如果想监听某个属性的数据变化,那么只需要 new 一个 watcher 并在 watcher 执行的时候用到那个属性就够了,使用的过程中该 watcher 会被加入到对应数据的依赖中,数据变化的时候就会得到通知。所以,如果想要实现 Vue 的 watch 和 computed,实际上只需要为对应的属性建立 watcher,并构造出执行时使用数据的函数即可,接下来展开讲一下。

一、watch实现原理:

借官网的例子用一下

<div id="demo">{{ fullName }}</div>
var vm = new Vue({ el: '#demo', data: { firstName: 'Foo', lastName: 'Bar', fullName: 'Foo Bar' }, watch: { firstName: function (val) { this.fullName = val + ' ' + this.lastName }, lastName: function (val) { this.fullName = this.firstName + ' ' + val } } })

watch 这段的意思就是要监听 firstName 和 lastName 的变化,当数据变化时执行对应的函数,给fullName赋值。

所以,根据开篇的思路,如果要监听 firstName 的变化,那么只需要在初始化的时候创建一个 watcher,watcher 在初始化过程(立即执行watcher,还有一种懒执行 watcher 不会在初始化中执行,后面会讲,此处可以忽略)中使用到 firstName 就可以了,这里的关键是如何构造这个函数,按照这个思路我们看一下 Vue 的实现。

  function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) {
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  }

首先 initWatch(vm, opts.watch) ,注意这里的顺序,initwatch 是在 initData 之后执行的,因为 watch 也是在已有的响应式基础上进行监听,所以要先初始化数据。

function initWatch (vm, watch) {
    for (var key in watch) {
      var handler = watch[key];
      if (Array.isArray(handler)) {
        for (var i = 0; i < handler.length; i++) {
          createWatcher(vm, key, handler[i]);
        }
      } else {
        createWatcher(vm, key, handler);  //以firstName为例,此处为:createWatcher(vm, 'firstName', watch.firstName)
      }
    }
  }

  function createWatcher (
    vm,
    expOrFn,
    handler,
    options
  ) {
    if (isPlainObject(handler)) {
      options = handler;
      handler = handler.handler;
    }
    if (typeof handler === 'string') {
      handler = vm[handler];
    }
    return vm.$watch(expOrFn, handler, options) //以firstName为例,此处为:vm.$watch('firstName',watch.firstName, options) 
 }

之后调用 $watch 为 watch 中监听的每个属性建立 warcher ,watch构造函数中会构造函数并执行。

    Vue.prototype.$watch = function (
      expOrFn,
      cb,
      options
    ) {
      var vm = this;
      if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
      }
      options = options || {};
      options.user = true;
      var watcher = new Watcher(vm, expOrFn, cb, options); //以firstName为例,此处为:new watcher('firstName',watch.firstName, undefined) 
      if (options.immediate) {
        try {
          cb.call(vm, watcher.value);
        } catch (error) {
          handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
        }
      }
      return function unwatchFn () {
        watcher.teardown();
      }
    };
  }

watcher函数逻辑

var Watcher = function Watcher (
    vm, 
    expOrFn, // 'firstName'
    cb, //watch.firstName 函数
    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
        );
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get();
  };

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var 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
  };

可以看到,new Watcher 时传入的表达式是 ‘firstName’,非函数类型,Vue 调用  parsePath(expOrFn=‘firstName’) 构造使用 firstName 的一个 getter 函数,从而建立依赖,开启监听。

  /**
   * Parse simple path.
   */
  var bailRE = new RegExp(("[^" + unicodeLetters + ".$_\\d]"));
  function parsePath (path) { //path === 'firstName'
    if (bailRE.test(path)) { 
      return
    }
    var segments = path.split('.');
    return function (obj) {
      for (var i = 0; i < segments.length; i++) {
        if (!obj) { return }
        obj = obj[segments[i]]; //首次循环 obj === vm ,即 vm.fisrtName
      }
      return obj
    }
  }

可以看到,new Watcher 时传入的表达式是 ‘firstName’,非函数类型,Vue 调用 parsePath(expOrFn=‘firstName’) 构造出使用 firstName 的一个 getter 函数,从而建立依赖,开启监听。所以,在这里需要看一下 parsePath 的逻辑:

this.getter = function (){
  return vm.firstName;
}

执行次函数就可以将 ‘fisrtName ’ 与当前的 watcher 关联起来,此时的 this.cb 即为 watch 中传入的 firstName 函数。据变化时会通知此 watcher 执行 this.cb。

this.cb = function (val) {
  this.fullName = val + ' ' + this.lastName
}

以上就是 Vue watch 的实现原理,其核心就是如何为侦听的属性构造一个 watcher 以及 watcher 的 getter 函数。

二、computed 实现原理:

同样借助官网的例子看下

  <div id="example">
    <p>Original message: "{{ message }}"</p>
    <p>Computed reversed message: "{{ reversedMessage }}"</p>
  </div>

 var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join('')
    }
  }
})

同样,如果要监听数据( 这里是 'message' )的变化,我们需要建立一个watcher 并构造出使用 ' message' 的函数在 watcher 执行的过程中进行使用。这里很显然新建 watcher 需要用到的就是 computed.reverseMessage 函数,不需要构造了(也就是不需要像 watch 中那样 调用 parsePath 来生成)。这里需要考虑一个问题,reversedMessage 是一个新增属性,vm上并未定义过响应式,所以此处还需要借助 Object.defineProperty 将 reverMessage 定义到 vm 上,看一下总体实现:

function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) {
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  }
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. watchers[key] = new Watcher( vm, getter || noop, // computed.reverseMessage 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 (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(vm, opts.computed) ,在 initComputed 中会执行两个步骤:

第一,为每个 key 建立一个 watcher ,watcher 的 getter 为对应 key 的函数。

var computedWatcherOptions = { lazy: true };
---
watchers[key] = new Watcher(
    vm,
    getter || noop, // computed.reverseMessage
    noop, // 回调 空 function(){}
    computedWatcherOptions // lazy: true
);

这里需要注意的是 initComputed 中创建的 watcher 为 lazy 模式 。

简单说下,Vue 的 watcher 根据传参不同可以分为两种,一种是常规(立即执行)模式 ,一种是 lazy (懒执行) 模式:常规模式的 watcher 在初始化时会直接调用 getter ,getter 会获取使用到的响应式数据,进而建立依赖关系;lazy 模式 watcher 中的 getter 不会立即执行,这部分可以看下面的 Watcher代码,执行的时机是在获取计算属性(watcher.evaluate())时,稍后会讲。

var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    debugger;

    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$1; // 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
        );
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get();
  };

this.value = this.lazy ? undefined : this.get();  处可以看到 lazy 模式的 watcher 不会立即执行。

第二、通过 defineComputed 调用 Object.defineProperty 将 key 定义到 vm 上。

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); //target === vm  key == 'reverseMessage'
  }

可以看到,计算属性的get 是由 createComputedGetter 创建而成,那么我们看下 createComputedGetter 的返回值:

ps:通过 sharedPropertyDefinition 的构造过程可以看到,如果传入的计算属性值为函数,那么相当于计算属性的 get ,此时不允许 set,如果需要对计算属性进行set,那么需要自定义传入 set、get 方法。

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

 createComputedGetter  返回了一个 computedGetter 函数,这个函数就是获取计算属性(reveserMessage)时的 get  函数,当获取 reveserMessage 的时候会调用 watcher.evaluate() ,看一下watcher.evaluate 的逻辑:

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


  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var 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
  };

可以看到 watcher 在 evaluate 中会直接调用 get 方法,get 方法会直接调用 getter 并返回获取到的值,而这里的 getter 就是前面新建 watcher 时早已传入的计算属性值,即 computed.reveseMessage 函数,执行getter过程中就会建立起对数据的依赖。

这里啰嗦两个小问题:

1、计算属性使用了懒执行模式,使用时才会运算并建立依赖,可能是考虑到两方面:一个是计算属性不一定会被使用,性能会被白白浪费;另一个计算属性中可能会存在比较复杂的运算逻辑,放在相对靠后的生命周期中比较合适。

2、计算属性函数的传入是由开发者自行传入的,需要注意数据监听开启的条件是数据被使用过,在使用过程中需要注意 if 条件语句的使用,最好把需要用到的数据都定义在最上层。

以上就是computed的实现原理。

总结:

本文主要讲了 Vuejs 中 watch 和 computed 的实现原理,核心就是要创建 watcher 并为 watcher 构造相应的 getter 函数,通过 getter 函数的执行进行绑定依赖。根据 getter 执行的时机不同 watcher 可以分为立即执行以及懒执行两种模式,立即执行模式 getter 会在构造函数中直接执行,懒执行模式 getter 需要调用 evaluate 来执行。在使用的场景上 watch 适合直接监听单个属性,不涉及复杂的监听逻辑场景,computed 适合涉及多个监听变化的逻辑,另外 computed 比较适合做数据代理,当某些数据产生的过程比较复杂很难改动的时候,我们可以通过 computed 代理拿到原有的数据直接进行处理,简直不要太爽~~

转载于:https://www.cnblogs.com/DevinnZ/p/11052878.html

猜你喜欢

转载自blog.csdn.net/weixin_33843409/article/details/94032732