This is the second day of my participation in the August Text Challenge.More challenges in August

What is reactive

One of the most common questions people are asked in interviews is, do you know what responsive is? What is the principle of the reactive formula?

Questions like these should be about how to say what the interviewer wants to know, not what we think he wants to know.

I think a common way to answer that question is,

First, a brief description of the basic concepts.

Then, it is analyzed from the use level and principle level.

Finally, to add to what we already know.

I think following this pattern will definitely get you in the interviewer’s good graces.

So I’m going to explain, in that order, how to answer the question, “What is reactive?”

Simple description

Reactive is a common feature of Web development. It is used in many Web frameworks, including but not limited to Vue2, Vue3, AngularJS, Knockout, etc. The nature of responsiveness is that when data is modified, it automatically triggers a series of events, often referred to as the publish/subscribe design pattern.

Use level & Principle level

Release subscription

✏️ Usage level

Because the nature of responsiveness is a publish-subscribe design pattern. It is implemented differently in different Web frameworks, so let’s take a look at how the publish-subscribe pattern is used.

(() = > {
    subscribe('event1'.(params) = > {
      console.log('Trigger Event1-1:' + params)
    })
    subscribe('event1'.(params) = > {
      console.log('Trigger Event1-2:' + params)
    })
  
  	publish('event1'.'rice')
})()

// Trigger event1-1: bibimbab
// Trigger event1-2: bibimbab
Copy the code

We hope to pass

  • Subscribe to an event and pass in a callback (caching the callback)
  • Publish to publish an event and pass in a parameter (execute all callbacks corresponding to the event)

🚀 Principle

Now that we’ve seen how to use the publish/subscribe design pattern, let’s look at how to implement it.

(() = > {
    const topics = new Map(a)/ / subscribe
    function subscribe (topic, callback) {
        if (topics[topic]) {
            topics[topic].push(callback)
        } else {
            topics[topic] = [callback]
        }
    }


  	/ / release
    function publish (topic, params) {
        (topics[topic] || []).forEach(fn= > fn(params))
    }
})()
Copy the code

Obviously, we need a Topics object to save all events and their corresponding callback functions, so we need to declare a Topics object to start with.

The data structure of final topics looks like:

const topics = {
  event1: [
    (params) = > {/* do something */},
    (params) = > {/* do something */},
    / *... * /].event2: [
    / *... * /]}Copy the code

Next, we use the subscribe constructor to subscribe to the event and store the event and callback into the Topics object. Publish is used to publish events, passing parameters to all callbacks corresponding to the current event.

At this point, we have implemented a publish and subscribe model.

📚 summary

The publish-subscribe model is ubiquitous in Web development as a responsive foundation. The publish-subscribe pattern is also used in the implementation below.

Let’s take Vue2 as an example:

The response formula of Vue2

In Vue2, there are many features that involve responsiveness, such as DATA, computed, Watch, and renderEffect (data, computed updates cause references to the page to update). Here, we take Data and computed as examples to explain the principle of responsiveness, and the same applies to other apis.

✏️ Usage level

Let’s take an example of how data and computed are used in Vue2.

new Vue({
  el: '#app'.template: '<div>{{computeCount}}</div>'.data: {
    count: 0,},computed: {
    computeCount() {
      return '[computed] ' + this.count
    }
  },
  mounted() {
    setInterval(() = > {
      this.count += 1
    }, 1000)}})Copy the code

IO/S/Quirky – He…

Running the code, you can see that every 1s the page displays ‘[computed] 0… ‘. That is, when this.count is updated, computeCount in computed automatically updates, triggering a re-rendering of the Vue template.

🚀 Principle

Here I will explain how computed works in Vue2. But before we do that, let’s approach the answer one question at a time.

In daily development, when we modify an object, how do we get changes to it?

Let’s say we want to print the latest count value when this.count changes.

As documented in MDN, Object.defineProperty [1] can detect an update to an attribute in an Object.

In this example, we can get the updated value of this.count with the following code:

let value = data.count
Object.defineProperty(data, 'count', {
  get() {
    return value
  },
  set(newValue) {
    if(value ! == newValue) {// Get the latest count
      console.log(newValue)
      value = newValue
    }
  }
})
Copy the code

Because we want to tell all computed functions that rely on count to be executed again when the value of count changes. But how do we know which computed functions depend on count?

For this question, let’s first examine what a function depends on a variable value.

We know that if a function is executed using this variable, then we can say that the function depends on the variable.

Does a definition that uses this variable at execution translate to a getter that executes that variable?

Let’s take a look at this code:

(() = > {
	const data = { count: 0 }
	Object.defineProperty(data, 'count', {
    get() {
      console.log('Depends on the variable count')}})const fn = () = > {
    console.log('execution of fn')
    data.count
    console.log('fn execution completed ')}}) ()Copy the code

By executing the above code, you can see that when fn executes, data.count is called and eventually the getter for count is fired. Finally print out the dependent variable count.

At this point, we can conclude that if we want to know which computed functions depend on the count variable, we can do this in the getter. The code is as follows:

+ let Listener = null

let value = data.count
+ let event = Symbol('')

Object.defineProperty(data, 'count', {
  get() {
+ Listener && subscribe(event, Listener)return value }, set(newValue) { if (value ! == newValue) {console.log(newValue) {console.log(newValue)+ publish(event, null)

      value = newValue
    }
  }
})

const computed = {
  computeCount() {
  	return '[computed] ' + this.count
  }
}

+ const fn = computed.computeCount

+ Listener = fn
+ fn()
+ Listener = null

Copy the code

Let’s explain this code.

First, you create a Listener to hold the current computed function.

We then create a unique event name through Symbol.

Remember the publish-subscribe implementation mentioned in the previous paragraph? We subscribe to the Symbol event using subscribe, and then call back to the current Listener. The Listener is null. It is true that the Listener does not exist, but this line of code is written in the getter of count, and the getter is not executed. When it is executed, the Listener will have a value.

After that, we add a publish line to the setter, which executes all callbacks to the Symbol event name cached in the getter.

In the last paragraph, we name this computed function computeCount fn, and then assign it to the Listener before fn is executed, so that when fn is actually executed, the getter for this.count is fired. Finally, the logic of subscribe is triggered, and the count variable is associated with the Listener. When we change the count value, we can do it through the publish notification computeCount function with the help of the publish-subscribe model.

So far, we have covered the implementation of computed in Vue2.

(() = > {
  const topics = new Map(a);/ / subscribe
  function subscribe(topic, callback) {
    if (topics.has(topic)) {
      topics.set(topic, topics.get(topic).concat(callback));
    } else{ topics.set(topic, [callback]); }}/ / release
  function publish(topic, params) {
    (topics.get(topic) || []).forEach((fn) = > fn(params));
  }

  // Vue2 computed
  let Listener = null;

  function proxy(state) {
    let value = state.count;
    let event = Symbol("");

    let p = Object.defineProperty(state, "count", {
      get() {
        if (Listener) {
          subscribe(event, Listener);
        }

        return value;
      },
      set(newValue) {
        if(value ! == newValue) { publish(event,null); value = newValue; }}});return p;
  }

  const component = {
    data: { count: 0 },
    computed: {
      computeCount() {
        const value = "[computed] " + this.count;
        console.log(value);
        returnvalue; ,}}};const state = proxy(component.data);

  const fn = component.computed.computeCount.bind(state);

  Listener = fn;
  fn();
  Listener = null;

  const moutedFn = function () {
    setInterval(() = > {
      this.count += 1;
    }, 1000); }; moutedFn.call(state); }) ();Copy the code

📚 summary

In Vue2, the key to the realization of responsiveness is as follows:

  • Hijacking the getter and setter for an Object with Object.defineProperty
  • The publish-subscribe pattern associates each object property with a listening event

The disadvantages of Vue2 responsive implementation are:

  • Object.defineproperty can’t listen on arrays effectively (Vue2 overwrites eight array methods like push and unshift by hijacking, but can’t get setters for subscript changes, hence $set API)

The resources

[1] developer.mozilla.org/zh-CN/docs/…