The stable version of Vue3 has been around for quite some time now, but there is an excellent Vue tutorial site Vue Mastery recommended for reading the documentation on Vue3. Vue3 Reactivity (Vue3 Responsive) is really good. After learning, I gained a lot. I summarized a Vue3 responsive notes according to the ideas of the course and restored the responsive principles manually.

Manually implement responsiveness

Reactive is a variable that depends on other variables and is updated in a reactive manner when other dependent variables are updated. So start from scratch and implement manual updates first.

Manual response of a single variable

Let’s look at the meanings of a few words,

  • dep 是 dependenceDependency.
  • effectTo produce a result for some reason; to have a lasting influence.
  • trackTrack, trace, trace
  • trigger“Refers to triggering.

If you are not familiar with the manual implementation of the code, please refer to MDN:

let price = 5;
let quantity = 2;
let total = 0;

// DEP is a "dependency" collection that holds many effects
let dep = new Set(a);/** * effect */ quantity */
let effect = () = > {
  total = price * quantity;
};

/** * track is to add the effect function to the deP set */
function track() {
  dep.add(effect);
}

/** * trigger is the execution of all effect functions stored in the deP set */
function trigger() {
  dep.forEach((effect) = > effect());
}

track();
effect();
console.log(total); // output: 10
Copy the code

The code above is the simplest implementation, with three steps:

  1. througheffectTo show the impacttotalThe dependence of
  2. throughtrackTo save theeffect
  3. throughtriggerTo perform theeffect

The final output must be the calculated total of 10.

Manually respond to multiple properties of an object

Let product = {price: 5, quantity: 2}, now if you want the product object to be responsive, you need to specify the response for each key.

DepsMap means map of dependence. That is, in a map, each key corresponds to the DEP of an attribute.

Map is used here. If you are not familiar with it, please refer to MDN

// Create a Map to store the DEps
const depsMap = new Map(a);/** * The change from the previous example is to specify a key that indicates which key of the object is stored in the deP */
function track(key) {
  let dep = depsMap.get(key);
  // Create a new deP if it does not exist
  if(! dep) { depsMap.set(key, (dep =new Set()));
  }
  // Add effect
  dep.add(effect);
}
/** * Also, a key is specified to indicate the effect */ in the DEP corresponding to the key of the object
function trigger(key) {
  let dep = depsMap.get(key);
  if (dep) {
    dep.forEach((effect) = >{ effect(); }); }}let product = { price: 5.quantity: 2 };
let total = 0;

let effect = () = > {
  total = product.price * product.quantity;
};

// The first call calculates total
effect();
console.log(total); // output: 10

// Store effect to the DEP corresponding to the Quantity key
track('quantity');
product.quantity = 3;
// Execute the QUANTITY key for effect saved by DEP
trigger('quantity');
console.log(total); // output: 15
Copy the code

In the above code, a manual response to the entire Product object is implemented.

Manually respond to multiple properties of multiple objects

Continuing with the code above, if you have multiple objects that need to be reactive, you need to set different DepSMaps for different objects. So create a WeakMap type variable named targetMap to store the depsMap of multiple objects. Target refers to the object that needs to be responded to.

The reason why Map type is used is that Map can use “object” as the key. WeakMap is convenient for garbage collection of the key (that is, the object being responded). If you are not familiar with WeakMap, you can refer to MDN

// Create a New WeakMap to store depsMap
const targetMap = new WeakMap(a);/** * depsMap */; /** * depsMap */
function track(target, key) {
  / / new πŸ‘‡
  let depsMap = targetMap.get(target);
  // If depsMap does not exist, create a new one
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
  }
  / / πŸ‘†
  let dep = depsMap.get(key);
  if(! dep) { depsMap.set(key, (dep =new Set()));
  }
  dep.add(effect);
}

/** * Also, when triggered, specify a target that indicates the effect */ in depsMap of the object being executed
function trigger(target, key) {
  / / new πŸ‘‡
  const depsMap = targetMap.get(target);
  if(! depsMap) {return;
  }
  / / πŸ‘†
  let dep = depsMap.get(key);
  if (dep) {
    dep.forEach((effect) = >{ effect(); }); }}let product = { price: 5.quantity: 2 };
let total = 0;
let effect = () = > {
  total = product.price * product.quantity;
};

// The first call calculates total
effect();
console.log(total); // output: 10
// Save effect to the DEP of the corresponding Quantity key in depsMap of the corresponding Product object
track(product, 'quantity');
product.quantity = 3;
// Execute effect stored in deP of the corresponding Quantity key in depsMap of the corresponding Product object
trigger(product, 'quantity');
console.log(total); // output: 15
Copy the code

The above code implements manual responses to different keys of different objects. At this point, the relationship between targetMap, depsMap, and DEP can be clearly represented using a diagram from the course:

Become automatic response

Continue upgrading the code to add automatic responses to the above code. Proxy and Reflect are used here, MDN for those unfamiliar.

const targetMap = new WeakMap(a);function track(target, key) {
  let depsMap = targetMap.get(target);
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
  }
  let dep = depsMap.get(key);
  if(! dep) { depsMap.set(key, (dep =new Set()));
  }
  dep.add(effect);
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if(! depsMap) {return;
  }
  let dep = depsMap.get(key);
  if (dep) {
    dep.forEach((effect) = >{ effect(); }); }}/ / new πŸ‘‡
/ * * *@description: Examples use Proxy and Reflect to implement automatic response *@param {Object} Target Specifies the object to respond to@return {Proxy} Returns the proxy */ for the object to respond to
function reactive(target) {
  const handlers = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver);
      // Save effect before accessing the target key
      track(target, key);
      return result;
    },
    set(target, key, value, receiver) {
      let oldValue = target[key];
      // It is important not to reverse the order of the following two steps
      // This step is actually assigned successfully
      let result = Reflect.set(target, key, value, receiver);
      // Get the new value
      if(result && oldValue ! = value) {// If you change the value of the target key, effect will be executed
        trigger(target, key);
      }
      returnresult; }};return new Proxy(target, handlers);
}
/ / πŸ‘†
let product = reactive({ price: 5.quantity: 2 });
let total = 0;

var effect = () = > {
  total = product.price * product.quantity;
};

// The first call calculates total
effect();
console.log(total); // output: 10

// Note: The difference here is that we do not manually invoke trigger to implement the automatic response
product.quantity = 3;
console.log(total); // output: 15
Copy the code

This code implements the automatic response, and the key core component is the Reactive function, which returns a Proxy that accesses the target object. First, before get returns, track is automatically called to save Effect to the corresponding location.

The trick is set, when we execute product. Quantity = 3; , the quantity will be set to 3 and then trigger automatically. The trigger calls the corresponding effect stored, calculates the latest total to 15, and realizes the automatic response.

Optimize the automatic response process

The above code implements the automatic response, but there are two obvious drawbacks:

  1. Cannot set multipleeffect.
  2. Before we set upquantity δΈΊ 3The time,triggerThe correspondingeffectHere,effectThe function executes to evaluatetotal“, will walk againproxy δΈ­ getThe process. So it triggerstrackBut here we don’t need to triggertrackSave it againeffect.

Let’s optimize the above two problems:

const targetMap = new WeakMap(a);let activeEffect = null; // πŸ‘ˆ New, whether to add the effect flag

function track(target, key) {
  // πŸ‘‡ added, save operation is performed only if activeEffect is true
  if (activeEffect) {
    let depsMap = targetMap.get(target);
    if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
    }
    let dep = depsMap.get(key);
    if(! dep) { depsMap.set(key, (dep =new Set()));
    }
    dep.add(activeEffect); // πŸ‘ˆ modified to add activeEffect instead of directly adding effect}}function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if(! depsMap) {return;
  }
  let dep = depsMap.get(key);
  if (dep) {
    dep.forEach((effect) = >{ effect(); }); }}function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver);
      track(target, key);
      return result;
    },
    set(target, key, value, receiver) {
      let oldValue = target[key];
      let result = Reflect.set(target, key, value, receiver);
      if(result && oldValue ! = value) { trigger(target, key); }returnresult; }};return new Proxy(target, handler);
}

/ / πŸ‘‡ added
Eff = eff = eff = eff = eff = eff = eff = eff = eff = eff = eff = eff
// The dep will be saved only when we call effect manually, and the get triggered by trigger will not be saved again
function effect(eff) {
  activeEffect = eff;
  activeEffect();
  activeEffect = null;
}
/ / πŸ‘†

let product = reactive({ price: 5.quantity: 2 });
let salePrice = 0;
let total = 0;

// πŸ‘‡ In the same way, the effect function should be modified to change every deP to be saved into an effect function parameter
// Manually set the initial value of total eh salePrice
effect(() = > {
  total = product.price * product.quantity;
});
// This eff will not be added to quantity
effect(() = > {
  salePrice = product.price * 0.9;
});
/ / πŸ‘†

console.log(total, salePrice); / / the output: 10, 4.5

// Setting quantity only recalculates total
product.quantity = 3;
console.log(total, salePrice); / / the output: 15, 4.5

// After price is set, effects corresponding to total and salePrice will be executed and recalculated
product.price = 10;
console.log(total, salePrice); // output: 30, 9
Copy the code

The code above addresses the problem of invalid reexecution saves by adding an activeEffect flag bit; And change effect() into effect(EFF) with parameters, solving many problems of effect.

So far, the implementation process of reactive methods in Vue3 Composition API has been implemented manually.

Realize the ref

In the Vue3 Composition API design, Reactive is used primarily to refer to types, and a ref method is provided specifically to respond to primitive types.

It basically adds a ref method that mimics Proxy with object accessors, getters/setters:

const targetMap = new WeakMap(a);let activeEffect = null;

function track(target, key) {
  if (activeEffect) {
    if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
    }
    let dep = depsMap.get(key);
    if(! dep) { depsMap.set(key, (dep =new Set())); } dep.add(activeEffect); }}function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if(! depsMap) {return;
  }
  let dep = depsMap.get(key);
  if (dep) {
    dep.forEach((effect) = >{ effect(); }); }}function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver);
      track(target, key);
      return result;
    },
    set(target, key, value, receiver) {
      let oldValue = target[key];
      let result = Reflect.set(target, key, value, receiver);
      if(result && oldValue ! = value) { trigger(target, key); }returnresult; }};return new Proxy(target, handler);
}

/ / πŸ‘‡ added
/ * * *@description: ref uses getters and setters, mimicking Proxy get and set *@param {Primary} raw
 * @return {Object} Return the response object */
function ref(raw) {
  const r = {
    get value() {
      // Save to targetMap before getting
      track(r, 'value');
      return raw;
    },
    set value(newVal) {
      raw = newVal;
      // When set, trigger effect update
      trigger(r, 'value'); }};return r;
}
/ / πŸ‘†

function effect(eff) {
  activeEffect = eff;
  activeEffect();
  activeEffect = null;
}

let product = reactive({ price: 5.quantity: 2 });
let salePrice = ref(0); // πŸ‘ˆ modify the salePrice is itself a responsive object
let total = 0;

// salePrice is itself a responsive object
effect(() = > {
  salePrice.value = product.price * 0.9; / / πŸ‘ˆ modification
});

// Note that the total price has been calculated in a different way, using discounted values
effect(() = > {
  total = salePrice.value * product.quantity; / / πŸ‘ˆ modification
});

console.log(total, salePrice); / / the output: 9, 4.5

product.quantity = 3;
console.log(total, salePrice); / / the output: 13.5, 4.5

product.price = 10;
console.log(total, salePrice); // output: 27, 9
Copy the code

Reactive can also respond to primitive types, so why provide a ref method? In the interview with Uvu, uVU says reactive adds more processing and is a useless burden to the original type.

To realize the computed

The last part about responsiveness is computed, so go ahead and implement it manually:

// the πŸ‘‡ code remains unchanged
const targetMap = new WeakMap(a);let activeEffect = null;

function track(target, key) {
  if (activeEffect) {
    let depsMap = targetMap.get(target);
    if(! depsMap) { targetMap.set(target, (depsMap =new Map()));
    }
    let dep = depsMap.get(key);
    if(! dep) { depsMap.set(key, (dep =new Set())); } dep.add(activeEffect); }}function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if(! depsMap) {return;
  }
  let dep = depsMap.get(key);
  if (dep) {
    dep.forEach((eff) = >{ eff(); }); }}function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver);
      track(target, key);
      return result;
    },
    set(target, key, value, receiver) {
      let oldValue = target[key];
      let result = Reflect.set(target, key, value, receiver);
      if(result && oldValue ! = value) { trigger(target, key); }returnresult; }};return new Proxy(target, handler);
}

function ref(raw) {
  const r = {
    get value() {
      track(r, 'value');
      return raw;
    },
    set value(newVal) {
      raw = newVal;
      trigger(r, 'value'); }};return r;
}

function effect(eff) {
  activeEffect = eff;
  activeEffect();
  activeEffect = null;
}
// the πŸ‘† code remains unchanged

/ / πŸ‘‡ added
/ * * *@descriptionA computed implementation encapsulates ref *@param {Function} Getter Value function *@return {Object} Ref returns the object */
function computed(getter) {
  // Create a responsive reference
  let result = ref();
  // Call the getter with effect wrapped, set the result to result.value, and save the EFF in targetMap
  effect(() = > (result.value = getter()));
  // Return result
  return result;
}
/ / πŸ‘†

let product = reactive({ price: 5.quantity: 2 });

/ / πŸ‘‡ modification
// salePrice is itself a reactive object
let salePrice = computed(() = > {
  return product.price * 0.9;
});

// Total is also a responsive object
let total = computed(() = > {
  return salePrice.value * product.quantity;
});
/ / πŸ‘†

console.log(total.value, salePrice.value); / / the output: 9, 4.5

product.quantity = 3;
console.log(total.value, salePrice.value); / / the output: 13.5, 4.5

product.price = 10;
console.log(total.value, salePrice.value); // output: 27, 9
Copy the code

As you can see, computed essentially encapsulates the ref method, encapsulates effect to call the getter, sets the result to result.value, and stores eff in the corresponding position in targetMap. Realize the response of computed.

Vue3 source code responsive implementation

Vue3 is written in Typescript as a whole. Reactivity is a separate module. The source code is located in the packages/reactivity/ SRC directory.

  • effect,track,triggerMethod, located ateffect.ts.
  • Proxy ηš„ get ε’Œ setthesehandlerMethod, located atbaseHandlers.ts.
  • reactiveMethods inreactive.ts, using theProxy.
  • refMethods inref.tsObject accessors are used.
  • computedMethods incomputed.ts, using theeffect ε’Œ ref.

About Vue Mastery

Vue Mastery is a fee-based course and 25% of the proceeds go to Vue, so start a membership wave if you’re interested. However, its membership is very expensive, you can have some clever ways to skip the fee verification, you can pay attention to the “Lin Jingyi notepad” public account to send “Vue3 response type” to get a wave.


Front-end notepad, not regularly updated, welcome to pay attention to!

  • Wechat official account: Lin Jingyi’s notepad
  • Blog: Lin Jingyi’s notebook
  • Nuggets column: Lin Jingyi’s notebook
  • Zhihu column: Lin Jingyi’s notebook
  • Github: MageeLin