Vue2.6.0源码阅读(三):数据观察

简介: Vue2.6.0源码阅读(三):数据观察

上一节我们大致看了一下实例化Vue时所做的事情,其中初始化data选项的部分我们跳过了,这一篇我们详细来了解一下。


初始化data选项


先看一下初始化data的方法:


function initData(vm) {
  let data = vm.$options.data
  // 获取data对象
  data = vm._data = typeof data === 'function' ?
    getData(data, vm) :
    data || {}
  // 检查是否是普通对象,不是则重置为空对象
  if (!isPlainObject(data)) {
    data = {}
  }
  // 代理data到实例上
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // 观察data
  observe(data, true /* asRootData */ )
}


从上一篇的data选项合并部分我们知道合并后最终返回的是一个函数mergedInstanceDataFn,真正的合并是在这个函数内,所以这里会调用getData方法:


export function getData(data, vm) {
  // #7573 当执行data的getter时禁止依赖收集,pushTarget上一篇已经介绍过,本质就是把Dep.target设为undefined
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}


执行data函数,最后会返回我们上面的data对象。接下来遍历data对象的属性,使用proxy方法将_data的属性访问代理到实例vm上,这样我们就可以直接通过this.xxx来访问到data对象的数据了。


最后执行了observe函数,这个方法就是用来开启数据观察的方法。


数据观察


export function observe(value, asRootData) {
  // 如果不是对象或者是虚拟dom对象则返回
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob
  // 存在__ob__属性则代表该对象之前已经观察过了
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&// 当前允许进行观察
    (Array.isArray(value) || isPlainObject(value)) &&// 只允许对数组和简单对象进行观察
    Object.isExtensible(value) &&// 并且该对象是可开展的,即可以给它添加新属性
    !value._isVue// 最后它不能是Vue实例
  ) {
    ob = new Observer(value)// 创建一个观察者实例
  }
  // 统计有多少个Vue实例对象将该对象作为根数据
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}


observe方法会判断一个对象是否已经被观察过了,如果没有的话,那么当该数据是数组或简单对象的话会给它创建一个Observer实例,接下来看Observer类:


export class Observer {
  value;
  dep;
  vmCount; // 使用该对象作为根数据的vm数量
  constructor(value) {
    // 目标对象
    this.value = value
    // 实例化一个依赖收集对象
    this.dep = new Dep()
    // vm数量
    this.vmCount = 0
    // 给目标对象添加一个被观察过了的标志
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 如果浏览器支持使用__proto__属性
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
}


Observer构造函数定义了几个变量,然后给目标对象添加了一个被观察的标志位,使用了def方法,这个方法也是源码中很常见的一个方法,来看看:


export function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,// 是否可枚举
    writable: true,// 可写
    configurable: true// 可配置、可删除
  })
}


其实就是使用Object.defineProperty方法来给对象定义属性,同时可以配置属性描述符。


Observer构造函数中的dep实例是用来收集依赖的,后面再看,接下来区分了数组和对象两种类型,我们一一来看。


观察数组


1.如果浏览器支持使用__proto__属性时,会调用protoAugment方法:


function protoAugment(target, src) {
  target.__proto__ = src
}
protoAugment(value, arrayMethods)


一个对象的__proto__属性会指向其构造函数的prototype对象,所以相当于把数组对象的原型对象由Array.prototype修改为arrayMethods


arrayMethods顾名思义就是数组的方法:


const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)


创建了一个以Array.prototype__proto__属性对象的对象,大致相当于new Array()生成的对象。


const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // 缓存原始方法
  const original = arrayProto[method]
  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
  })
})


总结来说,arrayMethods对象包含了数组的所有方法,且这些方法都是重写后的数组方法,方法内部会先执行数组原始方法,然后如果当前数组的操作会插入新数据的话那么会对新插入的数据也进行观察,最后,如果有该数组的依赖的话,会通知这些依赖更新。


2.如果不支持__proto__的话那么会调用copyAugment方法:


function copyAugment(target, src, keys ) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
copyAugment(value, arrayMethods, arrayKeys)


这个方法其实相当于使用Object.defineProperty给数组对象本身添加方法,我们都知道访问一个对象的属性或方法,如果对象本身存在,那么就不会去原型对象上查找,所以相当于把数组原始方法都给屏蔽了。


至于为什么要重写数组的方法,当然是为了能监听到数组的变化了,重写完数组的方法后,接下来会调用observeArray方法:


this.observeArray(value)
observeArray(items ) {
    for (let i = 0, l = items.length; i < l; i++) {
        observe(items[i])
    }
}


很简单,遍历数组,依次观察数组的每一项,observe方法我们前面介绍过了,它只会对数组和普通对象创建观察者对象,所以如果某一项是数组,那么又会再调用observeArray方法,其实就是递归的进行遍历观察。


观察对象


对象的话就直接调用walk方法:


this.walk(value)
walk(obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i])
    }
}


遍历对象自身可枚举的属性,然后依次调用defineReactive方法,这个方法前面也看到过,现在来看一下它的实现:


export function defineReactive(
  obj,
  key,
  val,
  customSetter,
  shallow
) {
  // 实例化一个dep,用来收集依赖,通过闭包保存
  const dep = new Dep()
  // 获取该属性原来的属性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果该属性是不可配置的,那么直接返回
  if (property && property.configurable === false) {
    return
  }
  // 保存属性原有的set和get
  const getter = property && property.get
  const setter = property && property.set
  // 如果没有传递val,且getter不存在或setter存在,那么val取当前对象上的值
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 如果该属性的值又是一个对象或数组,那么也需要递归进行观察
  let childOb = !shallow && observe(val)
  // 定义get和set
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const 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) {
      const value = getter ? getter.call(obj) : val
      // 值没有变化则直接返回
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      // #7981: 对于不带setter的访问器属性
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 观察新的值
      childOb = !shallow && observe(newVal)
      // 触发更新
      dep.notify()
    }
  })
}


这个方法比较长,但做的事情比较简单,首先通过闭包保存一个依赖收集实例dep,然后重新定义对象的该属性,转换成getset的形式,如果该属性原本就存在gettersetter存取描述符,那么仍然会使用原来的方法,否则通过闭包来使用val变量进行该属性值的维护。


get函数里,也就是读取该属性时,会进行依赖收集,在set函数里,也就是设置该属性时会触发依赖更新,这部分的内容我们后面再会。


总结一下数据观察的操作,首先从根data对象开始,深度优先进行遍历,如果是普通数组和对象的话,会给其创建一个关联的Observer实例,同时会给它创建一个依赖收集实例dep,紧接着如果它是数组,那么会拦截它的数组方法,然后遍历数组项依次进行观察,也就是重复前面的逻辑,如果是对象,那么会把它自身的所有可枚举属性都转换成gettersetter的形式,当然也会对它的值进行观察,同样是重复前面的逻辑。这些操作结束后,就相当于把整个data都转换成一个响应式对象了,当我们操作这个对象时,无论是给数组添加元素还是给对象设置新值Vue都能监听到,监听到用来干什么,后面再细说。




相关文章
|
9天前
|
JavaScript
Vue+element_Table树形数据与懒加载报错Error in render: “RangeError: Maximum call stack size exceeded“
本文讨论了在使用Vue和Element UI实现树形数据和懒加载时遇到的“Maximum call stack size exceeded”错误,指出问题的原因通常是因为数据中的唯一标识符`id`不唯一,导致递归渲染造成调用栈溢出。
22 1
Vue+element_Table树形数据与懒加载报错Error in render: “RangeError: Maximum call stack size exceeded“
在 Vue3 中,如何使用 setup 函数创建响应式数据?
在 Vue3 中,如何使用 setup 函数创建响应式数据?
|
23天前
|
JavaScript
vue学习(8)数据代理
vue学习(8)数据代理
29 1
|
2月前
|
JavaScript
Vue学习之--------深入理解Vuex之多组件共享数据(2022/9/4)
这篇文章通过一个实际的Vue项目案例,演示了如何在Vuex中实现多组件间共享数据。文章内容包括在Vuex的state中新增用户数组,创建Person.vue组件用于展示和添加用户信息,以及在Count组件中使用Person组件操作的数据。通过测试效果展示了组件间数据共享和状态更新的流程。
Vue学习之--------深入理解Vuex之多组件共享数据(2022/9/4)
|
8天前
|
JavaScript 前端开发 UED
组件库实战 | 用vue3+ts实现全局Header和列表数据渲染ColumnList
该文章详细介绍了如何使用Vue3结合TypeScript来开发全局Header组件和列表数据渲染组件ColumnList,并提供了从设计到实现的完整步骤指导。
|
8天前
|
开发框架 JavaScript 前端开发
手把手教你剖析vue响应式原理,监听数据不再迷茫
该文章深入剖析了Vue.js的响应式原理,特别是如何利用`Object.defineProperty()`来实现数据变化的监听,并探讨了其在异步接口数据处理中的应用。
|
21天前
|
JavaScript
Vue组件传值异步问题--子组件拿到数据较慢
Vue组件传值异步问题--子组件拿到数据较慢
23 0
|
2月前
|
JavaScript 前端开发
Vue学习之--------Vue中收集表单数据(使用v-model 实现双向数据绑定、代码实现)(2022/7/18)
这篇文章介绍了Vue中使用v-model实现表单数据收集的方法,包括基础知识、代码实例和测试效果,并提供了一些额外建议。
Vue学习之--------Vue中收集表单数据(使用v-model 实现双向数据绑定、代码实现)(2022/7/18)
|
2月前
|
JavaScript API
Vue学习之--------列表排序(ffilter、sort、indexOf方法的使用)、Vue检测数据变化的原理(2022/7/15)
这篇博客文章讲解了Vue中列表排序的方法,使用`filter`、`sort`和`indexOf`等数组方法进行数据的过滤和排序,并探讨了Vue检测数据变化的原理,包括Vue如何通过setter和数组方法来实现数据的响应式更新。
Vue学习之--------列表排序(ffilter、sort、indexOf方法的使用)、Vue检测数据变化的原理(2022/7/15)
|
2月前
|
JavaScript
Element - Vue使用slot-scope和v-for遍历数据为树形表格
这篇文章介绍了在Vue中使用`slot-scope`和`v-for`指令来遍历数据并将其渲染为树形表格的方法。
28 0
Element - Vue使用slot-scope和v-for遍历数据为树形表格