Vue 源码解析(实例化前) - 响应式数据的实现原理

前言

上一篇文章,大概的讲解了Vue实例化前的一些配置,若是没有看到上一篇,通道在这里:Vue 源码解析 - 实例化 Vue 前(一)javascript

在上一篇的结尾,我说这一篇后着重讲一下 defineReactive 这个方法,这个方法,其实就是你们能够在外面看见一些文章对 vue 实现响应式数据原理的过程。前端

在这里,根据源码,我决定在给你们讲一遍,看看和你们平时本身看的,有没有区别,若是有遗漏的点,欢迎评论vue

正文

先来一段 defineReactive 的源码:java

//在Object上定义反应属性。
function defineReactive ( obj, key, val, customSetter, shallow ) {
  var dep = new Dep();

  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }
  var getter = property && property.get;
  if (!getter && arguments.length === 2) {
    val = obj[key];
  }
  var setter = property && property.set;

  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 (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });
}
复制代码

在讲解这段源码以前,我想先在开始讲一下 Object 的两个方法 Object.defineProperty() Object.getOwnPropertyDescriptor() react

虽然不少前端的大佬知道它的做用,可是我相信仍是有一些朋友是不认识的,我但愿我写的文章,不仅是传达vue内部实现的一些精神,更能帮助一些小白去了解一些原生的api。git


defineProperty

在 MDN 上的解释是:github

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
复制代码

这里,其实就是用来实现响应式数据的核心之一,主要作的事情就是数据的更新, Object.defineProperty() 最多接收三个参数:obj , prop , descriptor编程

objapi

要在其上定义属性的对象。
复制代码

prop数组

要定义或修改的属性的名称。
复制代码

descriptor

将被定义或修改的属性描述符。
复制代码

返回值

被传递给函数的对象。
复制代码

在这里要注意一点:在ES6中,因为 Symbol类型的特殊性,用Symbol类型的值来作对象的key与常规的定义或修改不一样,而Object.defineProperty 是定义key为Symbol的属性的方法之一。

对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符数据描述符是一个具备值的属性,该值多是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是二者。

数据描述符和存取描述符均具备如下可选键值:

configurable

当且仅当该属性的 configurable 为 true 时,该属性描述符才可以被改变,同时该属性也能从对应的对象上被删除。

默认值: false
复制代码

enumerable

当且仅当该属性的 enumerable 为 true 时,该属性才可以出如今对象的枚举属性中。

默认为 false。
复制代码

数据描述符同时具备如下可选键值:

value

该属性对应的值。能够是任何有效的 JavaScript 值(数值,对象,函数等)。

默认为 undefined。
复制代码

writable

当且仅当该属性的 writable 为 true 时,value 才能被赋值运算符改变。

默认为 false。
复制代码

存取描述符同时具备如下可选键值:

get

一个给属性提供 getter 的方法,若是没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,可是会传入this对象(因为继承关系,这里的this并不必定是定义该属性的对象)。

默认为 undefined。
复制代码

set

一个给属性提供 setter 的方法,若是没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受惟一参数,即该属性新的参数值。

默认为 undefined。
复制代码

Object.getOwnPropertyDescriptor()

obj

须要查找的目标对象
复制代码

prop

目标对象内属性名称(String类型)
复制代码

descriptor

将被定义或修改的属性描述符。
复制代码

返回值

返回值其实就是 Object.defineProperty() 中的那六个在 descriptor
对象中可设置的属性,这里就不废话浪费篇幅了,你们看一眼上面就好
复制代码

defineReactive 的参数我就不一一列举的来说了,大概从参数名也能够知道大概的意思,具体讲函数内容的时候,在细讲。


Dep

var dep = new Dep();
复制代码

在一进入到 defineReactive 这个函数时,就实例化了一个Dep的构造函数,并把它指向了一个名为dep的变量,下面,咱们来看看Dep这个构造函数都作了什么:

var uid = 0;

var Dep = function Dep () {
  this.id = uid++;
  this.subs = [];
};

Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};

Dep.prototype.removeSub = function removeSub (sub) {
  remove(this.subs, sub);
};

Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};

Dep.prototype.notify = function notify () {
  var subs = this.subs.slice();
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};

Dep.target = null;
复制代码

在实例化 Dep 以前,给 Dep 添加了一个 target 的属性,默认值为 null;

Dep在实例化的时候,声明了一个 id 的属性,每一次实例化Dep的id都是惟一的;

而后声明了一个 subs 的空数组, subs 要作的事情,就是收集全部的依赖;

addSub

从字面意思,你们也能够看的出来,它就是作了一个添加依赖的动做;

removeSub

其实就是移除了某一个依赖,只不过实现没有在当前的方法里写,而是调用的一个 remove 的方法:

function remove (arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}
复制代码

这个方法,就是从数组中,移除了某一项;

depend

添加一个依赖数组项;

notify

通知每个数组项,更新每个方法;

这里 subs 调用了 slice 方法,官方注释是 “ stabilize the subscriber list first ” 字面意思是 “首先稳定订户列表”,这里我不是很清楚,若是知道的大佬,还请指点一下
复制代码

Dep.target 在 Vue 实例化以前一直都是 null ,只有在 Vue 实例化后,实例化了一个 Watcher 的构造函数,在调用 Watcher 的 get 方法的时候,才会改变 Dep.target 不为 null ,因为 Watcher 涉及的内容也不少,因此我准备单拿出一章内容,在 Vue 实例化以后去讲解,如今,咱们就暂时看成 Dep.target 不为空。

如今,Dep 构造函数讲解的就差很少了,咱们继续接着往下看:

var property = Object.getOwnPropertyDescriptor(obj, key);
复制代码

方法返回指定对象上一个自有属性对应的属性描述符并赋值给property;

if (property && property.configurable === false) {
    return
}
复制代码

咱们要实现响应式数据的时候,要看当前的 object 上面是否有当前要实现响应式数据的这个属性,若是没有,而且 configurable 为 false,那么就直接退出该方法。

在上面咱们介绍过 configurable 这个属性,若是它是 flase ,说明它是不容许被更改的,那么就确定不支持响应式数据了,那确定是要退出该方法的。

var getter = property && property.get;

if (!getter && arguments.length === 2) {
    val = obj[key];
}
复制代码

获取当前该属性的 get 方法,若是没有该方法,而且只有两个参数(obj 和 key),那么 val 就是直接从这个当前的 obj 里面获取。

var setter = property && property.set;
复制代码

获取当前属性的 set 方法。

var childOb = !shallow && observe(val);
复制代码

判断是否要浅拷贝,若是传的是 false ,那么就是要进行深拷贝,这个时候,就须要把当前的值传递给 observe 的方法:

observe

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

defineReactive 中,调用 observe 方法,只传了一个参数,因此这里是只有 value 一个值的,第二个值其实就是一个 boolean 值,用来判断是不是根数据;

function isObject (obj) {
    return obj !== null && typeof obj === 'object'
}
复制代码

首先,要检查当前的值是否是对象,或者说当前的值的原型是否在 VNode 上,那就直接 return 出当前方法, VNode 是一个构造函数,内容比较多,因此这一章暂时不讲,接下来单独写一篇去讲 VNode。

var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
  return hasOwnProperty.call(obj, key)
}
复制代码

这里用来判断对象是否具备该属性,而且对象上的该属性原型是否指向的是 Observer ;

若是是,说明这个值是以前存在的,那么变量 ob 就等于当前观察的实例;

若是不是,则是作以下判断:

var shouldObserve = true;
function toggleObserving (value) {
    shouldObserve = value;
}
复制代码

shouldObserve 用来判断是否应该观察,默认是观察;

var _isServer;
var isServerRendering = function () {
  if (_isServer === undefined) {
    /* istanbul ignore if */
    if (!inBrowser && !inWeex && typeof global !== 'undefined') {
      // detect presence of vue-server-renderer and avoid
      // Webpack shimming the process
      _isServer = global['process'] && global['process'].env.VUE_ENV === 'server';
    } else {
      _isServer = false;
    }
  }
  return _isServer
};
复制代码

是否支持服务端渲染;

Array.isArray(value)
复制代码

当前的值是不是数组;

isPlainObject(value)
复制代码

用来判断是不是Object;具体代码上一篇文章当中有描述,入口在这里:Vue 源码解析 - 实例化 Vue 前(一)

Object.isExtensible(value)
复制代码

判断一个对象是不是可扩展的

value._isVue
复制代码

判断是否能够被观察到,初始化是在 initMixin 方法里初始化的,这里暂时先不作太多的介绍。

这么多判断的整体意思,就是用来判断,当前的值,是不是被观察的,若是没有,那么就建立一个新的出来,并赋值给变量 ob;

asRootData 若是是 true,而且 ob 也存在的话,那么就给 vmCount 加 1;

最后返回一个 ob。


接下来,开始响应式数据的核心代码部分了:

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
    },
    set: function reactiveSetter (newVal) {
    }
});
复制代码

首先,要确保要监听的该属性,是可枚举、可修改的的;


get

var value = getter ? getter.call(obj) : val;
复制代码

先前,在前面把当前属性的 get 方法,传给 getter 变量,若是 getter 变量存在,那么就把当前的 getter 的 this 指向当前的 obj 并传给 value 变量;若是不存在,那么就把当前方法接收到的 val 参数传给 value 变量;

if (Dep.target) {
    dep.depend();
    if (childOb) {
      childOb.dep.depend();
      if (Array.isArray(value)) {
        dependArray(value);
      }
    }
}
return value
复制代码

每次在 get 的时候,判断 Dep.target 是否为空,若是不为空,那么就去添加一个依赖,调用实例对象 dep 的 depend 方法,这里在 Watcher 的构造函数里,还作了一些特殊处理,等到讲解 Watcher 的时候,我会把这里在带过去一块儿讲一下。

反正你们记着,在 get 的时候添加了一个依赖就好。

若是是存在子级的话,而且给子级添加一个依赖:

function dependArray (value) {
  for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
    e = value[i];
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      dependArray(e);
    }
  }
}
复制代码

若是当前的值是数组,那么咱们就要给这个数组添加一个监听,由于自己 Array 是不支持 defineProperty 方法的;

因此在这里,做者给全部的数组项,添加了一个依赖,这样每个数组选项,都有了本身的监听,当它被改变的时候,会根据监听的依赖,去作对应的更新。


set

var value = getter ? getter.call(obj) : val;
复制代码

这里,和 get 时候同样,获取当前的一个值,若是不存在,就返回函数接收到的值;

if (newVal === value || (newVal !== newVal && value !== value)) {
    return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter();
}
if (setter) {
    setter.call(obj, newVal);
} else {
    val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
复制代码

若是当前值和新的值同样,那就说明没有什么变化,这样就不须要改,直接 return 出去;

若是是在开发环境下,而且存在 customSetter 方法,那么就调用它;

若是当前的属性存在 set 方法,那么就把 set 方法指向 obj,并把 newVal 传过去;

若是不存在,那么就直接把值给覆盖掉;

若是不是浅拷贝的话,那么就把当前的新值传给 observe 方法,去检查是否已经被观察,而且把新的值覆盖到 childOb 上;

最后调用 dep 的 notify 方法去通知全部的依赖进行值的更新。


归纳

到这里,基本上 vue 实现的响应式数据的原理,抛析的就差很少了,可是总体涉及的东西比较多,可能看起来会比较费劲一些,这里我归纳一下:

  • 每次在监听某一个属性时,要先实例化一个队列 Dep,负责监听依赖和通知依赖;
  • 确认当前要监听的属性是否存在,而且是可修改的;
  • 若是没有接收到参数 val,而且参数只接收到2个,那么就直接把 val 设置成当前的属性的值,不存在就是 undefined;
  • 判断当前要监听的值是须要深拷贝仍是浅拷贝,若是是深拷贝,那么就去检查当前的值是否被监听,没有被监听,那么就去实例化一个监听对象;
  • 在调用 get 方法,获取到当前属性的值,不存在就接收调用该方法时接收到的值;
  • 检查当前的队列,要对哪个 obj 进行变动,若是存在检查的目标的话,那就添加一个依赖;
  • 若是存在观察实例的话,在去检查一下当前的值是不是数组,若是是数组的话,那么就作一个数组项的依赖检查;
  • 在更新值的时候,发现当前值和要改变的值是相同的,那么就不进行任何操做;
  • 若是是开发环境下,还会执行一个回调,该回调实在值改变前可是符合改变条件时执行的;
  • 若是当前的属性存在 setter 方法,那么就把当前的值传给 setter 方法,并让当前的 setter 方法的 this 指向当前的 obj,若是不存在,直接用新值覆盖旧值就好;
  • 若是是深拷贝的话,就去检查遍当前的值是否被观察,若是没有被观察,就进行观察;(上面你们可能有发现,它已经进行了一次观察,为何还要执行呢?由于上面是在初始化的时候去观察的,当该值改变之后,好比类型改变,是要进行从新观察,确保若是改变为相似数组的值的时候,还能够进行双向绑定)
  • 最后,通知全部添加对该属性进行依赖的位置。

结束语

对应 vue 的响应式数据,到这里就总结完了,将来在实例化 vue 对象的地方,会涉及到不少有关响应式数据的地方,因此建议你们好好看一下这里。

对于源码,咱们了解了做者的思想就好,咱们不必定要彻底按照做者的写法来写,咱们要学习的,是他的编程思想,而不是他的写法,其实好多地方我以为写的不是很合适,可是我不是很明白为何要这么作,也许是我水平还比较低,没有涉及到,接下来我会对这些疑问点,进行总结,去研究为何要这么作,若是不合适,我会在 github 中添加 issues 到时候会把连接抛出来,以供你们参考学习。

最后仍是老话,点赞,点关注,有问题了,评论区开喷就好

相关文章
相关标签/搜索