SoFunction
Updated on 2025-04-04

Detailed explanation of the implementation process of Vue reactive function

Proxy has the ability to intercept various operations on objects, such as the most basic get and set operations, and Reflect also has methods with the same name as these operations, such as (), (, which are exactly the same as the basic operations of the corresponding objects.

const data = {
    value: '1',
    get fn() {
        ();
        return ;
    }
};
; // 1
(data,'value'); // 1

In addition to this, Reflect has a third parameter in addition to being equivalent to basic object operations.receiver, that is, this object that specifies the underlying operation.

(data,'value',{<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->value: '2'}); // Will output2

For Proxy, it can only intercept the basic operations of the object, while for (), this is a composite operation, which consists of a get operation and an apply operation, that is, first obtain the value of fn through get, and then call the function corresponding to apply. Now, using the responsive system we created before to perform this composite operation, the result we expect is that while binding the fn attribute, the value of the value is also bound, because the value value is operated during the execution of the fn function. But the reality is that the value of value is not bound.

effect(() => {
	(); // Assume obj is a Proxy object that has been responsively proxyed})
 = '2'; // Change the value, the expected responsive operation is not executed

Here comes the issue pointed to by this in the fn() function. In fact, in the fn function, this points to the original data object, that is, because it operates on the original object, it does not touch the dependency collection. After understanding the cause of the problem, we can use the Reflect feature mentioned earlier and specify the actual this object of get operation as obj, so that we can successfully implement the functions we expect.

const obj = new Proxy(data, {
  get(target, key, receiver) { // get receives the third parameter, that is, the caller of the operation, corresponding to () is obj    track(target, key);
    return (target, key, receiver); // Change the original operation that directly returns to target[key] to  }
}

How it works

In js, an object must deploy 11 internal methods including [[GET]] and [[SET]]. In addition, the function has two additional methods: [[Call]] and [[Construct]]. When creating a Proxy object, the specified intercept function is actually to customize the internal methods and behaviors of the proxy object itself, rather than specifying it.

3. Proxy Object

(1) Agent read operation

All possible read operations to a normal object:

  • Access properties:
  • Determine whether a given key exists on an object or prototype; key in obkj
  • Loop through objects using for … in

First of all, for basic access attributes, we can use the get method to intercept.

const obj = new Proxy(data, {
  get(target, key, receiver) { // get receives the third parameter, that is, the caller of the operation, corresponding to () is obj    track(target, key);
    return (target, key, receiver); // Change the original operation that directly returns to target[key] to  }
}

Then, for the in operator, we use the has method to intercept.

has(target, key) {
    track(target, key);
    return (target,key);
}

Finally, for the for … in operation, we use the ownKeys method to intercept. Here we use and uniquely identify ITERATE_KEY and side effect functions to bind, because for the ownKeys operation, it traverses all attributes present on an object anyway and does not produce actual attribute read operations. Therefore, we need to use a unique identifier to mark the ownKeys operation.

ownKeys(target, key) {
  // Here the side effect function and unique identifier ITERATE_KEY are bound  track(target, ITERATE_KEY);
  return (target);
},

Correspondingly, when performing the assignment operation, the ITERATE_KEY logo is also required to process it accordingly.

function trigger(target, key) {
  const depsMap = (target);
  if (!depsMap) return;
  const effects = (key);
  const iterateEffects = (ITERATE_KEY); // Read ITERATE_KEY  const effectToRun = new Set();
  effects &&
    ((fn) => {
      if (fn !== activeEffect) {
        (fn);
      }
    });
  // Add side effect functions associated with ITERATE_KEY to effectsToRun  iterateEffects &&
    ((fn) => {
      if (fn !== activeEffect) {
        (fn);
      }
    });
  ((fn) => {
    if () {
      (fn);
    } else {
      fn();
    }
  });
}

Although the above code solves the problem of adding attributes, the problem of modifying attributes is followed. For the for … in loop, no matter how the properties of the original object are modified, it only needs to be traversed once, so we need to distinguish between adding and modifying operations. Here we use to check whether the attributes of the current operation already exist on the target object. If so, it means that the current operation type is 'SET', otherwise it means that it is 'ADD'. Then use type as the third parameter and pass it into the trigger function.

set(target, key, newVal, receiver) {
    const type = (target, key)
      ? "SET"
      : "ADD";
    (target, key, newVal, receiver);
    trigger(target, key, type);
},

(2) Agent delete operator

The proxy delete operator uses the deleteProperty method, because the delete operator deletes the attributes and causes the number of attributes to decrease. Therefore, when the operation type is DELETE, the operation of the for … in loop should also be triggered.

deleteProperty(target, key) {
    // Check whether the deleted key is its own attribute    const hadKey = (target, key);
    const res = (target, key);
    if (res && hadKey) {
      trigger(target, key, "DELETE");
    }
    return res;
},
// When the type is ADD or DELETE, only ITERATE_KEY related operations are performedif (type === "ADD" || type === "DELETE") {
    iterateEffects &&
      ((fn) => {
        if (fn !== activeEffect) {
          (fn);
        }
      });
}

4. Reasonable trigger response

(1) Improve response operations

When triggering a modification operation, if the new value and the old value are equal, there is no need to trigger a modification response operation.

set(target, key, newVal, receiver) {
    const oldVal = target[key];  // Get old value    const type = (target, key)
      ? "SET"
      : "ADD";
    const res = (target, key, newVal, receiver);
    if (oldVal !== newVal) { // Compare new and old values      trigger(target, key, type);
    }
    return res;
},

But there is a special case for conglomerates, that is, NaN === The value of NaN is false, so we need to make a special judgment on NaN.

(2) Encapsulate a reactive function

In fact, it is a simple package of new Proxy.

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key);
      return (target, key, receiver);
    },
    set(target, key, newVal, receiver) {
      const oldVal = target[key]; // Get old value      const type = (target, key)
        ? "SET"
        : "ADD";
      const res = (target, key, newVal, receiver);
      if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
        // Compare new and old values        trigger(target, key, type);
      }
      return res;
    },
    has(target, key) {
      track(target, key);
      return (target, key);
    },
    ownKeys(target, key) {
      // Here the side effect function and unique identifier ITERATE_KEY are bound      track(target, ITERATE_KEY);
      return (target);
    },
    deleteProperty(target, key) {
      // Check whether the deleted key is its own attribute      const hadKey = (target, key);
      const res = (target, key);
      if (res && hadKey) {
        trigger(target, key, "DELETE");
      }
      return res;
    },
  });
}

Now, we use reactive to create two reactive objects, child and parent, and then set the child prototype to parent. Then bind the side effect function to the function. When modifying the value, you can see that the side effect function has actually been executed twice. This is because the prototype of child is parent, and child itself does not have the attribute bar, so according to the rules of the prototype chain, the attribute bar will eventually be obtained from parent. Because during the process of searching the prototype chain, the properties on the parent were accessed, and an additional binding operation was performed, the final side effect function was executed twice.

const obj = {};
const proto = {
  bar: 1,
};
const child = reactive(obj);
const parent = reactive(proto);
(child, parent);
effect(() => {
  ();
});
 = 2; // Output 1 2 2

Here we compare the intercept functions of child and parent. We can find that the receiver's values ​​are the same, and the change is the value of target. Therefore, we can cancel the response operation triggered by parent by comparing the value of taregt.

// child's intercept functionget(target, key, receiver) {
// target is the original object obj// receiver is child}
// parent's intercept functionget(target, key, receiver) {
// target is a proto object// receiver is child}

Here we implement it by adding a raw operation. When accessing the raw property, the target value of the object will be returned.

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      if (key === "raw") {
        // Add a new value raw        return target;
      }
      track(target, key);
      return (target, key, receiver);
    },
    set(target, key, newVal, receiver) {
      const oldVal = target[key]; // Get old value      const type = (target, key)
        ? "SET"
        : "ADD";
      const res = (target, key, newVal, receiver);
      if (target === ) {
        // Compare the target value. If the receiver's target is the same as the current target, it means it is not a prototype chain operation.        if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
          // Compare new and old values          trigger(target, key, type);
        }
      }
      return res;
    }
  }
}

5. Deep and shallow response

In fact, the reactive we implemented earlier is just a shallow response, which means that only the first layer of the object has reactive responses. For example, for an obj:{bar{val:1}} object, when operating, we first get the bar from obj, but at this time the bar is just a normal object bar:{val:1}, so responsive operations cannot be performed. Here we make a judgment on the obtained value. If the value obtained is an object, call the reactive function recursively and finally get a deep response object.

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      if (key === "raw") {
        return target;
      }
      track(target, key);
      const res =  (target, key, receiver);
      if(typeof res === 'object') {
          return reactive(res);
      }
      return res;
    }
  }
}

But we do not expect deep responses at all times, so we adjust the reactive function.

function createReactive(obj, isShallow = false) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      if (key === "raw") {
        return target;
      }
      track(target, key);
      const res = (target, key, receiver);
      if (isShallow) return res; // If the shallow response is returned directly      if (typeof res === "object") {
        return reactive(res);
      }
      return res;
    },
    set(target, key, newVal, receiver) {
      const oldVal = target[key]; // Get old value      const type = (target, key)
        ? "SET"
        : "ADD";
      const res = (target, key, newVal, receiver);
      if (target === ) {
        if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
          // Compare new and old values          trigger(target, key, type);
        }
      }
      return res;
    },
    has(target, key) {
      track(target, key);
      return (target, key);
    },
    ownKeys(target, key) {
      // Here the side effect function and unique identifier ITERATE_KEY are bound      track(target, ITERATE_KEY);
      return (target);
    },
    deleteProperty(target, key) {
      // Check whether the deleted key is its own attribute      const hadKey = (target, key);
      const res = (target, key);
      if (res && hadKey) {
        trigger(target, key, "DELETE");
      }
      return res;
    },
  });
}
function reactive(obj) {
  return createReactive(obj, true);
}
function shallowReactive(obj) {
  return createReactive(obj, false);
}

6. Read-only and shallow read-only

To implement read-only, it only requires adding the third parameter isReadOnly to the createReactiv function.

function createReactive(obj, isShallow = false, isReadOnly = false) {
  return new Proxy(obj, {
    set(target, key, newVal, receiver) {
      if (isReadOnly) {
        (`property${key}It's read-only`);
        return true;
      }
      const oldVal = target[key]; // Get old value      const type = (target, key)
        ? "SET"
        : "ADD";
      const res = (target, key, newVal, receiver);
      if (target === ) {
        if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
          // Compare new and old values          trigger(target, key, type);
        }
      }
      return res;
    },
    deleteProperty(target, key) {
      if (isReadOnly) {
        (`property${key}It's read-only`);
        return true;
      }
      // Check whether the deleted key is its own attribute      const hadKey = (target, key);
      const res = (target, key);
      if (res && hadKey) {
        trigger(target, key, "DELETE");
      }
      return res;
    },
  }
}

Of course, for the properties of objects with read-only attributes, it is obvious that there is no need to add dependencies, so corresponding modifications are required to get.

function createReactive(obj, isShallow = false, isReadOnly = false) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      if (key === "raw") {
        // Get the initial object by obtaining the raw attribute        return target;
      }
      if (!isReadOnly) {
        // No need to make a connection when read-only        track(target, key);
      }
      const res = (target, key, receiver);
      if (isShallow) return res; // If the shallow response is returned directly      if (typeof res === "object") {
        // If the obtained value is an object, call the recursive function to obtain the deep response object        return reactive(res);
      }
      return res;
    },
  }
}

However, the above operations can only be shallow read-only, and deep read-only implementation is also very simple. Just judge the read-only mark and then add the read-only attribute recursively.

function createReactive(obj, isShallow = false, isReadOnly = false) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      if (key === "raw") {
        // Get the initial object by obtaining the raw attribute        return target;
      }
      if (!isReadOnly) {
        // No need to make a connection when read-only        track(target, key);
      }
      const res = (target, key, receiver);
      if (isShallow) return res; // If the shallow response is returned directly      if (typeof res === "object" && res !== null) {
        // If the obtained value is an object and the value of the read-only mark is true, recursively call the readonly function to obtain the deep read-only response object. Otherwise, recursively call the reactive function to obtain the deep response object        return isReadOnly ? readonly(res) : reactive(res);
      }
      return res;
    },

Then, like the reactive function, encapsulate the readonly function.

function readonly(obj) {
  return createReactive(obj, true, true);
}
function shallowReadonly(obj) {
  return createReactive(obj, false, true);
}

7. Proxy array

(1) Read and modify operations

Reading operations of arrays:

  • Access elements through index, arr[0]
  • Access array length,
  • for in iterate over arr object
  • for of iterating arr object
  • Prototype methods of arrays, find, concat, etc.

Modify array operations:

  • Modify the array by index, arr[0] = 1
  • Modify the array length, = 1
  • The stack and queue methods of arrays,
  • Prototype method for modifying arrays, etc.

For this operation through index access, it is actually the same as a normal object and can be directly intercepted through get. However, the operation of modifying the index is slightly different, because if the index > array length is currently set, the length of the array will be modified accordingly, and in the process of modifying the array length, it is also necessary to respond to the modification of the array length. At the same time, directly modifying the length attribute of the array will also have an impact. If it is less than the current array length, the elements within the difference will be clearly operated, otherwise there will be no impact on the previous elements.

First, we modify the array index settings accordingly:

function createReactive(obj, isShallow = false, isReadOnly = false) {
  return new Proxy(obj, {
    set(target, key, newVal, receiver) {
      if (isReadOnly) {
        // If the object is read only, an error message is prompted        (`property${key}It's read-only`);
        return true;
      }
      const oldVal = target[key]; // Get old value      // Determine the operation type. If it is an array type, judge based on the index size      const type = (target)
        ? Number(key) < 
          ? "SET"
          : "ADD"
        : (target, key)
        ? "SET"
        : "ADD"; // Get the operation type      const res = (target, key, newVal, receiver);
      if (target === ) {
        if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
          trigger(target, key, type, newVal); // Add the fourth parameter        }
      }
      return res;
    }
  }
}

Then modify the trigger function to determine whether it is an array and ADD operation, and then add the relevant operations of the length attribute

// The trigger function adds the fourth parameter newVal, that is, the value of the trigger responsefunction trigger(target, key, type) {
  const depsMap = (target); // First, take out the dependency table of the current object from the object bucket  if (!depsMap) return;
  const effects = (key); // Get the dependency set of the current key value from the dependency table  const iterateEffects = (ITERATE_KEY); // Try to get the dependency set of for in loop operation  const effectToRun = new Set(); // Create a dependency execution queue  if (type === "ADD" && (target)) {
    // If the operation type is ADD and the object type is an array, add the length-related dependency to the queue to be executed    const lengthEffects = ("length");
    lengthEffects &&
      ((fn) => {
        if (fn !== activeEffect) {
          (effectFn);
        }
      });
  }
  if ((target) && key === "length") {
    // For elements whose index is greater than or equal to the new length value, all associated functions need to be taken out and added to effectToRun to be executed    if (key >= newVal) {
      ((fn) => {
        if (fn !== activeEffect) {
          (fn);
        }
      });
    }
  }

(2) Array traversal

First of all, the for in loop will affect the operation of the for in loop. The main thing is to set the array value according to the index and modify the length attribute of the array. These two operations are actually operations on the length value of the array. Therefore, we only need to judge in the onwKeys method whether the current operation is an array. If it is an array, use the length attribute as the key and establish a connection.

ownKeys(target, key) {
      // Here the side effect function and unique identifier ITERATE_KEY are bound      track(target, (target) ? "length" : ITERATE_KEY); // Conduct dependency collection      return (target);
},

Then there is the for of loop, which mainly operates through index and length, so you can implement dependencies without performing additional operations. However, when using the for of loop, the attribute of the array will be read, which is a symbol value. In order to avoid unexpected errors and performance considerations, the value of the type for symbol needs to be isolated.

function createReactive(obj, isShallow = false, isReadOnly = false) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      if (key === "raw") {
        // Get the initial object by obtaining the raw attribute        return target;
      }
      if (!isReadOnly && typeof key !== "symbol") {
        // No need to establish a connection when the read-only case and the key value is symbol        track(target, key);
      }
      const res = (target, key, receiver);
      if (isShallow) return res; // If the shallow response is returned directly      if (typeof res === "object" && res !== null) {
        // If the obtained value is an object, call the recursive function to obtain the deep response object        return isReadOnly ? readonly(res) : reactive(res);
      }
      return res;
    },
  }
}                 

(3) Methods for searching arrays

Under normal circumstances, the method can trigger binding normally because the method will access the length attribute and index of the array object during the search process. However, in some special cases, such as when the array element is an object, under our current responsive system, some special cases will occur.

const obj = {};
const arr = reactive([arr]);
((arr)); // false

Running the above code, the result is false. This is because in our previous code design, if the value obtained by the read operation was a proxy object, then we will continue to proxy the object. After continuing to proxy, the object obtained is a brand new object.

if (typeof res === "object" && res !== null) {
  // If the obtained value is an object, call the recursive function to obtain the deep response object  return isReadOnly ? readonly(res) : reactive(res);
}

In this regard, we create a cache map to avoid the problem of repeated creation.

const reactiveMap = new Map();
function reactive(obj) {
  // Get the cached value of the current object  const existionProxy = (obj);
  // If the current object has a cache value, return it directly  if (existionProxy) return existionProxy;
  // Otherwise, create a new response object  const proxy = createReactive(obj, true);
  // Cache new objects  (obj, proxy);
  return proxy;
}

But at this time we will encounter a new problem, that is, if the original object, that is, obj, is passed in, it will also return false. This is because we will get the responsive object from arr, so we need to modify the default behavior.

const originMethod = ;
const arrayInstrumentations = {
  includes: function (...args) {
    // This is a proxy object, first search in the proxy object    let res = (this, args);
    if (res === false) {
      // If it cannot be found on the proxy object, then look for it on the original object      res = (, args);
    }
    return res;
  },
};
function createReactive(obj, isShallow = false, isReadOnly = false) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      if (key === "raw") {
        // Get the initial object by obtaining the raw attribute        return target;
      }
      // If the operation target is an array and the key is above arrayInstruments, then return the custom behavior      if ((target) && (key)) {
        return (arrayInstrumentations, key, receiver);
      }
    }
  }
}

In addition to include, indexof and lastIndexOf need to be processed similarly.

const arrayInstrumentations = {};
["includes", "indexof", "lastIndexof"].forEach((method) => {
  const originMethod = [method];
  arrayInstrumentations[method] = function (...args) {
    // this is a proxy object. First look up in the proxy object and store the result in res    let res = (this, args);
    // Res is false, which means that it is not found. Get the original array through, then search it in, and update the res value    if (res === false || res === -1) {
      res = (, args);
    }
    return res;
  };
});

(4) Methods to implicitly modify array

The main ones are push, pop, shift, unshift and splice. Taking push as an example, push also reads the length attribute while adding elements, which causes two independent side effect functions to affect each other. Therefore, we also need to rewrite the push operation to avoid this situation. Here we add a marker whether to track it, and set the marker to false before the push method is executed.

let shouldTrack = true; // Whether to track the mark["push"].forEach((method) => {
  const originMethod = [method];
  arrayInstrumentations[method] = function (...args) {
    // Tracking is prohibited before calling the original method    shouldTrack = false;
    // Default behavior    let res = (this, args);
    // After calling the original method, restore the original behavior, that is, allow tracking    shouldTrack = true;
    return res;
  };
});
function track(target, key) {
  if (!activeEffect || !shouldTrack) {
    // If there is no currently executed side effect function, no processing will be performed    return;
  }
}

Finally, modify so that type of behavior.

["push", "pop", "shift", "unshift", "splice"].forEach((method) => {
  const originMethod = [method];
  arrayInstrumentations[method] = function (...args) {
    // Tracking is prohibited before calling the original method    shouldTrack = false;
    // Default behavior    let res = (this, args);
    // After calling the original method, restore the original behavior, that is, allow tracking    shouldTrack = true;
    return res;
  };
});

This is the end of this article about the detailed explanation of the implementation process of Vue reactive function. For more related content of Vue reactive function, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!