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

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

内容简介:在上一章节我们已经粗略的分析了整个的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 是如何进行「依赖收集」,准确地追踪所需修改


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

查看所有标签

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

让大象飞

让大象飞

[美] 史蒂文·霍夫曼 / 周海云、陈耿宣 / 中信出版社 / 2017-3 / 69.00

这是一本为中国创业者量身定做的创业指南,将帮助创业者理解创新的基本方法、模式和硅谷的创业理念。作者霍夫曼频繁地穿梭于中美两地,与不同的创业者、投资人、政府负责人进行对话,积累了大量的来自中国创业者的第一手经验。在这本书里,从创业团队的人员配备到创业融资的成败再到团队的高效管理,从创业者的心理素质到创业者的独到眼光再到企业赖以生存的根本,360度无死角地呈现了一家公司从初创到惊艳到立足再到稳定的全过......一起来看看 《让大象飞》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试