1.watchEffect

WatchEffect is a new API added to Vue3 that is similar to Watch in that it can perform callbacks when it listens for changes in data. The difference is

  • You do not need to specify the data to listen for directly. You monitor what data is used in the callback function (it must be reactive data).
  • 2 By default, it will be executed initially to collect the data to be listened on.
  • 3 No configuration is required. Deep listening is performed by default.

Here are the basic uses

<script setup> import { reactive, ref, watchEffect } from "vue"; Let firstName = ref(" li "); Ref (close, 1) = ref(close, 1) and ref(close, 2) = ref(close, 2); let fullName = ""; Let obj = reactive({wife: {name: "reactive ",},}); watchEffect(() => { fullName = firstName.value + lastName.value; console.log(fullName); }); watchEffect(() => { console.log(obj.wife.name); }); Firstname. value = "firstName "; Lastname. value = "just "; Name = "feng Nan "; </script>Copy the code

As you can see from the console print, watchEffect is executed once by default, automatically collecting the data you want to listen for. Data changes trigger a callback, and can achieve deep listening.

2. Implementation idea

Let’s sort out the general idea of realizing the above functions.

  • First, a watchEffect must receive responsive data, which means we need to make sure that the data is listened for as it changes. In this regard, we can refer to the principle of Vue data responsiveness, and use the observer mode to intercept get and SET of data. When GET is triggered, dependent collection is carried out, that is, the current attribute is associated with the callback function specified in watchEffect, and the corresponding callback function is executed when set is triggered. You can use ES6 proxies to perform these operations.

  • Here’s what the watchEffect does. Since watchEffect is executed once by default, the first thing to do is execute the callback function. As mentioned earlier, watchEffect receives reactive data, and executing this callback must access the required variables, so dependency collection must be triggered. As mentioned earlier, dependency collection associates the current dependency with its corresponding callback function. So how do you access this callback function? Obviously, we need a global variable to store the callback function for the dependencies that are currently being collected. Mount the dependency collection globally before it is triggered to ensure that the callback is accessible. At the end of the collection, the global is kicked out, making room for the next dependency to store its callback.

To summarize, there are three things that watchEffect does.

  • 1 Global mount callback
  • 2 Perform the callback
  • 3 kick out the callback

Follow the above ideas to achieve. First, use Proxy to achieve data responsiveness.

3. Data responsiveness

3.1 the Proxy

Let’s start with a brief introduction to proxies. In the words of Teacher Ruan Yifeng, Proxy can be understood as a layer of “interception” before the target object. Access to the object by the outside world must pass this layer of interception first, so it provides a mechanism for filtering and rewriting access by the outside world. The basic usage is as follows:

// Target is the target object, and the handler parameter is also an object used to customize interception behavior. Var proxy = new proxy (target, handler);Copy the code

How can I customize the underlying interception behavior

const obj = { name: "xiaom", }; const objProxy = new Proxy(obj, { get(target, key) { console.log("get", key); /* Reflect's methods correspond to Proxy methods, so you can find the corresponding methods in the Reflect object. This allows the Proxy object to easily call the corresponding Reflect method, which does the default behavior */ return reflect.get (target, key); }, set(target, key, val) { console.log("set", key, val); const res = Reflect.set(target, key, val); return res; }}); console.log(objProxy.name); objProxy.name = "xiaom"; console.log(objProxy.name); // The following is the console output: get name xiaom set name xiaom get name xiaomCopy the code

As you can see, the Proxy uses the corresponding method on the Reflect object to complete the default behavior and return the Proxy object. More details about Proxy usage are not covered here, but will be shared later. Let’s use Proxy to make the data responsive

function observe(obj) { if (typeof obj ! == "object") { return obj; } return new Proxy(obj, {get(target, key, receiver) {//console.log(key + 'collected '); const res = Reflect.get(target, key, receiver); // Rely on collect(target, key); Return typeof obj === "object" &&res! == null ? observe(res) : res; }, set(target, key, val, receiver) { //console.log("set", key); const res = Reflect.set(target, key, val, receiver); Update (target, key); return res; }}); }Copy the code

Next, implement the two most critical steps: rely on the collection and listen to the execution

3.2 Implementing dependency collection

How does a dependency relate to its corresponding callback function?

The first thing that comes to mind is key-value pairs stored in a dictionary (Map). The key name is the dependency that is being collected. The key value is the corresponding callback function. The obvious drawback is that when we are responding to multiple objects, there may be a property of the same name in multiple objects, which will cause overwriting. So we should create a separate dictionary for each reactive object. This object is used as the key name and the corresponding dictionary as the key value. Store it in a global dictionary. However, we know that dictionaries cannot use objects as key names. Therefore, we will use the WeakMap structure of ES6. WeakMap only accepts objects as key names (except null) and does not accept values of other types as key names. Also note that an attribute (dependency) may have more than one callback function. This property may be collected in more than one watchEfect. Therefore, the key value for each attribute should be a Set structure.

With that in mind, dependency collection is implemented. First create a global WeakMap and a global variable to temporarily store callbacks.

// let cb = null; /* The data structure that stores dependencies. The overall structure is that the key name is the object that needs to be reacted, and the key value is a map structure. The Map takes each attribute of the object as a key name and the key value as a set structure. This Set stores all the callbacks that need to be triggered when this property changes. */ const targetMap = new WeakMap();Copy the code

Next, we implement dependency collection. The main procedure is to add a callback function to the collection corresponding to an attribute of a reactive object.

/** * @description implements dependency collection, * @param {Object} target Target Object * @param {any} key Current collected attribute */ function collect(target, key) { const effect = cb; If (effect) {if (effect) {let depMap = targetmap.get (target); // Create a map if (! depMap) { depMap = new Map(); targetMap.set(target, depMap); } deps = depmap. get(key); if (! deps) { deps = new Set(); depMap.set(key, deps); } deps.add(effect); Add a callback to the current attribute}}Copy the code
3.3 Implementing Listening

Next, implement listening execution. Remove all callback functions corresponding to the current changed attribute from the global WeakMap and execute them in turn. This step is relatively simple.

Function update(target, key) {const depMap = targetmap. get(target); if (! depMap) { return; } const deps = depMap.get(key); deps.forEach((dep) => dep()); }Copy the code

Now that you’re done with the data responsiveness, you can implement watchEffect.

4. Implement watchEffect

As summarized earlier, watchEffect has only three points. Globally mount the callback, execute the callback, and kick the callback.

function watchEffect(fn) { cb = fn; fn(); // Trigger dependent collection cb = null; }Copy the code

Now that everything is done, let’s test it out.

Let firstName = observe({value: "li"}); Let lastName = observ ({value: "observ"}); let fullName = ""; Let obj = observe({wife: {name: "",},}); watchEffect(() => { fullName = firstName.value + lastName.value; console.log(fullName); }); watchEffect(() => { console.log(obj.wife.name); }); Firstname. value = "firstName "; Lastname. value = "just "; Name = "feng Nan ";Copy the code

You can see that it can implement first execution, dependency collection, listening execution, deep listening. The behavior is consistent with Vue’s watchEffect. Finally, the complete code is attached

let cb = null; const targetMap = new WeakMap(); Function observe(obj) {if (typeof obj! == "object") { return obj; } return new Proxy(obj, { get(target, key, receiver) { const res = Reflect.get(target, key, receiver); collect(target, key); return typeof obj === "object" ? observe(res) : res; }, set(target, key, val, receiver) { const res = Reflect.set(target, key, val, receiver); update(target, key); return res; }}); Function collect(target, key) {const effect = cb; if (effect) { let depMap = targetMap.get(target); if (! depMap) { depMap = new Map(); targetMap.set(target, depMap); } let deps = depMap.get(key); if (! deps) { deps = new Set(); depMap.set(key, deps); } deps.add(effect); Function update(target, key) {const depMap = targetmap. get(target); if (! depMap) { return; } const deps = depMap.get(key); deps.forEach((dep) => dep()); } function watchEffect(fn) { cb = fn; fn(); cb = null; }Copy the code

Reference: es6.ruanyifeng.com/#docs/proxy