Say the word “responsive” and what immediately comes to mind? Responsive layout? Responsive programming?

Literally, something that is “responsive” causes the object to automatically change in response to conditions. For example, in “responsive Layout,” pages automatically display different styles depending on device size.

The same is true of the response in vue. js. When the data changes, the views that use that data are automatically updated accordingly.

Next I according to personal understanding, and we explore the vue. js response principle, if there is a mistake, welcome to point 😺~~

First, the use of vue. js response

Now I have a very simple requirement. After clicking the text “Leo” in the page, the text content is changed to “Hello, front-end self-study”.

We can manipulate the DOM directly to accomplish this requirement:

<span id="name">leo</span>
Copy the code
const node = document.querySelector('#name')
node.innerText = 'Hello, front end study.';
Copy the code

This is simple to implement, but it can become complicated when we need to change a lot of data (for example, the same data is referenced in multiple places).

While we’re at it, let’s take a look at how vue.js implements the above requirements:

<template> <div id="app"> <span @click="setName">{{ name }}</span> </div> </template> <script> export default { name: "App", data() { return { name: "leo", }; }, methods: {setName() {this.name = "Hello, front end study "; ,}}}; </script>Copy the code

If you look at the code above, we’re automatically updating the view by changing the data. When we have multiple references to the name, the view is automatically updated.

<template>
  <div id="app">
    <span @click="setName">{{ name }}</span>
    <span>{{ name }}</span>
    <span>{{ name }}</span>
    <span>{{ name }}</span>
  </div>
</template>
Copy the code

When we use the current mainstream front-end frameworks viee. js and React to develop business, we only need to pay attention to how the page data changes, because after the data changes, the view will automatically update, which frees us from the complicated DOM operation and improves the development efficiency.

Review the observer model

As mentioned above, “Automatically update the view by changing the data.” In other words, “when the data changes, the view is updated in response to the data being used.”

Does it have a familiar feeling? The data doesn’t have to care how many objects refer to it, it just needs to notify the referenced object when the data changes, and the referenced object responds. Yeah, it kind of smells like observer mode?

For more on the observer pattern, read my previous post on Illustrated Design Patterns: Observer Patterns (TypeScript).

1. Observer mode flow

The observer mode represents a “one-to-many” relationship, where n observers pay attention to 1 observed, and the observed can actively notify all observers. Take photo:

In this picture, fans just need to pay attention if they want to receive the latest articles of the “front-end study course” in time. New articles of the “front-end study course” will be actively pushed to each fan. In this process, the “front study” is the observed, and each “fan” is the observer.

2. The Observer Mode core

The core components of the observer mode include n observers and 1 observed. A simple observer pattern is implemented here:

2.1 Defining interfaces

// Observe the target interface
interface ISubject {
    addObserver: (observer: Observer) = > void; // Add an observer
    removeObserver: (observer: Observer) = > void; // Remove the observer
    notify: () = > void; // Notify the observer
}

// The observer interface
interface IObserver {
    update: () = > void;
}
Copy the code

2.2 Implementing the observed class

// Implement the observed class
class Subject implements ISubject {
    private observers: IObserver[] = [];

    public addObserver(observer: IObserver): void {
        this.observers.push(observer);
    }

    public removeObserver(observer: IObserver): void {
        const idx: number = this.observers.indexOf(observer);
        ~idx && this.observers.splice(idx, 1);
    }

    public notify(): void {
        this.observers.forEach(observer= >{ observer.update(); }); }}Copy the code

2.3 Implementing the observer class

// Implement the observer class
class Observer implements IObserver {
    constructor(private name: string) { }

    update(): void {
        console.log(`The ${this.name} has been notified.`); }}Copy the code

2.4 Test Code

function useObserver(){
    const subject: ISubject = new Subject();
    const Leo = new Observer("Leo");
    const Robin = new Observer("Robin");
    const Pual = new Observer("Pual");

    subject.addObserver(Leo);
    subject.addObserver(Robin);
    subject.addObserver(Pual);
    subject.notify();

    subject.removeObserver(Pual);
    subject.notify();
}

useObserver();
// [LOG]: "Leo has been notified." 
// [LOG]: "Robin has been notified." 
// [LOG]: "Pual has been notified." 
// [LOG]: "Leo has been notified." 
// [LOG]: "Robin has been notified." 
Copy the code

Object.defineproperty ()

Vue.js data responsive principle is based on the js standard built-in Object method object.defineProperty () method to implement, this method is not compatible with IE8 and FF22 and below version browser, This is why vue.js only works in browsers above these versions.

An understanding of object.defineProperty () is important for us to understand the responsive principles of vue.js.

Vue.js 3 uses the proxy method to implement the response, which is similar, we only need to understand object.defineProperty (), proxy is almost understood.

1. Concept introduction

The object.defineProperty () method directly defines a new property on an Object, or modifies an existing property on an Object, and returns the Object. The syntax is as follows:

Object.defineProperty(obj, prop, descriptor)
Copy the code
  • Description of input parameters:

Obj: source object for which attributes are to be defined; Prop: The name or Symbol of the property to be defined or modified; Descriptor: specifies the property descriptor that you want to define or modify, such as a different, an Enumerable, a value, a writable, a GET, and a set. For details, see the documentation.

  • Instructions for ginseng:

The modified source object.

Here’s a simple 🌰 example:

const leo = {};
Object.defineProperty(leo, 'age', { 
    value: 18.writable: true
})
console.log(leo.age); / / 18
leo.age = 22;
console.log(leo.age); / / 22
Copy the code

2. Realize the getter/setter

The object.defineProperty () method is the third parameter to the descriptor, which allows you to set the get and set descriptors:

  • getDescriptor: whenAccess the propertyThe default value isundefined ;
  • setDescriptor: whenModify this propertyThe default value isundefined

Once an object has getter/setter methods, we can simply call it a responsive object.

These two operators give us the possibility to intercept data for manipulation. Modify the previous example by adding getter/setter methods:

let leo = {}, age = 18;
Object.defineProperty(leo, 'age', { 
    get(){
        // to do something
      	console.log('Listening for request data');
        return age;
    },
    set(newAge){
        // to do something
      	console.log('Listening for modified data');
        age = newAge > age ? age : newAge
    }
})
leo.age = 20;  // Listen to modify data
console.log(leo.age); // Listen to request data // 18

leo.age = 10;  // Listen to modify data
console.log(leo.age); // Listen to request data // 10
Copy the code

Accessing the AGE property of a Leo object is handled through the GET descriptor, while modifying the age property is handled through the set descriptor.

Four, the realization of simple data response

In the previous two sections, we reviewed the “observer pattern” and the “object.defineProperty ()” method, which are important in the vue.js responsive principle.

Next, let’s implement a very simple data responsive change with the following requirements: Click the “Update data” button and the text is updated.

Next we will implement three classes:

  • DepThe observed class to generate the observed;
  • WatcherObserver class, used to generate observers;
  • ObserverClass,Implement responsive objects by transforming ordinary data into responsive data.

Use a graph to describe the relationship between the three. It doesn’t matter if you don’t understand it now, but you can review the graph after this section:

1. Implement the Compact observer pattern

Here’s a brief look at the previous review of the observer pattern:

// Implement the observed class
class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(watcher) {
        this.subs.push(watcher);
    }
    notify(data) {
        this.subs.forEach(sub= >sub.update(data)); }}// Implement the observer class
class Watcher {
    constructor(cb) {
        this.cb = cb;
    }
    update(data) {
        this.cb(data); }}Copy the code

The observer mode plays a very important role in the vue. js responsive principle. Among them:

  • DepThe observed class, which provides for collecting observers (addSub) method and notify the observer (notify) method;
  • WatcherObserver class that supports incoming callbacks when instantiated (cb) method and provides updates (update) method;

2. Implement a class that generates responses

This step requires the implementation of the Observer class, which, at its core, implements responsive objects by setting getters/setters for each property of the Object through the object.defineProperty () method. The goal is to transform normal data into responsive data.

Here’s an example of the simplest single-layer objects (deep objects are covered in the next section), such as:

let initData = {
    text: 'Hello, front end study.'.desc: Each morning, enjoy a good article at the front end. '
};
Copy the code

Next, implement the Observer class:

// Implement reactive classes (the simplest single-layer objects, not deep ones)
class Observer {
    constructor (node, data) {
        this.defineReactive(node, data)
    }

    // Implement data hijacking (core method)
    // Iterate through all the data in the data, adding getters and setters
    defineReactive(vm, obj) {
        // Redefines get and set for each attribute
        for(let key in obj){
            letValue = obj[key], dep =new Dep();
            Object.defineProperty(obj, key, {
                enumerable: true.configurable: true.get() {
                    // Create an observer
                    let watcher = new Watcher(v= > vm.innerText = v);
                    dep.addSub(watcher);
                    return value;
                },
                set(newValue) {
                    value = newValue;
                    // Notify all observersdep.notify(newValue); }}}Copy the code

At the heart of the above code is the defineReactive method, which iterates over each property in the original Object, instantiates an observed (Dep) for each property, and then calls the object.defineProperty () method separately, adding getters/setters for each property.

  • When accessing the data, the getter performs dependency collection (that is, adding observers) through instantiationWatcherCreates an observer and executes the observedaddSub()Method to add an observer;
  • When the data is modified, the setter performs a dispatch update (that is, notify the observer) by calling the observed’snotify()Method notifies all observers and executes the observerupdate()Methods.

3. Test code

To make it easier to observe data changes, we bind click events to the “Update Data” button to modify the data:

<div id="app"></div>
<button id="update">Update the data</button>
Copy the code

Test code is as follows:

// Initialize the test data
let initData = {
    text: 'Hello, front end study.'.desc: Each morning, enjoy a good article at the front end. '
};

const app = document.querySelector('#app');

// Step 1: Convert test data into reactive objects
new Observer(app, initData);

// Step 2: Initialize the page text content
app.innerText = initData.text;

// Step 3: Bind the button event and click to trigger the test
document.querySelector('#update').addEventListener('click'.function(){
    initData.text = 'We must always keep old memories and new hopes. `;
    console.log(Current time:The ${new Date().toLocaleString()}`)})Copy the code

The core of the test code is through instantiationObserver, convert the test data into responsive data, and then simulate the data changes to see the view change. Every time you click the “Update Data” button, you will see “Data changes!” in the console. “, indicating that we can already observe the change in the data through the setter.

Of course, you can also manually modify it in the consoleinitDataThe object of thetextAttribute to experience responsive change

So far, we have implemented very simple data responsive changes. Of course, vue. js is not as simple as this. Let’s understand this first, and look at the vue. js responsive principles in the next section.

This part of the code, I have put on my Github address: github.com/pingan8787/…

You can review this picture again to make the process clearer:

V. Vue.js reactive implementation

This section code: github.com/pingan8787/…

Here you can go back to the classic picture on the website and think about the previous example.

(Photo from cn.vuejs.org/v2/guide/re…)

The previous section implemented a simple data response. Let’s continue to refine the example and implement a simple vue.js response with the following test code:

// index.js
const vm = new Vue({
    el: '#app'.data(){
        return {
            text: 'Hello, front end study.'.desc: Each morning, enjoy a good article at the front end. '}}});Copy the code

Isn’t that interesting? Here is our final implementation of the project catalog:

Reactive/reactive/reactive/reactive/reactive/reactive/reactive/reactive/reactive/reactive/reactive/reactive/reactive/reactive/reactive // Implement the observer and observed (dependent on the collector)/vue. Js // implement the vue class as the main entry class/compile.js // Implement the compile template functionCopy the code

Now that you know what each file does, you can concatenate each step.

1. Implement the entry file

We first implement the entry file, including two simple files, index.html/index.js, to facilitate the following tests.

1.1 the index. HTML

<! DOCTYPEhtml>
<html lang="en">
<head>
    <script src="./vue.js"></script>
    <script src="./observer.js"></script>
    <script src="./compile.js"></script>
    <script src="./watcher.js"></script>
</head>
<body>
    <div id="app">{{text}}</div>
    <button id="update">Update the data</button>
    <script src="./index.js"></script>
</body>
</html>
Copy the code

1.2 the index. Js

"use strict";
const vm = new Vue({
    el: '#app'.data(){
        return {
            text: 'Hello, front end study.'.desc: Each morning, enjoy a good article at the front end. '}}});console.log(vm.$data.text)
vm.$data.text = 'Page data updated successfully! '; // Simulate data changes
console.log(vm.$data.text)
Copy the code

2. Implement the core entry vue.js

The vue. Js file is the entire responsive entry file for our implementation, exposing a vue class and mounting it globally.

class Vue {
    constructor (options = {}) {
        this.$el = options.el;
        this.$data = options.data();
        this.$methods = options.methods;

        // [Core process] converts normal data objects into responsive objects
        new Observer(this.$data);

        if (this.$el) {
            // The core process will parse the content of the template
            new Compile(this.$el, this)}}}window.Vue = Vue;
Copy the code

The Vue class takes a configuration option that works just like vue.js, including the $EL mount point, the $Data data object, and the $Methods list (not covered in this article).

The Oberser class is instantiated to convert a normal data object into a responsive object, and then the EL parameter is passed. If it exists, the Compile class is instantiated to parse the template content.

Conclusion underVueThis class workflow:

3. Implement the observer. Js

The observer.js file implements the observer class, which is used to transform ordinary objects into responsive ones:

class Observer {
    constructor (data) {
        this.data = data;
        this.walk(data);
    }

    // The core method converts the data object into a responsive object, setting getter and setter methods for each data property
    walk (data) {
        if (typeofdata ! = ='object') return data;
        Object.keys(data).forEach( key= > {
            this.defineReactive(data, key, data[key])
        })
    }

    // [core method] implements data hijacking
    defineReactive (obj, key, value) {
        this.walk(value);  // [core procedure] traverses the walk method to process deep objects.
        const dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true.configurable: true,
            get () {
                console.log('[getter] method execution ')
                Dep.target &&  dep.addSub(Dep.target);
                return value
            },
            set (newValue) {
                console.log('[setter] method executes')
                if (value === newValue) return;
                // [core procedure] when the newValue newValue is set as an object, it continues to be converted into a responsive object through the walk method
                if (typeof newValue === 'object') this.walk(newValue);
                value = newValue;
                dep.notify(); // [core procedure] executes the watch-notified method, notifying all observers to perform the update}}}})Copy the code

Compared to the Observer class implemented in Section 4, here’s a tweak:

  • increasewalkCore method, which is used to iterate over each property of an object, calling the data hijacking method separately (defineReactive());
  • indefineReactive()In the getter ofDep.targetObservers are added only when they exist, more on this in the next sectionDep.target;
  • indefineReactive()To determine the current new value (newValue) is an object, if it is, it is called directlythis.walk()Method to turn the current object into a reactive object again,Working with deep objects.

With the improved Observer class, we can transform a single or deeply nested ordinary object into a responsive object.

4. Realize the watcher. Js

The Dep observed class (dependency collector) and the Watcher observer class are implemented here.

class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(watcher) {
        this.subs.push(watcher);
    }
    notify(data) {
        this.subs.forEach(sub= >sub.update(data)); }}class Watcher {
    constructor (vm, key, cb) {
        this.vm = vm;   // VM: indicates the current instance
        this.key = key; // Key: indicates the data name of the current operation
        this.cb = cb;   // cb: indicates the callback after data changes

        Dep.target = this; // Globally unique
      
        $data[key] = this.vm.$data[key
        this.oldValue = this.vm.$data[key]; // Save the changed data as the old value, and determine whether to update it later

        // After the getters have been executed, perform the following cleanup
        Dep.target = null;
    }
    
    update () {
        console.log('Data has changed! `);
        let oldValue = this.oldValue;
        let newValue = this.vm.$data[this.key];
        if(oldValue ! = newValue) {// Compare the old and new values, and execute the callback only if there is a change
            this.cb(newValue, oldValue); }; }}Copy the code

Compared to the Watcher class implemented in Section 4, here’s a tweak:

  • In the constructor, addDep.targetValue operation;
  • In the constructor, addoldValueVariable, save the changed data as the old value, and then as the basis for judging whether to update;
  • inupdate()Method to add the current operation objectkeyThe new and old values of the corresponding values are compared, and the callback is performed only if they are different.

Dep.target is currently globally unique because only one subscriber is allowed to be processed at a time. Target indicates the target subscriber that is currently being processed, and is assigned null when the current subscriber is finished processing. Here dep.target is used in the getter for defineReactive().

With the improved Watcher class, when we operate on the value corresponding to the key of the current operation object, we can only perform the callback when the data changes, reducing the waste of resources.

4. Realize the compile. Js

Compile.js implements the template compilation of vue. js, such as converting the {{text}} template in HTML to the value of a specific variable.

There is a lot of content in the introduction of compile.js. Considering the length problem, and the core of this article introduces the principle of response, so we will not introduce the implementation of compile.js for the time being. Friends who are learning can download the file on my Github and download it directly. Github.com/pingan8787/…

5. Test code

At this point, we have converted the demo from section 4 into a simple vue.js response. Let’s open index.html and see what it looks like:

When index.js executes:

vm.$data.text = 'We must always keep old memories and new hopes. ';
Copy the code

The page was updated, and the text on the page changed from “Hello, front end study” to “We must always keep old memories and new hopes.” .

Here, our simple version of vue. js responsive principle to achieve good, can follow the article to see here friends, give you a big thumbs up 👍

Six, summarized

This article first introduces the core knowledge of vue.js responsiveness principle by reviewing the observer pattern and the object.defineProperty () method, and then shows you a simple example to achieve a simple response. Finally, by modifying this simple response example, Implement an example of a simple vue.js responsive principle.

I believe after reading this article, the friends of vue.js response principle of understanding will be more profound, I hope you clear your mind, and then have a good aftertaste ~

The resources

  1. Official Document – Deep responsive principles 
  2. Discussion on Vue Response Principle
  3. Data Response Principle of Vue