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

4,971 阅读13分钟

前言

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

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

在这里,根据源码,我决定在给大家讲一遍,看看和大家平时自己看的,有没有区别,如果有遗漏的点,欢迎评论

正文

先来一段 defineReactive 的源码:

//在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()

虽然很多前端的大佬知道它的作用,但是我相信还是有一些朋友是不认识的,我希望我写的文章,不只是传达vue内部实现的一些精神,更能帮助一些小白去了解一些原生的api。


defineProperty

在 MDN 上的解释是:

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

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

obj

要在其上定义属性的对象。

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 到时候会把链接抛出来,以供大家参考学习。

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