const toProxy = new WeakMap() const toRaw = new WeakMap() function reactive(target) { let observed = toProxy.get(target) // 原数据已有相应的响应式数据,直接返回 if (observed !== void 0) { return observed } // 原数据已经是响应式数据了 if (toRaw.get(target)) { return target } observed = new Proxy(target, handler) // 设置缓存 toProxy.set(target, observed) toRaw.set(observed, target) return observed } 复制代码
toProxy
和 toRaw
主要是缓存 原始数据
和 响应式数据
const isObject = (obj) => obj !== null && typeof obj === 'object' const hasOwnProperty = Object.prototype.hasOwnProperty const hasOwn = (val, key) => hasOwnProperty.call(val, key) const handler = { get: (target, key, receiver) => { const res = Reflect.get(target, key, receiver) // 收集依赖 track(target, key) // 递归寻找 return isObject(res) ? reactive(res) : res }, set: (target, key, value, receiver) => { const hadKey = hasOwn(target, key) const oldValue = target[key] const res = Reflect.set(target, key, value, receiver) // 触发更新 if (!hadKey || value !== oldValue) { trigger(target, key, oldValue, value) } return res }, } 复制代码
isObject(res) ? reactive(res) : res
由于 proxy
代理的对象只能代理到第一层,当对多层级的对象操作时,set
并不能感知到,但是 get
会触发,需要我们进行递归实现,利用 Reflect.get()
返回的“多层级对象中内层” ,再对“内层数据”做一次代理。
!hadKey || oldValue !== value
当输入
const arr = ['a', 'b'] const r = reactive(arr) r.push('c') // 结果 // ['a', 'b'] 2 'c' // ['a', 'b', 'c'] 'length' 3 复制代码
r.push('c')
会触发 set
执行两次,一次是值本身 'c'
,一次是 length
属性设置。
此时的 length
属性其实是属于数组本身的一个属性
为了避免触发多次 trigger
,通过判断 key
是否为 target
自身属性,以及设置 value
是否跟 target[key]
相等 可以确定 trigger
的类型,并且避免多余的 trigger
这边只做了逻辑或的判断,在Vue3源码中通过 !hadKey
和 value !== oldValue
分别进行 ADD
和 SET
类型的判断进行不同的操作
track
和 trigger
主要的作用于 effect
,下面我们分别实现一下 track
, trigger
和 effect
注:传送门 Proxy 、Reflect
const effectStack = [] // 存储effect function effect(fn, options = {}) { const reactiveEffect = createReactiveEffect(fn, options) if (!options.lazy) { reactiveEffect() } return reactiveEffect } function createReactiveEffect(fn, options) { const effect = function effect(...args) { return run(effect, fn, args) } effect.deps = [] effect.computed = options.computed effect.lazy = options.lazy return effect } 复制代码
function run(effect, fn, args) { if (!effectStack.includes(effect)) { try { // 将effect push到全局数组中 effectStack.push(effect) return fn(...args) } finally { // 清除已经收集过的effect effectStack.pop() } } } 复制代码
// 收集依赖最终数据的结构 targetMap = { target: { name: [effect], (Set对象) age: [effect] (Set对象) } } 复制代码
const targetMap = new WeakMap() // 缓存effect function track(target, key) { const effect = effectStack[effectStack.length - 1] if (effect === void 0) { return } let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } // 收集依赖时,通过 key 建立一个 Set let dep = depsMap.get(key) if (dep === void 0) { depsMap.set(key, (dep = new Set())) } if (!dep.has(effect)) { dep.add(effect) effect.deps.push(dep) } } 复制代码
首先全局会存在一个 targetMap
,它用来建立 数据 -> 依赖
的映射,它是一个 WeakMap
数据结构。
而 targetMap
通过查找 target
,可以获取到 depsMap
,它用来存放这个数据对应的所有响应式依赖。
depsMap
的每一项则是一个 Set
数据结构,而这个 Set
就存放着对应 key
的更新函数
const targetMap = new WeakMap() function trigger(target, key, oldValue, newValue) { const depsMap = targetMap.get(target) if (depsMap === void 0) { return } const effects = new Set() const computedRunners = new Set() if (key) { // 通过 key 找到所有更新函数,依次执行 let deps = depsMap.get(key) deps.forEach((effect) => { if (effect.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } const run = (effect) => effect() effects.forEach(run) computedRunners.forEach(run) } 复制代码
当然在我们实现完 effect
以后,computed
的实现就显得简单许多了
function computed(fn) { const runner = effect(fn, { computed: true, lazy: true }) return { effect: runner, get value() { return runner() }, } } 复制代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"></div> <button id="btn">加1</button> <script> // utils const isObject = (obj) => obj !== null && typeof obj === 'object' const hasOwnProperty = Object.prototype.hasOwnProperty const hasOwn = (val, key) => hasOwnProperty.call(val, key) const toProxy = new WeakMap() const toRaw = new WeakMap() const handler = { get: (target, key, receiver) => { const res = Reflect.get(target, key, receiver) track(target, key) return isObject(res) ? reactive(res) : res }, set: (target, key, value, receiver) => { const hadKey = hasOwn(target, key) const oldValue = target[key] const res = Reflect.set(target, key, value, receiver) if (!hadKey || value !== oldValue) { trigger(target, key, oldValue, value) } return res }, } function reactive(target) { let observed = toProxy.get(target) if (observed !== void 0) { return observed } if (toRaw.get(target)) { return target } observed = new Proxy(target, handler) toProxy.set(target, observed) toRaw.set(observed, target) return observed } const effectStack = [] const targetMap = new WeakMap() // 缓存effect // 依赖收集 function track(target, key) { const effect = effectStack[effectStack.length - 1] if (effect === void 0) { return } let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (dep === void 0) { depsMap.set(key, (dep = new Set())) } if (!dep.has(effect)) { dep.add(effect) effect.deps.push(dep) } } // 触发更新 function trigger(target, key, oldValue, newValue) { const depsMap = targetMap.get(target) if (depsMap === void 0) { return } const effects = new Set() const computedRunners = new Set() if (key) { let deps = depsMap.get(key) deps.forEach((effect) => { if (effect.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } const run = (effect) => effect() effects.forEach(run) computedRunners.forEach(run) } // 副作用 function effect(fn, options = {}) { const reactiveEffect = createReactiveEffect(fn, options) if (!options.lazy) { reactiveEffect() } return reactiveEffect } function createReactiveEffect(fn, options) { const effect = function effect(...args) { return run(effect, fn, args) } effect.deps = [] effect.computed = options.computed effect.lazy = options.lazy return effect } function run(effect, fn, args) { if (!effectStack.includes(effect)) { try { effectStack.push(effect) return fn(...args) } finally { effectStack.pop() } } } function computed(fn, options) { const runner = effect(fn, { ...options, computed: true, lazy: true }) return { effect: runner, get value() { return runner() }, } } </script> <script> const app = document.getElementById('app') const btn = document.getElementById('btn') const obj = reactive({ name: 'Charles', age: 18, location: { city: 'Guangzhou' } }) let double = computed(() => obj.age * 2) effect(() => { app.innerHTML = `My name is ${obj.name}, ${obj.age} years old, ${double.value}` }) btn.addEventListener('click', () => { obj.age += 1 }, false) </script> </body> </html> 复制代码
Vue3 中的数据侦测
Vue3 的响应式和以前有什么区别,Proxy 无敌?