话说vue3
已经发布,就引发了大量前端人员的关注,木得办法,学不动也得硬着头皮学呀,本篇文章就简单介绍一下「vue3的数据响应原理」,以及简单实现其reactive
、effect
、computed
函数,但愿能对你们理解vue3
响应式有一点点的帮助。话很少说,看下面栗子的代码和其运行的结果。html
<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
执行,以下图:前端
因此,针对上面的栗子,制定一些小目标,而后一一实现,以下:vue
reactive
其实数据响应式函数,其内部经过es6
的proxy api
来实现,
下面面其实经过简单几行代码,就能够对一个对象进行代理拦截了。react
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)
可是这么作的话有缺点,一、重复屡次写ob = reactive(person)
就会一直执行new Proxy
,这不是咱们想要的。理想状况应该是,代理过的对象缓存下来,下次访问直接返回缓存对象就能够了;二、同理屡次这么写ob = reactive(person); ob = reactive(ob)
那也要缓存下来。下面咱们改造一下上面的reactive
函数代码。git
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
是作不到深层代理的。例如es6
let person = { name: '张三', age: 10, hobby: { paly: ['basketball', 'football'] } } let ob = reactive(person) console.log(ob)
从上面的打印结果能够看出hobby
对象没有咱们上面的handlers
代理,也就是说当咱们对hobby
作一些依赖收集的时候是没有办法的,因此咱们改写一下handlers
对象。github
// 对象类型判断 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函数
的完整代码。api
到了这里离咱们的目标又近了一步,这里来实现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
函数。
其实上面尚未写好的这个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() -> 渲染视图
仍是先看用法,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: