preface

In this article, we will write a written copy of the responsiveness principle in VUE-Next from scratch. For length and ease of understanding, we will only implement the core API and ignore some marginal function points

The apis that this article will implement include

  • track
  • trigger
  • effect
  • reactive
  • watch
  • computed

Project structures,

We built our project with vite, which is hot these days

The version demonstrated in this article

  • node v12.16.1
  • npm v6.14.5
  • yarn v1.22.4

Let’s start by downloading the template

yarn create vite-app vue-next-reactivity
Copy the code

After downloading the template, go to the directory

cd vue-next-reactivity
Copy the code

Then install the dependencies

yarn install
Copy the code

Then we just keep the main.js file in the SRC directory, clear the rest and create the reactivity folder we’ll use

The entire file directory is shown, and the NPM run dev project is launched by typing

Handwritten code

The nature of the responsive principle

Before we start writing, let’s think about what is the responsive principle?

Let’s explain this from the use of VUe-Next

The responses used in Vue-Next fall into three categories

  • The template or render

When the variables used in the page change, the page is refreshed automatically

  • computed

The calculated properties are automatically changed when the variables used in the calculated properties function are changed

  • watch

When the listening value changes, the corresponding callback function is automatically triggered

These three points sum up the essence of the responsive principle

When a value changesautomaticTrigger the corresponding callback function

The callbacks here are the page refresh function in template, the recalculation function in computed, and the Watch callback, which is a callback anyway

So we’re going to implement the responsive principle and now we’re going to break it down into two questions

  • Listen for value changes
  • Trigger the corresponding callback function

We solved these two problems, and we wrote the reactive principle

Listen for value changes

Javascript provides two apis for listening for value changes

One is object.defineproperety, used in vue2. X

const obj = {};
let aValue = 1;
Object.defineProperty(obj, 'a', {
  enumerable: true.configurable: true,
  get() {
    console.log('I'm being read.');
    return aValue;
  },
  set(value) {
    console.log('I'm set.'); aValue = value; }}); obj.a;// I am read
obj.a = 2; // I am set
Copy the code

Another method is the proxy used in VUe-Next, which is also the method used in this handwriting

This approach addresses the four pain points of Object.defineproperety

  1. Cannot intercept additions and deletions of attributes on objects
  2. Cannot intercept calls on arrayspush pop shift unshiftMethods that affect the current array
  3. Intercepting excessive performance overhead for array indexes
  4. Unable to interceptSet MapEqual set type

Of course, mainly the first two

In vue2. X, the array index must be changed by this.$set. Many students think object.defineproperety can’t intercept an array index

The above 4 points can be perfect proxy solution, now let’s start to write a proxy interception!

The proxy to intercept

We create two files in the reactivity directory we created earlier

Utils.js holds some common methods

React.js stores the proxy interception method

We’ll start by adding the methods we’ll use to determine whether the object is native to utils.js

reactivity/utils.js

// Get the original type
export function toPlain(value) {
  return Object.prototype.toString.call(value).slice(8.- 1);
}

// Whether it is a native object
export function isPlainObject(value) {
  return toPlain(value) === 'Object';
}
Copy the code

reactivity/reactive.js

import { isPlainObject } from './utils';

// Only arrays and objects can be observed
function canObserve(value) {
  return Array.isArray(value) || isPlainObject(value);
}

// Intercept data
export function reactive(value) {
  // The value that cannot be listened on is returned directly
  if(! canObserve(value)) {return;
  }
  const observe = new Proxy(value, {
    // Intercepts the read
    get(target, key, receiver) {
      console.log(`${key}'was read);
      return Reflect.get(target, key, receiver);
    },
    // Intercept Settings
    set(target, key, newValue, receiver) {
      const res = Reflect.set(target, key, newValue, receiver);
      console.log(`${key}Is set to);
      returnres; }});// Returns the proxy instance being observed
  return observe;
}
Copy the code

reactivity/index.js

Methods derived

export * from './reactive';
Copy the code

main.js

import { reactive } from './reactivity';

const test = reactive({
  a: 1});const testArr = reactive([1.2.3]);

/ / 1
test.a; // A is read
test.a = 2; // A is set

/ / 2
test.b; // b is read

/ / 3
testArr[0]; // 0 is read

/ / 4
testArr.pop(); // pop read length read 2 read length set
Copy the code

You can see that we have added a reactive method to proxy intercepts objects and arrays and returns the corresponding proxy instance

1, 2, and 3 are pretty straightforward, so let’s explain number 4

Calling the POP method first triggers a GET intercept, printing that the POP was read

The pop method is then called to read the length of the array and trigger the GET intercept, printing that length was read

The pop method returns the currently deleted value, reads the value with array index 2, triggers a GET intercept, and prints that 2 was read

After pop, the array length is changed, the set intercept is triggered, and the print length is set

You can also try other ways of changing arrays

It can be summed up in one sentence

Length is read and reset when it affects the length of the array itself, and the index of the changed value is read and reset (push unshift)

Add a callback function

We use proxy to implement value interception to solve the first problem we raised

But instead of triggering the callback after the value changes, let’s add the callback

reactivity/reactive.js

import { isPlainObject } from './utils'; / / this column only arrays and objects can be observed the function canObserve (value) {return Array. The isArray (value) | | isPlainObject (value); }+ // hypothetical callback function
+ function notice(key) {
+ console.log(' ${key} was changed and triggered the callback function ');
+}Export function reactive(value) {if (! canObserve(value)) { return; } const observe = new Proxy(value, {// Get (target, key, receiver) {- console.log(' ${key} was read ');return Reflect.get(target, key, receiver); }, // Set (target, key, newValue, receiver) {const res = reflect. set(target, key, newValue, receiver);- console.log(' ${key} is set ');
+ // triggers the hypothetical callback function
+ notice(key);return res; }}); // Proxy instance return observe; }Copy the code

We trigger our hypothetical callback in the most intuitive way possible in a set intercept whose value is changed

main.js

import { reactive } from './reactivity';

const test = reactive({
  a: 1.b: 2}); test.a =2; // A is changed and the callback is triggered
test.b = 3; // b is changed and the callback is triggered
Copy the code

You can see that when the value changes, the corresponding log is output

But there are definitely problems with this example, and there are more than one, so let’s update it step by step

Collection of callback functions

The example above a and b are corresponding to a callback function notice, but the actual scenario, a and b may correspond to different callback functions respectively, if we just use a simple global variables to store the callback function, this is clearly not appropriate, if the latter will overwrite the former, then how can I let the callback function and corresponding between each value?

It is easy to think of the key-value object in JS. As the key value of the object, the attributes A and B can distinguish their respective value values

But collecting callback functions with objects is problematic

In the above column, we have a test object that has properties A and B. If we have another object test1 that also has properties A and B, then we are repeating it. This will trigger the repetition problem we mentioned earlier

Test and test1 are the same names in the same execution context, but they are in different execution contexts, which leads to the same duplication problem

To deal with this problem, use the pass-by-reference nature of JS objects

// 1.js
const obj = {
  a: 1};// 2.js
const obj = {
  a: 1};Copy the code

We define object obj with identical name attribute data structure in two folders, but we know that the two obJ are not equal because their memory points to different addresses

So if we could just use objects as keys, wouldn’t we be able to distinguish objects that look “identical”?

The answer is yes, but we have to change the data structure, because the key of an object in JS cannot be an object

Here, we need to use a new data structure Map and WeakMap of ES6

We illustrate the storage mode of this data structure with examples

Suppose we now have two objects with “the same” data structure, obj, each with its own attributes A and B, and changes to each attribute trigger different callback functions

// 1.js
const obj = {
  a: 1.b: 2
};

// 2.js
const obj = {
  a: 1.b: 2
};
Copy the code

Map and WeakMap are used for storage, as shown in the figure below

We define the global variable targetMap that stores the callback function as a WeakMap, and its key value is each object, which is two OBJ in this column. The value of targetMap is a Map. In this column, two OBJ have two attributes A and B respectively, and the key of Map is attribute A and B,Ma The value of p is the Set of callback functions corresponding to attributes A and b

Maybe you will have a question why targetMap uses WeakMap and each object’s properties store Map, this is because WeakMap can only use object as key,Map is an object or string, like the column of sub-attributes a and B can only use Map to store

Let’s use the actual API to further understand this storage structure

  • computed
const c = computed((a)= > test.a)
Copy the code

Here we need to put the () => test.a callback in the test.a collection, as shown in the figure

  • watch
watch((a)= > test.a, val => { console.log(val) })
Copy the code

Here we need to put the val => {console.log(val)} callback in the test.a collection, as shown in the figure

  • template
createApp({
  setup() {
    return (a)= > h('div', test.a); }});Copy the code

Here we need to put the DOM refresh function in test.a, as shown in the figure

Now that we know how to store callback functions, how do we put callback functions into our defined storage structure

I’m going to do that again

watch((a)= > test.a, val => { console.log(val) })
Copy the code

In this column, we need to put the callback function val => {console.log(val)}) into the Set of test.a, so we need to get the object test and the property a of the current object, if only through () => Test. a, we can only get the value of test.a, we can not know the specific object and attribute

Test. a = test.a; test.a = test.a; test.a = test.a; test.a

The first argument to get is the object being read, and the second argument is the property being read

So the collection of callback functions is inproxythegetProcessing in intercept

Now let’s implement this idea in code

First we create an effect.js file that holds the collection method and the trigger method of the callback function

reactivity/effect.js

// A collection of callback functions
const targetMap = new WeakMap(a);// Collect callback functions
export function track(target, key) {}// Trigger the callback function
export function trigger(target, key) {}Copy the code

Then rewrite the intercepting content in the proxy

reactivity/reactive.js

import { isPlainObject } from './utils';
+ import { track, trigger } from './effect';/ / this column only arrays and objects can be observed the function canObserve (value) {return Array. The isArray (value) | | isPlainObject (value); }- // The assumed callback function
- function notice(key) {
- console.log(' ${key} was changed and triggered the callback function ');
-}Export function reactive(value) {if (! canObserve(value)) { return; } const observe = new Proxy(value, {// Get (target, key, receiver) {+ // Collect callback functions
+ track(target, key);return Reflect.get(target, key, receiver); }, // Set (target, key, newValue, receiver) {const res = reflect. set(target, key, newValue, receiver);+ // Triggers the callback function
+ trigger(target, key);
- // Triggers the hypothetical callback function
- notice(key);return res; }}); // Proxy instance return observe; }Copy the code

What is not added here is the effect that makes it clear where the collection and trigger are

Now we add the track collection callback function and the trigger trigger callback function

reactivity/effect.js

// A collection of callback functions
const targetMap = new WeakMap(a);// Collect callback functions
export function track(target, key) {
  // Get a map of each object by object
  let depsMap = targetMap.get(target);
  if(! depsMap) {// When objects are collected for the first time we need to add a map collection
    targetMap.set(target, (depsMap = new Map()));
  }
  // Get the collection of callback functions for each property under the object
  let dep = depsMap.get(key);
  if(! dep) {// We need to add a set when the object attributes are first collected
    depsMap.set(key, (dep = new Set()));
  }
  // Add the callback function here
  dep.add((a)= > console.log('I'm a callback function'));
}

// Trigger the callback function
export function trigger(target, key) {
  // Get the map of the object
  const depsMap = targetMap.get(target);
  if (depsMap) {
    // Get the collection of callback functions corresponding to each attribute
    const deps = depsMap.get(key);
    if (deps) {
      // Trigger the callback function
      deps.forEach((v) = >v()); }}}Copy the code

And then run our demo

main.js

import { reactive } from './reactivity';

const test = reactive({
  a: 1.b: 2}); test.b;// Read the collection callback function

setTimeout((a)= > {
  test.a = 2; // There is no trigger because no callback is collected
  test.b = 3; // I am a callback
}, 1000);
Copy the code

Let’s look at the targetMap structure at this point

Key value {a: 1,b: Log (‘ I am a callback function ‘);} 2} 2} 3} 3} 3} 3} 3} 3} 3} 3} 4} 3} 4} 4} 4} 4}

That’s what happens when you use a graphic structure

You might think that collecting callback functions and reading test.b is an anti-human operation, because we haven’t talked about the corresponding API yet. Normal reading operations don’t need to be called manually

watch

A big problem with the above example is that we don’t have a custom callback function. The callback function is written out directly in the code

Now we will implement the custom callback function through Watch

The WATCH API in Vue-Next is quite extensive, and we’ll implement some of these types, which is enough to understand the reactive principle

The demo we will implement is as follows

export function watch(fn, cb, options) {}

const test = reactive({
  a: 1}); watch((a)= > test.a,
  (val) => { console.log(val); });Copy the code

Watch accepts three arguments

The first argument is a function that expresses the value being listened on

The second argument is a function that expresses the callback to be triggered if the listening value is changed. The first argument is the changed value and the second argument is the value before the change

The third argument is an object that has only one deep property, the deep table

Now all we need to do is call the callback function (val) => {console.log(val); } into test.a’s Set

So before () => test.a reads test.a, we need to store the callback in a variable

When the track function is triggered by reading test.a, the variable can be obtained in the track function and stored in the Set of corresponding attributes

reactivity/effect.js

// Set of callback functions const targetMap = new WeakMap();+ // The currently active callback function
+ export let activeEffect;

+ // Sets the current callback function
+ export function setActiveEffect(effect) {
+ activeEffect = effect;
+}Export function track(target, key) {// If (! activeEffect) { return; } let depsMap = targetmap.get (target); if (! Targetmap.set (target, (depsMap = new map ())); } let dep = depmap.get (key); if (! Depmap.set (key, (dep = new set())); } // Add the callback function here-dep.add (() => console.log(' I am a callback function '));
+ dep.add(activeEffect);Export function trigger(target, key) {// omit}Copy the code

Since the watch method is not in the same file as the track and trigger methods, we use export to export the variable activeEffect and provide a method setActiveEffect to modify it

This is also a way to use public variables in different modules

Now let’s create watch.js and add the watch method

reactivity/watch.js

import { setActiveEffect } from './effect';

export function watch(fn, cb, options = {}) {
  let oldValue;
  // Store the callback function before executing fn to get oldValue
  setActiveEffect((a)= > {
    // Make sure the callback triggers a new value
    let newValue = fn();
    // Trigger the callback function
    cb(newValue, oldValue);
    // Assign the new value to the old value
    oldValue = newValue;
  });
  // Read the value and collect the callback function
  oldValue = fn();
  // empty the callback function
  setActiveEffect(' ');
}
Copy the code

Very simple a few lines of code, before the implementation of fn read value to set the callback function through setActiveEffect in order to read the track function can get the current callback function activeEffect, after reading the empty callback function, complete

We also need to export the watch method

reactivity/index.js

export * from './reactive';
+ export * from './watch';
Copy the code

main.js

import { reactive, watch } from './reactivity';

const test1 = reactive({
  a: 1}); watch((a)= > test1.a,
  (val) => {
    console.log(val) / / 2;}); test1.a =2;
Copy the code

As you can see that the column executes normally and prints out 2, let’s look at the structure of the targetMap

{console.log(val) => {console.log(val); {console.log(val) => {console.log(val); }

The graphic structure of targetMap is as follows

computed

The other API additions of Watch will come later, and after feeling the responsive principle of thinking, we will strike while the iron is hot to implement computed functions

For the same computed API, there are several ways to write it in VUe-Next, and we will implement only the return value of the function

export function computed(fn) {}

const test = reactive({
  a: 1});const w = computed((a)= > test.a + 1);
Copy the code

But if we just write computed incoming functions, vue-Next has little to do with the responsive principle

Because the API read value provided in vue-next is not directly read w but W. value

We create computed. Js to complement computed functions

reactivity/computed.js

export function computed(fn) {
  return {
    get value() {
      returnfn(); }}; }Copy the code

As you can see, just a few lines of code, rerun the fn evaluation every time you read value

reactivity/index.js

Let’s export it again

export * from './reactive';
export * from './watch';
+ export * from './computed';
Copy the code

main.js

import { reactive, computed } from './reactivity';

const test = reactive({
  a: 1});const w = computed((a)= > test.a + 1);

console.log(w.value); / / 2
test.a = 2;
console.log(w.value); / / 3
Copy the code

You can see that the column works perfectly

Two problems arise

  • whyapiIs not read directlywbutw.valueIn the form of

This is the same reason why there is a ref. The proxy cannot intercept the underlying type, so it wraps the object with a value layer

  • vue-nextIn thecomputedDoes it really have nothing to do with the responsive principle

In fact, it does. In writing computed functions only, the responsive principle works

It can be seen that if we write w.value as we did before, fn will be executed once when we read, even if the value of W.value does not change. When there is a large amount of data, the performance impact will be significant

So how do we optimize?

It’s easy to think of doing fn once to compare the old and the new, but it’s really the same as before, because we’re still doing fn once

Here we can apply the reactive principle. Whenever the internal influence value test.a has been modified, we will re-execute fn to fetch the value, otherwise we will read the previously stored value

reactivity/computed.js

import { setActiveEffect } from './effect';

export function computed(fn) {
  // This value will only be true if the variable is changed the first time it comes in
  let dirty = true;
  / / the return value
  let value;
  // Set to true to re-fetch the next read
  function changeDirty() {
    dirty = true;
  }
  return {
    get value() {
      // When the flag is true, the variable needs to be changed
      if (dirty) {
        dirty = false;
        // Set variable control to
        setActiveEffect(changeDirty);
        / / get the value
        value = fn();
        // null-dependent
        setActiveEffect(' ');
      }
      returnvalue; }}; }Copy the code

We define a variable, dirty, to indicate whether the value has been changed, which is true

Similarly, we assign the callback function () => {dirty = true} to the intermediate variable activeEffect before each read, and then perform fn read. At this time, the callback is collected, and dirty is changed when the corresponding property changes

When we run the above example again, the program still works

Function changeDirty() {dirty =; function changeDirty() {dirty =; function changeDirty() {dirty = true; }

The graphic structure of targetMap is as follows

Extraction effect

In both Watch and computed, we have gone through three steps: set the callback => read the value (store the callback)=> empty the callback

In vue-Next’s source code, this step is extracted as a common function. In order to conform to vue-next’s design, we extracted this step, named effect

The first argument to a function is a function that, when executed, triggers a read of the variables in the function and collects the corresponding callback function

The second argument to the function is an object

There is a schedular attribute that expresses a specially specified callback function, which is the first argument if it is not available

There is a lazy attribute, which when true means that the function passed in with the first argument is not executed immediately. The default is false, which specifies the function passed in with the first argument immediately

reactivity/effect.js

// Set of callback functions const targetMap = new WeakMap(); Export let activeEffect; // Export let activeEffect;- // Sets the current callback function
- export function setActiveEffect(effect) {
- activeEffect = effect;
-}

+ // Sets the current callback function
+ export function effect(fn, options = {}) {
+ const effectFn = () => {
+ // Sets the currently active callback function
+ activeEffect = effectFn;
+ // Execute the fn collection callback function
+ let val = fn();
+ // null callback function
+ activeEffect = '';
+ return val;
+};
+ // Options configuration
+ effectFn.options = options;
+ // The function is executed for the first time by default
+ if (! options.lazy) {
+ effectFn();
+}
+ return effectFn;
+}Export function track(target, key) {// Const depsMap = targetmap. get(target); If (depsMap) {const deps = depmap. get(key); If (deps) {// triggers the callback function- deps.forEach((v) => v());
+ deps.forEach((v) => {
+ // Specially specified callback functions are stored in the schedular
+ if (v.options.schedular) {
+ v.options.schedular();
+}
+ // When no callback function is specified
+ else if (v) {
+ v();
+}
+});}}}Copy the code

reactivity/index.js

Export effect

export * from './reactive';
export * from './watch';
export * from './computed';
+ export * from './effect';
Copy the code

main.js

import { reactive, effect } from './reactivity';

const test = reactive({
  a: 1}); effect((a)= > {
  document.title = test.a;
});

setTimeout((a)= > {
  test.a = 2;
}, 1000);
Copy the code

Effect () => {document.title = test.a; } This callback is put into test.a. When test.a changes, the corresponding callback is triggered

TargetMap is shown

The graph structure is shown in figure

Similarly, we changed the notation in computed and watch to effect

reactivity/computed.js

import { effect } from './effect';

export function computed(fn) {
  // This value will only be true if the variable is changed the first time it comes in
  let dirty = true;
  let value;
  const runner = effect(fn, {
    schedular: (a)= > {
      dirty = true;
    },
    // This is not required for the first time
    lazy: true});/ / the return value
  return {
    get value() {
      // When the flag is true, the variable needs to be changed
      if (dirty) {
        value = runner();
        // null-dependent
        dirty = false;
      }
      returnvalue; }}; }Copy the code

reactivity/watch.js

import { effect } from './effect';

export function watch(fn, cb, options = {}) {
  let oldValue;
  const runner = effect(fn, {
    schedular: (a)= > {
      // When the dependency is executed, the new value is obtained
      let newValue = fn();
      // Trigger the callback function
      cb(newValue, oldValue);
      // Assign the new value to the old value
      oldValue = newValue;
    },
    // This is not required for the first time
    lazy: true});// Read values and collect dependencies
  oldValue = runner();
}
Copy the code

main.js

import { reactive, watch, computed } from './reactivity';

const test = reactive({
  a: 1});const w = computed((a)= > test.a + 1);

watch(
  (a)= > test.a,
  (val) => {
    console.log(val); / / 2});console.log(w.value); / / 2
test.a = 2;
console.log(w.value); / / 3
Copy the code

You can see the code executing normally, with the targetMap shown and two callback functions stored in attribute A

The targetMap graph structure is shown in figure 1

Add options for Watch

Let’s look at this example

import { watch, reactive } from './reactivity';

const test = reactive({
  a: {
    b: 1,}}); watch((a)= > test.a,
  (val) => {
    console.log(val); // No trigger}); test.a.b =2;
Copy the code

We observed test.a with watch, and when we changed test.a.b, the observed callback did not trigger, as students who have used vue will know, this situation should be solved with deep property

So how is deep implemented

Let’s recall the process of collecting callback functions

When test.a is read, the callback is collected in test.a, but test.a.b is not read, so the callback is not collected in test.a.b

So we just need to go through test in depth to read the properties while the callback is being collected

Another important thing to note here is that when we intercept objects using Reactive, we don’t intercept the second layer of the object

const test = {
  a: {
    b: 1,}};const observe = new Proxy(test, {
  get(target, key, receiver) {
    return Reflect.set(target, key, receiver); }}); test.a// Trigger interception
test.a.b // No interception is triggered
Copy the code

So we need to recursively proxy the intercepted values

reactivity/reactive.js

Const observe = new Proxy(value, {get(target, key, receiver) {// Collect callback function track(target, key);+ const res = Reflect.get(target, key, receiver);
+ return canObserve(res) ? reactive(res) : res;
- return Reflect.get(target, key, receiver);}, // Set (target, key, newValue, receiver) {const res = reflect. set(target, key, newValue, receiver); // Trigger the callback function trigger(target, key); return res; }});Copy the code

reactivity/watch.js

import { effect } from './effect';
+ import { isPlainObject } from './utils';

+ // Depth traversal value
+ function traverse(value) {
+ if (isPlainObject(value)) {
+ for (const key in value) {
+ traverse(value[key]);
+}
+}
+ return value
+}

export function watch(fn, cb, options = {}) {
+ let oldValue;
+ let getters = fn;
+ // Depth traversal value when the deep attribute is present
+ if (options.deep) {
+ getters = () => traverse(fn());
+}
+ const runner = effect(getters, {
- const runner = effect(fn, {Schedular: () => {// let newValue = runner(); // Trigger the callback cb(newValue, oldValue); // assign the newValue to the oldValue oldValue = newValue; }, // the first time not to execute lazy: true,}); // Read the value and collect the callback oldValue = runner(); }Copy the code

main.js

import { watch, reactive } from './reactivity';

const test = reactive({
  a: {
    b: 1,}}); watch((a)= > test.a,
  (val) => {
    console.log(val); // { b: 2 }
  },
  {
    deep: true}); test.a.b =2;
Copy the code

TargetMap is as follows, we added back functions not only on the object {a: {b: 1}}, but also on {b: 1}

The targetMap graph structure is shown in figure 1

As you can see, the “deep” property allows you to observe the data in depth. In the examples above, we are using objects. In fact, the “deep” property is also required for arrays, but arrays are handled differently

Array handling

import { watch, reactive } from './reactivity';

const test = reactive([1.2.3]);

watch(
  (a)= > test,
  (val) => {
    console.log(val); // No trigger}); test[0] = 2;
Copy the code

The above column will not fire because we only read test and there is nothing in targetMap

So in the case of arrays, we also fall into the category of deep observation, and when we do deep traversal, we need to read every item in the array

reactivity/watch.js

Function traverse(value) {if (isPlainObject(value)) {for (const key in value) {traverse(value[key]);  }}+ // handle arrays
+ else if (Array.isArray(value)) {
+ for (let i = 0; i < value.length; i++) {
+ traverse(value[i]);
+}
+}
  return value;
}
Copy the code

main.js

import { watch, reactive } from './reactivity';

const test = reactive([1.2.3]);

watch(
  (a)= > test,
  (val) => {
    console.log(val); / / (2, 2, 3]
  },
  {
    deep: true}); test[0] = 2;
Copy the code

Add deep to true in the column above to see that the callback fires

TargetMap is shown

The first Set item is a Symbol(symbol.tostringTag), which we don’t care about

We store each item of the array as a callback function, and we also store it on the length property of the array

Let’s look at one more example

import { watch, reactive } from './reactivity';

const test = reactive([1.2.3]);

watch(
  (a)= > test,
  (val) => {
    console.log(val); // No trigger
  },
  {
    deep: true}); test[3] = 4;
Copy the code

The above column will not trigger. If you are careful, you may remember that only three positions with index 0, 1 and 2 are collected in targetMap, and the newly added index 3 is not collected

How do we deal with this critical situation?

Remember when we first talked about parsing the array pop method under proxy, we summed it up in one sentence

Length is read and reset when it has a length effect on the array itself

Now when we increment the index, we actually change the length of the array itself, so the length will be reset, and now we have a method, if we can’t find the callback function in the new index, we can read the callback function stored in the array length

reactivity/reactive.js

Const observe = new Proxy(value, {get(target, key, receiver) {// Collect callback function track(target, key); const res = Reflect.get(target, key, receiver); return canObserve(res) ? reactive(res) : res; }, // Intercept set(target, key, newValue, receiver) {+ const hasOwn = target.hasOwnProperty(key);
+ const oldValue = Reflect.get(target, key, receiver);
    const res = Reflect.set(target, key, newValue, receiver);
+ if (hasOwn) {
+ // Sets the previous properties
+ trigger(target, key, 'set');
+ } else if (oldValue ! == newValue) {
+ // Add new attributes
+ trigger(target, key, 'add');
+}
- // Triggers the callback function
- trigger(target, key);return res; }});Copy the code

We use hasOwnProperty to determine if the current property is on the object. If the new index of the array is not on the object, it will trigger(target, key, ‘add’). This function

reactivity/effect.js

Export function trigger(target, key, type) {const depsMap = targetmap. get(target); If (depsMap) {// Get the set of callback functions for each attribute- const deps = depsMap.get(key);
+ let deps = depsMap.get(key);
+ // Gets the callback function stored in length when an array is added to an attribute
+ if (type === 'add' && Array.isArray(target)) {
+ deps = depsMap.get('length');
+}Schedular if (v.mask. Schedular) {v.mask. Schedular (); } else if (v) {v(); }}); }}}Copy the code

Then we deal with the case of type add, when type is add and the object is an array, we read the callback stored on length

You can see that with this rewrite, the column is working fine

conclusion

In fact, after reading this article, you will find that this is not a vUE source code anatomy, we did not post the corresponding source code in VUE-next, because I think it is better to think about how to implement from scratch rather than from the source code interpretation to think about why it is implemented

Of course, this article has only implemented the simple responsive principle, if you want to see the complete code can click here, although many function points are not implemented, but the general idea is the same, if you can read this question explained the idea, you can certainly understand vuE-next corresponding source code