Vue3 对比 Vue2 的变化
- 在 Vue2 的时候使用
defineProperty来进行数据劫持,通过getter&setter对属性进行重新。因此,性能差(与Vue3对比)。 - 当新增属性和删除属性时,无法监控变化。需要通过
$set&$delete实现。 - 数组不采用
defineProperty来进行劫持(浪费性能、对所有索引进行劫持造成性能浪费)需要对数组进行单独进行处理。
Vue3 中采用 Proxy 来实现响应式数据变化,从而解决上述问题。
reactive & effect
// operation.ts
const TrackOpType {
GET = 'get'
}
const TriggerOpType {
SET = 'set'
}
reactive
- 使用
Proxy&Reflect实现数据响应式 - 参数必须是数组或对象,否则会报警告
- 当传入同一对象时,会返回同一个
proxy - 当传入
proxy对象时,该proxy对象会直接被返回
// reacitve.js
const enum ReactiveFlags = {
IS_REACTIVE = '__v_isReactive'
}
const proxyMap = new WeakMap(); // 缓存 已经被 代理 的数据
function reactive(target) {
if(!isObject(target)) {
// target 如果不是数组或对象,则无法被 代理
return console.warn(`value can not be reactive ${target}`)
}
// 判断 target 是否已经被 代理 过
const exisitingProxy = proxyMap.get(target); //
if(exisitingProxy) {
// 如果被代理过,直接返回被代理过的数据
return exisitingProxy
}
// 判断 target 是否是一个已经被代理过的 proxy
if(target[ReactiveFlags.IS_REACTIVE]){
// 如果 target 是一个已经被代理过的 proxy,直接返回 target
return target
}
const proxy = new Proxy(target, mutableHandler);
// 将被代理的数据放入缓存
poxyMap.set(target,proxy)
return proxy
}
const mutableHandler = {
get(target,key,receiver) {
// 当被代理的对象访问 '__v_isReactive' 属性时,返回true,以判断该对象是否是被代理过的对象
if(key === ReactiveFlags.IS_REACTIVE) {
return true;
}
const res = Refelect(target,key,receiver)
track(target,TrackOpType.GET,key)
// 实现深层 track & trigger
if(isObject(res)) {
reactive(res)
}
return res
},
set(target,key,value,receiver) {
const oldValue = target[key]
const res = Refelect(target,key,value,receiver)
if(oldValue !== value) {
// 当新旧值,不相等时,则执行 trigger 触发副作用
trigger(target,TriggerOpType.SET,value,oldValue)
}
return res
}
}
effect
effect 副作用函数
-
参数必须是一个函数;
-
该函数会被立即执行一次,
-
该函数会被当做参数,创建一个
ReactiveEffct的实例 -
该实例,会记录实例的一些属性
- 是否处于激活状态
- 上一个
activeEffect(Vue3 采用的链表记录,上一个实例是谁,而 Vue2 采用 栈 实现该逻辑)
// effect.js
let activeEffect = null; // 记录处于运行状态的 effect,这样全局任何地方都可以拿到当前运行的 effect
class ReactiveEffect {
public active = true // 当前实例的状态
public parent = null // 上一个运行的 effect
public deps = []; // 保存 副作用 对应的 依赖
constructor(fn) {
this.fn = fn;
}
run() {
if(!this.active) {
// 如果当前实例,处于非激活状态,仅执行 fn 函数
return this.fn()
}
try {
this.parent = activeEffect; // 记录上一个运行的effect
activeEffect = this; // 将当前运行的 effect 改为 当前实例
return this.fn() // 执行函数
} finally {
// 当函数执行完成后,将当前运行的 effct 置为 上一个 实例
activeEffect = this.parent
}
}
}
function effect(fn) {
const _effect = new ReactiveEffect(fn)
_effect.run()
}
依赖收集
track 副作用追踪
- 当属性被
get时,则调用track,将当前的activeEffect放入当对应对象的属性上,以方便set的时候,能够一一取出,并被执行。
const targetMap = new WeakMap(); // 缓存 对象 属性上对应 依赖 的 effect
function track(target,type,key) {
const desMap = targetMap.get(target)
if(!desMap) {
const despMap = new Map()
targetMap.set(target,depsMap)
}
const deps = depsMap.get(key)
if(!deps) {
deps = new Set()
depsMap.set(key,deps)
}
trackEffects(deps)
}
function trackEffects(des) {
if(activeEffect) {
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
}
trigger 触发依赖的副作用函数
- 当属性被
set的时,则查找到对应属性依赖的副作用,并执行这些副作用
function trigger(target,type,key,value,oldValue) {
const depsMap = targetMap.get(target);
if(!depsMap) return
const deps = depsMap.get(key)
if(!deps) return
triggerEffect(deps)
}
function triggerEffect(deps) {
const effects = new Set(deps)
effects.forEach(effect => {
if(effect !== activeEffect) {
// 当副作用 不等于 当前副作用 时,执行该副作用,否则会出现 循环调用 error
effect.run()
}
})
}
// 循环调用副作用的例子
const obj = {
age: 16
}
effect(()=> {
obj.age = Math.random(); // 这里就会出现 循环调用,在副作用里对副作用对应的属性赋予随机值
app.innerHTML = obj.age
})
setTimeout(()=> {
obj.age = 18
})
- effect map 之间的关系
分支切换实现的原理
举个例子
<script>
const obj = {
name: 'jyp',
age: 18,
flag: true,
}
const state = reactive(obj)
effect(()=> {
console.log('render')
app.innerHTML = state.flag ? state.name : state.age
})
setTimeout(()=> {
state.flag = false;
setTimeout(()=> {
// 思考一下: name 改变了,effect 还需要执行吗?
// 答案:不应该执行,因为 flag = flase,所以该副作用应该是 age 改变时,才执行
// 现状:'render' 会被打印三次,最后一次是因为 name 改变,执行副作用的结果
// 期望: name 改变,不会引发 effect 执行
// 解决方案:在每次执行副作用前,清除该副作用之前所有依赖,然后重新添加依赖
state.name = 'zs'
},1000)
},2000)
</script>
- 代码思路
// effect.js
// 改造 triggerEffect 函数
function triggerEffect(deps) {
// deps 是 Set<ReactiveEffect>[] 数组
const effects = [...deps]; // 浅拷贝,因为待会清除时,会操作 deps,这样就避免 循环引用的问题
effects.forEach(effect => {
effect.run()
})
}
// 改造 ReactiveEffect 类中 run 方法
class ReactiveEffect {
run() {
if(!this.active){
return this.fn()
}
try {
this.parent = ativeEffect;
activeEffect = this.parent;
cleanupEffect(this) // 在执行函数之前,先清除依赖
return this.fn()
} finally {
activeEffect = this.parent
}
}
}
// cleanupEffect
function cleanupEffect(effect) {
let deps = effect.deps
if(deps) {
deps.forEach(dep => {
dep && dep.delete(effect) // 清除当前的 effect
})
}
effect.deps = [] // 将当前 effect 的 deps 记录,置为空数组
}
readonly
- readonly 基本思路与 reactive 基本一致,只是在 proxy
handler中的 getter & setter 中的处理不同。
// readonlyHandler 对象
const readonlyHanlder = {
get(target,key,receiver){
const res = Reflect(target,key,receiver)
track(target,TrackOpType.GET,key)
if(isObject(res)){
return readonly(res)
}
return res
},
set(target,key,value,receiver) {
return console.warn(`Set operation on "${String(key)}" failed: target is readonly`)
}
}
shallowReactive
-
shallowReactive 与 reactive最重要的区别在于,就只有一层数据是响应式的,其他的都是非响应式的。
-
Proxy的特点是,
- (1)会监听被代理的对象所有的取值与赋值操作,
- (2)取值逻辑,层层触发的。比如,
proxy.obj.name运行时,get方法会执行2次。第一次执行proxy.obj,第二次在第一次的结果上执行obj.name。如果obj不是proxy,那对obj的属性进行赋值操作时,不会走set方法。
-
要实现一层响应式,就只要在
get方法中直接返回Reflect.get返回的结果即可,而不对Reflect.get返回结果使用reactive递归产生的proxy对象即可。
const shallowReactiveHanlders = {
get(target,key,receiver) {
const res = Reflect.get(target,key,receiver)
track(target.TrackOpType.GET,key)
return res
},
set(target,key,value,receiver) {
const oldValue = target[key];
const res = Reflect.set(target,key,value,receiver);
if(oldValue !== value) {
trigger(target,TriggerOpType.SET,key,value,oldValue);
}
return res
}
}
shallowReadonly
- 数据不是响应式的,
- 只有第一层不可以
set
const shallowReadonlyHandlers = {
get(target,key,receiver) {
const res = Reflect(target,key,receiver)
return res
},
set(target,key,value,receiver) {
return console.log(`Set operation on "$String(key)" failed: target is readonly`)
}
}
isReactive & isReadony
-
判断一个对象是否被代理,在 Vue 内部主要是在访问该对象
ReactiveFlag.IS_REACTIVE这个属性。如果该对象是被代理过的,那么进入get方法后,返回 取反后的isReadonly的值。 -
如果该代理是 readonly 创建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true。
-
如果该对象是
isReadonly -
访问对象上
Reactive.IS_RAW属性, -
根据
isReaonly和shallow变量的值,取出对应WeakMap缓存的对象, -
判断
receiver是否与 缓存对象是否相等。- 相等就返回该对象
-
function isRective(value) {
if (isReadonly(value)) {
return isReactive((value as Target)[ReactiveFlags.IS_RAW]);
}
return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE]);
}
// hanlder 中 get 函数
const handers = {
get(target,key,receiver) {
if(key === ReactiveFlags.IS_REACTIVE) {
return !isReaonly
}else if (
key === ReactiveFlags.IS_RAW &&
receiver ===
(isReaodly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target;
}
}
}
- 同理
isReadony直接访问对象上的 ReactiveFlag.IS_READONLY 属性,然后get方法直接返回isReadonly属性
function isReadonly(value) {
return !!(value && value[ReactiveFlag.IS_READONLY])
}
// handler 中的 get 函数
const handlers = {
get(target,key,receiver) {
if(key === ReacitveFlag.IS_READONLY) {
return isReadonly
}
}
}
isProxy
- 检查对象是否是由 reactive 或 readonly 创建的 proxy
function isProxy(value) {
return isReactive(value) || isReadonly(value)
}