As the question, no prologue (if necessary, you can imagine 👽👽)

Some necessary pre-knowledge (📚📚📚):

  • ES6 Proxy (MDN: developer.mozilla.org/zh-CN/docs/…).
  • ES6 WeakMap (MDN: developer.mozilla.org/zh-CN/docs/…).
  • ES6 Map: (MDN: developer.mozilla.org/zh-CN/docs/…).
  • ES6 Set: (MDN: developer.mozilla.org/zh-CN/docs/…).
  • Vue3 Reactive API Basics (vue3js.cn/docs/zh/gui…)

What is reactive data? 🤯 🤯 🤯

On second thought, maybe I should fix the concept of Reactive Data to the new students, because not only the new students, but also many people in the big factory have a wrong understanding of reactive data

let numberOne = 1;
let numberTwo = 2;

document.write(numberOne + numberTwo);
Copy the code

We import the JS file above into an HTML file and open it in the browser. We will find that the browser outputs a 3 on the page

Let’s add a line of code under JS to try to change the value of numberOne

. numberOne =4;
Copy the code

So at this point in the page, the 3 is still sitting there, but we know that numberOne plus numberTwo is actually at 5

So we say that numberOne and numberTwo are not reactive data, whereas if the view layer (or any third party variable) can change depending on numberOne, we call numberOne reactive. It’s important to know that reactive is browser-independent

Let me give you one more example, just to reinforce the idea of responsiveness

let numberA = 1;
let numberB = 2;
let count = numberA + numberB;

console.log("count", count);

numberA = 3;

console.log("count", count); 

// In the example above, if the value of numberA changes, the value of count changes accordingly, we call numberA responsive
Copy the code

It is important to understand that a change in responsive data must trigger some side effect, whether it is a calculation of another variable or a change in the visual layer

Just as in Vue, a change in a responsive data always affects the re-rendering of the view layer, in our example above, a change in two numbers always affects the recalculation of count

Based on the example above, if I want the view layer to respond when numberA or numberB changes, then one of the most important things I need to do is I need to know when numberOne and numberTwo have changed, which makes sense, right

So what we’re going to do is we’re going to intercept changes to numberOne and numberTwo, and first of all, the basics, in Vue, if numberOne and numberTwo are a raw value, can we track their changes, and the answer is no, If we want to track a change in some variable, we must use a proxy

How many ways can we implement proxy?

  • Object.defineProperty
  • Proxy
  • Object accessor

Ref and Reactive are both implementations of proxies

ref

To implement ref, it’s very simple, first of all, we know that ref must return an object with a value

When we change the value of ref, the corresponding agent action will be triggered and the side effect function will be executed. Since we are operating on the view layer, our side effect is to re-render the page, just like vUE

// Our side effect operation
function render() {
  document.body.innerText = numberOne.value + numberTwo;
}
Copy the code
// Vue uses object accessor for ref proxy operations
function ref(rawValue) {
  return {
    get value() {
      return rawValue;
    },
    set value(newValue) {
      if (newValue === rawValue) return;
      rawValue = newValue;
      // Since this is a function, we can put our side effects hererender(); }}}Copy the code

We can experiment with our results

 function render() {
    document.body.innerText = numberOne.value + numberTwo;
  }

  function ref(rawValue) {
    return {
      get value() {
        return rawValue;
      },
      set value(newValue) {
        if (newValue === rawValue) return;
        rawValue = newValue;
        // Since this is a function, we can put our side effects hererender(); }}}let numberOne = ref(1);
  let numberTwo = 2;

  render(); // Let's do it once and let the page render stuff, just like vue render

  // We changed the values on the page, let's see if the page changes
  setTimeout(() = > {
    numberOne.value = 3;
  }, 1000)

Copy the code

We’ll notice that a second later, the page is rerendered

RawValue = newValue,rawValue = newValue,rawValue = newValue,rawValue = newValue,rawValue = newValue,rawValue = newValue

Trace & Trigger (Dependent collection/effect trigger)

That actually does one of our basic goals, but it’s not enough, because we know that when you become responsive data, people can do computations based on you (for example, computed), and our current side effect is fixed, which is to rerender the page, We can’t write anything like computed, which is our current weakness, and it doesn’t have much to do with REF, so what we need to do is build a dependency map

Watch me operate 💁💁💁

The same goes from simple to complex

// This Map is a key name that corresponds to all side effects that depend on this key (used to create a one-to-many relationship)
// Why use WeakMap, because WeakMap keys can only be objects
let depMap = new WeakMap(a);let activeEffect = null; // Effect currently running

// Why do I define this effect function to run the side effect function
// Because we know that side effects can be many, such as computed, render
// When I read a value, such as obj.a, if I want to collect its dependencies
// Do I have to be in a side effect function, such as obj.a in render
// The effect function I'm currently running is render
// So I use this effect to run the side effect function, to help me record the current side effect environment
// So we conclude that there is no need to collect dependencies to access reactive properties without side effects (e.g. Console. log)
function effect(eff) {
  activeEffect = eff;
  activeEffect();
  activeEffect = null;
}

The trace function is used to collect the effect function corresponding to the key name
function trace(target, key) {
  if (activeEffect) {
    // What do we do? We throw attributes and effects into the Map
    const currentWorkMap = depMap.get(target);

    if(! currentWorkMap) {// If the key is not present, the dependency is collected for the first time

      // activeEffect specifies the current running effect.
      // Because we must be in an effect to collect dependencies
      // The execution of an effect happens to be the execution of an activeEffect
      depMap.set(target, new Map().set(key, new Set([activeEffect])));
    } else {

      // If this is not the first time, the corresponding value of this key is a Map
      const matchTargetKey = currentWorkMap.get(key);
      if(! matchTargetKey) {// indicates that the Map has something, but the key has not been traced yet
        // Set is used because the value of Set cannot be repeated
        currentWorkMap.set(key, new Set([activeEffect]));
      } else {
        // I have SetmatchTargetKey.add(activeEffect); }}}}// We have a trigger method
// What does the trigger method do when we set the value of a responsive data
// Do we want all effects to be executed,
// This is what trigger is used to do
function trigger(target, key) {
  const currentMap = depMap.get(target);
  if(! currentMap)return; // If you can't find the Map, you can do it
  const currentWorkEffects = currentMap.get(key);
  if(! currentWorkEffects)return;
  // Execute all side effects directly
  currentWorkEffects.forEach(effect= > effect());
}
Copy the code

At this point, let’s modify our ref function and add a side effect function

function ref(rawValue) {
  const proxyObj = {
    get value() {
      // The value must be in effect
      Otherwise there is no need to trace dependencies
      trace(proxyObj, "value");
      return rawValue;
    },
    set value(newValue) {
      if (newValue === rawValue) return;
      rawValue = newValue;
      trigger(proxyObj, "value"); }}}// Set is triggered, Get is traced, nice
Copy the code

At this point, let’s try out our work again, and let’s have another side effect function called computedCount

// Side effect operation 1
function render() {
  document.body.innerText = numberOne.value + numberTwo;
}

// Side effect operation 2
let totalCount = 0;
function computedCount() { totalCount = numberOne.value + numberTwo; }... .// Let's start with totalCount effect
effect(computedCount);

// Then run render effect
effect(render);

console.log("totalCount", totalCount);

// Let's change numberOne to 4 and look at totalCount
numberOne.value = 4;
console.log("totalCount", totalCount);
Copy the code

At this point we find that both the page and the console output the desired result, so we also achieve a multi-listen effect

Once we have the trace and trigger methods, our ref is complete

Of course, vUE handles more details. For example, if you drop a reactive data to a REF, the ref will return it unchanged. That’s some details 🤖🤖🤖

reactive

Refs help us solve part of the reactive problem, but refs are really about proxies for raw values. Can you use a proxy to proxy raw values? Reactive is used to proxy objects. You’re using a.value to access an object, so you’re using a proxy in Reactive

With the trace and trigger above, our Reactive is very easy to write

function reactive(target) {
  let handler = {
    get(target, key, receiver) {
      trace(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      if (value === target[key]) return;
      Reflect.set(target, key, value, receiver); trigger(target, key); }}return new Proxy(target, handler);
}
Copy the code

That’s it, so we know that vue3’s responsive core principle is the broker + publish/subscribe model

Ok, today’s blog is over here, I hope this article on Vue3 a responsive principle of analysis can help you, see u 🧑🎄🧑🎄 port