vue3响应式基本原理
在vue3中,当我们通过ref或reactive方法,创建一个响应式对象时,vue会创建传入函数参数的对象的代理对象,来实现响应式。其本质是对其代理对象中的getter,setter等方法添加执行渲染函数的逻辑。当一个响应式变量的值改变,触发页面重新渲染时,我们不能让所有的组件都重新渲染,而是只让依赖于我们整个响应式变量的组件重新渲染,这就依赖于保存当前响应式变量所影响的所有组件的渲染函数的数组,他被叫做依赖数组,依赖数组中包含目前组件实例的渲染函数,当我们对数据进行修改时,本质上是调用其setter方法,而vue通过代理对象拦截了setter方法调用,转而执行代理对象的setter方法,而代理对象的setter方法,除了实现数值的更改外,还要遍历执行依赖数组中的渲染函数,触发页面重新渲染,完成响应式。
vue2的响应式实现和vue3不同:
- 在vue2中响应式是通过defineProperty实现的,与代理对象实现不同的是
- 代理对象可以监听数组,defineProperty不能,所以vue需要单独监听数组,使用数组的方法
- defineProperty只能监听属性,所以需要遍历,代理对象不需要,大大节省性能
- 代理对象可以使用get, set, deleteProperty, has, ownKeys五个拦截器,而defineProperty只有get和set,具有一定的局限性。
vue中的渲染逻辑处理
在vue3的响应式实现中,渲染页面的逻辑被包装在了一个effect方法中,effect方法我们称其为副作用函数。其通过调用componentEffect函数来判断,当前是第一次渲染,还是更新页面。如果是更新页面,则会执行组件的重新渲染逻辑,如果是第一次渲染,则会进行依赖数组的收集,并触发生命周期beforeUpdate。
vue中的依赖数组及依赖收集
想要针对当前响应式变量对页面的影响,完成响应式局部渲染,我们必须知道那些组件使用了当前的响应式变量,然后通过重新调用组件的渲染函数进行响应式,那么我们就需要在渲染函数第一次执行期间,进行依赖收集。
依赖收集的过程十分有趣,首先vue会调用activeEffect函数,而effect函数会作为参数传递,而effect最终会毫无疑问的执行渲染逻辑,完成页面渲染,为什么要通过activeEffect来作为参数接收执行呢?
activeEffect函数内部会将effect的值赋给一个函数外层变量,当然整个变量是公用的,而不独属于某个effect函数,当effect在执行渲染逻辑时,会触发响应式变量的get方法,此时,get方法可以将这个公共函数保存的effect函数的引用保存在当前变量的依赖数组中,以此可以完成响应式变量的依赖数组收集过程。
vue2.0中使用watcher来实现依赖收集以及effect相同的功能。在vue2.0中,有渲染watcher专门负责数据变化后的从新渲染视图。vue3.0改用effect来代替watcher达到同样的效果。
vue3中的映射关系
在一个组件实例中,多个响应式对象的key和其依赖数组的映射关系由Map来进行管理。在vue中,还包括对象和其代理对象的映射关系,其中包括rawtoreactive,reactivetoraw,rawtoreadonly,readonlytoraw,这四个分别是目标对象和代理对象,以及两者反过来代理对象和目标对象,而后两者指的是只读的,其他跟对应关系和前两者一样。这些映射关系对响应式理解以及手写响应式关系不大,不多说。
手写响应式
通过手写响应式代码,可以让我们更好的理解vue3中的响应式过程,以下时响应式的一种简单实现,过程不唯一,可作为参考。
//将对象依赖的渲染effect函数数组定义成一个类,方便为每个对象为自己创建依赖数组
class Depend {
constructor() {
//响应式对象依赖的所有渲染effect函数,使用set储存可以防止重复
this.reactiveFns = new Set()
}
addDepend(fn) {
//添加渲染effect函数
this.reactiveFns.add(fn)
}
depend() {
//watch函数监听当前对象的get方法在哪个渲染effect函数中被调用
//将该渲染effect函数添加进该对象的渲染effect函数依赖
if (reactiveFn) {
this.reactiveFns.add(reactiveFn)
}
}
notify() {
//对象更改时触发响应式,遍历该对象依赖的渲染effect函数并逐一调用
this.reactiveFns.forEach(fn => {
fn()
})
}
}
//从Map对象(映射关系)中获取依赖渲染effect函数的数组
//如果Map对象不存在,则创建Map对象,其内部保存映射关系为:对象 -->> Map对象,
//内部Map对象中也有映射关系:
//Map对象内key -->> 依赖数组(将此数组return出来给代理对象setter使用进行响应式)
//创建最外层映射关系(对象-->Map对象)
const targetMap = new WeakMap()
function getDepends(target, key) {
//获取当前对象对应的Map对象
let objMap = targetMap.get(target)
//如果获取不到Map对象,则创建
if (!objMap) {
//创建Map对象
objMap = new Map()
//向Map对象添加:对象-->Map对象
targetMap.set(target, objMap)
}
//获取当前对象对应的Map对象内部当前key对应的依赖数组
let depend = objMap.get(key)
//如果获取不到依赖数组,则创建一个
if (!depend) {
//创建依赖数组
depend = new Depend()
//向内部Map对象添加:key-->依赖数组
objMap.set(key, depend)
}
//返回依赖数组,供该对象的代理对象的setter使用,完成响应式
return depend
}
//此函数与vue中reactive对象功能一致
function reactive(obj) {
return new Proxy(obj, {
set: function(target, key, value, receiver) {
//从映射关系中获取该对象中key对应的依赖渲染effect函数的数组
//如果没有映射关系,则创建映射关系并获取依赖数组
const dep = getDepends(target, key)
//更改值
Reflect.set(target, key, value, receiver)
//调用依赖数组中所有的渲染effect函数,页面更新
dep.notify()
},
get: function(target, key, receiver) {
//同上
const dep = getDepends(target, key)
//向依赖数组中添加当前key依赖的渲染effect函数
dep.depend()
//获取值
return Reflect.get(target, key, receiver)
}
})
}
//========================================VUE2===========================================
//上面代理对象的方式使vue3,而vue2中使用的是defineProperty
// function reactive(obj) {
// Object.keys(obj).forEach(key => {
// let value = obj[key]
// Object.defineProperty(obj, key, {
// set: function(newValue) {
// value = newValue
// const dep = getDepends(obj, key)
// dep.notify()
// },
// get: function() {
// const dep = getDepends(obj, key)
// dep.depend()
// return value
// }
// })
// })
// return obj
// }
//=======================================================================================
//需要进行监听的对象,使用上面创建的reactive函数包裹(作用与vue中一致)
const obj = reactive({
name: "123",
age: 123
})
//创建函数监听,渲染effect函数放入此监听函数中
//可以将此渲染effect函数自动放入其依赖变量的依赖数组
//reactiveFn保存当前执行的渲染effect函数
let reactiveFn = null
function watchFn(fn) {
//将当前函数放入reactiveFn中
reactiveFn = fn
//执行当前函数,两个作用:
//1.页面需要第一次渲染,所以开始就要调用一次渲染effect函数
//2.通过渲染effect函数执行途中,其内部响应式对象会被调用getter方法
//从而将渲染effect函数添加进响应式对象的依赖数组中
fn()
//渲染effect函数执行完毕,将其清空
reactiveFn = null
}
//watch函数还具有的作用是防止普通函数被添加进依赖
//因为只有watch包裹的,也就是渲染effect函数,才能被添加进该对象的依赖数组
//内部function模拟渲染函数
//渲染函数处理template表达式,访问真实数据,调用代理函数getter方法
watchFn(function() {
console.log("bar:", obj.name)
console.log("bar:", obj.age)
})
//可以发现当函数内部变量修改时,函数自动重新执行,响应式完成
console.log("修改name-----------")
obj.name = "321"
console.log('修改age-----')
obj.age = 321