How to write Vue next response? Detailed explanation of this article

Posted by sorenchr on Thu, 18 Jun 2020 06:06:20 +0200

preface

1. This paper will write a response principle in Vue next from scratch. Due to the difficulty of space and understanding, we will only implement the core api and ignore some boundary function points

The APIs to be implemented in this article include

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

    2. Recently, many people asked me about the front end of their personal letters. Few people logged in to the blog didn't reply in time, so I built a front-end buckle skirt 519293536. If you have any questions, please come to me directly. I will try my best to help you. I seldom read blog letters

Project construction

We created the project with the recently popular vite

Versions demonstrated in this article

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

Let's download the template first

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

After the template is downloaded, enter the directory

cd vue-next-reactivity
 Copy code

Then install the dependency

yarn install
 Copy code

Then we only keep the main.js File, empty the rest and create the activity folder we want to use

 

 

The entire file directory is as shown in the figure. Enter the npm run dev project to start it

 

 

Handwritten code

The essence of responsive principle

Before we start to write, let's think about the principle of response?

Let's explain it from the use of Vue next

The response expressions used in Vue next are roughly divided into three

  • template or render

After the variables used in the page are changed, the page is refreshed automatically

  • computed

When the variables used in the calculation attribute function change, the calculation attribute changes automatically

  • watch

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

From the above three points, we can summarize the essence of response principle

When a value changes, the corresponding callback function will be triggered automatically

The callback function here is the page refresh function in template, the recalculate attribute value function in computed and the watch callback that is a callback function

So we're going to implement the responsive principle, and now we're going to split it into two questions

  • Change of monitoring value
  • Trigger the corresponding callback function

We solved these two problems and wrote the principle of response

Change of monitoring value

javascript provides two APIs to change the listening value

One is used in vue2.x Object.defineProperety

const obj = {};
let aValue = 1;
Object.defineProperty(obj, 'a', {
  enumerable: true,
  configurable: true,
  get() {
    console.log('I was read');
    return aValue;
  },
  set(value) {
    console.log('I was set up');
    aValue = value;
  },
});

obj.a; // I was read
obj.a = 2; // I was set up
Copy code

Another method is the proxy used in Vue next, which is also used in this handwriting

This method solves the problem Object.defineProperety Four pain points of

  1. Unable to block the addition and deletion of properties on the object
  2. Unable to block the methods that can affect the current array by calling push pop shift unshift on the array
  3. Too much performance overhead in intercepting array indexes
  4. Unable to block Set types such as Set Map

The first two, of course

As for the third point, the change of array index in vue2.x has to be set through this.$set, which leads to many students mistaking it Object.defineProperety In fact, it can't intercept array index. The reason why vue2.x didn't do this is that it's not cost-effective

The above four points of proxy can be solved perfectly. Now let's start to write a proxy interception!

proxy interception

We created two files in the activity directory

utils.js Store some common methods

reactive.js Method of storing proxy interception

Let's go first utils.js First add the method to be used to determine whether it is a native object

reactivity/utils.js

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

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

reactivity/reactive.js

import { isPlainObject } from './utils';

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

// Intercept data
export function reactive(value) {
  // Values that cannot be monitored are returned directly
  if (!canObserve(value)) {
    return;
  }
  const observe = new Proxy(value, {
    // Block read
    get(target, key, receiver) {
      console.log(`${key}Read it`);
      return Reflect.get(target, key, receiver);
    },
    // Intercept settings
    set(target, key, newValue, receiver) {
      const res = Reflect.set(target, key, newValue, receiver);
      console.log(`${key}Set up`);
      return res;
    },
  });
  // Return the observed proxy instance
  return observe;
}
Copy code

reactivity/index.js

Export method

export * from './reactive';
Copy 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 read

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

As you can see, we added a reactive method for proxy interception of objects and arrays, and returned the corresponding proxy instance

1, 2, 3 in the column are all well understood. Let's explain the fourth one

When we call the pop method, we first trigger the get intercept, and the Print Pop is read

After calling the pop method, the length of the array is read, the get intercept is triggered, and the print length is read.

The return value of pop method is the currently deleted value. It will read the value of array index 2 and trigger get intercept. Print 2 is read

After pop, the array length will be changed, which will trigger set interception, and the print length will be set

You can also try other ways to change the array

Can be summed up as a sentence

When there is an effect on the length of the array itself, the length will be read and reset, and the index of the changed value will also be read or reset (push unshift)

Add callback function

We use proxy to intercept the value and solve our first problem

But we didn't trigger the callback function after the value changed. Now let's supplement the callback function

reactivity/reactive.js

import { isPlainObject } from './utils';

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

+ // Assumed callback function
+ function notice(key) {
+   console.log(`${key}Changed and triggered callback function`);
+ }

// Intercept data
export function reactive(value) {
  // Values that cannot be monitored are returned directly
  if (!canObserve(value)) {
    return;
  }
  const observe = new Proxy(value, {
    // Block read
    get(target, key, receiver) {
-     console.log(`${key}Read it`);
      return Reflect.get(target, key, receiver);
    },
    // Intercept settings
    set(target, key, newValue, receiver) {
      const res = Reflect.set(target, key, newValue, receiver);
-     console.log(`${key}Set up`);
+     // Trigger hypothetical callback function
+     notice(key);
      return res;
    },
  });
  // Return the observed proxy instance
  return observe;
}

Copy code

In the most intuitive way, we trigger our hypothetical callback in the set interception when the 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 function is triggered
test.b = 3; // b is changed and the callback function is triggered
Copy code

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

But there must be something wrong with this list. There is more than one problem. Let's upgrade it step by step

Collection of callback functions

A and b in the above column all correspond to a callback function notice. In the actual scenario, a and b may correspond to different callback functions respectively. If we only use a simple global variable to store the callback function, it is obviously not appropriate. If the latter will cover the former, then how can we make the callback function correspond to each value?

It's easy to think of the key value object in js. If attribute a and attribute b are the key values of the object, they can distinguish their value values

 

 

But collecting callback functions with objects is problematic

In the above column, we have a test object with properties a and b. when we have another object test1, if it also has properties a and b, isn't it repeated? This will trigger the problem of repetition we mentioned earlier

 

 

Some students may say that it's not good to use test and test1 as attribute names in another layer. This method is also infeasible. In the same execution context, there won't be two identical variable names, but different execution contexts can, which leads to the problem of repetition mentioned above

To deal with this problem, we need to use the characteristics of js object passing by reference

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

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

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

So if we can directly use the object as the key value, can we distinguish the seemingly "same" object?

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

Here we will use a new data structure Map and WeakMap in es6

We illustrate the storage mode of this data structure with an example

Suppose now that we have two objects obj with the same data structure, each of which has its own properties a and b. the change of each property will trigger different callback functions

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

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

Use Map and WeakMap to store as shown in the following figure

We define the global variable targetMap for storing callback function as a WeakMap. Its key value is each object. In this column, there are two objs. The value value of targetMap is a Map. In this column, two objs have two attributes a and b respectively. The key of Map is the attribute a and b, and the value of Map is the Set callback function Set corresponding to the attributes a and b respectively

 

 

You may wonder why targetMap uses WeakMap and the Map used for storing the attributes of each object. This is because WeakMap can only use the object as the key, and Map can be an object or a string. For example, the sub attributes a and b of the above columns can only be saved with Map

Let's use the actual api to deepen our understanding of this storage structure

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

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

 

 

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

Here we need to add Val = >{ console.log (VAL)} callback function is placed in the set of test.a, as shown in the figure

 

 

  • template
createApp({
  setup() {
    return () => h('div', test.a);
  },
});
Copy 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 the callback function, let's think about how to put the callback function into our defined storage structure

Let's take the list above

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

In this column, we need to change the callback function Val = >{ console.log (VAL)}) put it into the Set set of test.a, so we need to get the object test and the property a of the current object. If we only pass () = > test.a, we can only get the value of test.a, and we cannot know the specific object and property

But in fact, after reading the value of test.a, you can get specific objects and properties in disguise

Do you remember that we used proxy to intercept the reading of test.a. the first parameter intercepted by get is the currently read object, and the second parameter is the currently read property

So the collection of callback functions is handled in get interception of proxy

Now, let's use the code to implement the ideas we just made

First we create effect.js This file is used to store the collection method and trigger method of the callback function

reactivity/effect.js

// Callback function collection
const targetMap = new WeakMap();

// Collect callback functions
export function track(target, key) {
}
// Trigger callback function
export function trigger(target, key) {
}
Copy code

Then rewrite the intercepted content in the proxy

reactivity/reactive.js

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

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

- // Assumed callback function
- function notice(key) {
-   console.log(`${key}Changed and triggered callback function`);
- }

// Intercept data
export function reactive(value) {
  // Values that cannot be monitored are returned directly
  if (!canObserve(value)) {
    return;
  }
  const observe = new Proxy(value, {
    // Block read
    get(target, key, receiver) {
+     // Collect callback functions
+     track(target, key);
      return Reflect.get(target, key, receiver);
    },
    // Intercept settings
    set(target, key, newValue, receiver) {
      const res = Reflect.set(target, key, newValue, receiver);
+     // Trigger callback function
+     trigger(target, key);
-     // Trigger hypothetical callback function
-     notice(key);
      return res;
    },
  });
  // Return the observed proxy instance
  return observe;
}
Copy code

What we haven't added here is that we can clearly see the location of the collection and trigger

Now let's supplement the track collection callback function and trigger callback function

reactivity/effect.js

// Callback function collection
const targetMap = new WeakMap();

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

// Trigger 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 property
    const deps = depsMap.get(key);
    if (deps) {
      // Trigger callback function
      deps.forEach((v) => v());
    }
  }
}
Copy code

Then run our demo

main.js

import { reactive } from './reactivity';

const test = reactive({
  a: 1,
  b: 2,
});

test.b; // Read collection callback function

setTimeout(() => {
  test.a = 2; // No triggers because no callbacks are collected
  test.b = 3; // I am a callback function
}, 1000);
Copy code

Let's take a look at the targetMap structure at this time

 

 

There is a key value {a: 1,b: 2} in targetMap. Its value value is also a Map. There is a key value b in this Map. The value of this Map is the Set of callback functions. Now there is only one () we have written = > console.log ('I'm a callback function')

It's like this with graphic structure

 

 

You may think that to collect callback functions and read test.b once is an anti-human operation. This is because we haven't talked about the corresponding api yet. The normal read operation doesn't need such a manual call, and the api will handle it by itself

watch

There is a big problem in the above columns, that is, we do not have a custom callback function, which is directly written dead in the code

Now we will implement the custom callback function through watch

There are many APIs for watch in Vue next, and we will implement some of them, which is enough for us to understand the principle of response

The demo we will implement is as follows

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

const test = reactive({
  a: 1,
});

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

watch takes three parameters

The first parameter is a function that represents the value being monitored

The second parameter is a function to express the callback to be triggered after the listening value changes. The first parameter is the value after the change, and the second parameter is the value before the change

The third parameter is an object with only one deep attribute. Deep table depth observation

Now what we need to do is to put the callback function (VAL) = >{ console.log (VAL);} in the Set set of test.a

So before () = > test.a can read test.a, we need to store the callback function with a variable

When reading test.a to trigger the track function, you can get this variable in the track function and store it in the Set of corresponding properties

reactivity/effect.js

// Callback function collection
const targetMap = new WeakMap();

+ // Currently active callback function
+ export let activeEffect;

+ // Set current callback function
+ export function setActiveEffect(effect) {
+   activeEffect = effect;
+ }

// Collect callback functions
export function track(target, key) {
  // No callback function is activated, exit directly and do not collect
  if (!activeEffect) {
    return;
  }
  // Get the map of each object through the object
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    // When objects are first collected, we need to add a map collection
    targetMap.set(target, (depsMap = new Map()));
  }
  // Get the callback function collection of each property under the object
  let dep = depsMap.get(key);
  if (!dep) {
    // When the object properties are collected for the first time, we need to add a set set
    depsMap.set(key, (dep = new Set()));
  }
  // Add callback function here
- dep.add(() => console.log('I am a callback function'));
+ dep.add(activeEffect);
}

// Trigger callback function
export function trigger(target, key) {
  // ellipsis
}
Copy code

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

This is also a way to use common 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 callback function before executing fn to get oldValue
  setActiveEffect(() => {
    // Make sure that the callback function triggers a new value
    let newValue = fn();
    // Trigger callback function
    cb(newValue, oldValue);
    // New value assigned to old value
    oldValue = newValue;
  });
  // Read values and collect callback functions
  oldValue = fn();
  // Null callback function
  setActiveEffect('');
}
Copy code

Very simple lines of code, set the callback function through setActiveEffect before executing fn to read the value, so that the current callback function activeEffect can be obtained in the track function when reading, and empty the callback function after reading, and then it is finished

We also need to export the watch method

reactivity/index.js

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

main.js

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

const test1 = reactive({
  a: 1,
});

watch(
  () => test1.a,
  (val) => {
    console.log(val) // 2;
  }
);

test1.a = 2;
Copy code

You can see that the normal execution of column sub prints out 2. Let's take a look at the structure of targetMap

There is a key value {a:1} in targetMap, and its value value is also a Map. There is a key value a in this Map, and the value of this Map is the callback function (VAL) = >{ console.log (val); }

 

 

The graph structure of targetMap is as follows

 

 

computed

We will go back to other api supplements of watch. After feeling the thinking of responsive principle, we can take advantage of the hot iron to realize the function of computed

There are many ways to write the same calculated api in Vue next. We will only write the function return value

export function computed(fn) {}

const test = reactive({
  a: 1,
});

const w = computed(() => test.a + 1);
Copy code

However, if we only implement the written method of computed incoming function, in fact, it has little to do with the responsive principle in Vue next

Because the api read value provided in Vue next is not directly read W, but w.value

We create computed.js , complementing the computed function

reactivity/computed.js

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

You can see just a few lines of code. Each time you read value, run fn evaluation again

reactivity/index.js

Let's export it again

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

main.js

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

const test = reactive({
  a: 1,
});

const w = computed(() => test.a + 1);

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

You can see the perfect operation of Liezi

Here are two questions

  • Why api is written in the form of w.value instead of directly reading w

This is the same reason as ref. proxy cannot intercept the basic type, so it needs to add a layer of value to wrap the object

  • Does the computed in Vue next really have nothing to do with the responsive principle

In fact, there is a relationship. In the writing method of only realizing the computed input function, the principle of response enables optimization

We can see that if the value of w.value does not change according to our previous writing method, we will execute fn once when we read it. When the amount of data increases, the performance will be greatly affected

How can we optimize it?

It's easy to think of executing fn once to compare old and new values, but this is the same as before, because we still execute fn once

Here we can use the response principle. As long as the internal influence value test.a is modified, we will execute fn again to get the value, otherwise we will read the previously stored value

reactivity/computed.js

import { setActiveEffect } from './effect';

export function computed(fn) {
  // This value will be true only after the variable is changed. It will be true the first time it comes in
  let dirty = true;
  // Return value
  let value;
  // Set to true to indicate that the next read needs to be retrieved
  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 value
        value = fn();
        // Air control dependency
        setActiveEffect('');
      }
      return value;
    },
  };
}
Copy code

We have defined a variable "dirty" to express whether the value has been modified or not. If it has been modified, it will be true

Similarly, before reading the value, we assign the callback function () = > {directory = true} to the intermediate variable activeEffect, and then perform fn reading. At this time, the callback is collected. When the corresponding property is changed, the directory is also changed

Let's run the above column again. The program is still running normally

Let's take a look at the structure of targetMap. targetMap has a key value {a:1}, and its value value is also a Map. In this Map, there is a key value a, and the value of this Map is the callback function changedirty() {dirty = true;}

 

 

The graph structure of targetMap is as follows

 

 

Extract effect

In watch and computed, we have gone through three steps: setting callback function = > reading value (storing callback function) = > clearing callback function

In the source code of Vue next, this step is extracted as a common function. In order to conform to the design of Vue next, we extract this step, named effect

The first parameter of the function is a function. After the function is executed, it will trigger the reading of each variable in the function and collect the corresponding callback function

The second argument to the function is an object

There is a schedule attribute that represents a specially specified callback function. If there is no such attribute, the callback function is the first parameter

There is a lazy attribute. If it is true, the function passed in by the first parameter does not need to be executed immediately. The default is false, that is, the function passed in by the first parameter is specified immediately

reactivity/effect.js

// Callback function collection
const targetMap = new WeakMap();

// Currently active callback function
export let activeEffect;

- // Set current callback function
- export function setActiveEffect(effect) {
-  activeEffect = effect;
- }

+ // Set current callback function
+ export function effect(fn, options = {}) {
+   const effectFn = () => {
+     // Set the currently active callback function
+     activeEffect = effectFn;
+     // Execute fn collection callback function
+     let val = fn();
+     // Empty callback function
+     activeEffect = '';
+     return val;
+   };
+   // options configuration
+   effectFn.options = options;
+   // Default first execution function
+   if (!options.lazy) {
+     effectFn();
+   }
+   return effectFn;
+ }

// Collect callback functions
export function track(target, key) {
  // ellipsis
}

// Trigger 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 property
    const deps = depsMap.get(key);
    if (deps) {
      // Trigger callback function
-     deps.forEach((v) => v());
+     deps.forEach((v) => {
+       // The specially specified callback function is stored in the scheduler
+       if (v.options.schedular) {
+         v.options.schedular();
+       }
+       // Trigger directly when no callback function is specified
+       else if (v) {
+         v();
+       }
+     });
    }
  }
}
Copy code

reactivity/index.js

Export effect

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

main.js

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

const test = reactive({
  a: 1,
});

effect(() => {
  document.title = test.a;
});

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

The first time the effect is self executing, it will () = >{ document.title =test.a;} this callback function is put into test.a. when test.a changes, trigger the corresponding callback function

Target map is as shown in the figure

 

 

The figure structure is as shown in the figure

 

 

Similarly, we change the writing method in computed and watch and replace it with effect

reactivity/computed.js

import { effect } from './effect';

export function computed(fn) {
  // This value will be true only after the variable is changed. It will be true the first time it comes in
  let dirty = true;
  let value;
  const runner = effect(fn, {
    schedular: () => {
      dirty = true;
    },
    // No execution for the first time
    lazy: true,
  });
  // Return value
  return {
    get value() {
      // When the flag is true, the variable needs to be changed
      if (dirty) {
        value = runner();
        // Air control dependency
        dirty = false;
      }
      return value;
    },
  };
}
Copy code

reactivity/watch.js

import { effect } from './effect';

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

main.js

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

const test = reactive({
  a: 1,
});

const w = computed(() => test.a + 1);

watch(
  () => test.a,
  (val) => {
    console.log(val); // 2
  }
);

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

You can see that the code executes normally, targetMap is as shown in the figure, and two callback functions are stored in attribute a

 

 

The graph structure of targetMap is as shown in the figure

 

 

options to supplement watch

Let's take a look at this list

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

const test = reactive({
  a: {
    b: 1,
  },
});

watch(
  () => test.a,
  (val) => {
    console.log(val); // No trigger
  }
);

test.a.b = 2;
Copy code

We use watch to observe test.a. when we change test.a.b, the observed callback is not triggered. Students who have used vue will know that this situation should be solved by using the deep attribute

So how does deep work

Let's recall the process of callback function collection

When test.a is read, the callback function is collected into test.a, but test.a.b is not read here, so the callback function is not collected into test.a.b naturally

So we only need to go through the test in depth and read the properties when we collect the callback function

It should also be noted that when we intercept an object with reactive, we will not 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 intercept
test.a.b // Intercept will not be triggered
Copy code

So we need to recursively proxy the intercept value

reactivity/reactive.js

const observe = new Proxy(value, {
  // Block read
  get(target, key, receiver) {
    // Collect callback functions
    track(target, key);
+   const res = Reflect.get(target, key, receiver);
+   return canObserve(res) ? reactive(res) : res;
-   return Reflect.get(target, key, receiver);
  },
  // Intercept settings
  set(target, key, newValue, receiver) {
    const res = Reflect.set(target, key, newValue, receiver);
    // Trigger callback function
    trigger(target, key);
    return res;
  },
});
Copy 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 exists
+ if (options.deep) {
+   getters = () => traverse(fn());
+ }
+ const runner = effect(getters, {
- const runner = effect(fn, {
    schedular: () => {
      // When the dependency is executed, the new value is obtained
      let newValue = runner();
      // Trigger callback function
      cb(newValue, oldValue);
      // New value assigned to old value
      oldValue = newValue;
    },
    // No execution for the first time
    lazy: true,
  });
  // Read values and collect callback functions
  oldValue = runner();
}
Copy code

main.js

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

const test = reactive({
  a: {
    b: 1,
  },
});

watch(
  () => test.a,
  (val) => {
    console.log(val); // { b: 2 }
  },
  {
    deep: true,
  }
);

test.a.b = 2;
Copy code

targetMap is as follows, we not only added the return function on the object {A: {b: 1}}, but also added the return function on {b: 1}

 

 

The graph structure of targetMap is as shown in the figure

 

 

You can see that after adding the deep attribute, you can observe the data in depth. In the above columns, we all use objects. In fact, deep observation is also necessary for arrays, but the array processing is a little different. Let's see the differences

Array processing

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

const test = reactive([1, 2, 3]);

watch(
  () => test,
  (val) => {
    console.log(val); // No trigger
  }
);

test[0] = 2;
Copy code

The above columns will not be triggered, because we only read the test and there is nothing in the targetmap

 

 

So in the case of arrays, we also belong to the deep observation category. When traversing the depth, we need to read every item of the array

reactivity/watch.js

// Depth traversal value
function traverse(value) {
  // Working with objects
  if (isPlainObject(value)) {
    for (const key in value) {
      traverse(value[key]);
    }
  }
+ // Working with arrays
+ else if (Array.isArray(value)) {
+   for (let i = 0; i < value.length; i++) {
+     traverse(value[i]);
+   }
+ }
  return value;
}
Copy code

main.js

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

const test = reactive([1, 2, 3]);

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

test[0] = 2;
Copy code

Add "deep" as "true" in the above column to see that the callback is triggered

Target map is as shown in the figure

 

 

The first item Set is a symbol( Symbol.toStringTag )We don't have to worry about it

We store the callback function for each item of the array, and also store it on the length attribute of the array

Let's have another look at Liezi

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

const test = reactive([1, 2, 3]);

watch(
  () => test,
  (val) => {
    console.log(val); // No trigger
  },
  {
    deep: true,
  }
);

test[3] = 4;
Copy code

The above columns will not be triggered. Careful students may remember that only three locations with index 0 1 2 are collected in targetMap, and those with index 3 are not collected

 

 

How should we deal with this critical situation?

Do you remember that we first talked about the parsing of array pop method under proxy? At that time, we summed it up as a sentence

Length will be read and reset when there is a length effect on the array itself

Now we have changed the length of the array itself by adding new value through the index, so the length will be reset. Now we have a method. When we can't find the callback function on the new index, we can read the callback function stored on the length of the array

reactivity/reactive.js

const observe = new Proxy(value, {
  // Block read
  get(target, key, receiver) {
    // Collect callback functions
    track(target, key);
    const res = Reflect.get(target, key, receiver);
    return canObserve(res) ? reactive(res) : res;
  },
  // Intercept settings
  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) {
+     // Set previous properties
+     trigger(target, key, 'set');
+   } else if (oldValue !== newValue) {
+     // Add a new property
+     trigger(target, key, 'add');
+   }
-   // Trigger callback function
-   trigger(target, key);
    return res;
  },
});
Copy code

We use hasOwnProperty to determine whether the current property is on the object. It is obvious that the new index of the array is not available. At this time, we will go to trigger(target, key, 'add'); this function

reactivity/effect.js

// Trigger callback function
export function trigger(target, key, type) {
  // Get the map of the object
  const depsMap = targetMap.get(target);
  if (depsMap) {
    // Get the collection of callback functions corresponding to each property
-   const deps = depsMap.get(key);
+   let deps = depsMap.get(key);
+   // Get the callback function stored on the length directly when the array adds a new attribute
+   if (type === 'add' && Array.isArray(target)) {
+     deps = depsMap.get('length');
+   }
    if (deps) {
      // Trigger callback function
      deps.forEach((v) => {
        // The specially specified callback function is stored in the scheduler
        if (v.options.schedular) {
          v.options.schedular();
        }
        // Trigger directly when no callback function is specified
        else if (v) {
          v();
        }
      });
    }
  }
}
Copy code

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

You can see that with such an override, the column can run normally

summary

1. In fact, after reading this article, you will find that this article is not an anatomy of vue source code. We didn't post the corresponding source code in vue next in the whole process, because I think it's better to think about how to realize it from scratch than from the source code interpretation

2. Of course, this article only implements the simple responsive principle. If you want to see the complete code, you can click here Although many function points have not been implemented, the general ideas are the same. If you can read the ideas explained in this question, you can definitely understand the corresponding source code in Vue next
3. Recently, I asked many people about the front-end problems in their private letters. Few people logged in to the blog didn't reply in time, so I built a front-end buckle skirt 519293536. If you have any questions, please come to me directly. I will try my best to help you. I seldom read blog letters

The text and pictures of this article come from the Internet and my own ideas. They are only for learning and communication. They have no commercial use. The copyright belongs to the original author. If you have any questions, please contact us in time for handling



Topics: Javascript Attribute Vue npm REST