今天看啥  ›  专栏  ›  热情的刘大爷

Vue 源码解析 - 实例化 Vue 前(二)

热情的刘大爷  · 掘金  · 前端  · 2019-01-14 06:38
阅读 116

Vue 源码解析 - 实例化 Vue 前(二)

前言

上一篇文章,大概的讲解了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 到时候会把链接抛出来,以供大家参考学习。

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




原文地址:访问原文地址
快照地址: 访问文章快照