Vue 3 中的响应式原理可谓是很是之重要,经过学习 Vue3 的响应式原理,不只能让咱们学习到 Vue.js 的一些设计模式和思想,还能帮助咱们提升项目开发效率和代码调试能力。 javascript
在这以前,我也写了一篇《探索 Vue.js 响应式原理》 ,主要介绍 Vue 2 响应式的原理,这篇补上 Vue 3 的。 html
因而最近在 Vue Mastery 上从新学习 Vue3 Reactivity 的知识,此次收获更大。本文将带你们从头开始学习如何实现简单版 Vue 3 响应式,帮助你们了解其核心,后面阅读 Vue 3 响应式相关的源码可以更加驾轻就熟。前端
当咱们在学习 Vue 3 的时候,能够经过一个简单示例,看看什么是 Vue 3 中的响应式:vue
<!-- HTML 内容 --> <div id="app"> <div>Price: {{price}}</div> <div>Total: {{price * quantity}}</div> <div>getTotal: {{getTotal}}</div> </div>
const app = Vue.createApp({ // ① 建立 APP 实例 data() { return { price: 10, quantity: 2 } }, computed: { getTotal() { return this.price * this.quantity * 1.1 } } }) app.mount('#app') // ② 挂载 APP 实例
经过建立 APP 实例和挂载 APP 实例便可,这时能够看到页面中分别显示对应数值:java
当咱们修改 price
或 quantity
值的时候,页面上引用它们的地方,内容也能正常展现变化后的结果。这时,咱们会好奇为什么数据发生变化后,相关的数据也会跟着变化,那么咱们接着往下看。react
在普通 JS 代码执行中,并不会有响应式变化,好比在控制台执行下面代码:git
let price = 10, quantity = 2; const total = price * quantity; console.log(`total: ${total}`); // total: 20 price = 20; console.log(`total: ${total}`); // total: 20
从这能够看出,在修改 price
变量的值后, total
的值并无发生改变。github
那么如何修改上面代码,让 total
可以自动更新呢?咱们其实能够将修改 total
值的方法保存起来,等到与 total
值相关的变量(如 price
或 quantity
变量的值)发生变化时,触发该方法,更新 total
便可。咱们能够这么实现:设计模式
let price = 10, quantity = 2, total = 0; const dep = new Set(); // ① const effect = () => { total = price * quantity }; const track = () => { dep.add(effect) }; // ② const trigger = () => { dep.forEach( effect => effect() )}; // ③ track(); console.log(`total: ${total}`); // total: 0 trigger(); console.log(`total: ${total}`); // total: 20 price = 20; trigger(); console.log(`total: ${total}`); // total: 40
上面代码经过 3 个步骤,实现对 total
数据进行响应式变化:api
① 初始化一个 Set
类型的 dep
变量,用来存放须要执行的反作用( effect
函数),这边是修改 total
值的方法;
② 建立 track()
函数,用来将须要执行的反作用保存到 dep
变量中(也称收集反作用);
③ 建立 trigger()
函数,用来执行 dep
变量中的全部反作用;
在每次修改 price
或 quantity
后,调用 trigger()
函数执行全部反作用后, total
值将自动更新为最新值。
(图片来源:Vue Mastery)
一般,咱们的对象具备多个属性,而且每一个属性都须要本身的 dep
。咱们如何存储这些?好比:
let product = { price: 10, quantity: 2 };
从前面介绍咱们知道,咱们将全部反作用保存在一个 Set
集合中,而该集合不会有重复项,这里咱们引入一个 Map
类型集合(即 depsMap
),其 key
为对象的属性(如: price
属性), value
为前面保存反作用的 Set
集合(如: dep
对象),大体结构以下图:
(图片来源:Vue Mastery)
实现代码:
let product = { price: 10, quantity: 2 }, total = 0; const depsMap = new Map(); // ① const effect = () => { total = product.price * product.quantity }; const track = key => { // ② let dep = depsMap.get(key); if(!dep) { depsMap.set(key, (dep = new Set())); } dep.add(effect); } const trigger = key => { // ③ let dep = depsMap.get(key); if(dep) { dep.forEach( effect => effect() ); } }; track('price'); console.log(`total: ${total}`); // total: 0 effect(); console.log(`total: ${total}`); // total: 20 product.price = 20; trigger('price'); console.log(`total: ${total}`); // total: 40
上面代码经过 3 个步骤,实现对 total
数据进行响应式变化:
① 初始化一个 Map
类型的 depsMap
变量,用来保存每一个须要响应式变化的对象属性(key
为对象的属性, value
为前面 Set
集合);
② 建立 track()
函数,用来将须要执行的反作用保存到 depsMap
变量中对应的对象属性下(也称收集反作用);
③ 建立 trigger()
函数,用来执行 dep
变量中指定对象属性的全部反作用;
这样就实现监听对象的响应式变化,在 product
对象中的属性值发生变化, total
值也会跟着更新。
若是咱们有多个响应式数据,好比同时须要观察对象 a
和对象 b
的数据,那么又要如何跟踪每一个响应变化的对象?
这里咱们引入一个 WeakMap 类型的对象,将须要观察的对象做为 key
,值为前面用来保存对象属性的 Map 变量。代码以下:
let product = { price: 10, quantity: 2 }, total = 0; const targetMap = new WeakMap(); // ① 初始化 targetMap,保存观察对象 const effect = () => { total = product.price * product.quantity }; const track = (target, key) => { // ② 收集依赖 let depsMap = targetMap.get(target); if(!depsMap){ targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if(!dep) { depsMap.set(key, (dep = new Set())); } dep.add(effect); } const trigger = (target, key) => { // ③ 执行指定对象的指定属性的全部反作用 const depsMap = targetMap.get(target); if(!depsMap) return; let dep = depsMap.get(key); if(dep) { dep.forEach( effect => effect() ); } }; track(product, 'price'); console.log(`total: ${total}`); // total: 0 effect(); console.log(`total: ${total}`); // total: 20 product.price = 20; trigger(product, 'price'); console.log(`total: ${total}`); // total: 40
上面代码经过 3 个步骤,实现对 total
数据进行响应式变化:
① 初始化一个 WeakMap
类型的 targetMap
变量,用来要观察每一个响应式对象;
② 建立 track()
函数,用来将须要执行的反作用保存到指定对象( target
)的依赖中(也称收集反作用);
③ 建立 trigger()
函数,用来执行指定对象( target
)中指定属性( key
)的全部反作用;
这样就实现监听对象的响应式变化,在 product
对象中的属性值发生变化, total
值也会跟着更新。
大体流程以下图:
(图片来源:Vue Mastery)
在上一节内容中,介绍了如何在数据发生变化后,自动更新数据,但存在的问题是,每次须要手动经过触发 track()
函数搜集依赖,经过 trigger()
函数执行全部反作用,达到数据更新目的。
这一节未来解决这个问题,实现这两个函数自动调用。
这里咱们引入 JS 对象访问器的概念,解决办法以下:
track()
函数自动收集依赖;trigger()
函数执行全部反作用;那么如何拦截 GET 和 SET 操做?接下来看看 Vue2 和 Vue3 是如何实现的:
Object.defineProperty()
函数实现;Proxy
和 Reflect
API 实现;须要注意的是:Vue3 使用的 Proxy
和 Reflect
API 并不支持 IE。
Object.defineProperty()
函数这边就很少作介绍,能够阅读文档,下文将主要介绍 Proxy
和 Reflect
API。
一般咱们有三种方法读取一个对象的属性:
.
操做符:leo.name
;[]
: leo['name']
;Reflect
API: Reflect.get(leo, 'name')
。这三种方式输出结果相同。
Proxy 对象用于建立一个对象的代理,从而实现基本操做的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。语法以下:
const p = new Proxy(target, handler)
参数以下:
p
的行为。咱们经过官方文档,体验一下 Proxy API:
let product = { price: 10, quantity: 2 }; let proxiedProduct = new Proxy(product, { get(target, key){ console.log('正在读取的数据:',key); return target[key]; } }) console.log(proxiedProduct.price); // 正在读取的数据: price // 10
这样就保证咱们每次在读取 proxiedProduct.price
都会执行到其中代理的 get 处理函数。其过程以下:
(图片来源:Vue Mastery)
而后结合 Reflect 使用,只需修改 get 函数:
get(target, key, receiver){ console.log('正在读取的数据:',key); return Reflect.get(target, key, receiver); }
输出结果仍是同样。
接下来增长 set 函数,来拦截对象的修改操做:
let product = { price: 10, quantity: 2 }; let proxiedProduct = new Proxy(product, { get(target, key, receiver){ console.log('正在读取的数据:',key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver){ console.log('正在修改的数据:', key, ',值为:', value); return Reflect.set(target, key, value, receiver); } }) proxiedProduct.price = 20; console.log(proxiedProduct.price); // 正在修改的数据: price ,值为: 20 // 正在读取的数据: price // 20
这样便完成 get 和 set 函数来拦截对象的读取和修改的操做。为了方便对比 Vue 3 源码,咱们将上面代码抽象一层,使它看起来更像 Vue3 源码:
function reactive(target){ const handler = { // ① 封装统一处理函数对象 get(target, key, receiver){ console.log('正在读取的数据:',key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver){ console.log('正在修改的数据:', key, ',值为:', value); return Reflect.set(target, key, value, receiver); } } return new Proxy(target, handler); // ② 统一调用 Proxy API } let product = reactive({price: 10, quantity: 2}); // ③ 将对象转换为响应式对象 product.price = 20; console.log(product.price); // 正在修改的数据: price ,值为: 20 // 正在读取的数据: price // 20
这样输出结果仍然不变。
经过上面代码,咱们已经实现一个简单 reactive()
函数,用来将普通对象转换为响应式对象。可是还缺乏自动执行 track()
函数和 trigger()
函数,接下来修改上面代码:
const targetMap = new WeakMap(); let total = 0; const effect = () => { total = product.price * product.quantity }; const track = (target, key) => { let depsMap = targetMap.get(target); if(!depsMap){ targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if(!dep) { depsMap.set(key, (dep = new Set())); } dep.add(effect); } const trigger = (target, key) => { const depsMap = targetMap.get(target); if(!depsMap) return; let dep = depsMap.get(key); if(dep) { dep.forEach( effect => effect() ); } }; const reactive = (target) => { const handler = { get(target, key, receiver){ console.log('正在读取的数据:',key); const result = Reflect.get(target, key, receiver); track(target, key); // 自动调用 track 方法收集依赖 return result; }, set(target, key, value, receiver){ console.log('正在修改的数据:', key, ',值为:', value); const oldValue = target[key]; const result = Reflect.set(target, key, value, receiver); if(oldValue != result){ trigger(target, key); // 自动调用 trigger 方法执行依赖 } return result; } } return new Proxy(target, handler); } let product = reactive({price: 10, quantity: 2}); effect(); console.log(total); product.price = 20; console.log(total); // 正在读取的数据: price // 正在读取的数据: quantity // 20 // 正在修改的数据: price ,值为: 20 // 正在读取的数据: price // 正在读取的数据: quantity // 40
(图片来源:Vue Mastery)
在上一节代码中,还存在一个问题: track
函数中的依赖( effect
函数)是外部定义的,当依赖发生变化, track
函数收集依赖时都要手动修改其依赖的方法名。
好比如今的依赖为 foo
函数,就要修改 track
函数的逻辑,多是这样:
const foo = () => { /**/ }; const track = (target, key) => { // ② // ... dep.add(foo); }
那么如何解决这个问题呢?
接下来引入 activeEffect
变量,来保存当前运行的 effect 函数。
let activeEffect = null; const effect = eff => { activeEffect = eff; // 1. 将 eff 函数赋值给 activeEffect activeEffect(); // 2. 执行 activeEffect activeEffect = null;// 3. 重置 activeEffect }
而后在 track
函数中将 activeEffect
变量做为依赖:
const track = (target, key) => { if (activeEffect) { // 1. 判断当前是否有 activeEffect let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } dep.add(activeEffect); // 2. 添加 activeEffect 依赖 } }
使用方式修改成:
effect(() => { total = product.price * product.quantity });
这样就能够解决手动修改依赖的问题,这也是 Vue3 解决该问题的方法。完善一下测试代码后,以下:
const targetMap = new WeakMap(); let activeEffect = null; // 引入 activeEffect 变量 const effect = eff => { activeEffect = eff; // 1. 将反作用赋值给 activeEffect activeEffect(); // 2. 执行 activeEffect activeEffect = null;// 3. 重置 activeEffect } const track = (target, key) => { if (activeEffect) { // 1. 判断当前是否有 activeEffect let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } dep.add(activeEffect); // 2. 添加 activeEffect 依赖 } } const trigger = (target, key) => { const depsMap = targetMap.get(target); if (!depsMap) return; let dep = depsMap.get(key); if (dep) { dep.forEach(effect => effect()); } }; const reactive = (target) => { const handler = { get(target, key, receiver) { const result = Reflect.get(target, key, receiver); track(target, key); return result; }, set(target, key, value, receiver) { const oldValue = target[key]; const result = Reflect.set(target, key, value, receiver); if (oldValue != result) { trigger(target, key); } return result; } } return new Proxy(target, handler); } let product = reactive({ price: 10, quantity: 2 }); let total = 0, salePrice = 0; // 修改 effect 使用方式,将反作用做为参数传给 effect 方法 effect(() => { total = product.price * product.quantity }); effect(() => { salePrice = product.price * 0.9 }); console.log(total, salePrice); // 20 9 product.quantity = 5; console.log(total, salePrice); // 50 9 product.price = 20; console.log(total, salePrice); // 100 18
思考一下,若是把第一个 effect
函数中 product.price
换成 salePrice
会如何:
effect(() => { total = salePrice * product.quantity }); effect(() => { salePrice = product.price * 0.9 }); console.log(total, salePrice); // 0 9 product.quantity = 5; console.log(total, salePrice); // 45 9 product.price = 20; console.log(total, salePrice); // 45 18
获得的结果彻底不一样,由于 salePrice
并非响应式变化,而是须要调用第二个 effect
函数才会变化,也就是 product.price
变量值发生变化。
代码地址:
https://github.com/Code-Pop/vue-3-reactivity/blob/master/05-activeEffect.js
熟悉 Vue3 Composition API 的朋友可能会想到 Ref,它接收一个值,并返回一个响应式可变的 Ref 对象,其值能够经过 value
属性获取。
ref:接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具备指向内部值的单个 property .value。
官网的使用示例以下:
const count = ref(0) console.log(count.value) // 0 count.value++ console.log(count.value) // 1
咱们有 2 种方法实现 ref 函数:
rective
函数const ref = intialValue => reactive({value: intialValue});
这样是能够的,虽然 Vue3 不是这么实现。
const ref = raw => { const r = { get value(){ track(r, 'value'); return raw; }, set value(newVal){ raw = newVal; trigger(r, 'value'); } } return r; }
使用方式以下:
let product = reactive({ price: 10, quantity: 2 }); let total = 0, salePrice = ref(0); effect(() => { salePrice.value = product.price * 0.9 }); effect(() => { total = salePrice.value * product.quantity }); console.log(total, salePrice.value); // 18 9 product.quantity = 5; console.log(total, salePrice.value); // 45 9 product.price = 20; console.log(total, salePrice.value); // 90 18
在 Vue3 中 ref 实现的核心也是如此。
代码地址:
https://github.com/Code-Pop/vue-3-reactivity/blob/master/06-ref.js
用过 Vue 的同窗可能会好奇,上面的 salePrice
和 total
变量为何不使用 computed
方法呢?
没错,这个能够的,接下来一块儿实现个简单的 computed
方法。
const computed = getter => { let result = ref(); effect(() => result.value = getter()); return result; } let product = reactive({ price: 10, quantity: 2 }); let salePrice = computed(() => { return product.price * 0.9; }) let total = computed(() => { return salePrice.value * product.quantity; }) console.log(total.value, salePrice.value); product.quantity = 5; console.log(total.value, salePrice.value); product.price = 20; console.log(total.value, salePrice.value);
这里咱们将一个函数做为参数传入 computed
方法,computed
方法内经过 ref
方法构建一个 ref 对象,而后经过 effct
方法,将 getter
方法返回值做为 computed
方法的返回值。
这样咱们实现了个简单的 computed
方法,执行效果和前面同样。
这一节介绍如何去从 Vue 3 仓库打包一个 Reactivity 包来学习和使用。
准备流程以下:
git clone https://github.com/vuejs/vue-next.git
yarn install
yarn build reactivity
上一步构建完的内容,会保存在 packages/reactivity/dist
目录下,咱们只要在本身的学习 demo 中引入该目录的 reactivity.cjs.js 文件便可。
const { reactive, computed, effect } = require("./reactivity.cjs.js");
在源码的 packages/reactivity/src
目录下,有如下几个主要文件:
effect
/ track
/ trigger
;reactive
方法并建立 ES6 Proxy;
(图片来源:Vue Mastery)
本文带你们从头开始学习如何实现简单版 Vue 3 响应式,实现了 Vue3 Reactivity 中的核心方法( effect
/ track
/ trigger
/ computed
/ref
等方法),帮助你们了解其核心,提升项目开发效率和代码调试能力。
我是王平安,若是个人文章对你有帮助,请点个 赞👍🏻 支持我一下
个人公众号:前端自习课,每日清晨,享受一篇前端优秀文章。欢迎你们加入个人前端群,一块儿分享和交流技术,vx: pingan8787
。