汪图南
  • RAG

    • RAG
  • 快速入门
  • 高级技巧
前端面试之道
  • 打包工具

    • Webpack
    • Rollup
  • TypeScript

    • TypeScript基础
    • TypeScript类型挑战
  • CSS预编译器

    • SASS
  • 自动化测试

    • Vue应用测试
  • Vue2.0源码分析
  • Vue3.0源码分析
  • 数据结构和算法(基础)
  • LeetCode(刷题)
  • JavaScript书籍

    • 你不知道的JavaScript(上)
    • 你不知道的JavaScript(中下)
    • JavaScript数据结构和算法
    • JavaScript设计模式与开发实践
    • 深入理解ES6
  • Git书籍

    • 精通Git
Github
  • RAG

    • RAG
  • 快速入门
  • 高级技巧
前端面试之道
  • 打包工具

    • Webpack
    • Rollup
  • TypeScript

    • TypeScript基础
    • TypeScript类型挑战
  • CSS预编译器

    • SASS
  • 自动化测试

    • Vue应用测试
  • Vue2.0源码分析
  • Vue3.0源码分析
  • 数据结构和算法(基础)
  • LeetCode(刷题)
  • JavaScript书籍

    • 你不知道的JavaScript(上)
    • 你不知道的JavaScript(中下)
    • JavaScript数据结构和算法
    • JavaScript设计模式与开发实践
    • 深入理解ES6
  • Git书籍

    • 精通Git
Github
  • 介绍

    • 介绍和参考
  • 源码目录设计和架构设计

    • 设计
  • Rollup构建版本

    • Rollup基础知识
    • Vue中的Rollup构建
  • 从入口到构造函数整体流程

    • 整体流程
    • initGlobalAPI流程
    • initMixin流程
    • stateMixin流程
    • eventsMixin流程
    • lifecycleMixin流程
    • renderMixin流程
  • 响应式原理

    • 介绍
    • 前置核心概念
    • props处理
    • methods处理
    • data处理
    • computed处理
    • watch处理
    • 深入响应式原理
    • 依赖收集
    • 派发更新
    • nextTick实现原理
    • 变化侦测注意事项
    • 变化侦测API实现
  • 虚拟DOM和VNode

    • 虚拟DOM
    • VNode介绍
    • Diff算法
  • 组件化

    • 介绍
    • $mount方法
    • render和renderProxy
    • createElement
    • createComponent
    • 合并策略
    • update和patch
    • 组件生命周期
    • 组件注册
  • 编译原理

    • 介绍
    • compileToFunctions
    • parse模板解析
    • optimize优化
    • codegen代码生成
  • 扩展

    • 扩展
    • directive指令
    • filter过滤器
    • event事件处理
    • v-model
    • 插槽
    • Keep-Alive
    • Transition
    • Transition-Group
    • Vue.use插件机制
  • Vue-Router

    • 介绍
    • 路由安装
    • matcher介绍
    • 路由切换
    • 内置组件
    • 路由hooks钩子函数
  • Vuex

    • 介绍
    • Vuex安装
    • Vuex初始化
    • Vuex辅助API
    • Store实例API

变化侦测API实现

在上一节中,我们分析了变化侦测一些问题,在这一节中我们来分析一下为了解决这些问题,Vue.js是如何实现相关API的。

Vue.set实现

Vue.set和vm.$set引用的是用一个set方法,其中set方法被定义在observer/index.js文件中:

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
}

在代码分析之前,我们来回顾一下Vue.set或者vm.$set的用法:

export default {
  data () {
    return {
      obj: {
        a: 'a'
      },
      arr: []
    }
  },
  created () {
    // 添加对象新属性
    this.$set(this.obj, 'b', 'b')
    console.log(this.obj.b) // b

    // 往数组中添加新元素
    this.$set(this.arr, 0, 'AAA')
    console.log(this.arr[0]) // AAA

    // 通过索引修改数组元素
    this.$set(this.arr, 0, 'BBB')
    console.log(this.arr[0]) // BBB
  }
}

代码分析:

  • set方法首先对传入的target参数进行了校验,其中isUndef判断是否为undefined,isPrimitive判断是否为JavaScript原始值,如果满足其中一个条件则在开发环境下提示错误信息。
export default {
  created () {
    // 提示错误
    this.$set(undefined, 'a', 'a')
    this.$set(1, 'a', 'a')
    this.$set('1', 'a', 'a')
    this.$set(true, 'a', 'a')
  }
}
  • 随后通过Array.isArray()方法判断了target是否为数组,如果是再通过isValidArrayIndex判断是否为合法的数组索引,如果都满足则会使用变异splice方法往数组中指定位置设置值。其中,还重新设置了数组的length属性,这样做是因为我们传入的索引可能比现有数组的length还要大。

  • 接着判断是否为对象,并且当前key是否已经在这个对象上,如果已经存在,则我们只需要进行重新复制即可。

  • 最后,通过defineReactive方法在响应式对象上面新增一个属性,defineReactive方法已经在之前介绍过,这里不再累述。在defineReactive执行完毕后,马上进行派发更新,通知响应式数据的依赖立即更新,可以说以下两段代码是set方法核心中的核心:

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

Vue.delete实现

解决完新增属性的问题后,我们来解决以下删除属性的情况,Vue.delete和vm.$delete使用的是同一个delete方法,它被定义在observer/index.js文件中:

export function del (target: Array<any> | Object, key: any) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  ob.dep.notify()
}

在代码分析之前,我们来回顾以下Vue.delete或者vm.$delete的用法:

export default {
  data () {
    return {
      obj: {
        a: 'a'
      },
      arr: [1, 2, 3]
    }
  },
  created () {
    // 删除对象属性
    this.$delete(this.obj, 'a')
    console.log(this.obj.a) // undefined
    // 删除数组元素
    this.$delete(this.arr, 1)
    console.log(this.arr)   // [1, 3]
  }
}

代码分析:

  • 首先判断了待删除的target不能为undefined或者原始值,如果是则在开发环境下提示错误。
export default {
  created () {
    // 提示错误
    this.$delete(undefined, 'a')
    this.$delete(1, 'a')
    this.$delete('1', 'a')
    this.$delete(true, 'a')
  }
}
  • 随后通过Array.isArray()方法判断了target是否为数组,如果是再通过isValidArrayIndex判断是否为合法的数组索引,如果都满足则会使用变异splice方法删除指定位置的元素。
  • 接着判断当前要删除的属性是否在target对象中,如果不在则直接返回,什么都不做。
  • 最后,通过delete操作符删除对象上的属性,然后ob.dep.notify()进行派发更新,通知响应式对象上的依赖进行更新。

Vue.observable实现

Vue.observable是在Vue2.6+版本才会有的一个全局方法,它的作用是让一个对象变成响应式:

const obj = {
  a: 1,
  b: 2
}
const observeObj = Vue.observable(obj)
console.log(observeObj.a) // 触发getter

observeObj.b = 22 // 触发setter

这个全局方法是在initGlobalAPI的过程中被定义的,initGlobalAPI我们在之前已经介绍过,这里不在累述:

export default function initGlobalAPI (Vue) {
  // ...
  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }
  // ...
}

我们可以看到observable的实现很简单,在方法内部仅仅只是调用了observe方法,然后返回这个obj。关于observe的代码实现,我们在之前的章节中已经介绍过了,这里不再过多的进行说明:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
最后更新时间: 2025/5/6 15:36
贡献者: wangtunan
Prev
变化侦测注意事项