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都能监听到,监听到用来干什么,后面再细说。




相关文章
|
23天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
39 1
|
23天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
44 1
|
2月前
|
JavaScript
Vue组件传值异步问题--子组件拿到数据较慢
Vue组件传值异步问题--子组件拿到数据较慢
230 58
|
29天前
|
API
vue3知识点:响应式数据的判断
vue3知识点:响应式数据的判断
27 3
|
1月前
|
存储 缓存 JavaScript
vue表单案例练习:vue表单创建一行数据及删除数据的实现与理解
vue表单案例练习:vue表单创建一行数据及删除数据的实现与理解
47 2
|
2月前
|
JavaScript
Vue+element_Table树形数据与懒加载报错Error in render: “RangeError: Maximum call stack size exceeded“
本文讨论了在使用Vue和Element UI实现树形数据和懒加载时遇到的“Maximum call stack size exceeded”错误,指出问题的原因通常是因为数据中的唯一标识符`id`不唯一,导致递归渲染造成调用栈溢出。
93 1
Vue+element_Table树形数据与懒加载报错Error in render: “RangeError: Maximum call stack size exceeded“
|
1月前
|
JavaScript
vue3,使用watch监听props中的数据
【10月更文挑战第3天】
1240 2
在 Vue3 中,如何使用 setup 函数创建响应式数据?
在 Vue3 中,如何使用 setup 函数创建响应式数据?
|
1月前
|
JavaScript 索引
vue 表格数据上下移动并增加背景色
vue 表格数据上下移动并增加背景色
36 0
|
1月前
|
JavaScript 前端开发 API
vue尚品汇商城项目-day03【20.获取Banner轮播图的数据+21.使用swiper轮播图插件】
vue尚品汇商城项目-day03【20.获取Banner轮播图的数据+21.使用swiper轮播图插件】
34 0