Vue数据改变为什么视图不更新(Object.defineProperty && proxy Reflect)

260 阅读3分钟

Vue是一种用于构建用户界面的渐进式框架,通过双向绑定的机制,实现了数据的响应式更新。但是,有时候我们会遇到数据改变了,但是视图却没有及时更新的情况。

1. 数据响应式机制 Object.defineProperty && proxy Reflect

Object.defineProperty

Object.defineProperty(obj, prop, descriptor)方法会直接在一个对象上定义一个新属性,或修改一个对象现有属性,并返回此对象,其参数具体为:

  • obj:要定义属性的对象
  • prop:要定义或修改的属性名称symbol
  • descriptor:要定义或修改的属性描述符

上述可以看出一些限制,比如:目标是对象属性,不是整个对象;一次只能定义或修改一个属性

Vue的数据响应式机制是通过数据劫持来实现的。当我们将一个普通的JavaScript对象传给Vue实例的data选项时,Vue会将其转换为响应式对象。Vue会遍历对象的属性,并对每个属性使用Object.defineProperty方法设置getter和setter来劫持数据的读取与修改。当修改数据时,Vue会通过setter方法捕获数据的变化,并自动触发视图的更新。

Object.defineProperty实际是通过定义修改对象属性来实现数据劫持,缺点也无法被忽略:

  • 只能拦截对象属性的getset操作,比如无法拦截delete、in、方法调用等操作

  • 动态添加新属性(响应式丢失)

    • 保证后续使用的属性要在初始化声明data时进行定义
    • 使用this.$set设置新属性
  • 通过delete删除属性(响应式丢失)

    • 使用this.$delete删除属性
  • 使用数组索引替换/新增元素(响应式丢失)

    • 使用this.$set设置新元素
  • 使用数组push、pop、shift、unshift、splice、sort、reverse原生方法改变原数组时(响应式丢失)

    • 使用重写/增强后的push、pop、shift、unshift、splice、sort、reverse方法
  • 一次只能对一个属性实现数据劫持,需要遍历对所有属性进行劫持

    • vue不能提前知道用户传入的对象都有什么属性,经过类似Object.keys() + for循环的方式获得所有的key -> value
  • 数据结构复杂时(属性值为引用类型数据),需要通过递归进行处理

Proxy & Reflect

由于在vue2中使用Object.defineProperty带来的缺陷,导致在vue2中不得不提供了一些额外的方法(如:Vue.set、Vue.delete)解决问题,而在vue3中使用了proxy的方式来实现数据劫持,而上述的缺陷在Proxy中都可以得到解决。

Proxy

Proxy主要是创建了一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

  • new Proxy(target, handler) 是针对整个对象进行的代理,而不是某个属性
  • 代理对象拥有读取、修改、删除、新增、是否存在属性等操作相应的捕捉器
    • get()属性读取操作的捕捉器
    • set()属性设置操作的捕捉器
    • deleteProperty()delete操作符的捕捉器
    • ownKeys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()的捕捉器
    • has()in操作符的捕捉器
Reflect

Reflect是一个内置的对象,它提供拦截JavaScript操作的方法,这些方法与Proxy Handler提供的方法是一一对应的,其Reflect不是一个函数对象,即不能进行实例化,其所有属性和方法都是静态的。

  • Reflect.get(target, propertyKey[,receiver])获取对象身上某个属性的值,类似于target[name]
  • Reflect.set(target, propertyKey, value[,receiver])将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true
  • Reflect.deleteProperty(target, propertyKey)作为函数delete操作符,相当于执行delete target[name]
  • Reflect.ownKeys(targer)返回一个包含所有自身属性(不包含继承属性)的数组。(类似于Object.keys(),但不会受enumerable影响)
  • Reflect.has(target, propertyKey)判断一个对象是否存在某个属性,和in运算符的功能完全相同

Proxy为什么需要Reflect呢?

Proxyget(target, key, receiver)、set(target, key, newVal, receiver)的捕获器中都能接到前面所列举的参数:

  • target 指的是原始数据对象
  • key指的是当前操作的属性名
  • newVal指的是当前操作接受到的最新值
  • receiver指向的是当前操作 正确的上下文

2. 异步更新队列

Vue的视图更新是异步执行的,Vue会将一个事件循环中的所有数据更新操作都添加到一个队列中,然后在下一个循环'tick'时,批量执行队列中的更新操作。这样做的好处是可以避免不必要的视图更新操作,提高性能

3. 异步更新引发的问题

由于Vue的视图更新是异步执行的,在某些情况下可能会导致数据改变了,但是视图没有及时更新的问题。常见的情况包括:

  • 监听数组元素的变化时,直接修改数组某个索引位置的元素,Vue无法检测到改变。解决方法是使用Vue提供的变异方法(如splice)或者重新赋值一个新的数组。
  • 监听对象属性的变化时,直接给对象赋值一个新的属性,Vue无法检测到改变。解决方法是使用Vue提供的Vue.set或者this.$set方法来新增属性
  • 在Vue生命周期钩子函数中改变数据,可能会导致视图更新延迟。解决方法是使用Vue.nextTick方法或者使用Vue的异步更新方法$nextTick来确保在 DOM 更新完毕后再执行相关操作。

4. 强制更新视图

如果希望在数据改变后立即更新视图,可以使用Vue提供的$forceUpdate方法。这个方法会强制触发视图的重新渲染,但是会跳过所有优化,因此使用时需要谨慎。

总结:

Vue的数据响应式机制是通过数据劫持来实现的,通过异步更新队列批量执行视图更新操作。但是在某些情况下,数据改变后视图没有及时更新,可以通过避免直接修改数组或对象的方式,使用Vue提供的变异方法或者设置新的属性来解决,也可以使用Vue的异步更新方法$nextTick来确保在DOM更新完毕后再执行相关操作。如果需要立即更新视图,可以使用Vue的$forceUpdate方法。

无论是vue2还是vue3响应式的核心都是数据劫持/代理、依赖收集、依赖更新,只不过由于实现数据劫持方式的差异从而导致具体实现的差异。在vue3中值得注意的是:

  • 普通对象类型可以直接配合proxy提供的捕获器实现响应式
  • 数组类型也可以直接复用大部分和普通对象类型的捕获器,但其对应的查找方法和隐式修改length的方法仍然需要被重写/增强
  • 为了支持集合类型的响应式,也对其对应的方法进行了重写/增强
  • 原始值数据类型 主要通过 ref 函数来进行响应式处理,不过内容不会对 原始值类型 使用 reactive(或 Proxy) 函数来处理,而是在内部自定义 get value(){} 和 set value(){} 的方式实现响应式,毕竟原始值类型的操作无非就是 读取 或 设置,核心还是将 原始值类型 转变为了 普通对象类型
    • ref 函数可实现原始值类型转换为响应式数据,但 ref 接收的值类型并没只限定为原始值类型,若接收到的是引用类型,还是会将其通过 reactive 函数的方式转换为响应式数据