Vue.set 源码解析

用过 Vue 的同学应该都晓得 Vue.set 这个 api ,在 Vue2.x 组件实例初始化之后,动静给 data 选项增加属性是不会触发响应的,如果心愿动静增加的属性也能触发响应式机制,这个时候就能够用 Vue.set 这个 api 。理解过原理的同学应该不多,但面试常常会问到,因而这边剖析一下 Vue.set 源码实现。

这个 api 是做什么的

首先看源码之前,举荐看一下官网文档的形容,这样更容易了解源码。

留神文档中形容的几个重点:向响应式对象中增加属性,确保新增加的属性也是响应式的,且触发视图更新,传入的对象不能是 Vue 实例,或者 Vue 实例的根数据对象。

从文档上来看,set 办法能够用于对象,也可用于数组:

// 在对象上增加一个属性,同时触发响应this.$set(this.someObj, 'a', 1);// 应用下标批改数组,同时触发响应this.$set(this.arr, 1, 2333);

这边须要留神下,set 办法能够在两个中央拜访到,一种是作为 Vue 结构器的静态方法,另一种是实例办法,两个办法是完全一致的:

// Vue 结构器的静态方法Vue.set();// 作为实例办法,这是全局 Vue.set 的别名vm.$set();

在哪里看源码

因为 set 办法是挂载在 Vue 结构器上的静态方法,那么 set 必然是在 Vue 结构器初始化过程中挂载下来的,因而咱们能够去看一下 global-api/index.js ,在第 44 行进行 set 的挂载:

Vue.set = set

而后就顺藤摸瓜,找一下 set 办法从哪里引入的:

import { set, del } from '../observer/index'

这会应该很分明了,从 observer/index 引入,而且还是命名导出,set 的源码在:

node_modules/vue/src/core/observer/index.js:201

首先先看下这个模块都做了那些事件。这个模块次要用于将一般 JS 对象转换为响应式对象,定义了一个类和若干办法:

// 用于将察看对象的属性进行 getter/setter 转换,实现依赖收集 (collect dependencies) 和派发更新 (dispatch updates)export class Observer {}// 用于创立 Observer 实例,如果曾经是 Observer 实例就不再反复创立export function observe()// 在对象中定义响应式属性export function defineReactive()// 在对象中设置一个属性,如果属性不存在,则触发更改告诉 (change notification)export function set()// 删除一个对象属性并触发告诉export function del()

其中 Observer 类、observe 办法和 defineReactive 办法的调用关系是:

  • 调用 observe 办法,传入须要察看的值;
  • observe 办法中创立 Observer 实例,对传入的值进行察看;
  • Observer 类的结构器中,调用 walk 实例办法将对象的每个属性进行遍历,而后调用 defineReactive 办法将对象属性进行 getter/setter 转换;
总结一下,observer 办法能够察看一个对象,而 defineReactive 是察看对象的一个属性

响应式机制剖析

首先在 observer 办法中判断传入的值是否为 JavaScript 对象,如果是,创立 Observer 实例进行察看,简化后的代码如下:

export function observe(value) {  if (!isObject(value) || value instanceof VNode) {    // 不是对象,或者是 VNode 的实例,不进行察看    return  }  let ob: Observer | void  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {    // 如果是响应式对象就不再察看    ob = value.__ob__  } else {    // 非响应式对象,创立 Observer 实例进行察看    ob = new Observer(value)  }  return ob}

Observer 类中,判断传入的是对象还是数组,如果是数组,则须要重写数组的变更办法,而后观测数组,如果是对象,间接观测对象。简化后的代码如下:

export class Observer {  constructor (value) {    this.value = value    // 将 __ob__ 属性增加到传入的对象上,用于将对象标记为响应式对象    def(value, '__ob__', this)    if (Array.isArray(value)) {      // 如果是数组,则重写数组的变更办法      protoAugment(value, arrayMethods)      // 观测数组      this.observeArray(value)    } else {      // 观测对象      this.walk(value)    }  }  walk (obj) {    const keys = Object.keys(obj)    for (let i = 0; i < keys.length; i++) {      // 应用 defineReactive 将对象的每个属性进行 getter/setter 转换      defineReactive(obj, keys[i])    }  }  observeArray (items) {    for (let i = 0, l = items.length; i < l; i++) {      // 调用 observe 察看对象的每个元素(如果是对象就察看,根本类型不察看)      observe(items[i])    }  }}

defineReactive 办法中,给对象的属性增加 getset 办法。简化后的代码如下:

export function defineReactive(obj, key, val) {  const dep = new Dep()  // 如果对象的 configurable: false 则不进行观测  const property = Object.getOwnPropertyDescriptor(obj, key)  if (property && property.configurable === false) {    return  }  // 深度观测子属性  let childOb = observe(val)  // 应用 Object.defineProperty 将对象属性进行 getter/setter 转换  Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    get: function reactiveGetter () {      // 拜访对象属性的时候,触发 get 办法      return val    },    set: function reactiveSetter (newVal) {      // 设置属性的时候触发 set 办法      if (val === newVal) {        // 新值和旧值一样,不触发响应        return      }      // 设置新值      val = newVal      // 对新值进行观测(如果是对象)      childOb = observe(newVal)      // 触发告诉更新视图      dep.notify()    }  })}

Vue.set 源码剖析

Vue.set 残缺代码如下:

export function set (target: Array<any> | Object, key: any, val: any): any {  if (process.env.NODE_ENV !== 'production' &&    (isUndef(target) || isPrimitive(target))  ) {    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)  }  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  }  const ob = (target: any).__ob__  if (target._isVue || (ob && ob.vmCount)) {    process.env.NODE_ENV !== 'production' && 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(ob.value, key, val)  ob.dep.notify()  return val}

首先判断传入 target 的类型,如果是 undefinednull 或者原始类型,打印正告信息:

if (process.env.NODE_ENV !== 'production' &&    (isUndef(target) || isPrimitive(target))) {    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}

接着如果是数组,就调用数组的 splice 办法增加元素(Vue 重写了数组变更办法,调用 splice 会触发响应,上面会进行剖析):

if (Array.isArray(target) && isValidArrayIndex(key)) {  // 这行的作用是,如果传入下标大于数组长度,例如 [0, 1], 2, 2  // 那么会将数组长度改为 3 ,失去 [0, 1, empty]  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}
这个时候文档中说的 target 必须是响应式就发挥作用了,如果不是响应式,只能批改属性,不能触发视图更新了

那么前面对应的逻辑应该是传入的属性不存在于对象中,接下来首先判断的是传入对象是否为 Vue 实例或者 Vue 实例的根数据对象:

const ob = (target: any).__ob__if (target._isVue || (ob && ob.vmCount)) {  process.env.NODE_ENV !== 'production' && 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 办法将这个属性进行 getter/setter 转换,并且触发告诉更新视图:

defineReactive(ob.value, key, val)ob.dep.notify()return val

Vue 2.x 怎么监听数组变动

JS 的数组操作方法有一些会批改原数组:

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse

这些办法被称为变更办法。相比之下,还有一些是非变更办法,例如 mapfilterconcatslice ,这些办法不会批改原数组,而是返回一个新数组。

应用非变更办法,即新数组替换旧数组,能够触发响应从而更新视图,变更办法因为间接批改原数组,在 Vue 2.x 的响应式机制下监听不到数组变动,因而实践上是不能触发响应的,但实际上咱们应用变更办法都能够触发视图更新。

在 Vue 2.x 官网文档中提到,Vue 对数组的变更办法进行了包裹,所以也能触发视图更新。这里的包裹,能够了解为一种封装,那么 Vue 到底是怎么封装了。

咱们来看一下 observer/index.js 中的 Observer 类,简化后的代码如下:

export class Observer {  constructor(value) {    this.value = value;    if (Array.isArray(value)) {      // 解决数组      protoAugment(value, arrayMethods);      // 察看数组      this.observeArray(value);    } else {      // 察看对象      this.walk(value);    }  }}

这里的 protoAugment 办法实际上就是一个工具函数,定义如下:

function protoAugment(target, src: Object) {  target.__proto__ = src;}

那么再联合下面的用法,protoAugment 实际上就是把传入数组实例的 __proto__ 指针指向的内容给换掉了,换成了 arrayMethods

不得不说 Vue 这个解决还是十分棒的,没有全局净化 Array.prototype 原型链,只是对须要响应式转换的数组实例进行解决。

// 只是对某个实例产生影响,没有净化原型链,举荐[].__proto__ = arrayMethods;// 净化原型链,不举荐Array.prototype = arrayMethods;

而后接下来看 arrayMethods 是怎么解决的,这部分源码在:

node_modules/vue/src/core/observer/array.js

首先利用原型式继承,将 Array.prototype 作为原型创立空对象:

const arrayProto = Array.prototypeexport const arrayMethods = Object.create(arrayProto)

而后在 arrayMethods 下面重写数组办法,对数组的变更办法进行拦挡操作,同时触发告诉更新视图:

const methodsToPatch = [  'push',  'pop',  'shift',  'unshift',  'splice',  'sort',  'reverse']methodsToPatch.forEach(function (method) {  const original = arrayProto[method]  // 在 arrayMethods 下面重写数组办法笼罩原来的办法  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)    // 触发告诉更新视图    ob.dep.notify()    // 返回数组原来办法执行的后果    return result  })})

再次称誉一下,Vue 采纳的是原型式继承 + 重写,没有间接批改原型链,思路值得借鉴。

// 原型式继承,没有批改原型链,举荐const arrayMethods = Object.create(Array.prototype);arrayMethods[methodsToOverride] = function() {};// 批改原型链了,不举荐Array,prototype[methodsToOverride] = function() {};

这边用到了一个 dep 办法,作用就是在对象上增加属性:

export function def (obj, key, val, enumerable) {  Object.defineProperty(obj, key, {    value: val,    enumerable: !!enumerable,    writable: true,    configurable: true  })}

总结一下,Vue 应用了原型式继承,重写了数组办法(这里重写指的是面向对象编程中的重写),对数组变更办法进行了拦挡操作,同时触发告诉更新视图。

为什么 Object.freeze() 能够晋升渲染性能

Vue 官网举荐对不须要响应式的数据应用 Object.freeze() 进行解决,这样能够进步渲染性能,然而为什么 Object.freeze() 能够进步性能呢?

在上面代码中,应用 Object.getOwnPropertyDescriptor 打印一下对象解冻前和解冻后的变动:

let obj = { a: 1 }Object.getOwnPropertyDescriptor(obj, 'a') /** * { *     "value": 1, *     "writable": true, *     "enumerable": true, *     "configurable": true * } */Object.freeze(obj)Object.getOwnPropertyDescriptor(obj, 'a')/** * { *    "value": 1, *    "writable": false, *    "enumerable": true, *    "configurable": false * } */

咱们看到,在对象解冻之后,writableconfigurable 都变为 false ,也就是说这个时候对象的属性值无奈再批改,也无奈通过 Object.defineProperty 批改对象属性的 descriptor 。如果尝试用 Object.defineProperty 批改对象属性的 descriptor ,则会报如下谬误:

Uncaught TypeError: Cannot redefine property: xxx

那么在 defineReactive 办法中,有这样一段代码:

const property = Object.getOwnPropertyDescriptor(obj, key)if (property && property.configurable === false) {    return}

这段代码,外表上是一个校验,如果对象中存在 configurable: false 的属性,则不进行 getter/setter 转换。但实际上,这段代码也是逃生舱,如果不心愿对象进行响应式转换,只需 Object.freeze() 解冻对象即可。因为省略了响应式转换,无需再对对象进行深度遍历和递归解决,以及 getter/setter 转换,因而能够在肯定水平上晋升渲染性能。

参考

Vue.set - Vue 官网文档

我仿佛发现了vue的一个bug