Vue源码阅读(7):将数据转换成响应式的

 我的开源库:

这篇博客主要讲解 Vue 是如何将数据转换成响应式的,将数据转换成响应式是进行依赖收集和变化侦测的基础。

1,对数据响应式处理的入口

第一小节是为了让大家了解响应式处理的代码在整体源码中的位置。

1-1,执行 new Vue(core/instance/index.js)

function Vue (options) {
  // 执行 vm 原型上的 _init 方法,该方法在 initMixin 方法中定义
  this._init(options)
}

// 写入 vm._init
initMixin(Vue)

vue 里面执行 _init 方法,该方法定义在 initMixin 方法中。

1-2,_init 方法(core/instance/init.js)

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // 初始化 state,包括 props、methods、data、computed、watch
    initState(vm)

    // 如果配置中有 el 的话,则自动执行挂载操作
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

初始化数据的方法是 initState。

1-3,initState 方法(core/instance/state.js)

export function initState (vm: Component) {
  vm._watchers = []
  const 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)
  // 初始化监听属性
  // nativeWatch的作用:Firefox has a "watch" function on Object.prototype...
  if (opts.watch && opts.watch !== nativeWatch) {
    // 进行侦听属性的初始化过程
    initWatch(vm, opts.watch)
  }
}

initState 方法内进行了一系列状态的初始化,我们以 initData 为例。

1-4,initData 方法(core/instance/state.js)

// 初始化我们配置中写的 data 对象,传递的参数(vm)是当前 Vue 的实例
function initData (vm: Component) {
  // observe data
  observe(data, true /* asRootData */)
}

1-5,observe 方法(core/observer/index.js)

export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 如果这个值不是一个对象、或者这个值是一个虚拟节点实例的话,在这里直接 return
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  // 声明要返回的 ob 变量
  let ob: Observer | void
  // 如果当前 value 有 __ob__ 属性,且这个属性是 Observer 类的实例的话
  // 直接将 value.__ob__ 赋值给 ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 传递进来的 value 并不是响应式的,在这里。通过 new Observer(value) 将其转换成响应式的
    // 并且返回 new 出来的实例
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

在这里通过执行 ob = new Observer(value) 将数据转换成响应式的,所谓响应式的数据是指 Vue 能够侦测到该数据的使用和变更。

2,借助 Observer 类将数据转换成响应式的

响应式的数据是指 Vue 能够侦测到该数据的使用和变更。

侦测数据的使用是为了依赖收集。

侦测数据的变更是为了触发依赖更新

2-1,class Observer(core/observer/index.js)

export class Observer {
  // 被处理成响应式的数据,可以是对象类型或者是数组类型
  value: any;
  dep: Dep;
  // number of vms that has this object as root $data
  // 将此对象作为根 $data 的 vms 数量
  vmCount: number;

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // 对象类型值和数组类型值有不同的处理,在这里进行 if else 判断。
    if (Array.isArray(value)) {
      const augment = hasProto 
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      // 用于将对象中的属性都转换成响应式的
      this.walk(value)
    }
  }

  /**
   * 该方法用于将 obj 中所有的 key 都转换成响应式的
   * 具体的做法是遍历 keys,每个 key 都执行 defineReactive 方法
   * defineReactive 方法用于将对象中具体的 key 转换成响应式的
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }

  /**
   * 用于将数组中的元素都转换成响应式的
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      // 对数组中的每个元素都执行 observe 方法
      observe(items[i])
    }
  }
}

Observer 类的构造方法中会判断处理的数据是不是数组类型,对象类型和数组类型的处理方式是不一样的,对象类型会执行 walk 方法。

walk 方法会遍历对象的 key,执行 defineReactive 方法,在 defineReactive 方法中将对象中的 key 都转换成 Object.defineProperty 的形式,这样 Vue 就能监控到对象属性的使用和变更了。

2-2,defineReactive 方法(core/observer/index.js)

export function defineReactive (
  // 对象
  obj: Object,
  // key
  key: string,
  // 值
  val: any,
  customSetter?: ?Function,
  // 浅的
  shallow?: boolean
) {
  // 如果 val 是一个对象类型的话,那么这个 dep 将用于保存 val 的依赖列表
  // 数组类型值的依赖列表保存在 observer.dep 中
  const dep = new Dep()

  const getter = property && property.get
  const setter = property && property.set

  // 这个 childOb(Observer类的实例)中的 dep 是用来保存数组类型值的依赖的
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 在此进行依赖收集
    get: function reactiveGetter () {
      // 触发执行上面拿到的 getter
      const value = getter ? getter.call(obj) : val
      / 下面是依赖收集的操作 /
      // 如果 Dep 上的静态属性 target 存在的话
      if (Dep.target) {
        // 向 dep 中添加依赖,依赖是 Watcher 的实例
        dep.depend()
        if (childOb) {
          // childOb.dep 用来存储数组类型值的依赖
          childOb.dep.depend()
          // 如果值是数组类型的话
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      // getter 返回值
      return value
    },
    // 在此进行派发更新
    set: function reactiveSetter (newVal) {
      // 触发依赖的更新
      dep.notify()
    }
  })
}

在 defineReactive 方法的开头创建了一个 Dep 类的实例,这个 dep 实例是用于存储对象类型值依赖的,我们可以看到在下面的 get 中,执行 dep.depend() 进行依赖的收集,依赖收集的细节,下面再说。

如果数据是数组类型的话,还需要执行数组类型值对应 dep 实例的 depend 方法,数组类型的 dep 实例存储在 Observer 实例上面。关于数组类型值为什么要多这一步处理?以及 dep 为什么存储在 Observer 实例上面,我们在下一小节细讲一下,要不然容易引起困惑。

2-3,数组类型值额外处理的原因

假设上面 defineReactive 方法的 obj 参数为:

let obj = {
  names: ['tom', 'jack', 'rose']
}

key 参数为 'names',value 参数为 ['tom', 'jack', 'rose']。

那么上面的这个 names 属性的变更有两种方式:

(1)obj.names = ['小明', '小红', '小山']

(2)obj.names.push('alice')

第一种方式是直接对 obj.names 重新设值,这种变更数据的方式 Object.defineProperty set 是能够侦测到变化的,这在 set 中执行 dep.notify() 触发依赖更新就可以了。

但是第二种方式使用数组原型上的方法变更数据,这种变更方法 Object.defineProperty set 是侦测不到的。所以 Vue 另辟新径,重写了数组的原型方法,如果用户执行数组的原型方法变更数组的话,Vue 就能够在重写的原型方法中执行依赖更新的操作。

Vue 在重写的原型方法中执行依赖更新操作的前提是:在重写的原型方法中能够拿到这个数组对应的 dep 实例,而在 defineReactive 方法中创建的 dep 实例,在重写的原型方法中是获取不到的。Vue 的解决方案是在 Observer 实例上创建一个专门服务于数组收集依赖的 dep 实例,因为Observer 的实例会被定义在值的 '__ob__' 属性上,所以在数组的原型方法中,可以通过 this.__ob__.dep 获取到数组对应的 Dep 实例,这就解决了问题。相关代码如下:

(1)将为数组类型值的 dep 设值到 Observer 实例上

export class Observer {
  value: any;
  dep: Dep;

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__', this)
  }
}

 (2)在 get 中为数组类型值收集依赖,依赖存储到 childOb.dep 中

// 这个 childOb(Observer类的实例)中的 dep 是用来保存数组类型值的依赖的
let childOb = !shallow && observe(val)

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 在此进行依赖收集
    get: function reactiveGetter () {
      // 触发执行上面拿到的 getter
      const value = getter ? getter.call(obj) : val
      / 下面是依赖收集的操作 /
      // 如果 Dep 上的静态属性 target 存在的话
      if (Dep.target) {
        // 向 dep 中添加依赖,依赖是 Watcher 的实例
        dep.depend()
        if (childOb) {
          // childOb.dep 用来存储数组类型值的依赖
          childOb.dep.depend()
          // 如果值是数组类型的话
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      // getter 返回值
      return value
    },
    // 在此进行派发更新
    set: function reactiveSetter (newVal) {
      ......
    }
  })

 (3)在重写的原型方法中,获取 dep,并且触发依赖更新

[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    // 通知 ob.dep 中的依赖
    ob.dep.notify()
    // 在最后,返回 Array 方法执行的结果
    return result
  })
})

2-4,数组类型数据原型方法的重写

export class Observer {
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
    } else {
      // 用于将对象中的属性都转换成响应式的
      this.walk(value)
    }
  }
}

hasProto 方法用于判断当前的浏览器环境支不支持 __proto__。

如果支持原型的话,就将重写的数组原型方法赋值到数组的 __proto__ 属性上。

function protoAugment (target, src: Object, keys: any) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

如果不支持 __proto__ 的话,直接将重写的原型方法赋值到数组值上面。

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

加下来看看 Vue 是如何重写原型方法的,上面的 arrayMethods 就是包含重写原型方法的对象。

// 拿到 Array 的 prototype 原型对象
const arrayProto = Array.prototype
// 利用 Object.create() 创建一个新的对象,并且这个新的对象的原型链(__proto__)指向 arrayProto。
// 这样的话,我们只需要将一些需要改写的方法定义到 arrayMethods 对象中即可。
// 这样的话,我们既可以访问到 arrayMethods 对象中已经改写了的方法,也能访问到 arrayProto 对象中未改写的方法
// ^o^ 完美!
export const arrayMethods = Object.create(arrayProto)

// 能够改变数组内容方法的数组
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
  // 进行遍历处理
.forEach(function (method) {
  // 缓存原生的相应方法
  const original = arrayProto[method]
  // 定义该 method 对应的自定义方法
  def(arrayMethods, method, function mutator (...args) {
    // 执行原生方法拿到执行结果值,在最后将这个结果值返回
    const result = original.apply(this, args)
    // 这里的 this 是执行当前方法的数组的实例。在 Vue 中,每个数据都会有 __ob__ 属性,这个属性
    // 是 Observer 的实例,该实例有一个 dep 属性(Dep 的实例),该属性能够收集数组的依赖
    const ob = this.__ob__
    // 数组有三种新增数据的方法。分别是:'push','unshift','splice'
    // 这些新增的数据也需要变成响应式的,在这里,使用 inserted 变量记录新增的数据
    let inserted
    switch (method) {
      // 如果当前的方法是 push 或者 unshift 的话,新增的数据就是 args,将 args 设值给 inserted 即可
      case 'push':
      case 'unshift':
        inserted = args
        break
      // 如果当前的方法是 splice 的话,那么插入的数据就是 args.slice(2)
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 如果的确新增了数据的话,将 inserted 作为参数执行 observer.observeArray() 方法,把新增的每个元素都变成响应式的
    if (inserted) ob.observeArray(inserted)
    // 通知 ob.dep 中的依赖
    ob.dep.notify()
    // 在最后,返回 Array 方法执行的结果
    return result
  })
})

(1)首先使用变量 arrayProto 保存数组原先的原型对象。

(2)然后创建对象 arrayMethods,原型链指向 arrayProto。

(3)最后在 arrayMethods 中重写那些能够改变数组本身的原型方法,在重写的原型函数中会执行 ob.dep.notify() 

这样做的好处是:只重写了那些能够改变数组内容的原型方法,其余的原型方法借助于原型链还使用其原本实现。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值