The responsive principle in Vue 3 is very important. By learning the responsive principle of Vue3, not only can we learn some design patterns and ideas of , but 160d531af61dc0 can help us improve project development efficiency and code debugging capabilities. .
Before this, I also wrote an article "Exploring the Principles of Vue.js Responsiveness" , which mainly introduces the principle of Vue 2 responsiveness, and this article supplements Vue 3.
So I recently re-learned Vue3 Reactivity on Vue Mastery, and this time I learned more. This article will take you to learn how to implement a simple version of Vue 3 responsive from the beginning to help you understand its core. Later, reading the source code related to Vue 3 responsive will be more handy.
1. Responsive use of Vue 3
1. Usage in Vue 3
When we are learning Vue 3, we can use a simple example to see what is responsive in Vue 3:
<!-- 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 实例
Just create an APP instance and mount an APP instance. At this time, you can see that the corresponding values are displayed on the page:
When we modify the price
or quantity
, where they are referenced on the page, the content can also display the changed results normally. At this time, we will be wondering why the relevant data will also change after the data changes, so we will continue to look down.
2. Realize the responsiveness of a single value
In the execution of ordinary JS code, there will be no responsive changes. For example, execute the following code in the console:
let price = 10, quantity = 2;
const total = price * quantity;
console.log(`total: ${total}`); // total: 20
price = 20;
console.log(`total: ${total}`); // total: 20
It can be seen from this that after modifying price
variable, the value of total
has not changed.
So how to modify the above code so that total
can update automatically? In fact, we can modify total
saved value method, until the total
variables (such as the correlation value price
or quantity
when changes in the value of the variable) occurs, the trigger method, update total
can. We can achieve this:
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
The above code realizes the responsive change total
① Initialize a Set
variable of type dep
, which is used to store the side effect ( effect
function) that needs to be executed. Here is the method to total
② Create the track()
function to save the side effects that need to be executed in the dep
variable (also called collection side effects);
③ Create the trigger()
function to execute all the side effects in the dep
After each modification to price
or quantity
after calling the trigger()
function to execute all side effects, the total
will be automatically updated to the latest value.
(Image source: Vue Mastery)
3. Realize the responsiveness of a single object
Usually, our object has multiple attributes, and each attribute needs its own dep
. How do we store these? For example:
let product = { price: 10, quantity: 2 };
From the previous introduction, we know that we store all the side effects in a Set
collection, and the collection will not have duplicates. Here we introduce a Map
type collection (that is, depsMap
), whose key
is an object attribute (such as: price
attribute) , value
Set
collection (such as: dep
object) that saved the side effects before, the general structure is as follows:
(Image source: Vue Mastery)
Implementation code:
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
The above code implements responsive changes total
① Initialize a Map
variable of type depsMap
, which is used to store the attributes of each object that needs to be changed in response ( key
is the attribute of the object, and value
is the previous Set
collection);
② Create the track()
function to save the side effects that need to be executed depsMap
variable (also called collection side effects);
③ Create the trigger()
function to execute all the side effects of the specified object properties in the dep
In this way, the responsive change of the monitoring object is realized, product
object changes, and the total
will be updated accordingly.
4. Realize the responsiveness of multiple objects
If we have multiple responsive data, for example, we need to observe a
and b
at the same time, then how to track each object that responds to changes?
Here we introduce an WeakMap type , and set the object to be observed as key
, the value of which is the Map variable used to store the properties of the object. code show as below:
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
The above code implements responsive changes total
① Initialize a WeakMap
variable of type targetMap
to observe each reactive object;
② Create the track()
function to save the side effects that need to be executed into target
) (also called collection side effects);
③ Create the trigger()
function to execute all the side effects of the specified attribute ( key
target
In this way, the responsive change of the monitoring object is realized, product
object changes, and the total
will be updated accordingly.
The general process is as follows:
(Image source: Vue Mastery)
Two, Proxy and Reflect
In the previous section, how to automatically update the data after the data changes, but the problem is that each time you need to manually track()
function, and trigger()
function to achieve the purpose of data update.
This section will solve this problem in the future and realize the automatic call of these two functions.
1. How to achieve automatic operation
Here we introduce the concept of JS object accessor, the solution is as follows:
- When reading (GET operation) data, automatically execute the
track()
function to automatically collect dependencies; - When modifying (SET operation) data, automatically execute the
trigger()
function to execute all side effects;
So how to intercept GET and SET operations? Next, take a look at how Vue2 and Vue3 are implemented:
- In Vue2, use ES5's
Object.defineProperty()
function to achieve; - In Vue3, use ES6's
Proxy
andReflect
API implementation;
It should be noted that the Proxy
and Reflect
APIs used by Vue3 do not support IE.
Object.defineProperty()
function is not introduced here, you can read the document, the following will mainly introduce Proxy
and Reflect
API.
2. How to use Reflect
Usually we have three methods to read the properties of an object:
- Use the
.
operator:leo.name
; - Use
[]
:leo['name']
; - Use
Reflect
API:Reflect.get(leo, 'name')
.
The output results of these three methods are the same.
3. How to use Proxy
The Proxy object is used to create a proxy for an object, so as to realize the interception and customization of basic operations (such as attribute lookup, assignment, enumeration, function call, etc.). The syntax is as follows:
const p = new Proxy(target, handler)
The parameters are as follows:
- target: The target object to be wrapped by Proxy (it can be any type of object, including native arrays, functions, or even another proxy).
- handler: An object that usually takes a function as an attribute. The function in each attribute defines the behavior of the
p
Let's experience the Proxy API through official documents:
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
This ensures that every time we read proxiedProduct.price
will execute the get processing function of the proxy. The process is as follows:
(Image source: Vue Mastery)
Then combined with Reflect, just modify the get function:
get(target, key, receiver){
console.log('正在读取的数据:',key);
return Reflect.get(target, key, receiver);
}
The output result is still the same.
Next, add the set function to intercept object modification operations:
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
This completes the get and set functions to intercept the operation of reading and modifying the object. In order to facilitate the comparison of the Vue 3 source code, we abstract the above code to make it look more like the Vue 3 source code:
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
In this way, the output result remains unchanged.
4. Modify track and trigger functions
Through the above code, we have implemented a simple reactive()
function, used to convert a normal object . But there is still a lack of automatic execution of track()
and trigger()
functions. Next, modify the above code:
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
(Image source: Vue Mastery)
Three, activeEffect and ref
On a section of code, there is also a problem: track
function dependent ( effect
function) is defined externally, when a change occurs dependent, track
must manually modify the name of the method which relies upon the collection function dependencies.
Such as the current is dependent foo
function, it is necessary to modify track
logic function, it may be so:
const foo = () => { /**/ };
const track = (target, key) => { // ②
// ...
dep.add(foo);
}
So how to solve this problem?
1. Introduce activeEffect variable
Next, introduce the activeEffect
variable to save the currently running effect function.
let activeEffect = null;
const effect = eff => {
activeEffect = eff; // 1. 将 eff 函数赋值给 activeEffect
activeEffect(); // 2. 执行 activeEffect
activeEffect = null;// 3. 重置 activeEffect
}
Then use the activeEffect
variable as a dependency in the track
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 依赖
}
}
The usage is modified as follows:
effect(() => {
total = product.price * product.quantity
});
This can solve the problem of manually modifying dependencies, which is also the way Vue3 solves this problem. After perfecting the test code, it is as follows:
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
Consider, if the first effect
function product.price
replaced salePrice
how:
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
The results obtained are completely different, because salePrice
not a responsive change, but needs to call the second effect
function to change, that is, the value of the product.price
Code address:
https://github.com/Code-Pop/vue-3-reactivity/blob/master/05-activeEffect.js
2. Introduce ref method
Friends who are familiar with the Vue3 Composition API may think of Ref, which receives a value and returns a responsive variable Ref object , whose value can be obtained through the value
attribute.
ref: accepts an internal value and returns a responsive and variable ref object. The ref object has a single property .value that points to an internal value.
Examples of using the official website are as follows:
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
We have 2 ways to implement the ref function:
- uses
rective
function
const ref = intialValue => reactive({value: intialValue});
This is possible, although Vue3 does not implement this.
- Use the object's property accessor (calculated properties)
Attribute methods include: getter and setter .
const ref = raw => {
const r = {
get value(){
track(r, 'value');
return raw;
},
set value(newVal){
raw = newVal;
trigger(r, 'value');
}
}
return r;
}
The usage is as follows:
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
The same is true for the core of ref implementation in Vue3.
Code address:
https://github.com/Code-Pop/vue-3-reactivity/blob/master/06-ref.js
Fourth, implement a simple Computed method
Students who have used Vue may be curious, why don’t the salePrice
and total
computed
method?
computed
, and then we will implement a simple 060d531af665e1 method together.
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);
Here we will function as an argument passed computed
method computed
by the method ref
constructing a ref object method, then effct
method, getter
method return value as computed
return value of the method.
In this way, we have implemented a simple computed
method with the same effect as before.
Five, source code learning suggestions
1. Build reactivity.cjs.js
This section introduces how to Vue 3 warehouse to learn and use.
The preparation process is as follows:
- Download the latest Vue3 source code from Vue 3 warehouse
git clone https://github.com/vuejs/vue-next.git
- Installation dependencies:
yarn install
- Build the Reactivity code:
yarn build reactivity
- Copy reactivity.cjs.js to your learning demo directory:
The content built in the previous step will be saved in the packages/reactivity/dist
directory, and we only need to import the reactivity.cjs.js file in this directory in our learning demo.
- Introduced in the learning demo:
const { reactive, computed, effect } = require("./reactivity.cjs.js");
2. Vue3 Reactivity file directory
In the packages/reactivity/src
directory of the source code, there are the following main files:
- effect.ts: used to define
effect
/track
/trigger
; - baseHandlers.ts: define Proxy handlers (get and set);
- reactive.ts: define
reactive
method and create ES6 Proxy; - ref.ts: defines the object accessor used by the reactive ref;
- computed.ts: the method of defining computed properties;
(Image source: Vue Mastery)
Six, summary
This article takes you from the beginning to learn how to implement a simple version of Vue 3 responsive, and implements the core methods in Vue3 Reactivity ( effect
/ track
/ trigger
/ computed
/ ref
and other methods), to help you understand its core, improve project development efficiency and code Debugging capability .
Reference article
Recommended in the past
I’m Wang Pingan, if my article is helpful to you, please give me a thumbs up 👍🏻 Support me
My official account: front-end self-study class, every morning, enjoy an excellent front-end article. Welcome everyone to join my front-end group, share and exchange technology together, vx: pingan8787
.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。