话说 vue3
已经发布,就引起了大量前端人员的关注,木得办法,学不动也得硬着头皮学呀,本篇文章就简单介绍一下「vue3 的数据响应原理」,以及简单实现其 reactive
、effect
、computed
函数,希望能对大家理解 vue3
响应式有一点点的帮助。话不多说,看下面栗子的代码和其运行的结果。
<div id="root"></div>
<button id="btn"> 年龄 +1</button>
const root = document.querySelector('#root')
const btn = document.querySelector('#btn')
const ob = reactive({
name: '张三',
age: 10
})
let cAge = computed(() => ob.age * 2)
effect(() => {root.innerHTML = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>`
})
btn.onclick = function () {ob.age += 1}
上面带代码,是每点击一次按钮,就会给 obj.age + 1
然后执行effect
,计算属性也会相应的 ob.age * 2
执行,如下图:
所以,针对上面的栗子,制定一些小目标,然后一一实现,如下:
- 1、实现 reactive 函数
- 2、实现 effect 函数
- 3、把 reactive 和 effect 串联起来
- 4、实现 computed 函数
实现 reactive 函数
reactive
其实数据响应式函数,其内部通过 es6
的proxy api
来实现,
下面面其实通过简单几行代码,就可以对一个对象进行代理拦截了。
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: '张三',
age: 10
}
let ob = reactive(person)
但是这么做的话有缺点,1、重复多次写 ob = reactive(person)
就会一直执行 new Proxy
,这不是我们想要的。理想情况应该是,代理过的对象缓存下来,下次访问直接返回缓存对象就可以了;2、同理多次这么写ob = reactive(person); ob = reactive(ob)
那也要缓存下来。下面我们改造一下上面的reactive
函数代码。
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 = {
name: '张三',
age: 10
}
let ob = reactive(person)
ob = reactive(person) // 返回都是缓存的
ob = reactive(ob) // 返回都是缓存的
console.log(ob.age) // 10
ob.age = 20
console.log(ob.age) // 20
这样子调用 reactive()
返回都是我们第一次的代理对象啦(ps:WeakMap 是弱引用)。缓存做好了,但是还有新的问题,如果代理 target
对象层级嵌套比较深的话,上面的 proxy
是做不到深层代理的。例如
let person = {
name: '张三',
age: 10,
hobby: {paly: ['basketball', 'football']
}
}
let ob = reactive(person)
console.log(ob)
从上面的打印结果可以看出 hobby
对象没有我们上面的handlers
代理,也就是说当我们对hobby
做一些依赖收集的时候是没有办法的,所以我们改写一下 handlers
对象。
// 对象类型判断
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)
// TODO: effect 收集
return isObject(res) ? reactive(res) : res
},
set (target, key, value, receiver) {const result = Reflect.set(target, key, value, receiver)
// TODO: trigger effect
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
}
上面的代码通过在 get
里面添加 return isObject(res) ? reactive(res) : res
,意思是当访问到某一个对象时候,如果判断类型是「object」,那么就继续调用 reactive
代理。上面也是我们的 reactive 函数
的完整代码。
实现 effect 函数
到了这里离我们的目标又近了一步,这里来实现 effect 函数
,首先我们先看看effect
的用法。
effect(() => {root.innerHTML = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>`
})
第一感觉看起来很简单嘛,就是函数当做参数传进去,然后调用传进来函数,完事。下面代码最简单实现
function effect(fn) {fn()
}
但是到这里,所有人都看出来缺点了,这只是执行一次呀?怎么跟响应式联系起来呀?还有后面 computed
怎么基于这个实现呀?等等。带着一大堆问题,通过改写 effect
和增加 effect
功能去解决这一系列问题。
function effect (fn, options = {}) {const effect = createReactiveEffect(fn, options)
// 不是理解计算的,不需要调用此时调用 effect
if (!options.lazy) {effect()
}
return 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
}
在 createReactiveEffect
函数中:创建一个新的 effect
函数,并且给这个 effect
函数挂在一些属性,为后面做 computed
准备,这个 effect
函数里面调用 run
函数(此时还没有实现), 最后在返回出新的effect
。
在 effect
函数中:如果判断 options.lazy
是false
就调用上面创建一个新的 effect
函数,里面会调用 run
函数。
把 reactive 和 effect 串联起来
其实上面还没有写好的这个 run
函数的作用,就是把 reactive
和 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)
,这里的 fn 就是
fn = () => {root.innerHTML = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>`
}
执行上面的 fn
访问到 ob.name
、ob.age
、cAge.value
(这是 computed 得来的),这样子就会触发到proxy
的getter
,就是执行到下面的 handlers.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
}
}
聪明的小伙伴看到这里已经看出来,上面 handlers.get
函数里面 track
的作用是依赖收集,而 handlers.set
里面 trigger
是做派发更新的。
下面补全 track
函数代码
// 存储 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]
}
}
// ps: targetMap 是 WeakMap 数据结构,为了直观和理解就用对象表示
// [effect] 是 Set 数据结构,为了直观和理解就用数组表示
track
执行完毕之后,handlers.get
就会返回 res
,进行一系列收集之后,fn 执行完毕,run
函数最后就执行 finally {activeEffectStack.pop()}
,因为effect
已经收集结束了,清空为了下一个 effect
收集做处理。
依赖收集已经完毕了,但是当我们更新数据的时候,例如 ob.age += 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
上面那一套流程了,最后拿到数据是更改后新的数据,然后更改视图。
下面简单弄一个帮助理解的流程图,实在不能理解,大家把仓库代码拉下来,debuger 执行一遍
targetMap = {name: [effect],age: [effect]
}
ob.age += 1 -> set() -> trigger() -> age: [effect] -> effect() -> run() -> fn() -> getget() -> 渲染视图
实现 computed 函数
还是先看用法,let cAge = computed(() => ob.age * 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
}
}
}
值得注意的是,我们上面 effet 函数里面有个判断
if (!options.lazy) {effect()
}
如果 options.lazy
为true
就不会立刻执行,就相当于 let cAge = computed(() => ob.age * 2)
不会立刻执行 runner 函数,当 cAge.value
才真正的执行。
最后,所有的函数画成一张流程图。
如果文章有哪些不对,请各位大佬指出来,我有摸鱼时间一定会修正过来的。
至此,所有的的小目标我们都已经完成了,撒花✿✿ヽ (°▽°) ノ✿
ps:
源码地址(大家可以 clone 下来执行一遍)
博客文章地址(这里有新的阅读体验,也有微信,欢迎来撩)