Vue.js技术内幕 9-11 响应式
响应式原理
- Vue 响应式 是一种机制,可以声明式地描述数据(data, computed, ref 等),当这些数据发生变化时,Vue 会自动检测到变化,并自动更新所有依赖这些数据的地方(如 DOM、计算属性、侦听器等)。即 修改数据,视图自动更新。
- 底层原理:读取数据时进行依赖收集 + 修改数据时进行派发更新。
Vue2响应式实现
-
Vue2使用Object.defineProperty API劫持数据的变化,在数据被访问时收集依赖,然后在数据被修改时通知依赖更新。
-
工作流程
- 初始化/数据观测
- 遍历data函数返回对象的所有属性
- 对每个属性,使用Object.defineProperty重新定义,将其转换为getter/setter
- 对于对象和数组,会递归遍历实现响应式
- 依赖收集
- 在模板编译或渲染函数执行过程中,会读取数据
- 读取数据会调用getter,Vue会讲当前的渲染器或计算属性(统称为Watcher)收集为这个属性的依赖,并保存起来,方便后续数据变动时通知这些依赖。
- 派发更新
- 修改数据时调用setter,Vue会触发setter
- setter会通知之前收集的所有依赖(Watcher)进行更新
- 这些Watcher会重新执行,从而更新视图
- Vue2响应式局限性
- 无法检测属性的添加和删除:由于Object.defineProperty是在初始化时遍历属性进行响应式处理的,后期新增的属性无法被响应式。(必须使用Vue.set或this.$set)
- 对数组的限制:无法拦截数组的索引设置和修改长度的操作。Vue2通过重写数组的7个方法实现响应式(push、pop、shift、unshift、splice、sort、reverse)
- 性能:初始化时需要递归遍历整个对象,对于大型对象,性能开销较大。
Vue3响应式实现
- Vue3使用Proxy实现响应式
- 工作流程
- 初始化
- 使用new Proxy(target, handler)包装原始对象
- handler对象定义了get、set、deleteProperty等
- 依赖收集
- 当读取对象的任意属性时,会触发get函数,get函数的核心是执行track函数收集依赖。
- 每次执行track函数,就会把当前激活的副作用函数activeEffect作为依赖,然后将其收集到与target相关的depsMap所对应key下的依赖集合dep中。
- 收集依赖的目的是实现响应式对象,即当数据变化时自动做一些事情,如执行某些函数。因此,收集的依赖就是数据变化后执行的副作用函数。
- 派发更新
- 当修改、添加或删除对象的任意属性时,会触发set函数,set函数的核心是执行trigger函数派发通知。
- 每次执行trigger函数,就是根据target和key从targetMap中找到所有相关的副作用函数并遍历执行—遍。
- trigger函数做的事情:
- 从targetMap中获取target对应的依赖集合depsMap;
- 创建运行的effects集合;
- 根据key从从depsMap中找到对应的effects并添加到effects集合中;
- 遍历effects执行相关的副作用函数。
-
Vue3响应式优势
- 完美的数组支持,可以拦截数组索引的修改和length的变化
- 监测属性的增删操作,可以拦截新增属性和删除属性的操作,不再需要Vue.set/Vue.delete
- 更优的性能
- 惰性递归,只有在访问深层对象属性时才会将其转换为响应式,减少了初始化开销
- 更好的内存管理,使用WeakMap等数据结构,依赖关系更清晰
- 支持更多数据结构,原生支持Map,Set,WeakMap,WeakSet
-
Vue3中的响应式API
- ref(value),接受一个内部值,返回一个响应式的、可更改的ref对象。通过.value属性访问其值。
- reactive(object),返回一个响应式代理Proxy。
- computed(getter),接受一个getter,返回一个只读的响应式ref对象。
- watch(effect, callback),侦听一个或多个响应式数据源,并在其变化时调用一个回调函数。
- watchEffect(effect),立即执行一个函数,并自动追踪其依赖,并在依赖变更时重新运行它。
Vue 计算属性
-
计算属性,定义一个计算方法,然后根据一些依赖的响应式数据计算出新值并返回。当依赖发生变化时,计算属性会自动重新计算并获取新值。若依赖未发生变化,则使用缓存的计算属性值。
-
计算属性特点
- 1.延时计算,访问计算属性时才执行computed getter函数计算
- 2.缓存,缓存上一次的计算结果,依赖值发生变化时才会重新计算
-
计算属性运行机制
-
computed内部有两个变量,_dirty,用来判断是否需要重新计算,_value,表示计算属性每次计算后的结果。
-
首先,执行trackRefValue,对计算属性本身做依赖收集。
-
然后判断dirty属性,接着执行计算属性内部effect对象的run函数,并进一步执行computed getter,即count.value+1
const count = ref(1) // 1. 直接传入getter const plusOne = computed(() => count.value + 1) // 2. 修改computed的返回值,需传入一个对象 const plusTwo = computed({ get: () => count.value + 1, set: val => { count.value = val - 1 } })
-
侦听器Watch
-
侦听器,用于观测响应式数据的变化,然后自动执行某些逻辑,并且它的执行时机也有多种,可以同步执行,在渲染前执行,也可以在渲染后执行。即使侦听器观测的响应式数据在同一个Tick内多次被修改,在非同步的情况下,它的回调函数也只会执行一次。
-
watch 实现原理
-
标准化source,source可以是getter函数、响应式对象、响应式对象数组,所以需要标准化传入的参数
-
创建job,cb(newVal, oldVal, onInvalidate)。侦听一个值的变化,值变了则执行回调函数,回调函数里可以访问新值和旧值。在内部创建—个job,它是对回调函数做的一层封装,维护新值旧值的计算和存储,以及是否要执行回调函数,当侦听的值发生变化时就会执行job。
-
创建scheduler,根据某种调度的方式去执行某种函数。在watch API中,主要影响到的是回调函数的执行方式。
-
创建effect
-
返回销毁函数,销毁函数内部会执行effect.stop函数让effect失活,并清理effect的相关依赖,这样就可以停止对数据的侦听。同时,如果是在组件中注册的watcher也会移除组件effects对这个effect的引用。
// 1. watch可以侦听一个getter函数,必须返回一个响应式对象 watch(()=> state.count, (count, prevCount) => { }) // 2. watch侦听一个响应式对象,当响应式对象更新后,会执行对应的回调函数 watch(count, (count, prevCount) => { }) // 3. watch 侦听多个响应式对象,任意一个响应式对象更新后,都会执行对应的回调函数 watch([count1, count2], ([count1, count2], [prevCount1, prevCount2]) => { })
-
-
watchEffect,作用是注册—个副作用函数,副作用函数内部可以访问响应式对象,当内部响应式对象变化,立即执行这个函数。
watchEffect(() => console.log(count.value))
-
watch VS watchEffect
- (1)侦听的源不同。watch API可以侦听一个或多个响应式对象,也可以侦听一个getter函数,而watchEffect API侦听的是一个普通函数,只要内部访问了响应式对象即可,这个函数并不需要返回响应式对象。
- (2)没有回调函数。watchEffect API没有回调函数,在副作用函数的内部,响应式对象发生变化后,会再次执行这个副作用函数。
- (3)立即执行。watchEffect API在创建好watcher后,会立刻执行它的副作用函数,而watch API需要配置immediate为true,才会立即执行回调函数。
参考&感谢各路大神
- [Vue.js技术内幕-黄轶]