preface

It has been more than a year since vuE3 was released. Recently, I started to polish the source code. I am really ashamed to say nothing and turn over a new leaf! Direct source code is boring

This is just a simplified version of my own understanding of the reactive principle

The target

Our goal for today

  • 1. Thoroughly understand the principle of response formula by starting from REF
  • 2. Understand how the effect side function is executed in response

The principle of ref function

What is the ref function

Takes an internal value and returns a reactive and mutable REF object. The ref object has a single property.value pointing to an internal value.Copy the code

The current ref function returns an Object containing get and set, converted to ES5, which is a value that Object.defineProperty listens for

Cut the crap. Look at the code

// Determine if it is an object
const isObject = (val) = >val ! = =null && typeof val === 'object';

// function of ref
function ref(val) {
  Value ->proxy = value->proxy = value->proxy
  // We don't use value access in the object case
  return isObject(val) ? reactive(val) : new refObj(val);
}

// Create a responsive object
class refObj {
  constructor(val) {
    this._value = val;
  }
  get value() {
    // Trigger GET after the first execution to collect dependencies
    track(this.'value');
    return this._value;
  }
  set value(newVal) {
    console.log(newVal);
    this._value = newVal;
    trigger(this.'value'); }};Copy the code

When you change the value, you trigger the get and set of the current object to achieve the responsiveness

It’s easy to find that it’s an object, and then we can do things in get and set, like establish a relationship between the side effects and the current value, which is a dependency collection

But there’s a problem, if you pass an object in ref, when you new the current object, it doesn’t work, because you can’t listen to the value in it

This is where vuE3’s well-known Proxy comes in

Proxy

The specific use method, I will not introduce, VUE3 out for such a long time, I believe we all understand his characteristics, directly on the code

// We will not consider nested objects in the object for the sake of understanding the principle
// The object is reactive
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // Reflect is used to perform the default action on an object, more formally and functionally
      // Both Proxy and Object methods Reflect
      const res = Reflect.get(target, key, receiver);
      track(target, key);
      return res;
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return res;
    },
    deleteProperty(target, key) {
      const res = Reflect.deleteProperty(target, key);
      trigger(target, key);
      returnres; }}); }Copy the code

In the above code, the object type is also changed into a reactive object, the next is the key place, the two reactive object get and set to do dependency collection, and the corresponding side effect update

Since you need side effects, why not collect them first, so effect is the bridge function

Effect to realize

So what does this effect do in general? It simply wraps the current side effect function and executes it, triggering the get in the side effect function, where the current side effect is collected

// Save temporary dependency functions for wrapping
const effectStack = [];

// In order to generalize the method in the source code, it also passes in many parameters to be compatible with different situations
// We want to understand the principle, just need to wrap fn
function effect(fn) {
  // Wrap the current dependency function
  const effect = function reactiveEffect() {
    // Error handling is also included in the mock source code, in order to prevent you from writing errors, which is the genius of the framework
    if(! effectStack.includes(effect)) {try {
        // Put the current function on a temporary stack, in order to trigger get in the following execution, can be found in the dependency collection of the current variable dependencies to establish relationships
        effectStack.push(fn);
        // Execute the current function and start to rely on the collection
        return fn();
      } finally {
        // Execute successfully unstackeffectStack.pop(); }}; }; effect(); }Copy the code

His principle is more clever, using a stack, the currently executing side effect function is temporarily stored, fetched in get, stored in the dependent object.

In this case, it is natural to have a function to collect dependencies (this dependency can be a render function or a side effect, in our example, there is no view rendering related, both side effects), so define a track function to collect dependencies

Track to achieve

Cut to the code

// Dependency map objects can only accept objects
let targetMap = new WeakMap(a);// Establish relationships in collected dependencies
function track(target, key) {
  // Retrieve the last data content
  const effect = effectStack[effectStack.length - 1];
  // If the current variable has dependencies
  if (effect) {
    // Check whether target exists in the current map
    let depsMap = targetMap.get(target);
    // If not
    if(! depsMap) {// New Map stores the current WeakMap
      depsMap = new Map(a); targetMap.set(target, depsMap); }// Get the set of response functions corresponding to key
    let deps = depsMap.get(key);
    if(! deps) {// Establish the relationship between the current key and its dependencies, because a key can have multiple dependencies
      // To prevent duplicate dependencies, use set
      deps = new Set(a); depsMap.set(key, deps); }// Store the current dependency
    if(! deps.has(effect)) { deps.add(effect); }}}Copy the code

Track is particular about it. In fact, it establishes the corresponding relationship between each key and dependency of the current responsive object, so as to notify all dependency updates when the key is changed. For fear that everyone will not understand, it kindly draws a picture for everyone to understand

Based on the structure in the diagram, we can see all the dependent data structures

Next we need to send out updates using the trigger function

The trigger to realize

The following code

// Used to trigger updates
function trigger(target, key) {
  // Get all dependencies
  const depsMap = targetMap.get(target);
  // If there are dependencies, pull them out
  if (depsMap) {
    // Get the set of response functions
    const deps = depsMap.get(key);
    if (deps) {
      // Execute all response functions
      const run = (effect) = > {
        // The source code has asynchronous scheduling tasks, which we omit hereeffect(); }; deps.forEach(run); }}}Copy the code

From the above code, it is very simple to extract the dependencies corresponding to the currently modified key, and perform all of them, which is called dispatching updates. At this point, the basic responsivity principle is basically over. It’s that easy and fun! After the attachment of their own drawings of the response flow chart, for everyone to understand, wrong place please comment

The last

My own understanding of the VUE responsive module to this all over, of course, there are a lot of compatible source processing, high-end writing. We are only studying the principle here, so we will not go into the details. If you are interested, please go to the Reactivity module for a detailed study. End attached can run the complete source code, try it yourself!

// Save temporary dependency functions for wrapping
const effectStack = [];

// Dependency map objects can only accept objects
let targetMap = new WeakMap(a);// Determine if it is an object
const isObject = (val) = >val ! = =null && typeof val === 'object';
// function of ref
function ref(val) {
  Value ->proxy = value->proxy = value->proxy
  // We don't use value access in the object case
  return isObject(val) ? reactive(val) : new refObj(val);
}

// Create a responsive object
class refObj {
  constructor(val) {
    this._value = val;
  }
  get value() {
    // Trigger GET after the first execution to collect dependencies
    track(this.'value');
    return this._value;
  }
  set value(newVal) {
    console.log(newVal);
    this._value = newVal;
    trigger(this.'value'); }};// We will not consider nested objects in the object for the sake of understanding the principle
// The object is reactive
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // Reflect is used to perform the default action on an object, more formally and functionally
      // Both Proxy and Object methods Reflect
      const res = Reflect.get(target, key, receiver);
      track(target, key);
      return res;
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return res;
    },
    deleteProperty(target, key) {
      const res = Reflect.deleteProperty(target, key);
      trigger(target, key);
      returnres; }}); }// At this point, the current ref object is already listening for data changes
const newRef = ref(0);
// But there is still no responsiveness, so how does it achieve responsiveness ---- rely on collection, trigger updates =
// for dependency collection
// In order to generalize the method in the source code, it also passes in many parameters to be compatible with different situations
// We want to understand the principle, just need to wrap fn
function effect(fn) {
  // Wrap the current dependency function
  const effect = function reactiveEffect() {
    // Error handling is also included in the mock source code, in order to prevent you from writing errors, which is the genius of the framework
    if(! effectStack.includes(effect)) {try {
        // Put the current function on a temporary stack, in order to trigger get in the following execution, can be found in the dependency collection of the current variable dependencies to establish relationships
        effectStack.push(fn);
        // Execute the current function and start to rely on the collection
        return fn();
      } finally {
        // Execute successfully unstackeffectStack.pop(); }}; }; effect(); }// Establish relationships in collected dependencies
function track(target, key) {
  // Retrieve the last data content
  const effect = effectStack[effectStack.length - 1];
  // If the current variable has dependencies
  if (effect) {
    // Check whether target exists in the current map
    let depsMap = targetMap.get(target);
    // If not
    if(! depsMap) {// New Map stores the current WeakMap
      depsMap = new Map(a); targetMap.set(target, depsMap); }// Get the set of response functions corresponding to key
    let deps = depsMap.get(key);
    if(! deps) {// Establish the relationship between the current key and its dependencies, because a key can have multiple dependencies
      // To prevent duplicate dependencies, use set
      deps = new Set(a); depsMap.set(key, deps); }// Store the current dependency
    if(! deps.has(effect)) { deps.add(effect); }}}// Used to trigger updates
function trigger(target, key) {
  // Get all dependencies
  const depsMap = targetMap.get(target);
  // If there are dependencies, pull them out
  if (depsMap) {
    // Get the set of response functions
    const deps = depsMap.get(key);
    if (deps) {
      // Execute all response functions
      const run = (effect) = > {
        // The source code has asynchronous scheduling tasks, which we omit here
        effect();
      };
      deps.forEach(run);
    }
  }
}
effect(() = > {
  console.log(11111);
  // In my implementation of effect, set cannot be triggered because it is not compatible to demonstrate the principle, otherwise it will be an infinite loop
  // The vue source code triggers the compatibility process in effect only once
  newRef.value;
});

 newRef.value++;
Copy the code