一、Vue2的响应式原理
Vue2.js主要是运用Object.defineProperty()方法直接在一个对象的具体属性上通过设置get和set,来拦截对象属性的get和set属性。
1.1 相关源码分析
const dep = new Dep() //依赖管理器
export function defineReactive(obj, key, val){
val = obj[key] //计算出对应的key的值
observe(val)
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){ //触发依赖收集
If(Dep.target){ //之前赋值的当前watcher实例
dep.addDep(Dep.target) //收集起来 放入上面的dep依赖管理器内
。。。
}
},
set(newVal){ //派发更新
If(newVal === val){ //相同
return;
}
val = newVal // 赋值
observe(val); // 若新值变成响应式的
dep.notify() //通知更新
})
}
分析:利用observe()方法将数据编程响应式数据,observe()为Observer类的工厂方法,最后一句为return new Observer(value)。执行new Observer(value)将传入的对象挂载到当前的this下,然后再利用walk()进行遍历,递归其对象的每一项,对每一项依次传入defineReactive()方法中执行,使数据都变为响应式数据。
收集依赖:利用get属性,将依赖项都存入Dep依赖管理器中。
派发更新:利用set属性,若数据重新赋值,需要用observe()方法将新的数据变为响应式,同时触发依赖管理器dep.notify(),即对dep中数组收集起来的watcher挨个触发update(),发出视图更新通知。
let uid = 0
class Dep{
constructor() {
this.id = uid++
this.subs = []
}
notify() { // 通知
const subs = this.subs.slice()
for(let i = 0, i < subs.length; i++) {
subs[i].update() // 挨个触发watcher的update方法
}
}
}
利用update()将watcher实例传入queueWatcher()方法内,即收集到一个队列中。
class Watcher{
...
update() {
queueWatcher(this)
}
}
注意:若同一个watcher内触发了多次更新,只会更新一次对应的watcher。
function queueWatcher(watcher) {
const id = watcher.id
if(has[id] == null) { // 如果某个watcher没有被推入队列
...
has[id] = true // 已经推入
queue.push(watcher) // 推入到队列
}
...
nextTick(flushSchedulerQueue) // 下一个tick更新(是this.$nextTick方法的原始方法)
}
派发更新的粒度上组件级别的,如何高效更新试图是Diff算法及其之后的操作。
因为watcher有before属性,执行传入的before方法触发beforeUpdate()钩子,执行watcher.run(),执行执行getAndInvoke(),执行this.get(),再执行一次vm._update(vm._render()),vm._render()方法利用creatElement()生成虚拟节点,vm._update()里面执行patch()方法进行Diff算法,新旧虚拟节点进行对比,生成真实节点,更新视图。
2.Object.defineProperty()存在的问题
1. 只能拦截对象属性的get和set操作,无法拦截delete、in、方法调用等操作。
2. 一次只能对一个属性实现数据挟持,需要遍历所有属性进行挟持。
3. 无法响应式处理新增属性和删除属性。需使用 this.$set()
设置新属性,使用 this.$delete()
删除属性。
4. 无法监听数组下标的变化。
5. 对于数组而言,大部分操作都拦截不到,需要对Array原型支持的方法进行对应改写。
6. 直接对原对象进行直接改写属性监听。
二、Vue3的响应式原理
利用Proxy实现数据挟持,创建一个对象的代理,实现基本操作的拦截和自定义。本质上是通过拦截对象的内部方法的执行实现代理。
Vue3中提供了reactive()和ref()两个方法将目标数据变为响应式数据。
利用Proxy实现数据劫持的优势:
1. 解决了使用Object.defineProperty()继续数据劫持的缺点,完美监听任何方式导致的数据改变行为。
2. 对于整个拦截对象直接进行挟持,无需遍历属性依次添加定义get和set属性。
3. 对于原对象生成拦截对象,然后对拦截对象进行相应监听行为,确保原对象不变。
2.1 相关源码分析
从源码看,核心是creatReactiveObject()函数
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 若目标对象是响应式的只读数据,则直接返回
if (isReadonly(target)) {
return target
}
// 否则将目标数据尝试变成响应式数据
return createReactiveObject(
target,
false,
mutableHandlers, // 对象类型的 handlers
mutableCollectionHandlers, // 集合类型的 handlers
reactiveMap
)
}
creatReactiveObject()函数内为一些判断处理
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 非对象类型直接返回
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 目标数据的 __v_raw 属性若为 true,且是【非响应式数据】或 不是通过调用 readonly() 方法,则直接返回
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 目标对象已存在相应的 proxy 代理对象,则直接返回
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 只有在白名单中的值类型才可以被代理监测,否则直接返回
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 创建代理对象
const proxy = new Proxy(
target,
// 若目标对象是集合类型(Set、Map)则使用集合类型对应的捕获器,否则使用基础捕获器
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// 将对应的代理对象存储在 proxyMap 中
proxyMap.set(target, proxy)
return proxy
}
具体的实现又在不同数据类型的捕获器中,即collectionHandlers和baseHandles,其对于于reactive()函数中的createReactiveObject()函数传递的mutableCollectionHandlers和mutableHandlers。
学习参考:听说你很了解 Vue3 响应式?