深入了解 Vue 响应式原理(数据拦截)

栏目: JavaScript · 发布时间: 5年前

内容简介:在上一章节我们已经粗略的分析了整个的Vue 的源码,但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析。这一章节我们针对1. 深入了解 Vue 响应式原理(数据拦截)来进行分析。我们在上一章节中已经分析了,在初始化Vue实例的时候,会执行

在上一章节我们已经粗略的分析了整个的Vue 的源码,但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析。

  1. 深入了解 Vue 响应式原理(数据拦截)
  2. 深入了解 Vue.js 是如何进行「依赖收集」,准确地追踪所需修改
  3. 深入了解 Virtual DOM
  4. 深入了解 Vue.js 的批量异步更新策略
  5. 深入了解 Vue.js 内部运行机制,理解调用各个 API 背后的原理

这一章节我们针对1. 深入了解 Vue 响应式原理(数据拦截)来进行分析。

initState

我们在上一章节中已经分析了,在初始化Vue实例的时候,会执行 _init 方法, 其中会执行 initState 方法, 这个方法非常重要, 其对我们 new Vue 实例化对象时,传递经来的参数 props , methods , data , computed , watch 的处理。 其代码如下:

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);
    }
  }
复制代码

这一章节,我们只分析对 data 的处理, 也就是 initData(vm) 方法, 其代码如下(删除了异常处理的代码):

function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
 
    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 */);
  }
复制代码

从上面的代码分析,首先可以得出如下一个

总结:

  1. data里面的key一定不能和methods, props里面的key重名
  2. proxy(vm, "_data", key); 只是将 data 里面的属性重新挂载(代理)在 vm 实例上,我们可以通过如下两种方式访问 data 里面的数据, 如 vm.visibility 或者 vm._data.visibility 效果是一样的。 observe(data, true /* asRootData */); 是最重要的一个方法,下面我们来分析这个方法

observe

observe 中文翻译就是 观察 , 就是将原始的 data 变成一个 可观察的对象 , 其代码如下(删除了一些逻辑判断):

function observe (value, asRootData) {
    ob = new Observer(value);
  }
复制代码

这个方法就是 new 了一个 Observer 对象, 其构造函数如下:

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);
    }
  };
复制代码

这个方法里面有对 Array 做特殊处理,我们现在传递的对象是一个 Object , 但是里面 todos 是一个数组,我们后面会分析数组处理的情况, 接下来调用 this.walk 方法,就是遍历对象中的每一个属性:

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

defineReactive$$1 方法通过 Object.defineProperty 来重新封装 data , 给每一个属性添加一个 getter , setter 来做数据拦截

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 方法就是利用 Object.defineProperty 来设置 data 里面已经 存在 的属性来设置 getter , setter , 具体 getset 在什么时候发挥效用我们先不分析。

var childOb = !shallow && observe(val); 是一个递归调 observe 来拦截所有的子属性。

data 中的属性 todos 是一个数组, 我们又回到 observe 方法, 其主要目的是通过 ob = new Observer(value); 来生成一个 Observer 对象:

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);
    }
  };
复制代码

这里可以看出对 Array 有特殊的处理,下面我们我们来具体分析 protoAugment 方法

protoAugment(数组)

protoAugment(value, arrayMethods); 传了两个参数,第一个参数,就是我们的数组,第二个参数 arrayMethods 需要好好分析,是 Vue 中对 Array 的特殊处理的地方。

其源码文件在 vue\src\core\observer\array.js 下,

  1. 首先基于 Array.prototype 原型创建了一个新的对象 arrayMethods
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
复制代码
  1. 重写了 Array 如下 7 个方法:
var methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
  ];
复制代码
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})
复制代码

总结:从上面可知, Vue 只会对上述七个方法进行监听, 如果使用Array 的其他的方法是不会触发Vue 的双向绑定的。比如说用 concat , map 等方法都不会触发双向绑定。

this.$set

上面已经分析了 Object , Array 的数据监听,但是上面的情况都是在初始化 Vue 实例的时候,已经知道了 data 中有哪些属性了,然后对每个属性进行数据拦截,现在有一种情况就是,如果我们有需要需要给 data 动态的添加属性,我们该怎么做呢?

Vue 单独开放出了一个接口 $set , 他挂载在 vm 原型上,我们先说下其使用方式是: this.$set(this.newTodo,"name", '30')

function set (target, key, val) {
    if (isUndef(target) || isPrimitive(target)
    ) {
      warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
    }
    if (Array.isArray(target) && isValidArrayIndex(key)) {
      target.length = Math.max(target.length, key);
      target.splice(key, 1, val);
      return val
    }
    if (key in target && !(key in Object.prototype)) {
      target[key] = val;
      return val
    }
    var ob = (target).__ob__;
    if (target._isVue || (ob && ob.vmCount)) {
      warn(
        'Avoid adding reactive properties to a Vue instance or its root $data ' +
        'at runtime - declare it upfront in the data option.'
      );
      return val
    }
    if (!ob) {
      target[key] = val;
      return val
    }
    defineReactive$$1(ob.value, key, val);
    ob.dep.notify();
    return val
  }
复制代码

通过上面的分析,使用 $set 方法,需要注意如下几点:

  1. target 不能是 undefined , null , string , number , symbol , boolean 六种基础数据类型
  2. target 不能直接挂载在 Vue 实例对象上, 而且不能直接挂载在root data 属性上

$set 最终调用 defineReactive$$1(ob.value, key, val); 方法去动态添加属性, 并且给该属性添加 gettersetter

动态添加的属性,同样也需要动态更新视图,则是调用 ob.dep.notify(); 方法来动态更新视图

总结

  1. 如果 data 属性是一个 Object , 则将其将其进行转换,主要是做如下两件事情:
  1. 给对象添加一个 __ob__ 的属性, 其是一个 Observer 对象
  1. 遍历 data 的说有属性('key'), 通过 Object.defineProperty 设置其 gettersetter 来进行数据拦截
  1. 如果 data (或者子属性)是一个 Array , 则将其原型转换成 arrayMethods (基于 Array.prototype 原型创建的一个新的对象,但是重新定义了 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse')七个方法,来进行对 Array 的数据拦截(这也就是Vue 对数组操作,只有这七个方法能实现双向绑定的原因)
深入了解 Vue 响应式原理(数据拦截)

在这篇文章我们已经分析了 Vue 响应式原理 , 我们接下来会继续分析 深入了解 Vue.js 是如何进行「依赖收集」,准确地追踪所需修改


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

有限与无限的游戏

有限与无限的游戏

[美]詹姆斯·卡斯 / 马小悟、余倩 / 电子工业出版社 / 2013-10 / 35.00元

在这本书中,詹姆斯·卡斯向我们展示了世界上两种类型的「游戏」:「有限的游戏」和「无限的游戏」。 有限的游戏,其目的在于赢得胜利;无限的游戏,却旨在让游戏永远进行下去。有限的游戏在边界内玩,无限的游戏玩的就是边界。有限的游戏具有一个确定的开始和结束,拥有特定的赢家,规则的存在就是为了保证游戏会结束。无限的游戏既没有确定的开始和结束,也没有赢家,它的目的在于将更多的人带入到游戏本身中来,从而延续......一起来看看 《有限与无限的游戏》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

html转js在线工具
html转js在线工具

html转js在线工具