The project has been initialized through the previous article and integrated ts
and jest
. This article implements the reactive
method in the reactive module in Vue3.
Prerequisite knowledge
If you are proficient in Map, Set, Proxy, and Reflect, you can skip this part directly.
Map
Map is a collection used to store key-value pairs and remember the original insertion order of the keys. The keys and values can be any type of data.
Initialize, add, get
let myMap = new Map() myMap.set('name', 'wendZzoo') myMap.set('age', 18) myMap.get('name') myMap.get('age')
A key in the Map can only appear once. It is unique in the Map collection. Repeated settings will be overwritten.
myMap.set('name', 'jack')
Map keys and values can be any type of data
myMap.set({<!-- -->name: 'wendZzoo'}, [{<!-- -->age: 18}])
Delete
let myMap = new Map() myMap.set('name', 'Tom') myMap.delete('name')
When the key
data type is an object, you need to use the corresponding reference to delete the key-value pair
let myMap = new Map() let key = [{<!-- -->name: 'Tom'}] myMap.set(key, 'Hello') myMap.delete(key) // If using a different reference try deleting the key-value pair // it won't work properly // Because Map cannot recognize that these two references are the same key myMap.set([{<!-- -->name: 'Tom'}], 'Hello') myMap.delete([{<!-- -->name: 'Tom'}])
Set
Set is a collection data structure that allows storing unique values without duplicates. Set objects can store any type of value, including primitive types and object references.
let mySet = new Set() mySet.add('wendZzoo') mySet.add(18) mySet.add({<!-- -->province: 'jiangsu', city: 'suzhou'})
Iterable
for (let key of mySet) {<!-- --> console.log(key) }
Proxy
The Proxy object is used to create a proxy for an object to implement interception and customization of basic operations (such as property lookup, assignment, enumeration, function calling, etc.).
The premise of Vue’s responsiveness is the need for data hijacking. There are two ways to hijack property access in JS: getters/setters and Proxies. Vue 2 uses getters/setters purely to support the limitations of older browsers, while in Vue 3 Proxy is used to create responsive objects.
When creating a Proxy object, you need to provide two parameters: the target object target (the object being proxied) and a handler object handler (the method used to define the interception behavior).
Among them, the commonly used handler methods are get and set.
The handler.get() method is used to intercept the read attribute operation of the object. For complete usage, please refer to: MDN
It receives three parameters:
- target: target object
- property: the obtained property name
- receiver: Proxy or object inheriting Proxy
const obj = {<!-- -->name: 'wendZzoo', age: 18} let myProxy = new Proxy(obj, {<!-- --> get: (target, property, receiver) => {<!-- --> console.log('Collect dependencies') return target[property] } }) //Execute myProxy.name //Execute myProxy.age
The handler.set() method is a capturer for setting attribute values. For complete usage reference: MDN
It receives four parameters
- target: target object
- property: the property name or Symbol to be set,
- value: new attribute value
- receiver: the object that was initially called. Usually the proxy itself, but the handler’s set method may also be called indirectly on the prototype chain or in other ways (so not necessarily the proxy itself)
const obj = {<!-- -->name: 'wendZzoo', age: 18} let myProxy = new Proxy(obj, {<!-- --> get: (target, property, receiver) => {<!-- --> console.log('Collect dependencies') return target[property] }, set: (target, property, value, receiver) => {<!-- --> console.log('trigger dependency') target[property] = value return true } }) myProxy.name = 'Jack' myProxy.age = 20
Proxy provides a mechanism to implement custom behaviors by intercepting and modifying the operations of the target object. Where the get and set methods print logs, that is where Vue3 implements dependency collection and triggers dependencies.
Reflect
Reflect is a built-in object that provides methods to intercept JS operations. This allows it to work perfectly with Proxy. Proxy provides the timing and location for object interception, and Reflect provides interception methods.
Reflect is not a constructor, so it cannot be called with new. It is more like a Math object, called as a function, and all its properties and methods are static.
Commonly used methods include get and set.
The Reflect.get method allows you to get the property value from an object. For complete usage reference: MDN
It receives three parameters:
- target: the target object that needs a value
- propertyKey: the key value of the value to be obtained
- receiver: If a getter is specified in the target object, the receiver is the this value when the getter is called.
let obj = {<!-- -->name: 'wendZzoo', age: 18} Reflect.get(obj, 'name') Reflect.get(obj, 'age')
The Reflect.set method allows setting properties on an object. For complete usage reference: MDN
It receives three parameters:
- target: the target object to set the attribute
- propertyKey: the name of the property being set
- value: set value
- Receiver: If a setter is encountered, the receiver is the this value when the setter is called.
let obj = {<!-- -->} Reflect.set(obj, 'name', 'wendZzoo') let arr = ['name', 'address'] Reflect.set(arr, 1, 'age') Reflect.set(arr, 'length', 1)
Change directory
Create a new folder reactivity
under src
, and create new effect.ts
and reactive.ts
.
Delete the index.spec.ts
used to verify the installation of jest
in the previous article under the tests
folder, and create a new effect.spec .ts
and reactive.spec.ts
.
reactive
First write a single test to clarify the required results, and then implement the function based on this requirement. Vue3’s reactive
method returns a responsive proxy of an object. The proxy object is different from the source object, but it can have the same nested structure as the source object.
The single test can be written like this: reactive.spec.ts
import {<!-- --> reactive } from "../reactivity/reactive"; describe("reactive", () => {<!-- --> it("happy path", () => {<!-- --> let original = {<!-- --> foo: 1 }; let data = reactive(original); expect(data).not.toBe(original); expect(data.foo).toBe(1); }); });
Based on these two assertions, the current reactive
method is implemented. Vue3 is implemented using Proxy.
reactive.ts
export function reactive(raw) {<!-- --> return new Proxy(raw, {<!-- --> get: (target, key) => {<!-- --> let res = Reflect.get(target, key); // TODO dependency collection return res; }, set: (target, key, value) => {<!-- --> let res = Reflect.set(target, key, value); // TODO trigger dependency return res; }, }); }
Run reactive
single test to verify whether the method is implemented correctly. Execute yarn test reactive
effect
This API is not mentioned separately on the official website. You can find it in the in-depth responsive system article in the advanced topic.
effect
is directly translated as effect, which means to make it work. This makes it the function we pass in, so the function of effect
is the function we pass in. To take effect, that is, to execute this function.
Usage examples
import {<!-- --> reactive, effect } from "vue"; let user = reactive({<!-- --> age: 10, }); let nextAge; function setAge() {<!-- --> effect(() => {<!-- --> nextAge = user.age + 1; }); console.log(nextAge); } function updateAge() {<!-- --> user.age + + ; console.log(nextAge); }
When effect
is not used to act on nextAge
, the updateAge
method is directly triggered, and the output nextAge
is undefined
;
Call setAge
, execute the function in effect
and assign a value to nextAge
, and age
in responsive data user
code> changes, nextAge
also continues to execute the function in effect
.
Single test
Then the single test of effect
can be written like this:
import {<!-- --> effect } from "../reactivity/effect"; import {<!-- --> reactive } from "../reactivity/reactive"; describe("effect", () => {<!-- --> it("happy path", () => {<!-- --> let user = reactive({<!-- --> age: 10, }); let nextAge; effect(() => {<!-- --> nextAge = user.age + 1; }); expect(nextAge).toBe(11); }); });
The effect
method receives a method and executes it.
effect.ts
class ReactiveEffect {<!-- --> private _fn: any; constructor(fn) {<!-- --> this._fn = fn; } run() {<!-- --> this._fn(); } } export function effect(fn) {<!-- --> let _effect = new ReactiveEffect(fn); _effect.run(); }
Execute the passed in fn
parameters by extracting it into a Class
class.
Then execute all single tests to verify whether they are successful and execute yarn test
Dependency collection
Modify the effect
single test and add an assertion to determine whether nextAge
is also updated when age
changes?
import {<!-- --> effect } from "../reactivity/effect"; import {<!-- --> reactive } from "../reactivity/reactive"; describe("effect", () => {<!-- --> it("happy path", () => {<!-- --> let user = reactive({<!-- --> age: 10, }); let nextAge; effect(() => {<!-- --> nextAge = user.age + 1; }); expect(nextAge).toBe(11); // + + + updater user.age + + ; expect(nextAge).toBe(12); }); });
When executing the single test, it was found that it failed because Proxy
did not implement dependency collection and triggering dependencies, that is, there were two TODOs in reactive.ts
.
However, we must first understand what dependence is?
Quoting official examples:
let A0 = 1 let A1 = 2 let A2 = A0 + A1 console.log(A2) // 3 A0=2 console.log(A2) // Still 3
When we change A0, A2 does not update automatically.
So how do we do this in JavaScript? First, in order to be able to rerun the calculated code to update A2, we need to wrap it into a function:
let A2 function update() {<!-- --> A2 = A0 + A1 }
Then, we need to define a few terms:
- The update() function has a side effect, or simply an effect, because it changes the state of the program.
- A0 and A1 are considered dependencies of this action because their values are used to perform this action. Therefore, this function can also be said to be a subscriber that it depends on.
Therefore, we can boldly and informally say that dependence refers to the dependence of an observer (usually a view or a side effect function) on data. When an observer needs access to specific data, it becomes a dependency on that data.
What about Dependency Collection?
Dependency collection is used to track and manage data dependencies. Often used to implement reactive systems, where changes in data automatically trigger related update operations.
When data changes, related views or operations can be automatically updated to keep the data and interface synchronized. Dependent collection can help us establish the association between data and views to ensure that data changes are automatically reflected in the views.
From a code level, when reading an object, that is, during a get operation, dependencies are collected and the target object, key in the object, and Dep instance are associated and mapped.
Define the dependency collection method track
in effect.ts
.
class ReactiveEffect {<!-- --> private _fn: any; constructor(fn) {<!-- --> this._fn = fn; } run() {<!-- --> reactiveEffect = this; this._fn(); } } let targetMap = new Map(); export function track(target, key) {<!-- --> // target -> key -> dep let depMap = targetMap.get(target); if (!depMap) {<!-- --> // init depMap = new Map(); targetMap.set(target, depMap); } let dep = depMap.get(key); if (!dep) {<!-- --> // init dep = new Set(); depMap.set(key, dep); } dep.add(reactiveEffect); } let reactiveEffect; export function effect(fn) {<!-- --> let _effect = new ReactiveEffect(fn); _effect.run(); }
Trigger dependencies
Dependencies are triggered when setting object properties, that is, when performing a set
operation. Execute all function functions in the Set
structure of the dep
mounted on each attribute.
export function trigger(target, key) {<!-- --> let depMap = targetMap.get(target); let dep = depMap.get(key); for (const effect of dep) {<!-- --> effect.run(); } }
At this point, execute all single tests again, yarn test
Summary
- Start with a single test to clarify the functions of the function methods that need to be implemented.
- To distribute and implement function points, that is, to split function points, a simple version of the
reactive
method is initially implemented. It only requires that the original data and the data after the proxy are different, but the data structure must be the same, like a deep copy. - Through the
Class
class, theeffect
method can self-execute its passed function parameters. - Dependency collection, mapping data relationships through two
Map
structures and oneSet
structure, and storing allfn
indep
. Theeffct
instance is obtained through a global variablereactiveEffect
. When dependencies are triggered later, each item indep
is directly executed. - Trigger dependency, obtain
dep
through mapping relationship, becausedep
is aSet
structure, iterable, and loop each execution.