响应式原理V2 vs V3
vue2的方式使用Object.defineProperty(),拦截每一个key,需要递归遍历对象所有key,速度慢,数组响应式需要额外实现'push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort',新增或删除属性无法监听,需要使用特殊api,不支持Map、Set、Class等数据结构。
vue3的方式使用Proxy代理整个对象,从而侦查数据变化。
造个轮子
首先实现reactive(obj),借助Proxy代理传入的obj,这样可以拦截对obj的各种访问;
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦截
// http://es6.ruanyifeng.com/#docs/proxy const observed = new Proxy(obj, baseHandler) return observed
}
下面是依赖收集的实现,原理如下图:
相关api有
- effect(fn):传入fn,返回的函数将是响应式的,内部代理的数据发生变化,它会再次执行
- track(target, key):建立响应式函数与其访问的目标(target)和键(key)之间的映射关系
- trigger(target, key):根据track()建立的映射关系,找到对应响应式函数并执行它
基本结构:
// 临时存储响应式函数 const effectStack = []
// 将传入fn转换为一个响应式函数
function effect(fn, options = {}) { }
// 存放响应式函数和目标、键之间的映射关系 const targetMap = new WeakMap()
// 依赖收集,创建映射关系
function track(target, key) { }
// 根据映射关系获取响应函数
function trigger(target, key) { }
实现effect()/track()/trigger()
function effect(fn, options = {}) {
// 创建reactiveEffect
const e = createReactiveEffect(fn, options)
// 执行一次触发依赖收集
e()
return e
}
function createReactiveEffect(fn, options) {
// 封装一个高阶函数,除了执行fn,还要将自己放入effectStack为依赖收集做准备 const effect = function reactiveEffect(...args) {
if (!effectStack.includes(effect)) {
try {
// 1.effect入栈 effectStack.push(effect) // 2.执行fn
return fn(...args)
} finally {
// 3.effect出栈 effectStack.pop()
}
}
return effect
}
function track(target, key) {
// 获取响应式函数
const effect = effectStack[effectStack.length - 1]
if (effect) {
// 获取target映射关系map,不存在则创建
let depMap = targetMap.get(target)
if (!depMap) {
depMap = new Map()
targetMap.set(target, depMap)
}
// 获取key对应依赖集合,不存在则创建
let deps = depMap.get(key)
if (!deps) {
deps = new Set()
depMap.set(key, deps)
}
// 将响应函数添加到依赖集合
deps.add(effect)
}
}
function trigger(target, key) {
// 获取target对应依赖map
const depMap = targetMap.get(target)
if (!depMap) {
return
}
// 获取key对应集合
const deps = depMap.get(key)
if (deps) {
// 执行所有响应函数
deps.forEach(dep => dep())
}
}
// 测试
const state = reactive({ foo: 'foo' })
effect(() => {
console.log('effect', state.foo);
})
结合视图验证一下
<script src="03-reactivity.js"></script>
<script>
const obj = { name: 'kkb', age: 8 }
const data = reactive(obj)
// effect()定义我们的更新函数
effect(() => {
app.innerHTML = ` <h1>${data.name}今年${data.age}岁了</h1>`
})
// 修改一下数值
setInterval(() => {
data.age++
}, 1000);
</script>
计算属性也很常用,可以基于effect实现
const double = computed(() => data.age * 2)
// effect()定义我们的更新函数
effect(() => {
app.innerHTML = ` <h1>${data.name}今年${data.age}岁了</h1> <p>乘以2是${double.value}岁</p>`
})
computed(fn):可以使传入fn使之成为响应式函数,fn内部依赖的数值发生变化,该函数应该重新执行 获得最新的计算结果。
网友评论