vue2源码系列-深入响应式原理Vue.set

简介: 前面我们在 vue2源码系列-响应式原理 中介绍了 vue 中的整个响应式实现及流程,其中跳过了某些细节性的代码,现在我们再去好好学习研究一番

前面我们在 vue2源码系列-响应式原理 中介绍了 vue 中的整个响应式实现及流程,其中跳过了某些细节性的代码,现在我们再去好好学习研究一番

入口


我们在 defineReactive 函数里发现这么一段代码

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
  }
  //...
}
复制代码


我们知道在 dep.depend() 中会将当前订阅实例 watcher 添加进属性的 dep 中。那下面的 childOb.dep.depend() 又是干嘛的呢?

Vue.set函数


答案在于 Vue.set,我们来看看其实现吧


依旧从入口开始,在 src/core/global-api/index.js 中定义了静态属性 set

Vue.set = set
复制代码

set 的定义在 src/core/observer/index.js

export function set (target: Array<any> | Object, key: any, val: any): any {
  // 如果是数组 直接调用splice方法 
  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
}
复制代码


咋一看 set 函数并不复杂,无非是判断一些不同的情况。如果是真正符合条件参数的再为其添加属性,监测新值,同时再调用下 dep 的通知。


但其复杂之处就在于 ob.dep.notify()。之前我们学习vue响应式的时候说到当值更新时会触发属性 set 函数并调用属性对应的 dep.notify,而这里调用的是属性值的 dep -> ob.dep, 其定义在 Observer 类中

// Observer contructor
this.dep = new Dep()
复制代码


我们有必要梳理下 Vue.set 的实现流程

Vue.set实现流程


假设在模板中有这么一段

<span>{{deep.name}}</span>
复制代码
data() {
  return {
    deep: {}
  }
}
复制代码

  1. defineReactive 函数中会为属性值 {} 生成新的监测实例并返回
let childOb = !shallow && observe(val)
复制代码


其中 childObnew Observer() 中为其添加了 dep 属性指向新的 dep 实例

  1. 劫持属性 deepget 函数

  2. 渲染函数调用 this.deep.name 触发 deepget 函数,将当前订阅者添加进了 childObdep 的订阅者中
if (childOb) {
  childOb.dep.depend()
  // 数组的情况我们待会再讲
  if (Array.isArray(value)) {
    dependArray(value)
  }
}
复制代码

注意我们没有劫持deep.name的get,因为此时并deep是个空对象,没有任何属性值,更不会遍历属性为其添加get


  1. 开发者调用 Vue.set(deep, 'name', 'xxx') 添加 name 属性

  2. Vue.set 函数中调用 ob.dep.notify() 通知更新


这里的 ob 实际指向了 deep 的值 {},之前我们在 为其添加了 dep 属性,在 中添加了和 deep 相同的 watcher 实例。所以 ob.dep.notify() 通知了 watcher 更新,和 deepsetdep.notify() 本质是一样的。


为数组元素添加属性


继续来看这段还未破解的代码

// defineReactive -> get
if (Array.isArray(value)) {
  dependArray(value)
}
复制代码


为什么要有这段代码呢?我们来看看这么个情况

<span>{{deep[0].name}}</span>
复制代码
data() {
  return {
    deep: [{}]
  }
}
复制代码
Vue.set(this.deep[0], 'name', 'xxx')
复制代码


我们为 deep[0] 赋值新属性 name,按照我们之前的学习成果来看看是否会触发更新


  1. 是否会触发属性的 dep.notify()


我们现在相当于触发 deep[0].nameset 函数,但是之前没有 name 属性,想必也不会触发自定义 set 了,所以不会更新


  1. 是否会在 Vue.set 中触发 ob.dep.notify()?


答案是会的,但是我们应该看看此时的 ob 是?


obdeep[0] 的值 {},但是按照 childOb.dep.depend() 来看,其是在遍历 deep 的属性值函数 defineReactive 中为其添加订阅的。


根据 vue 实现原理,我们是不会对数组进行属性遍历的

// Observe
if (Array.isArray(value)) {
  if (hasProto) {
    protoAugment(value, arrayMethods)
  } else {
    copyAugment(value, arrayMethods, arrayKeys)
  }
  // 在这边将遍历value进行observe 不会走walk进行defineReactive
  this.observeArray(value)
} else {
  this.walk(value)
}
复制代码


所以虽然调用了 ob.dep.notify(),但实际 ob.dep 并没有添加渲染函数的 {{deep[0].name}} 对应的 watcher。但是通过神来一笔


// defineReactive -> get
if (Array.isArray(value)) {
  dependArray(value)
}
复制代码


这样在 defineReactive(vm._data, deep, [{}]) 的时候就会触发 dependArray(value) 完成数组元素 dep 对应 watcher 实例的订阅


dependArray


我们也来稍微看看 dependArray 的实现

function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    // 编辑递归数组元素完成e.__ob__.dep.depend() 实现上上...级属性的 get 中订阅实例的订阅
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
复制代码


总结


Vue.set 的实现看起来并不复杂,但是其中的弯弯绕绕还是比较绕人的。本篇的分析其实是比较全面的,但是因为表达原因可能有些地方比较难看懂,大家可以一起交流探讨,错误的地方希望指正。下篇将继续分析响应式原理的其它细节。


相关文章
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
386 2
|
JavaScript 前端开发 算法
vue渲染页面的原理
vue渲染页面的原理
403 56
|
JavaScript 前端开发 UED
vue2和vue3的响应式原理有何不同?
大家好,我是V哥。本文详细对比了Vue 2与Vue 3的响应式原理:Vue 2基于`Object.defineProperty()`,适合小型项目但存在性能瓶颈;Vue 3采用`Proxy`,大幅优化初始化、更新性能及内存占用,更高效稳定。此外,我建议前端开发者关注鸿蒙趋势,2025年将是国产化替代关键期,推荐《鸿蒙 HarmonyOS 开发之路》卷1助你入行。老项目用Vue 2?不妨升级到Vue 3,提升用户体验!关注V哥爱编程,全栈开发轻松上手。
989 2
|
移动开发 JavaScript API
Vue Router 核心原理
Vue Router 是 Vue.js 的官方路由管理器,用于实现单页面应用(SPA)的路由功能。其核心原理包括路由配置、监听浏览器事件和组件渲染等。通过定义路径与组件的映射关系,Vue Router 将用户访问的路径与对应的组件关联,支持哈希和历史模式监听 URL 变化,确保页面导航时正确渲染组件。
|
JavaScript 前端开发 开发者
Vue是如何劫持响应式对象的
Vue是如何劫持响应式对象的
251 18
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
562 17
|
JavaScript
Vue 双向数据绑定原理
Vue的双向数据绑定通过其核心的响应式系统实现,主要由Observer、Compiler和Watcher三个部分组成。Observer负责观察数据对象的所有属性,将其转换为getter和setter;Compiler解析模板指令,初始化视图并订阅数据变化;Watcher作为连接Observer和Compiler的桥梁,当数据变化时触发相应的更新操作。这种机制确保了数据模型与视图之间的自动同步。
|
监控 JavaScript 算法
深度剖析 Vue.js 响应式原理:从数据劫持到视图更新的全流程详解
本文深入解析Vue.js的响应式机制,从数据劫持到视图更新的全过程,详细讲解了其实现原理和运作流程。
|
8月前
|
存储 JavaScript Java
(Python基础)新时代语言!一起学习Python吧!(四):dict字典和set类型;切片类型、列表生成式;map和reduce迭代器;filter过滤函数、sorted排序函数;lambda函数
dict字典 Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。 我们可以通过声明JS对象一样的方式声明dict
472 2