Writing in the front

Vuejs, as a standout among many MVVM(Model-view-ViewModel) frameworks, is undoubtedly worth deep learning for any front-end developer.

No doubt VueJs has many contents worthy of further study, but the core data-responsive Reactive module is the high-end related content in our daily work and one of the core contents in VueJs.

I won’t bother you here about how important the responsivity principle in Vuejs is. I’m sure you all understand its importance.

But WHAT I want to emphasize here is that the so-called responsive principle is essentially a sublimation implementation based on Js code. You may find it difficult, but it is only because you do not know him.

After all, as long as you’re familiar with JavaScript, it shouldn’t be too much of a problem.

Today we are going to talk a little bit about how Reactive is the core module of Vuejs based on the latest Vuejs 3.2.

Front knowledge

  • ES6 Proxy & Reflect

Proxy is the Api that ES6 provides for us to hijack raw objects, and Reflect’s built-in Api also provides interception operations for raw objects.

Here we mainly use their get and set traps.

  • Typescript

TypeScript is self-explanatory; I’ll use TypeScript to write the code in this article.

  • Esbuild

EsBuild is a new bundle build tools that uses Go internally to package and integrate our code.

  • Pnpm

PNPM is an excellent package management tool, and here we will mainly use it to implement Monorepo.

If you haven’t already installed PNPM on your computer, it is easy to install PNPM. Just one line NPM install -g PNPM.

Set up the environment

To do a good job, he must sharpen his tools. We’ll start by building a crude development environment that makes it easy to build our TypeScript into Iife and make it available for direct use in the browser.

Since the article focuses on the responsive part of the content, building the environment is not our focus. So I’m not going to go into the details of setting up a build environment.

If you are interested, you can follow me to build this simple organization structure. If you don’t want to do it, that’s fine. Our focus will be on the code that follows.

Initialize the project directory

First we create a simple folder named vue and execute PNPM init -y to initialize package.json.

Next we create:

  • pnpm-workspace.yamlfile

This is a YAML configuration file for the PNPM implementation of Monorepo, which we will fill in slightly.

  • .npmrcfile

This is the configuration file for NPM.

  • packages/reactivitydirectory

We will implement the core responsive principle code in this directory. As mentioned above, the VUe3 directory architecture is based on the Monorepo structure, so this is a separate directory of modules for maintaining responsive dependencies.

Of course, the contents of each package can be treated as a separate project, so they initialize their package.json with PNPM init-y in the reactivity directory.

Also create new Packages /reactivity/ SRC files as source code for the reactivity module.

  • packages/sharedirectory

Also, like its folder name, this directory holds all vuejs tool methods and shares them with other modules for introduction.

It needs to maintain the same directory structure as ReActivity.

  • scripts/build.jsfile

We need to create an additional scripts folder and create scripts/build.js to store the script files at build time.

At this point, the directory looks like the figure.

Install dependencies

Now let’s install the dependency environments we need to use in turn, before we start installing the dependencies. Let’s start by populating the corresponding.npmrc file:

shamefully-hoist = true
Copy the code

By default PNPM installed dependencies will resolve the issue of ghost dependencies. Check out this article on ghost dependencies.

Here we configure shamefully-hoist = true which means we need a dependency lift in third party packages, which is called ghost dependencies.

This is because we will introduce the source Vue later to compare the implementation to it.

You can see what it means in detail here.

In the meantime, let’s fill in the following code at pnpm-workspace. Yaml:

Packages: # all packages - in packages/ and components/ subdirectories'packages/**'
  # - 'components/**'Package # - not included in the test folder'! **/test/**'
Copy the code

Since the package code is organized in monorepo’s way, we need to tell PNPM where our repO working directory is.

Here we specify Packages/as the Monorepo working directory, and each folder under our Packages will be treated as a separate project by PNPM.

Next we will install the required dependencies:

pnpm install -D typescript vue esbuild minimist -w
Copy the code

Note that -w stands for — workshop-root, which means we will install dependencies in the top-level directory, so packages can share them.

Minimist is the core parsing module for Node-Optimist, and its main job is to resolve the environment variables when executing node scripts.

Fill the build

Let’s fill in some of the build logic.

Change the package. The json

First, let’s switch to the project and directory and modify the pacakge.json for the entire repo.

{
  "name": "@vue"."version": "1.0.0"."description": ""."main": "index.js"."scripts": {
    "dev": "node ./scripts/dev.js reactivity -f global"
  },
  "keywords": []."author": ""."license": "ISC"."devDependencies": {
    "esbuild": "^ 0.14.27"."typescript": "^ 4.6.2." "."vue": "^ 3.2.31"}}Copy the code
  • First we change the package name to scope, @vue to indicate that the package is an organizational package.

  • Next, we modify the scripts. /scripts/dev.js will be executed when PNPM run dev is run, passing in a reactivity parameter and the global environment variable -f.

Change package.json within your project

Next we need to change the package.json (PCK) inside each rePOP. Let’s use the reactivity module as an example. I won’t go through share.

{
  "name": "@vue/reactive"."version": "1.0.0"."description": ""."main": "index.js"."buildOptions": {
    "name": "VueReactivity"."formats": [
      "esm-bundler"."esm-browser"."cjs"."global"]},"keywords": []."author": ""."license": "ISC"
}
Copy the code
  • First, we change the name in the Reactivity package to the scope @vue/reactive.

  • Secondly, we added some custom configurations for PCK, respectively:

    • Buildoptions. name This option represents the name of the variable that the module will mount globally when the package builds IIFE.

    • Buildoptions. formats This option indicates the module specification that needs to be exported when the module is packaged.

Fill the scripts/dev. Js

After that, let’s switch to scripts/dev.js to implement the packaging logic:

// scripts/dev.js
const { build } = require('esbuild');
const { resolve } = require('path');
const argv = require('minimist')(process.argv.slice(2));

// Get the minimist parameter
const target = argv['_'];
const format = argv['f'];

const pkg = require(resolve(__dirname, '.. /packages/reactivity/package.json'));

const outputFormat = format.startsWith('global')?'iife'
  : format.startsWith('cjs')?'cjs'
  : 'esm';

// Package the output file
const outfile = resolve(
  __dirname,
  `.. /packages/${target}/dist/${target}.${outputFormat}.js`
);

// Call ESbuild's NodeApi to perform packaging
build({
  entryPoints: [resolve(__dirname, `.. /packages/${target}/src/index.ts`)],
  outfile,
  bundle: true.// Package all dependencies in
  sourcemap: true.// Whether sourceMap is required
  format: outputFormat, // The output file format is IIFE, CJS, ESM
  globalName: pkg.buildOptions? .name,// after packaging, the global registered variable name takes effect under IIFE
  platform: outputFormat === 'cjs' ? 'node' : 'browser'./ / platform
  watch: true.// Repackage to detect file changes
});
Copy the code

The script is already commented in detail, so I’ll be a little verbose here.

Node. /scripts/dev. Js reactivity -f global when we run NPM run dev.

Therefore, when executing the corresponding dev.js, we get the corresponding environment variables target and format through minimist to represent the package and mode we need to package, respectively. Of course, you can also intercept them by yourself through process.argv.

Then we change the -f passed in to iIFE mode by judging if it is global, and execute esbuild’s Node Api to package the corresponding module.

Note that ESbuild supports typescript by default so no additional processing is required.

Of course, we have not created a corresponding entry file in each package at this point. Let’s create two packages/reactivity/SRC/index. The ts and packages/share/SRC/index. The ts file as entry.

At this point, when you run NPM run dev, you will find that the packaged JS file is generated:

The words at the end of the context

At this point, we have initially implemented a project build process for a simplified version of Vuejs. If interested in in-depth understanding of the complete process of students can view the corresponding source code.

Of course, the idea of dynamic packaging based on environment variables is described in detail in react-Webpack5-typescript for engineered multi-page applications. If you are interested, check it out.

In fact, I don’t need to go into the building ideas here, but just go into the responsive part of the code. However, this process has helped me optimize my projects for multi-page applications in my daily work.

Therefore, I think it is necessary to talk a little bit about this process, and hope that you can use Vuejs construction ideas to design your project construction process in future business scenarios.

Response principle

We’ve spent a little bit of time building this, but we’re finally getting down to business with the responsive principles section.

First of all, I’m going to emphasize it a little bit before I start. The code in this article is not a comparison with the source code to achieve the principle of responsiveness, but the implementation idea and implementation process is not inconsistent with the source code.

This is because there is a lot of conditional branching and error handling in the source code, as well as data structures such as arrays, sets and maps.

We will consider only the basic objects here, but I will cover the other data types in more detail in a later article.

At the same time, I will post the corresponding source code address at the end of each step for you to refer to the source code for comparison.

Prior to the start

Before we get into the responsivity principle, I want to give you a little bit of background. Because some students may not understand the source code in Vue3.

There is a core Api Effect in VueJs. This Api was exposed to developers after Vue 3.2. Before 3.2, VueJs internal methods were not available to developers.

To put it simply, all of our templates (components) are eventually wrapped in Effect, and effect is re-executed when data changes, so the responsivity principle in VUejS can be said to be based on Effect.

Of course, all you need to know is that the final component will compile into an effect, and when the responsive data changes, the effect function will be reexecuted to update the rendered page.

We’ll also look at how effect and responsiveness are related later.

Basic directory structure

Let’s start by creating some basic directory structures:

  • Reactivity/SRC /index.ts is used to import and export modules in a unified manner

  • Reactivity/SRC/reactivity. Ts is used to maintain reactive Api.

  • Reactivity/SRC /effect.ts Users maintain effect-related apis.

In this step, we first create a new file in the reActivity:

Reactive base logic processing

Let’s start with the related reactive. Ts.

Thinking to comb

In terms of how Vuejs implements data responsiveness, it simply uses the Proxy Api internally to access/set data for hijacking.

For data access, dependency collection is required. Record the dependent effects in the current data. When the data is modified, it will also trigger the update and re-execute the current dependent effects. In a nutshell, this is called the responsivity principle.

For the moment, you can think of Effect as a function that is re-executed when the data changes and the function is re-rendered.

To achieve a goal

Before we start writing the code, let’s look at how it’s used. Let’s start by looking at how reactive works with Effect:

<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Document</title>
</head>

<body>
  <div id="app"></div>
  <script src="https://unpkg.com/vue@next"></script>
  <script>
    const {
      reactive,
      effect
    } = Vue

    const obj = {
      name: '19Qingfeng'
    }

    // Create reactive data
    const reactiveData = reactive(obj)

    // Create effect dependent reactive data
    effect(() = > {
      app.innerHTML = reactiveData.name
    })

    // Update responsive data after 0.5s
    setTimeout(() = > {
      reactiveData.name = 'wang.haoyu'
    }, 500)
  </script>
</body>

</html>
Copy the code

For those unfamiliar with Effect and reactive data, try this code in your browser.

First we use the Reactive Api to create a reactive data reactiveData.

After that, we create an effect that takes a FN as an argument. The logic behind this effect is pretty simple: it sets the content of the app element with the id to the value of reactiveData.name.

Note that the fn passed by this effect relies on the name property of the reactiveData reactiveData, which is often referred to as dependency collection.

When an effect is created, fn is executed immediately so the app element will render the corresponding 19Qingfeng.

After 0.5s, when the timer reaches the time, we modify the name attribute of the reactiveData responsive data, then the effCT dependent on the modified attribute will be triggered to re-execute, which is also commonly referred to as triggered update.

So it looks like the result of rendering 19Qingfeng first. After 0.5s, the innerHTML of the app was modified to render the page again due to a change in the responsive data that caused the effect to be re-executed.

This is a very simple and typical reactive data Demo, and we will implement this logic step by step based on the results.

Implementation of basic Reactive methods

To implement a basic version of the Reactive method, see 👉.

We’ve already mentioned that VueJs is essentially a Proxy & Reflect hijacking of reactive data, so it’s natural to think of code like this:

// reactivity/src/reactivity.ts

export function isPlainObj(value: any) :value is object {
  return typeof value === 'object'&& value ! = =null;
}

const reactive = (obj) = > {
  // Pass in non-objects
  if(! isPlainObj(obj)) {return obj;
  }

  // Declare reactive data
  const proxy = new Proxy(obj, {
      get() {
          // dosomething
      },
      set() {
          // dosomething}});return proxy;
};
Copy the code

The code above is very simple: we create a Reactive Object that accepts an Object of type Object.

We check the obj passed in by the function, and return it if it is an object.

Next, we will create a proxy proxy object based on the object obj passed in. A set of logic is implemented by hijacking the proxy object against get traps (when accessing object properties) and set (when modifying the value of the proxy object).

Depend on the collection

We mentioned earlier that reactive data against Reactive is dependent collection when a GET trap is triggered.

Here you can simply think of dependency collection as recording which effects the current data is being used by, and we will implement this step by step.

// reactivity/src/reactivity.ts

export function isPlainObj(value: any) :value is object {
  return typeof value === 'object'&& value ! = =null;
}

const reactive = (obj) = > {
  // Pass in non-objects
  if(! isPlainObj(obj)) {return obj;
  }

  // Declare reactive data
  const proxy = new Proxy(obj, {
    get(target, key, receiver) {
      // Rely on the collection method track
      track(target, 'get', key);

      // Call the Reflect Api to get the raw data, which you can simply call target[key]
      let result = Reflect.get(target, key, receiver);
      // Dependencies are handled recursively for objects
      if (isPlainObj(result)) {
        return reactive(result);
      }

      // Reflect works with Reflect to solve the problem of recursively relying on this when accessing the GET attribute
      return result;
    },
    set() {
      // dosomething}});return proxy;
};
Copy the code

Here we fill in the logic for the get trap in Proxy:

  • When accessing properties in a reactive object Proxy, dependency collection is first performed on the corresponding properties. The track method is mainly relied on.

  • If the value corresponding to the key of the reactive object is still an object, the reactive method is called recursively for processing.

Note that when you’re recursively reactive, it’s lazy. In other words, you’re only recursing when you’re calling it, not when you’re initializing the obJ that’s passed in.

Of course, the dependency collection here mainly relies on the track method, which will be implemented in detail later.

Depend on the collection

Next, let’s look at the logic in the set trap, which triggers the execution of the corresponding Effect when a property modification to the proxy object is triggered.

Let’s look at the logic in the corresponding set trap:

// reactivity/src/reactivity.ts

export function isPlainObj(value: any) :value is object {
  return typeof value === 'object'&& value ! = =null;
}

const reactive = (obj) = > {
  // Pass in non-objects
  if(! isPlainObj(obj)) {return obj;
  }

  // Declare reactive data
  const proxy = new Proxy(obj, {
    get(target, key, receiver) {
      // Rely on the collection method track
      track(target, 'get', key);

      // Call the Reflect Api to get the raw data, which you can simply call target[key]
      let result = Reflect.get(target, key, receiver);
      // Dependencies are handled recursively for objects
      if (isPlainObj(result)) {
        return reactive(result);
      }

      // Reflect works with Reflect to solve the problem of recursively relying on this when accessing the GET attribute
      return result;
    },
    // Trigger updates when setting
    set(target, key, value, receiver) {
      const oldValue = target[key];
      // Reflect works with Reflect to solve the problem of recursively relying on this when accessing the GET attribute
      const result = Reflect.set(target, key, value, receiver);
      // If the two changes are the same, no update will be triggered
      if(value ! == oldValue) {// Trigger the update
        trigger(target, 'set', key, value, oldValue);
      }

      returnresult; }});return proxy;
};
Copy the code

Again, we’ve filled it with the logic in the corresponding set trap that fires when a reactive object is set. We will trigger the corresponding trigger logic in the set trap to trigger the update: re-execute the dependent effect.

I have explained in detail why we need to cooperate with Refelct when using Proxy in this article. Those interested can check out 👉 [Why Proxy must work with Reflect?] .

Above we have completed the basic logic of reactive. Ts file and left two core methods track & trigger methods.

Before implementing both methods, let’s look at how Effect is implemented.

Effect file

Effect basic use

Let’s cut the view to effcet.ts and remind ourselves a little bit about the use of the Effect Api:

const {
  reactive,
  effect
} = Vue

const obj = {
  name: '19Qingfeng'
}

// Create reactive data
const reactiveData = reactive(obj)

// Create effect dependent reactive data
effect(() = > {
  app.innerHTML = reactiveData.name
})
Copy the code

Basic principle of effect

As we saw above, the Effect Api has the following features:

  • Effect takes a function as an input parameter.

  • When effect(fn) is called, the internal function is called directly once.

  • Second, when the dependent reactive data in effect changes. We expect effect to be reexecuted, as in this case effect depends on the value on reactiveData.name.

Let’s start by implementing a simple Effect Api:

function effect(fn) {
  // Call Effect to create an Effect instance
  const _effect = new ReactiveEffect(fn);

  // The function inside Effect is executed once by default
  _effect.run();

  // Create the effect function return value: _effect.run() (bind this to the _effect instance)
  const runner = _effect.run.bind(_effect);
  // The returned runner function mounts the corresponding _effect object
  runner.effect = _effect;

  return runner;
}
Copy the code

Here we create a basic Effect Api that takes a function fn as an argument.

When we run effect, we create a const _effect = new ReactiveEffect(fn); Object.

We also call the _effle.run () instance method to execute the incoming FN immediately. The reason we need to execute the incoming FN immediately is explained above: when the code executes to effect(fn), it actually executes the fn function immediately.

Our _effect.run() call actually executes fn internally, too, and we’ll recall a bit from the previous Demo that when this code executes effect(fn) it executes:

// ...
effect(() = > { app.innerHTML = reactiveData.name })
Copy the code

The incoming fn () => {app.innerhtml = reactiveData.name} immediately modifies the contents of the app node.

Also, we mentioned earlier that because reactiveData is a proxy object, we actually trigger its GET trap when we access its properties.

// effect.ts

export let activeEffect;

export function effect(fn) {
  // Call Effect to create an Effect instance
  const _effect = new ReactiveEffect(fn);

  // The function inside Effect is executed once by default
  _effect.run();

  // Create the effect function return value: _effect.run() (bind this to the _effect instance)
  const runner = _effect.run.bind(_effect);
  // The returned runner function mounts the corresponding _effect object
  runner.effect = _effect;

  return runner;
}


/** * Reactive Effect */
export class ReactiveEffect {
  private fn: Function;
  constructor(fn) {
    this.fn = fn;
  }

  run() {
    try {
         activeEffect = this;
        // The run method simply executes the fn passed in
        return this.fn();
    } finally {
        activeEffect = undefined}}}Copy the code

This is a very simple implementation of ReactiveEffect, which has a very simple interior that simply records the incoming FN and has a run instance method that executes the recorded FN function when the run method is called.

Also, we declare an activeEffect variable inside the module. When we call Run Effect (fn), it actually goes through the following steps:

  • First call effect(fn) in user code

  • VueJs internally executes the effect function and creates an _effect instance object. Call the _effect.run() instance method immediately.

  • The key is in the so-called _effect.run() method.

  • First, when the _effect.run() method is called, we execute activeEffect = this to change the declared activeEffect into the current corresponding _effect instance object.

  • Meanwhile, the run() method next calls the fn() function passed in.

  • When fn() is executed, if the passed fn() function has reactive() data wrapped around it, it will actually get into the corresponding GET trap.

  • When getting into the get trap for reactive data, don’t forget to declare the global activeEffect variable. We can get the activeEffect variable (that is, the _effect variable created) in the get trap for reactive data.

What we need to do next is simple:

The global activeEffect object (_effect) that the data depends on is recorded in the GET trap of reactive data, which is our legacy track method.

At the same time:

When changing reactive data, we only need to find the _effect that is currently dependent on the data. Recalling _effect.run() while modifying data is equivalent to reexecuting fn in effect (fn). So this is not equivalent to modifying the data page automatically updated? This step is called dependency collection, which is the trigger method we left behind.

Track & trigger methods

Let’s go back to the legacy of track and trigger logic and try to implement it.

Here we will implement both methods in effect.ts and export them for use in reactive.

Thinking to comb

As mentioned above, the core idea is that when the code executes to effect(FN), it calls the corresponding FN function internally. When FN is executed, the get of the reactive data dependent on FN will be triggered. When GET is triggered, we can record the association between the (activeEffect) _effect object and the corresponding reactive data.

When the responsive data changes, we take out the associated _effect object and re-call _effect.run() to re-execute the fn function passed in by effect(fn).

Some of you are already reacting to this. We have a table that records the corresponding activeEffect(_effect) and the corresponding responsive data, so we naturally think of using a WeakMap to store this relationship.

The first reason why WeakMap is used for storage is that the key value we need to store is of non-string type, which obviously only Map can do. Secondly, the key of WeakMap does not affect garbage collection mechanism.

Creating a mapping table

As we analyzed above, we need a global mapping table to maintain associations between _effect instances and dependent reactive data:

So we naturally think of maintaining the mapping relationship through a WeakMap object, then how to design the WeakMap object? I won’t keep you in suspense here.

Let’s recall the above Demo:

// ...
const {
  reactive,
  effect
} = Vue

const obj = {
  name: '19Qingfeng'
}

// Create reactive data
const reactiveData = reactive(obj)

// Create effect dependent reactive data
effect(() = > {
  app.innerHTML = reactiveData.name
})

// Add an effect dependency logic to the Demo above
effect(() = > {
   app2.innerHTML = reactiveData.name
})
Copy the code

First, for the reactiveData reactiveData, which is an object, the effect in the above code depends on the name attribute of the reactiveData object.

Therefore, we only need to associate the name property in the current reactive object with the corresponding effect.

Also, properties that are specific to the same reactive object, such as the name property here, are dependent on multiple effects. Naturally, we can imagine that the properties of a responsive data can depend on multiple effects.

Based on the above analysis, the following structure is designed for this mapping table in Vuejs:

When an effect relies on the corresponding reactive data, as in the Demo above:

The global WeakMap we created will first take the original object (the object before proxy) of the responsive object as the key, and value is a Map object.

At the same time, effect internally uses an attribute of the above object, then the value of the object (the Map we created just now) of the WeakMap object. We’re going to Set key to be the property used in this Map and value to be a Set object.

Why the value of the corresponding attribute is a Set is very simple. This property can be dependent on multiple effects. So its value is a Set object, and when this property is dependent on an effect, the corresponding _effect instance object is added to the Set.

For those of you who might be a little fuzzy with this map at first glance, it doesn’t matter, and I’m going to describe this in code. You can understand it in combination with the code and the text.

Track to achieve

Let’s look at the implementation of the track method:

// * A relational Hash table for storing reactive data and Effect
const targetMap = new WeakMap(a);/** * the dependency collection function enters the track function * when the Getter for reactive data is triggered@param Target Specifies the original object to be accessed@param Where does the track come from@param Key Accesses the key */ of a reactive object
export function track(target, type, key) {
  // Effects that are not currently active do not need to collect dependencies
  if(! activeEffect) {return;
  }

  // Find if the object exists
  let depsMap = targetMap.get(target);
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
  }

  // Check whether there is a Set effect corresponding to the key
  let deps = depsMap.get(key);
  if(! deps) { depsMap.set(key, (deps =new Set()));
  }

   // Set itself can be used to determine the performance optimization point
  constshouldTrack = ! deps.has(activeEffect) && activeEffect;if (shouldTrack) {
    // * Collect dependencies and put effect into the corresponding keys in the corresponding target in the corresponding WeakMapdeps.add(activeEffect); }}Copy the code

We analyze the track method above line by line, which we mentioned earlier. It is triggered in reactive. Ts when a dependency collection is performed on a reactive property (triggering a proxy get trap), and forgotten friends can go back and look at it again.

First, it determines whether the current activeEffect exists, which is called an actvieEffect. In other words, like this:

// ...

app.innerHTML = reactiveData.name
Copy the code

So do we need to do dependency collection? Although it is true that reactiveData is reactiveData, we did not use it in the template. It does not have any associated effect, so dependency collection is completely unnecessary.

And in this case:

effect(() = > {
    app.innerHTML = reactiveData.name
})
Copy the code

Only we are in Effect (FN), when the corresponding reactive data is used in FN. To put it simply, the dependency collection of reactive data only makes sense when activeEffects exist.

Second, the next step is to look in the global targetMap to see if the original object corresponding to the reactive data already exists -> depsMap. If this object is collected for the first time, we need to set the key to target and the value to a new Map in targetMap.

  // Find if the object exists
  let depsMap = targetMap.get(target);
  if(! depsMap) {// Create a Map as value and place target as key in depsMap
    targetMap.set(target, (depsMap = new Map()));
  }
Copy the code

In the meantime, we will continue with the DEPS returned in the previous step, which is a Map. Internally, it records the properties in the object that are collected by dependency.

We go back to see if the name attribute exists, which is obviously the first dependency collection. So it will:

  // Check whether there is a Set effect corresponding to the key
  let deps = depsMap.get(key);
  if(! deps) {// Create a set if it does not exist
    depsMap.set(key, (deps = new Set()));
  }
Copy the code

At this point, as in the Demo above, the track function is fired when the FN in Effect hits the get trap for responsive data.

We will first set key to obj (the original object of reactiveData) and value to a Map for the global targetMap object.

Second, we Set the key again in the Map that we created to be the property that the reactive object needs to collect, that is, the name of the object that we accessed in effect, and the value is a Set.

The dependency collection effect can be achieved only by recording the activeEffct instance in the Set.

At this point, the corresponding object and its associated effect will be stored in targetMap.

The trigger to realize

Of course, we have collected the relevant responsive data and the corresponding effect it depends on through the corresponding track method above.

Then, if we change the responsive data (trigger the set trap), we just need to find the corresponding record of the effect object, call its effect.run() to execute again, so that the page changes with the data.

Let’s look at the trigger method:

// ... effect.ts

/** * triggers the update function *@param Target The source object * that triggers the update@param Type type *@param Key The source object key * that triggers the update@param Value Specifies the value * of the key change of the source object that triggered the update@param OldValue The original value */ of the source object that triggers the update
export function trigger(target, type, key, value, oldValue) {
  // In simple terms, every time it triggers I pull out the corresponding Effect and execute it, the page will be updated
  const depsMap = targetMap.get(target);
  if(! depsMap) {return;
  }
  let effects = depsMap.get(key);
  if(! effects) {return;
  }

  effects = new Set(effects);
  effects.forEach((effect) = > {
    / / the current government
    if(activeEffect ! == effect) {Run () to clear the current effect dependencies. Run fn in Effect to collect dependencies and update the vieweffect.run(); }}); }Copy the code

Then we add the corresponding trigger logic in effect.ts. The logic of trigger is very simple. Whenever reactive data triggers a set trap for modification, the corresponding trigger function is triggered.

It will accept the corresponding five arguments, which we have indicated in the function comment.

When triggering the modification of responsive data, we first go back to the targetMap to find the value of the key corresponding to the original object. Naturally, we have saved the corresponding value in track, so of course we can get a Map object.

Since there is Set Set corresponding to effect whose key is name and value is dependent on this attribute in this Map object, we only need to take out the corresponding modified attribute in turn, for example, we call:

// ...
const {
  reactive,
  effect
} = Vue

const obj = {
  name: '19Qingfeng'
}

// Create reactive data
const reactiveData = reactive(obj)

// Create effect dependent reactive data
effect(() = > {
  app.innerHTML = reactiveData.name
})

// Modify reactive data to trigger set traps
reactiveData.name = 'wang.haoyu'
Copy the code

When we call reactiveData.name = ‘wang.haoyu’, we will get it layer by layer

  • DepsMap (Map) object whose key is obj in targetMap.

  • Get the Set from depsMap that holds the effect dependent on the responsive object property.

  • Iterate over all effects in the current Set to effect.run() to re-execute the FN function recorded in the effect object.

Because we call trigger in a set trap in reactive. Ts after the data has been modified, trigger causes fn in effect(FN) to be re-executed, So naturally fn() re-executes app.innerhtml to become the latest Wang.haoyu.

The whole reactive core principle isn’t that hard at all, right? The core idea is that for data access, dependency collection is needed. Record the dependent effects in the current data. When the data is modified, it will also trigger the update and re-execute the current dependent effects.

Stage summary

In fact, I have written more than 8K words here. I originally intended to walk you through the logic of reactivity in Vue 3.2, including various boundary cases.

For example, the code in this article only implements a beggar’s version of the reactive principle, and some other boundary cases, such as:

  • Handling when multiple effects are nested.

  • Calls to the same object by Reactive multiple times, or for reactive objects that have already been wrapped around Reactive.

  • Cleanup of the previous dependency collection each time an update is triggered.

  • Shallow, readonly, and more…

In fact, I have not considered these boundary cases in the article. If some friends are interested in this aspect, I will write another article to continue the code this time to achieve a complete reactive method.

But look beyond the appearance to the essence. The main data-responsive core principle of VueJs is the idea expressed in the code in this article.

In this code address, I have also implemented a relatively complete version of the condensed version of the ReActivity module, interested students can consult by themselves.

Of course, you can also read the source code directly. After all, the Vue 3 code is actually more human than the Vue 2 code.

Written in the end

At the end of the article, thank you to every friend who can read to the end.

To be honest, I am not sure how many small partners will accept such a simplified source style of articles. If necessary, I will continue to update VueJs’s simplified articles, such as improving the reactivity logic, or ref, computed, watch, and so on.

Interested partners can follow here, grasp the real-time dynamic.