赞
踩
大家都知道vue2的时候是使用了Object.defineProperties方法通过set和get来进行数据的劫持,从而达到响应式数据。
在vue3中则是使用es6 proxy代理,将对象进行转化拦截,使用weakMap做弱引用缓存。当触发代理的get时就会调用track,在track里面会把对应的effect收集到targetMap(targetMap就是map数据)
set时会执行trigger,trigger函数如果有获取有对应的effect,就会触发effect。
下面通过手动一个案例来进行深度的一个学习,彻底认识reactive,effect,computed等函数具体是如何实现的
<div id="app"></div>
<button id="btn">数量+1</button>
const root = document.querySelector('#app')
const btn = document.querySelector('#btn')
const ob = reactive({
name: '小明',
number: 11
})
let cNumber = computed(() => ob.number * 2)
effect(() => {
root.innerHTML = `<h1>${ob.name}---${ob.number}---${cAge.value}</h1>`
})
btn.onclick = function () {
ob.number += 1
}
老规矩,倒推思想,如上述代码,每点击一次按钮,就会给obj.number + 1然后执行effect,计算属性也会相应的 ob.number * 2 执行,最终我们要去一一实现上面的功能;
1.首先,我们需要一个reactive用来接收对象的函数,其内部通过es6的proxy api来实现的
(Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法)
const handlers = { get (target, key, receiver) { return Reflect.get(target, key, receiver) }, set (target, key, value, receiver) { return Reflect.set(target, key, value, receiver) } } function reactive (target) { observed = new Proxy(target, handlers) return observed } let person = { name: '小明', number: 11 } let ob = reactive(person)
但是这样会有很多的问题,每执行一次reactive就new了一个proxy,有点浪费资源,所以我们要进行优化,把代理过的对象都缓存下来,下次直接去访问缓存对象。通过弱引用WeakMap进行缓存,跟es6 map用法差不多,只不过map是强引用,见如下代码:
const toProxy = new WeakMap() // 缓存代理过的对象 const toRaw = new WeakMap() // 缓存被代理过的对象 // handlers 跟上面的一样,为了篇幅这里省略 function reactive (target) { let observed = toProxy.get(target) // 如果是缓存代理过的 if (observed) { return observed } if (toRaw.has(target)) { return target } observed = new Proxy(target, handlers) toProxy.set(target, observed) // 缓存observed toRaw.set(observed, target) // 缓存target return observed } let person = { number: '小明', age: 11 } let ob = reactive(person) ob = reactive(person) // 返回都是缓存的 ob = reactive(ob) // 返回都是缓存的 console.log(ob.number) // 10 ob.number = 20 console.log(ob.number) // 20
这时候缓存就做好了,但是还有一个问题,如果代理target对象层级比较深的话,proxy是做不到深层代理的,所以继续优化
// 对象类型判断 const isObject = val => val !== null && typeof val === 'object' const toProxy = new WeakMap() // 缓存代理过的对象 const toRaw = new WeakMap() // 缓存被代理过的对象 const handlers = { get (target, key, receiver) { const res = Reflect.get(target, key, receiver) return isObject(res) ? reactive(res) : res//⭐️⭐️⭐️⭐️⭐️⭐️ }, set (target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver) return result } } function reactive (target) { let observed = toProxy.get(target) // 如果是缓存代理过的 if (observed) { return observed } if (toRaw.has(target)) { return target } observed = new Proxy(target, handlers) toProxy.set(target, observed) // 缓存observed toRaw.set(observed, target) // 缓存target return observed }
嗯,这下应该差不多了,isObject(res) ? reactive(res) : res。。。。。。判断是否是对象,如果是的话,继续执行reactive
2.实现effect
effect(() => { root.innerHTML = `<h1>${ob.name}---${ob.number}---${cAge.value}</h1>` }) function effect (fn, options = {}) { const effect = createReactiveEffect(fn, options) if (!options.lazy) { effect() } return effect } //创建一个新的effect函数,并且给这个effect函数挂在一些属性, //为后面做computed准备,这个effect函数里面调用run函数, 最后在返回出新的effect function createReactiveEffect(fn, options) { const effect = function effect(...args) { return run(effect, fn, args) // 里面执行fn } // 给effect挂在一些属性 effect.lazy = options.lazy effect.computed = options.computed effect.deps = [] return effect } const activeEffectStack = [] // 声明一个数组,来存储当前的effect,订阅时候需要 function run (effect, fn, args) { if (activeEffectStack.indexOf(effect) === -1) { try { // 把effect push到数组中 activeEffectStack.push(effect) return fn(...args) } finally { // 清除已经收集过得effect,为下个effect做准备 activeEffectStack.pop() } } }
上面的代码,把传进来的effect推送到一个activeEffectStack数组中,然后执行传进来的fn(…args),就是effect (fn)里面的fn。也就触发了effect中的参数函数,
这样子就会触发到proxy的getter,就是执行到下面的handlers.get函数,接下来就是在set和get中进行依赖收集与派发更新
const handlers = {
get (target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// effect 收集 ⭐️⭐️⭐️⭐️⭐️
track(target, key)
return isObject(res) ? reactive(res) : res
},
set (target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
const extraInfo = { oldValue: target[key], newValue: value }
// trigger effect ⭐️⭐️⭐️⭐️⭐️
trigger(target, key, extraInfo)
return result
}
}
// 存储effect const targetMap = new WeakMap() function track (target, key) { // 拿到上面push进来的effect const effect = activeEffectStack[activeEffectStack.length - 1] if (effect) { let depsMap = targetMap.get(target) if (depsMap === void 0) { depsMap = new Map() // targetMap如果不存在target 的 Map 就设置一个 targetMap.set(target, depsMap) } let dep = depsMap.get(key) if (dep === void 0) { dep = new Set() // 如果depsMap里面不存在key 的 Set 就设置一个 depsMap.set(key, dep) } if (!dep.has(effect)) { // 收集当前的effect dep.add(effect) // effect 收集当前的dep effect.deps.push(dep) } } }
从run函数里面的activeEffectStack拿到当前的effect,如果有effect,就从targetMap里面拿depsMap,targetMap如果不存在target 的 Map 就设置一个targetMap.set(target, depsMap),再从depsMap 里面拿 key 的 Set ,如果depsMap里面不存在 key 的 Set 就设置一个depsMap.set(key, dep),下面就是收集前的effect和effect 收集当前的dep了。收集完毕后,targetMap的数据结构就类似下面的样子的了
// track的作用就是完成下面的数据结构
targetMap = {
target: {
name: [effect],
age: [effect]
}
}
// targetMap 是WeakMap 数据结构,为了理解就用对象表示
// [effect] 是 Set数据结构,为了理解就用数组表示
track执行完毕之后,handlers.get就会返回 res,进行一系列收集之后,fn执行完毕,run函数最后就执行finally {activeEffectStack.pop()},因为effect已经收集结束了,清空为了下一个effect收集做处理。
依赖收集已经完毕了,但是当我们更新数据的时候,例如ob.number += 1,更改数据会触发proxy的getter,也就是会调用handlers.set函数,里面就执行了trigger(target, key, extraInfo),trigger函数如下
// effect 的触发 function trigger(target, key, extraInfo) { // 拿到所有target的订阅 const depsMap = targetMap.get(target) // 没有被订阅到 if (depsMap === void 0) { return; } const effects = new Set() // 普通的effect const computedRunners = new Set() // computed 的 effect if (key !== void 0) { let deps = depsMap.get(key) // 拿到deps订阅的每个effect,然后放到对应的Set里面 deps.forEach(effect => { if (effect.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } const run = effect => { effect() } // 循环调用effect computedRunners.forEach(run) effects.forEach(run) }
上面的代码的意思是,拿到对应key的effect,然后执行effect,然后执行run,然后执行fn,然后就是get上面那一套流程了,最后拿到数据是更改后新的数据,然后更改视图。
3.实现computed函数
还是先看用法,let cNumber = computed(() => ob.number * 2),上面写effect的时候,有很多次提到为computed做准备,其实computed就是基于effect来实现的,下面我们看代码
function computed(fn) {
const getter = fn
// 手动生成一个effect,设置参数
const runner = effect(getter, { computed: true, lazy: true })
// 返回一个对象
return {
effect: runner,
get value() {
value = runner()
return value
}
}
}
如果options.lazy为true就不会立刻执行,就相当于let cNumber = computed(() => ob.number * 2)不会立刻执行runner函数,当cNumber.value才真正的执行。
原文链接:https://blog.csdn.net/qq_40513881/article/details/113866967
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。