​Vue2响应式原理及defineProperty无法检测增删属性、数组方法改变元素问题的解决

目录

与Vue3区别

使用

Vue2 : data(){ return { data} }

Vue3 : ref (initVal)和 reactive(initObj)

Vue2: Observer标记响应式+监听,Dep管理Watcher 依赖

Vue3:track 收集依赖,trigger 触发更新【WeakMap,Map,Set 来实现】 

类型推导:TypeScript

vue2:文档注释来指定类型type:

vue3:script setup lang脚本语言="ts"

defineProperty 无法监听:vue3中的proxy已解决

删除属性

解决:Vue.delete/this.$delete触发更新【vue3 不再提供】

修改属性

通用:Vue.set/this.$set将属性变为响应式

数组方法

过程:

1.初始化

initProps():父组件传的 props 列表,proxy() 把属性代理到当前实例上

vm._props.xx 变成 vm.xx

initData():判断data和props、methods是否重名,proxy() 把属性代理到当前实例上

this.xx

Observe:标记响应式,监听

observe():递归监听,变成响应式对象(除了vnode/非引用类型)

defineReactive():劫持对象属性的 getter 和 getter

 2.依赖收集

1.挂载前 生成一个组件渲染watcher

2. Dep.target 赋值为当前渲染 watcher 并压入栈targetStack(为了恢复用)

3.vm._render() 生成并渲染 vnode

4.访问数据,触发getter,dep收集watcher

5.更新数据,触发setter,遍历通知所有watcher更新

Dep类:管理Watcher

subs: Array

static target: ?Watcher:全局的 Watcher,同一时间只能存在一个全局的 Watcher

Watcher类:依赖/观察者/订阅者

watcher.run() :执行回调,传旧值新值

新旧Dep实例数组

3.派发/异步更新

queueWatcher()优化:watcher队列,nextTick后执行

依赖的渲染结果:父watcher在子前,user watcher在渲染watcher前

参考链接


Vue3区别

使用

Vue2 : data(){ return { data} }

Vue3 : ref (initVal)reactive(initObj)

<script setup>
  import { ref, reactive, toRefs } from "vue"
  const name = ref('参宿')
  const state = reactive({
    name: 'Jerry',
    sex: '男'
  })
</script>

Vue2: Observer标记响应式+监听Dep管理Watcher 依赖

Vue3:track 收集依赖trigger 触发更新【WeakMap,Map,Set 来实现 

类型推导:TypeScript

由于Proxy是基于原生的Proxy对象实现的,所以可以更好地支持TypeScript等静态类型检查工具,提供更准确的类型推导和代码提示。

vue2:文档注释来指定类型type:

<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  props: {
    message: {
      type: String, // 指定属性的类型
      required: true, // 属性是否必须
      default: 'Hello, Vue 2', // 默认值
    },
  },
};
</script>

vue3:script setup lang脚本语言="ts"

<script setup lang="ts">

</script>

defineProperty 无法监听:vue3中的proxy已解决

以下this为vue实例vm

删除属性

解决:Vue.delete/this.$delete触发更新【vue3 不再提供】

Vue.delete(object, propertyName);
this.$delete(object, propertyName);

等同于

delete删除属性后,手动调用Vue实例的$forceUpdate()触发更新

delete this.myObject.myProperty;
this.$forceUpdate();

修改属性

Vue2 不能检测直接修改属性:

如 this.showMoreArr[idx] = false;

如 this.showMoreArr.length = newLength;

因为 直接修改 的 内部结构,而不是通过 对象属性的 赋值触发的

通用:Vue.set/this.$set将属性变为响应式

Vue.set(object, propertyName, value);
this.$set(object, propertyName, value);

this.$set(this.showMoreArr, idx, false);

数组方法

Vue2 可以自动检测以下数组方法的调用,并更新视图:

  1. push():向数组的末尾添加一个或多个元素,并返回新的长度。
  2. pop():删除数组的最后一个元素,并返回这个元素。
  3. shift():删除数组的第一个元素,并返回这个元素。
  4. unshift():向数组的开头添加一个或多个元素,并返回新的长度。
  5. splice():通过删除现有元素和/或添加新元素来更改数组的内容。
  6. sort():对数组的元素进行排序,并返回数组。
  7. reverse():颠倒数组中元素的顺序。

如this.showMoreArr.splice(idx, 1, false);  // 移除位于 idx 的元素,并添加新值 false

等同于

重写数组方法,vm.$forceUpdate()手动响应式处理

// 在Vue组件中重写数组的方法
methods: {
  customPush(item) {
    // 保存原始的 push 方法
    const originalPush = Array.prototype.push;

    // 重写 push 方法
    Array.prototype.push = function (...args) {
      // 调用原始的 push 方法
      originalPush.apply(this, args);

      // 手动触发响应式更新
      this.$forceUpdate();
    };

    // 添加新元素
    this.array.push(item);

    // 恢复原始的 push 方法
    Array.prototype.push = originalPush;
  }
}

过程:

  • 先初始化一个 dep 实例
  • 如果是对象就调用 observe,递归监听,以保证不管结构嵌套多深,都能变成响应式对象
  • 然后调用 Object.defineProperty() 劫持对象属性的 getter 和 getter
  • 如果获取时,触发 getter 会调用 dep.depend()观察者 push 到依赖的数组 subs 里去,也就是依赖收集
  • 如果更新时,触发 setter 会做以下操作
    • 新值没有变化或者没有 setter 属性的直接跳出
    • 如果新值是对象就调用 observe() 递归监听
    • 通过对应的所有依赖(Watcher),然后调用 dep.notify() 派发更新

1.初始化

在 new Vue 初始化的时候,会对组件的数据 props 和 data 进行初始化

export function initMixin (Vue: Class<Component>) {
  // 在原型上添加 _init 方法
  Vue.prototype._init = function (options?: Object) {
    ...
    vm._self = vm
    initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
    initEvents(vm) // 初始化事件:$on, $off, $emit, $once
    initRender(vm) // 初始化渲染: render, mixin(混入,data,methods...)

    callHook(vm, 'beforeCreate') // 调用生命周期钩子函数

    initInjections(vm) // 初始化 inject(子孙传值)
    initState(vm) // 初始化组件数据:props, data, methods, watch, computed
    initProvide(vm) // 初始化 provide

    callHook(vm, 'created') // 调用生命周期钩子函数
    ...
  }
}

命名前缀

$:公共属性

_:私有属性

响应式数据相关: initProps()initData()observe()

initProps():父组件传的 props 列表,proxy() 把属性代理到当前实例

vm._props.xx 变成 vm.xx

  • 遍历父组件传进来的 props 列表
  • 校验每个属性的命名、类型、default 属性等,都没有问题就调用 defineReactive 设置成响应式
  • 然后用 proxy() 把属性代理到当前实例上,如把 vm._props.xx 变成 vm.xx,就可以访问
// 把不在默认 vm 上的属性,代理到实例上
// 可以让 vm._props.xx 通过 vm.xx 访问
if (!(key in vm)) {
proxy(vm, _props, key)
}
//vue自定义的proxy 函数,将 key 代理到组件实例上。这意味着你可以直接通过 vm.key 访问这个属性,而不必使用 vm._props.key

区别于js中的new Proxy(target, handler)

initData():判断data和props、methods是否重名,proxy() 把属性代理到当前实例

this.xx

  • 初始化一个 data,并拿到 keys 集合
  • 遍历 keys 集合,来判断有没有和 props 里的属性名或者 methods 里的方法名重名的
  • 没有问题就通过 proxy() 把 data 里的每一个属性都代理到当前实例上,就可以通过 this.xx 访问了
  • 最后再调用 observe 监听整个 data
if (!isReserved(key)) {
      // 都不重名的情况下,代理到 vm 上
      // 可以让 vm._data.xx 通过 vm.xx 访问
      proxy(vm, `_data`, key)

Observe:标记响应式,监听

observe():递归监听,变成响应式对象(除了vnode/非引用类型)

  • 如果是 vnode 的对象类型或者不是引用类型,就直接跳出
  • 否则就给没有添加 Observer 的数据添加一个 Observer,也就是监听者

Virtual DOM节点(vnode)vnode对象通常用于表示虚拟DOM树的节点,而不是真实的数据对象。这些节点描述了组件的结构,而不是数据的值。

Vue的响应式系统是建立在对象的引用类型上(如Object、Array)

而基本数据类型(如Number、String、Boolean)或null等,它们是不可变的,无法被Vue追踪到变化

defineReactive():劫持对象属性的 getter 和 getter

  var obj = {};  //定义一个空对象
    Object.defineProperty(obj, 'val', {//定义要修改对象的属性
        get: function () {
            console.log('获取对象的值')
        },
        set: function (newVal) { 
            console.log('设置对象的值:最新的值是'+newVal);
        }
    });
    obj.hello = 'hello world'

 2.依赖收集

1.挂载前 生成一个组件渲染watcher

渲染watcher掌管当前组件的视图更新

2. Dep.target 赋值为当前渲染 watcher 并压入栈targetStack(为了恢复用)

3.vm._render() 生成并渲染 vnode

vm 实例,也就是平常用的 this

4.访问数据,触发getter,dep收集watcher

5.更新数据,触发setter,遍历通知所有watcher更新

 每个响应式数据都有一个Dep来管理它的一个/多个依赖

Dep类:管理Watcher

subs: Array<Watcher>

static target: ?Watcher:全局的 Watcher,同一时间只能存在一个全局的 Watcher

dep.target 的作用是建立依赖关系和追踪数据的Watcher

因为更新异步的特性,如果同时有多个全局 Watcher 在同一时间被触发,可能导致不可预测的结果,甚至可能引发性能问题。

通过在全局只维护一个 dep.target,Vue 确保在任何时刻只有一个 Watcher 在执行更新操作,避免了潜在的竞争条件和性能问题。

  1. Watcher 对象被创建:当你创建一个 Watcher 对象,它会将自身设置为当前的 dep.target。这是因为该 Watcher 正在计算或依赖于响应式数据,因此需要建立依赖关系。

  2. 在计算属性的求值过程中:如果你有一个计算属性(computed),当该计算属性的值被求值时,Vue 会将当前的 dep.target 设置为该计算属性的 Watcher,以建立依赖关系。

  3. 在渲染过程中:当组件渲染时,Vue 会创建一个渲染组件的 Watcher,该 Watcher 负责渲染组件的模板。在渲染过程中,当前的 dep.target 会被设置为渲染 Watcher,以确保建立正确的依赖关系。

let uid = 0
export default class Dep {
  static target: ?Watcher;//可选属性可以不存在或者是 null 或 undefined
  subs: Array<Watcher>;
  id: number;
  constructor () {
    this.id = uid++//确保每个 Dep 实例具有唯一的标识符
    this.subs = []
  }
  ...
  depend () {
    if (Dep.target) {
      // 调用 Watcher 的 addDep 函数
      Dep.target.addDep(this)
    }
  }
  // 派发更新
  notify () {
    ...
  }
}
// 同一时间只有一个观察者使用,赋值观察者
Dep.target = null
const targetStack = []//管理当前活动的观察者的栈

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Watcher类:依赖/观察者/订阅者

watcher.run() :执行回调,传旧值新值

新旧Dep实例数组

let uid = 0
export default class Watcher {
  ...
  constructor (
    vm: Component,
    ...
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // Watcher 实例持有的 Dep 实例的数组
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    ...
  }
  get () 
    // 该函数用于缓存 Watcher
    // 因为在组件含有嵌套组件的情况下,需要恢复父组件的 Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用回调函数,也就是upcateComponent,对需要双向绑定的对象求值,从而触发依赖收集
      value = this.getter.call(vm, vm)
    } catch (e) {
      ...
    } finally {
      // 深度监听
      if (this.deep) {
        traverse(value)
      }
      // 恢复Watcher
      popTarget()
      // 清理不需要了的依赖
      this.cleanupDeps()
    }
    return value
  }
  // 依赖收集时调用
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // 把当前 Watcher push 进数组
        dep.addSub(this)
      }
    }
  }
  // 清理不需要的依赖(下面有)
  cleanupDeps () {
    ...
  }
  // 派发更新时调用(下面有)
  update () {
    ...
  }
  // 执行 watcher 的回调
  run () {
    ...
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

3.派发/异步更新

queueWatcher()优化:watcher队列,nextTick后执行

优化:在每次数据改变的时候不会都触发 watcher 回调,而是把这些 watcher 都添加到一个队列里,然后在 nextTick 后才执行(下次 DOM 更新循环结束之后,执行延迟回调,就可以拿到更新后的 DOM 相关信息)

依赖的渲染结果:父watcher在子前,user watcher在渲染watcher前

 1.如果同一个 watcher 被多次触发,只会被推入到更新队列中一次,可以避免重复修改相同的dom,这种去除重复数据,对于避免不必要的计算和 DOM 操作是非常重要的

2.同步任务执行完毕

3.开始执行异步 watcher 队列的任务,一次性更新 DOM

// 定义watcher类
class Watcher {
  update() {
    // 放到watcher队列中,异步更新
    queueWatcher(this);
  }
  // 触发更新
  run() {
    this.get();
  }
}

// 队列中添加watcher
function queueWatcher(watcher) {
  const id = watcher.id;
  // 先判断watcher是否存在 去掉重复的watcher
  if (!has[id]) {
    queue.push(watcher);
    has[id] = true;
    if (!pending) {
      pending = true;
      // 使用异步更新watcher
      nextTick(flushSchedulerQueue);
    }
  }
}

let queue = []; // 定义watcher队列
let has = {}; // 使用对象来保存id,进行去重操作
let pending = false; // 如果异步队列正在执行,将不会再次执行

// 执行watcher队列的任务
function flushSchedulerQueue() {
  queue.forEach((watcher) => {
    watcher.run();
    if (watcher.options.render) {
      // 在更新之后执行对应的回调: 这里是updated钩子函数
      watcher.cb();
    }
  });
  // 执行完成后清空队列 重置pending状态
  queue = [];
  has = {};
  pending = false;
}

参考链接

深入浅出 Vue 响应式原理源码剖析 - 掘金

纯干货!图解Vue响应式原理 - 掘金

​Vue3响应式原理-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值