4

Vue 3.0 中的 reative 和 Vue 2.6 中提供的一个全局 API Vue.observable 相同,都是用于让一个对象可响应,首先来对比一下他们之间的差异:

reactiveVue.observable
基于 Proxy 实现基于 Object.defineProperty  实现
对 代理对象 进行操作直接操作 源对象
返回一个可响应的 代理对象返回一个可响应的 源对象

reactivity工作流程

在 Vue 3.0 中, reactivity 被独立出来,没有任何依赖,可以用于任何想做响应式数据的地方
先抛开源码中实现的复杂判断,来看一下他的主要工作流程
reactivity.png

关键方法实现

本文主要实现了其中几个关键的方法:

reactive

  • 创建响应式对象,在 Proxy 中定义 get 及 set 捕获器,对传入的 源对象 代理对象 进行拦截处理
  • get 捕获到当前对象的属性也是对象,要进行递归
  • 定义基于 WeakMap 的 reactiveMap 管理代理对象,如果传入的 object 已经有记录,直接返回 此对象代理对象 ,如果没有,按照正常流程走
/** * 处理器对象,定义捕获器 */ const handlers = { set(target, key) { Reflect.set(...arguments) trigger(target, key) }, get(target, key) { track(target, key) return typeof target[key] === 'object' ? reactive(target[key]) : Reflect.get(...arguments) }, } /** * 定义响应式对象,返回proxy代理对象 * @param {*} object */ function reactive(object) { if (reactiveMap.has(object)) return reactiveMap.get(object) const proxy = new Proxy(object, handlers) reactiveMap.set(object, proxy) return proxy }

effect

  • 副作用,创建用于管理 effect 的栈 effectStack ,将 effect 先入栈用于依赖收集,执行一次该 effect ,进入 get 捕获阶段,捕获完毕之后进入 finally 将其在栈中移出
/** * 副作用函数 * @param {*} fn */ function effect(fn) { try { // 将需要执行的effect入栈,用于依赖收集过程中与key的关系对应 effectStack.push(fn) // 执行该effect,进入proxy的get拦截 return fn() } finally { // 依赖收集完毕及所有get流程走完,当前effect出栈 effectStack.pop() } }

track

  • effect 执行后数据触发 get 捕获器, 在此过程中调用 track 进行依赖收集
  • 定义 targetMap ,以 WeakMap 的方式收集依赖,管理目标对象 target 及其对应的 key 
  • 第二层用于管理 key 及其对应的 effect ,上面流程图可以看到数据的结构和层次划分
/** * 依赖收集 * @param {*} target * @param {*} key */ function track(target, key) { // 初始化依赖Map let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } // 第二层依赖使用Set存放key对应的effect let dep = depsMap.get(key) if (!dep) { targetMap.get(target).set(key, (dep = new Set())) } // 取当前栈中的effect存入第二层依赖中 const activeEffect = effectStack[effectStack.length - 1] activeEffect && dep.add(activeEffect) }

trigger

  • 修改在 effect 中指定过的内容时会触发 set 捕获器,在此过程中 trigger 负责执行当前 target 下 key 对应的 effect ,完成响应式的过程
/** * 触发响应,执行effect * @param {*} target * @param {*} key */ function trigger(target, key) { const depsMap = targetMap.get(target) if (depsMap) { const effects = depsMap.get(key) effects && effects.forEach(run => run()) } }

computed

  • 这里采用简单粗暴的方式,直接返回一个 effect 
/** * 计算属性 * @param {*} fn */ function computed(fn) { return { get value() { return effect(fn) }, } }

效果展示

对象属性响应式,多层嵌套

const object = { o: { a: 1 } } const proxy = reactive(object) effect(() => { console.log(`proxy.o.a: ${proxy.o.a}`) })
  • 首次调用打印一次,重新赋值后再次响应,调用 effect 

1.jpg

响应式调色器

  • 配置响应式对象,指定其rgb属性,顺便测一下 computed 
const object = { r: 0, g: 0, b: 0 } const proxy = reactive(object) const computedObj = computed(() => proxy.r * 2) effect(() => { const { r, g, b } = proxy document.getElementById('r').value = r document.getElementById('b').value = b document.getElementById('g').value = g document.getElementById( 'color' ).style.backgroundColor = `rgb(${r},${g},${b})` document.getElementById('color_text').innerText = `rgb:${r},${g},${b}` const { value } = computedObj document.getElementById( 'computed_text' ).innerText = `computed_text: r*2=${value}` })
  • 拖动 rgb 3个 range 时各自的变化会体现在颜色块上

2.jpg

  • 对象依赖关系 targetMap 结构如下

3.jpg

  • reactiveMap 结构如下

4.jpg

总结

  • 通过上述内容可以了解到 Vue 3.0 中的响应式原理

    • reactive 创建响应式对象
    • effect 副作用,调用自身收集依赖,数据变更后重新调用该函数
    • track 依赖收集
    • trigger 触发依赖中对应的 effect 
    • computed 计算属性,对应属性值变更调用其 effect 
  • 过程中还能更加熟悉一些前置基础知识

    • 代理与反射: Proxy Reflect 
    • JavaScript 标准内置对象: WeakMap Map Set 
    • 语句执行使用技巧: try finally 

参考资料


__mxin
39 声望1 粉丝

个人简介:撸猫、撸狗、撸码、吃