image.png

demo:

  1. const { reactive, effect } = VueObserver
  2. const origin = {
  3. count: 0
  4. }
  5. # 1. 将数据代理变为响应式数据
  6. const state = reactive(origin)
  7. # 2. 依赖响应式数据的 动作:Dep依赖
  8. const fn = () => {
  9. const count = state.count
  10. console.log(`set count to ${count}`)
  11. }
  12. effect(fn) // 用effect包裹一下
  13. # 3. 修改data,可以看到fn被执行
  14. 控制台执行 state.count++
  15. 输出 set count to 1

问题的点:
为啥引用了data的函数,还得用effect函数包裹一下才行?
触发的本身不知道么

effect: 主动执行了下这个函数,然后触发了getter,触发getter之后的track,生成依赖收集表
track
run:当setter的时候执行依赖收集表里的func
image.png 这里effect即是watcher的功能
而之前的dep有个专门的Dep类来管理这个一对多的依赖关系
现在都内化到了effect的track里,
effect管理
export const targetMap = new WeakMap();
export const effectStack = [];

回答下问题,为啥非要包裹一下
可能是,某个data的属性更改后,可以触发setter,但是无法得到触发这个动作的所在环境:当前函数体等
所以需要包裹了一下才能收集这个函数体

我的想法:
如果真的要用一个高阶函数包裹一下才能实现的话
那需要包裹的是,每个组件的render函数
怎么能高效的对每个类的render进行包裹呢
前两天看了个技术贴,正好将proxy的妙用,对类的函数进行包裹实现log的。。

我现在还想了解下vue的patch阶段
上面的响应式解决的问题是:
data->view:现在是data触发后,全局render
看了文章之后,
我可以:不是在变更时主动触发render了。
而是在每个组件的render那进行主动把自己监听,等待render的过程

这是解决思路1
还有解决思路是:还是全局render
但是render的时候,会对待更新的内容进行patch对比

负责对新旧 VNode 进行比对,并以合适的方式更新DOM,也就是我们常说的 patch

vue的patch就是diff找到更新的节点,dom进行更新的情况
但是对于canvas不适用
下面去strucV看下咋实现的

初始化阶段

  1. 把 origin 对象转化成响应式的 Proxy 对象 state。
  2. 把函数 fn() 作为一个响应式的 effect 函数。

当一个普通的函数 fn() 被 effect() 包裹之后,就会变成一个响应式的 effect 函数,
而 fn() 也会被立即执行一次

由于在 fn() 里面有引用到 Proxy 对象的属性,所以这一步会触发对象的 getter,从而启动依赖收集。

除此之外,这个 effect 函数也会被压入一个名为”activeReactiveEffectStack“(此处为 effectStack)的栈中,供后续依赖收集的时候使用

  1. export function effect (fn) {
  2. // 构造一个 effect
  3. const effect = function effect(...args) {
  4. return run(effect, fn, args)
  5. }
  6. // 立即执行一次
  7. effect() // 立即执行一次,触发 data.getter 完成依赖收集
  8. return effect
  9. }
  10. export function run(effect, fn, args) {
  11. if (effectStack.indexOf(effect) === -1) {
  12. try {
  13. // 往池子里放入当前 effect
  14. effectStack.push(effect)
  15. // 立即执行一遍 fn()
  16. // fn() 执行过程会完成依赖收集,会用到 effect
  17. return fn(...args)
  18. } finally {
  19. // 完成依赖收集后从池子中扔掉这个 effect
  20. effectStack.pop()
  21. }
  22. }
  23. }

image.png

依赖收集

依赖收集阶段最重要的目的,就是建立一份”依赖收集表“
targetMap 是一个 WeakMap:
key:reactive(origin)里的origin,即代理前的原始数据
val:依赖该state的dep们
而dep们是用map结构维护的
key:getter当前触发的data属性,这里是state.count里的count属性
val:触发过该属性值所对应的各个 effect

image.png

如此建立起:{ target -> key -> dep } 的对应关系
image.png

  1. // getter
  2. export function track (target, operationType, key) {
  3. const effect = effectStack[effectStack.length - 1]
  4. if (effect) {
  5. let depsMap = targetMap.get(target)
  6. if (depsMap === void 0) {
  7. targetMap.set(target, (depsMap = new Map()))
  8. }
  9. let dep = depsMap.get(key)
  10. if (dep === void 0) {
  11. depsMap.set(key, (dep = new Set()))
  12. }
  13. if (!dep.has(effect)) {
  14. dep.add(effect)
  15. }
  16. }
  17. }

响应阶段

依赖收集表的作用在于:
当一个数据源属性更改时,会触发依赖该数据源的多个依赖更新;
当多个数据源属性更改时,触发一一对应的 多个依赖更新
比如下面的例子:
Proxy:{ count: 0, age: 18 }
image.png
当修改对象的某个属性值的时候,会触发对应的 setter。
下面就是 根据setter的key找到dep,执行dep的过程:

  1. export function trigger (target, operationType, key) {
  2. // 取得对应的 depsMap
  3. const depsMap = targetMap.get(target)
  4. if (depsMap === void 0) {
  5. return
  6. }
  7. // 取得对应的各个 dep
  8. const effects = new Set()
  9. if (key !== void 0) {
  10. const dep = depsMap.get(key)
  11. dep && dep.forEach(effect => {
  12. effects.add(effect)
  13. })
  14. }
  15. // 简化版 scheduleRun,挨个执行 effect
  16. effects.forEach(effect => {
  17. effect()
  18. })
  19. }

扩:proxy数据监听的性能提升

实际上,Vue3 并非简单的通过 Proxy 来递归侦测数据, 而是通过 get 操作来实现内部数据的代理,并且结合 WeakMap 来对数据保存,这将大大提高响应式数据的性能。

rawToReactive 这个数据的代理也是递归的,只是他是weekMap,从而性能相对object要提高不少

  1. const rawToReactive = new WeakMap()
  2. const reactiveToRaw = new WeakMap()
  3. vue3在对数据进行代理处理时候,还开了这两个map,是干啥的?

参考

一张图理清 Vue 3.0 的响应式系统
Vue3中的数据侦测