Source: overall codesandbox. IO/s/reactivit…

What is reactive

Responsivity refers to the change of one state in response to another state.

For example, when the water in the water dispenser at home has been boiled, the indicator will change from red to yellow. As the state of water changes, the state of the indicator light will respond to its change and make corresponding adjustments.

In this example, if you do not execute sum manually every time obj. A and obj. B change, the sumValue will never change.

let obj = {
  a: 10.b: 20
}

let sumvalue = 0

function sum() {
  sumvalue = obj.a + obj.b
}

sum()
console.log(sumvalue) // output: 30

obj.a = 30

console.log(sumvalue) // output: 30
Copy the code

Logically speaking, SumValue is associated with A and B. If these two values change, SumValue will respond accordingly to obtain the latest calculated value. But in terms of code, this relationship is not established directly, but needs to be manually executed to update, so is there a way to make it automatically “respond”?

Let’s go ahead and introduce @vue/reactivity to try it out. Codepen codepen. IO /arronkler/p… View the effect (Open the console in the lower left corner and view it in the console)

import { effect, reactive } from '@vue/reactivity'

let obj = reactive({
  a: 10.b: 20
})

let sumvalue = 0

effect(function sum() {
  sumvalue = obj.a + obj.b
})

console.log(sumvalue) // output: 30

obj.a = 30

console.log(sumvalue) // output: 50
Copy the code

We have wrapped the OBj object and the sum function reactive and effect, respectively, making the obj a “reactive object” and the sum function a “side effect function”. When obj. A changes, we do not execute sum, but the value of sumValue changes by itself. In other words, sumValue responds to obj. How does it do that?

Sum accesses the a and B properties of the responder object obj during execution, and both properties are automatically associated with sum. When a or B changes, the sum side effect function is automatically reexecuted, and the sumvalue is evaluated and updated. The sum side effect function is the “response” to changes in a or B.

A response is the response of one state A to another state B. The response here is a verb, it’s a behavior, and it corresponds to the code in what we call the side effect function. It’s just that this function is changing our state A, so you can also say that state A is responding to state B.

What does a responsive module look like

Now that we know what responsiveness is, what do we need to implement responsiveness?

In the example above, we’ve introduced Reactive and effect, one for creating reactive objects and one for creating side effects, so you might say that the basic building blocks for reactive are made up of side effects and reactive objects. This is not entirely true. Responsive objects can be broken down into two core points, namely tracks and triggers. The reason for this split is that some modules we will implement in the future will not use Reactive directly, but will use a combination of track and trigger in a specific form, which you will understand more later.

So, what should a responsive module have, the most basic is the following three parts

  1. Effect side effect function
  2. Track Side effects Collector (Dependency Collection)
  3. Trigger Side effects trigger (send updates)

The main core implementation of responsiveness is the combination of side effect functions/side effect collectors/side effect triggers. Track records the side effect function that is currently being executed. Trigger is used to trigger the side effect that is being recorded. The so-called responsive object means that the track side effect is carried out when the attribute of the object is accessed, and then the trigger is executed when the attribute is changed. Such an object is combined with the side effect function, thus forming the responsive type.

All the other modules in Vue3’s Reactivity module are built around these three functions, including the well-known reactive/Ref/computed. At this point, in fact, our handwriting process is very clear, first implement effect and track/trigger, followed by a specific combination of these three functions. Looks like this feeling is a few simpler!

If you don’t understand the above, follow the steps below to write it down.

Write a reactivity module by hand

To prepare

You have to Get your hands dirty to really understand

Before writing the code, follow me to establish the basic structure of the project,

It is recommended to create a Vanilla Typescript project directly in CodesandBox. Our project will use Typescript, but these are very simple syntactical features that you can follow even if you are not familiar with Typescript. Come and try it!

This is the project structure I set up after I created the project on CodesandBox. You can create the directory as follows.

|- /src
    |- /reactivity  Create a reactivity directory
    |- index.ts
|- index.html
|- package.json
|- tsconfig.json
Copy the code

Effect function: A side effect function that responds to changes in data

Let’s start with the side effect function. A side effect function is actually a function that is executed when responsive data is changed. So how do we create a side effect function?

import { effect } from '@vue/reactivity'

let effectedFn = effect(() = > {
    // effect code will rerun when trigger
}, {
    lazy: true
})
Copy the code

As in the previous example, I pass in an arrow function with effect, which becomes a side function (effectedFn) when executed by effect (that is, the effect function returns the side function). The second argument provides some options. Here we provide lazy as false, which means that the arrow function is wrapped as effectedFn but will not execute at the moment (that is, lazy execution, or lazy execution). If true or left blank, It will be executed immediately when the side effect function is created. (This is important for later computations)

To implement this effect, create an effect.ts file in the reactivity directory.

// src/reactivity/effect.ts
// -------------------------

interface EffectFn<T = any> {
  (): T;
}

// Side effects stack, the top of which is currently active side effects, will be tracked by track
const effectStack: EffectFn<any= > [] [];// Create a side effect function
export function effect(fn: () => any, options? :any) :EffectFn {
  const effectFn: EffectFn = function () {
    if(! effectStack.includes(effectFn)) { effectStack.push(effectFn);let res = fn();
      effectStack.pop();
      
      return res
    }
  };

  if(! options? .lazy) {// Whether to delay execution
    effectFn();
  }

  return effectFn;
}
Copy the code

As you can see from the code, effect actually creates a new wrapper function, effectFn (which internally will actually call the function we passed in) and returns. When an effectFn is called, it pushes the current effect onto the effectStack and then pops it off the stack after executing the function.

The lazy option is easy to understand, but what does the effectStack do? Why do you need a Stack?

The answer: effecStack is used to track the current side effect function changes as effect is nested.

As we know, a stack is a linear data structure with lifO. Normal javascript functions are called stack, and when a function executes, the execution context for that function is created and pushed onto the function call stack (or execution context stack). When the function is finished, to go back to the previous function and continue with the subsequent code, it is only necessary to pop the current execution context out of the function call stack to restore the execution state of the previous function.

The effectStack is similar in that a normal function call tracks the nesting of functions through the stack, and the effectStack tracks the nesting of “side functions” through the properties of the stack.

In this case, another side effect is created in a side effect function execution:



/ / [0]
effect(function outer() {
    track() //【1】 track outer()

    effect(function inner() {
        track() // 【2】 track inner()
    })
    
    track() // 【3】 track outer()
})

/ / [4]

// Suppose there is a track function to track side effects
function track() {
    const currentEffect = effectStack[effecStack.length - 1]
    // do track
}

Copy the code

In the example above, the effectStack changes as follows:

In other words, when the track function is executed, the outer side effect function will be collected in both [1] and [3], while the inner side effect will be collected in [2], which is consistent with the expectation.

Computed/Watch, which we’ll talk about later, is actually based on this nested effect structure in the Vue project, so this effectStack is a nice trick.

Track and trigger: Collecting and triggering side effect functions

These two functions are used to collect and trigger the side effect function, which pushes itself into the effectStack during execution. This is where track comes in, again in the effect.ts file

// src/reactivity/effect.ts
// -------------------------

/ /...

// Get the side effect function at the top of the current stack
function getCurrentEffect() {
  return effectStack[effectStack.length - 1];
}

// Record the side effect function on which a key in the target object depends
const targetMap = new WeakMap(a);export type ObjKeyType = string | number | symbol;

// Establish a side effect tracking relationship
export function track(target: any, key: ObjKeyType) {
  if (effectStack.length === 0) return;

  // The key dependency table of the current object
  let depMap = targetMap.get(target);
  if(! depMap) { depMap =new Map(a); targetMap.set(target, depMap); }// The current object, the current key, the corresponding side effect set
  let deps = depMap.get(key);
  if(! deps) { deps =new Set<EffectFn>();
    depMap.set(key, deps);
  }

  // Add top of stack side effects as dependencies
  deps.add(getCurrentEffect());
}
Copy the code

The track function is easy to understand because it records the side effects of a particular key on an object. The important thing to understand here is that targetMap is the data structure that stores the side effects function on which track and trigger are run.

  • TargetMap itself is a WeakMap object whose key is an object (to be set as responsive) and value is a Map (let’s say called keyMap)
  • The keyMap itself is a Map object, where the key is the attribute name of the responsive object and the value is a Set (let’s say called depSet)
  • A depSet is a collection that contains side effects, and the nature of the Set makes it non-repetitive

TS in Vue3 source code is described as follows:

// {target -> key -> dep}
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
Copy the code

So, by executing track in the side effect function, you can associate an object’s key with the side effect function, and then simply “respond” by pulling out the side effect function at the appropriate time and executing it again. Let’s first write the trigger that executes the “response” and then try to use it together.

// src/reactivity/effect.ts
// -------------------------

/ /...


// Trigger side effects
export function trigger(target: any, key: ObjKeyType) {
  const depsMap = targetMap.get(target);
  if(! depsMap) {// Skip without track
    return;
  }

  let deps = depsMap.get(key);
  if (deps) {
    deps.forEach((efn: EffectFn) = >efn()); }}Copy the code

Trigger is a simple action that executes all side effects associated with a particular key of a particular object.

Let’s test this by combining effect, trigger, and track in the index.ts file

// src/index.ts
// ---------------

import { effect, track, trigger } from "./reactivity/effect";

// Create a normal object
let o: any = {
  _uid: 0
};

// Define a uid attribute on it and do track in getter and trigger in setter
Object.defineProperty(o, "uid", {
  get() {
    track(o, "uid");
    return this._uid;
  },
  set(value) {
    this._uid = value;
    trigger(o, "uid"); }}); effect(() = > {
  console.log("uid:" + o.uid);
});

o.uid = 20
o.uid = 30

/* console output uid:0 uid:20 uid:30 */
Copy the code

After execution, we can see in the console that the function passed in 0, 20, 30, effect is executed three times. The first is passed in, where we access O.ID and thus fire its getter, where we track the side effect function. We then assign o.id to trigger via its setter, which triggers the side effect function (the function that runs console.log) that was just tracked. So that’s the basic response.

Give some exercises:

  • Try it onconsole.logThe place of theo.uidtoo._uidAnd see if you can still respond?
  • Pass the lazy option to effect and see if it works? How do you get it to work if it doesn’t work?

Reactive: Upgrades ordinary objects to responsive objects

DefineProperty (object.defineProperty) {defineProperty (object.defineProperty) {defineProperty (object.defineProperty); Yes, that’s what Vue2 does. However, Vue3 uses Proxy to replace this traversal method and directly Proxy the entire object, which is convenient and fast. And there are more benefits:

  1. Delay some code execution.Object.definePropertyYou need to deeply traverse the code from the start
  2. New attributes added can also respond.

So let’s define reactive!

Create a react.ts file in the Reactivity folder and write the following code

// src/reactivity/reactive.ts
// ---------------

import { track, trigger } from "./effect";

export function reactive(obj: any) {
  const proxy = new Proxy(obj, {
    // Access properties
    get(target, key, receiver) {
      track(target, key);

      let res = Reflect.get(target, key);
      
      // If the value is an object, the depth is converted to a responsive object
      if (Object.prototype.toString.call(res) === "[object Object]") {
        reactive(res);
      }

      return res;
    },
    
    // Modify attributes
    set: function (target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      returnres; }});return proxy;
}

Copy the code

Isn’t reactive very simple? We just use Proxy to make a layer of Proxy for the incoming object. When accessing its properties, we execute track defined before to collect side effects, and when modifying property values, trigger triggers the collected side effects function. This ordinary object is transformed into a reactive object, and we return the propped reactive object. Then, wherever only the reactive object is used, we have the ability to automatically collect side effects and trigger side effects.

Reactive, as we said earlier, is simply a specific combination of track and trigger.

Value: If the proxy gets a value that is also an object, we will recursively call reactive on that object to make it responsive. (shallowReactive in VUe3 omits this step)

Try it out in index.ts

// src/index.ts
// ---------------

import { effect } from "./reactivity/effect";
import { reactive } from "./reactivity/reactive";

let sum = 0
let obj = reactive({
  a: 0.b: 20
});

effect(function summary() = >{
  sum = obj.a + obj.b
  console.log(sum);
});

obj.a = 20;
obj.b = 40;


/* console output 30 40 60 */
Copy the code

The summary function is executed once when it is defined as a side effect function, printing 30, and then executed automatically when obj. A and obJ. B change, resulting in 40 and 60.

Ref: The original value becomes the syntactic sugar of a responsive object

Getting a raw value to Reflect is not as easy as getting an object to Reflect. This is due to language features. We can use Proxy/Reflect to “metaprogram” an object in JS (that is, change its default behavior), but there is no way to change its default behavior.

So as you might imagine, using ref in VUe3 gives you a raw value, but it returns an object, and you have to go through.value to get the real value. It would be too easy to manipulate objects to make them responsive, since we’ve already implemented one above.

We can use the accessor property as a value directly, as follows:

// src/reactive/ref.ts
// ---------------

import { track, trigger } from "./effect";

export function ref(value? :any) {
  let res = {
    get value() {
      track(res, "value");
      return value;
    },
    set value(v: any) {
      value = v;
      trigger(res, "value"); }};return res;
}
Copy the code

Let’s try out the ref in the index.ts file by clearing it and writing the following code:

// src/index.ts
// ---------------

import { effect } from "./reactivity/effect";
import { ref } from "./reactivity/ref";

let obj = ref(10);

effect(() = > {
  console.log(obj.value);
});

obj.value = 20;
obj.value = 30;

/* console output 10 20 30 */

Copy the code

We found that the side effect function was executed three times, with the initial value of 10 and later values of 20 and 30 after changing the value, which is the same result as when we used the same module in VUe3.

This completes the transformation of an ordinary value into a reactive value.

Computed: The value of a responsive object that responds to another

Take a look at a very useful and popular function, computed, that allows one value to respond to changes in one or more values. The reactive style we mentioned earlier mainly involves executing specific side effects when the properties of a reactive object change, rather than having one value respond directly to another value change. With computed, you can think about how, if you could do that, you could do it.

This time we will do the reverse, first write the test code in index.ts:

// src/index.ts
// ---------------

import { reactive } from "./reactivity/reactive";
import { computed } from "./reactivity/computed";
import { effect } from "./reactivity/effect";

let obj = reactive({
  a: 10.b: 20
});

let sum = computed(function summary() {
  return obj.a + obj.b;
});

effect(() = > {
  console.log("sum is: ", sum.value);
});

obj.a = 20;
obj.b = 30;


/* expected console output sum is: 30 sum is: 40 sum is: 50 */
Copy the code

Looking at the code above, we can at least think about the following:

  1. You can do whatever you want in the summary function, but ultimately you need to return a value, and that’s what sum is going to be
  2. The sum value changes dynamically, and if it’s just a raw value, it doesn’t change dynamically anyway, so the computed value has to be an object value, right
  3. The return value of computed is not just an ordinary object, but should also be responsive, otherwise the side effects function that uses computed values cannot respond when computed values change
  4. The incoming summary is automatically executed when the responsive object changes, and it must be wrapped up in the side effects function, otherwise it cannot be traced and triggered

Let’s see, with these ideas, can you write a computed algorithm?

Create a computed. Ts file in the reactivity directory, write it yourself, and then look at the code below!

// src/reactivity/computed.ts
// ---------------

import { effect, track, trigger } from "./effect";

export function computed(getter: any) {
  let value: any = null;
  let res: any = null;

  effect(function inner() = >{
    value = getter();
    trigger(res, "value");
  });

  res = {
    get value() {
      track(this."value");
      returnvalue; }};return res;
}
Copy the code

This implementation actually satisfies the characteristics we analyzed above, as well as the test code we wrote in index.ts, and produces the desired output.

The getter that is passed in is wrapped with an effect, so that it can get the value returned by the getter. When the reactive object in the getter changes, the side effect function inner will be run again and get the new value. This keeps computed values responsive all the time.

In addition, we built an res object, provided only a get value() method to return our value above, and did a dependency collection to collect side effects that use res.value. Then trigger the side effects in inner, the side effects function that wraps the getter on top, so that the current computed value change triggers the external side effects function.

conclusion

At this point, a mini version of VUe3’s responsive module is pretty much implemented. I believe that after reading this article and doing it again, you have a basic understanding of vue3 responsive module is how to achieve, this time to look at vue3 responsive module actual source code, the problem is not big.

In this paper, the overall source: codesandbox. IO/s/reactivit…


Other articles:

  • How to build a Vue component library: Arronkler. Top /post/build_…
  • The evolution of the browser process architecture: arronkler. Top/post/browse…
  • Network request resources optimization: arronkler. Top/post/networ…