We know from the following points:

  1. What problem does two-way data binding help us solve?
  2. What is two-way data binding?
  3. What is the principle of two-way data binding?
  4. How is two-way data binding implemented?

Depending on your current technical familiarity, choose which points interest you.

If you are already familiar with the VUE framework, take a look at my implementation of bidirectional binding. If you have a better one, please feel free to share. If you’re not familiar with VUE, you can go through it in its entirety, which will help you a little bit later.

What problem does bidirectional binding help us solve?

Before we look at it, we can answer the question: Why two-way binding? Fast-forward a decade and look at the front end.

The purpose of the Vue

You was working on a prototype at Google when he came across AngularJS and found that the framework had data-binding capabilities but was too bloated. I wrote a lightweight version of it and Vuejs was born. The original version of Vuejs borrowed a lot from AngularJS.

So bidirectional binding, of course, is not a unique feature of Vue, but a common solution that has been used by a wide range of frameworks.

So let’s take a step back in time and see what AngularJS was invented to solve!

The advent of single-page apps

While AngularJS has been around since 2009, the single-Page Application (SPA) has been around since 2003. The main practice is to use JavaScript in Web browsers to display and control user interfaces (UIs). Implement application-level program logic and communicate with the Web server.

In order to achieve SPA, before 2009, the mainstream solution was still based on jQuery and other libraries combined with Ajax technology for development, because the requirements and interactions were relatively simple at that time, but in response to the rich requirements, large projects are often very bloated and difficult to maintain.

Problems and benefits of solution

AngularJS introduced front-end modularity, semantic tagging, dependency injection, and two-way data binding. The direct solution is to improve the development efficiency of the project and reduce the maintenance cost.

The benefits are mainly reflected in:

  • Project quality: opened the front-end engineering development mode, managed front-end projects by modules and standardization;
  • Research and development efficiency: reduced the amount of code for developers, all kinds of UI frameworks supported by frames were born;
  • Team management: and the way modules and components are developed so that different role members can independently focus on UI and business logic;

As for what data binding brings? It is like the heart of the frame, the basis of the birth of the frame.

What is two-way data binding?

Slave one-way binding

Did you ever think that when we were doing native page development, the concept of “data binding” was involved? There certainly is, as in the following example:

<p></p>
Copy the code
const data = { value: 'hello' }
document.querySelector('p').innerText = data.value;
Copy the code

Control DOM presentation through JavaScript, that is, Data (Data) to template (DOM) binding, which is Data one-way binding.

To bidirectional binding

And bidirectional binding is on this basis, and extended the reverse binding effect, is template to data binding.

The above example extends the following:

<input onkeyup="change(event)" />
<p></p>
Copy the code
const data = { value: ' ' }
const change = e= > {
    // Update the input value
    data.value = e.target.value;
    // Display the synchronized values
    document.querySelector('p').innerText = data.value
}
Copy the code

What we are going to do with one-way binding is that data and templates interact, and changes on one side are immediately updated on the other. In this simple example, we learned about bidirectional binding, and Vue encapsulates a modular abstraction under this concept.

Composition of bidirectional binding

In the example above, we saw that two-way binding involves “templates” and “data,” which are the core elements of two-way binding and the core technical points of the front-end framework.

Take Vue3 as an example:

  • Virturl DOM, which is an upgrade to template manipulation and management;
  • Data binding, which is the upgrade of data logic management;

Now that we know what data binding is and what it means, it’s time to look at how the Vue framework implements it. But before we analyze the implementation, we must first understand how it works.

What is the principle of bidirectional binding?

Understand the role of bidirectional binding in the framework

Because Vue is a framework of two-way data binding, and the whole framework is composed of three parts:

  • Data layer (Model) : application data and business logic, business code written for developers;
  • View layer: display effect of application, various UI components, code composed of Template and CSS;
  • Business Logic Layer (ViewModel) : the core encapsulated by the framework, which is responsible for associating data with views;

The above layered architecture solution can be called by a technical term: MVVM. (Detailed knowledge of MVVM is beyond the scope, just a clear understanding of the role and place of bidirectional binding in this architecture.)

The core function of the control layer here is “two-way data binding”. Naturally, we only need to understand what it is to further understand how data binding works.

Understand the ViewModel

Its main responsibilities are:

  1. Update views after data changes;
  2. Update data after view changes;

Then, it can be concluded that it is mainly composed of two parts:

  1. Observers: Observe data, keep track of any changes to the data, and notify the view of updates;
  2. Parsers: Look at the UI, keep track of all interactions happening in the view, and update the data.

Combine the two and you have a framework with two-way data binding.

How is bidirectional binding implemented?

Implementation listener

Make sure it is a standalone feature whose job is to listen for changes in data and provide notification.

There are three common ways to listen for changes in data:

  • Observer mode (publish + subscribe);
  • Data hijacking;
  • Dirty;

Vue uses a combination of the first two. Let’s take a look at how they are used.

Observer model

The observer pattern is a pattern of object behavior. It defines a one-to-many dependency between objects, and when an object’s state changes, all dependent objects are notified and automatically updated.

In the observer mode, the publisher of notifications is dominant. It does not need to know who its observers are when it sends notifications. Any number of observers can subscribe and receive notifications.

Example code is as follows:

/** * publisher */
function Subject() {
  
  // All subscribers of a single publisher
  this.observers = [];
  
  // Add a subscriber
  this.attach = function(callback) {
    this.observers.push(callback);
  };
  
  // Through all subscribers
  this.notify = function(value) {
    this.observers.forEach(callback= > callback(value));
  };
}

/** * subscriber */
function Observer(queue, key, callback) {
  queue[key].attach(callback);
}

/ / = = = =

// Manually update data
function setData(data, key, value) {
    data[key] = value;

    // Notify all subscribers of this value that the data has been updated
    messageQueue[key].notify(value);
}

/ / = = = =

// Message queue
const messageQueue = {};

/ / data
const myData = { value: "" };

// Add a subscriptable entry to each data attribute
for (let key in myData) {
  messageQueue[key] = new Subject();
}

// Subscribe to value changes
Observer(messageQueue, "value", value => {
  console.warn("value updated:", value);
});

// Update data
setData(myData, "value"."hello world.");
setData(myData, "value".100);
setData(myData, "value".true);
Copy the code

You can see how this works on codepen (you need to open the console in the lower left corner to see the output, not involving DOM at this point) : codepen. IO /nachao/pen/…

Message queue

As we can see, simple subscribe and publish functions exist independently of each other, and therefore require a message queue to associate them.

In the example above, the message queue is used as a global store variable, whereas in the framework it is wrapped, and each new Vue() has a separate queue, as we demonstrate below.

The data was hijacked

In fact, in the case of data listening, the observer already meets the requirements. But why is it different from Vue? Because Vue has been optimized to add data hijacking.

An object.definePropoType feature was added to ECMAScript 5 in 2009 (usage is beyond the scope of this article, please check out developer.mozilla.org/zh-CN/docs/…). To be able to define getters and setters for object properties, which is great because everything in JavaScript is an object.

So our setData(myData, ‘value’, 100); Mydata. value = 100; Is written in a way. It’s much simpler in terms of syntax and usage.

Here is the changed code:

/ / publisher
function Subject() {
  this.observers = [];
  this.attach = function(callback) {
    this.observers.push(callback);
  };
  this.notify = function(value) {
    this.observers.forEach(callback= > callback(value));
  };
}

/ / subscriber
function Observer(queue, key, callback) {
  queue[key].attach(callback);
}

/ / = = = =

// Data interceptor
function Watcher(data, queue) {
  for (let key in data) {
    let value = data[key];
    Object.defineProperty(data, key, {
      enumerable: true.configurable: true.get: (a)= > value,
      set: newValue= > {
        value = newValue;

        // Notify all subscribers of this value that the data has been updatedqueue[key].notify(value); }}); }return data;
}

/ / = = = =

// Message queue
const messageQueue = {};

/ / data
const myData = Watcher({ value: "" }, messageQueue);

// Add each data attribute to the observer's message queue
for (let key in myData) {
  messageQueue[key] = new Subject();
}

// Subscribe to value changes
Observer(messageQueue, "value", value => {
  console.warn("value updated:", value);
});

// Update data
myData.value = "hello world.";
myData.value = 100;
myData.value = true;
Copy the code

Codepen. IO /nachao/pen/…

Of course, ES2015 has been out for some time, so we should use the new solution Proxy (please know the specific use of developer.mozilla.org/zh-CN/docs/…). , mainly because Vue3 uses this solution. As for the performance and specific differences, I will share them separately when I have the opportunity, but I won’t go into details here. Let’s see how to write the new syntax.

Code:

/ / publisher
function Subject() {
  this.observers = [];
  this.attach = function(callback) {
    this.observers.push(callback);
  };
  this.notify = function(value) {
    this.observers.forEach(callback= > callback(value));
  };
}

/ / subscriber
function Observer(queue, key, callback) {
  queue[key].attach(callback);
}

/ / = = = =

// Data interceptor - proxy mode
function ProxyWatcher(data, queue) {
  return new Proxy(data, {
    get: (target, key) = > target[key],
    set(target, key, value) {
      target[key] = value;

      // Notify all subscribers of this value that the data has been updatedqueue[key].notify(value); }}); }/ / = = = =

// Message queue
const messageQueue = {};

/ / data
const myData = ProxyWatcher({ value: "" }, messageQueue);

// Add each data attribute to the observer's message queue
for (let key in myData) {
  messageQueue[key] = new Subject();
}

// Subscribe to value changes
Observer(messageQueue, "value", value => {
  console.warn("value updated:", value);
});

// Update data
myData.value = "hello world.";
myData.value = 100;
myData.value = true;
Copy the code

Codepen. IO /nachao/pen/…

Now that we’ve done the data layer functionality in two-way binding, any data changes can be immediately known and associated with anything we want to do, such as update the view.

Next is the template operation, that is, Compile template parsing function.

Template resolution (binding views to data)

For DOM operations, the common methods are as follows:

  • Native or library-based DOM manipulation;
  • Convert the DOM to a Virtual DOM, and then compare and update it;
  • Use native Web Component technology;

The differences and performance of the above three types will be shared separately later. The Virtual DOM approach is used in Vue because it consumes much less performance than direct DOM manipulation and does not have Web Component compatibility.

The essence of these three methods is to update the DOM display effect, but in different ways, to simplify the principle of bidirectional binding, we will use the first method. The virtual DOM has a number of independent third-party libraries that you can explore if you are interested.

Parsers and DOM manipulation

The main tasks here are:

  • Parse all the specific features in the template, such as v-model, V-text, {{}} syntax, etc.
  • Associated data is displayed to the DOM;
  • Associated events are bound to the DOM;

Therefore, the following functions only need to be fulfilled: Show data to a template.

The code is as follows:

<div id="app">
  <input v-model="value" />
  <p v-text="value"></p>
</div>
Copy the code
// Template parsing
function Compile(el, data) {

  // Associate custom features
  if (el.attributes) {
    [].forEach.call(el.attributes, attribute => {
      if (attribute.name.includes('v-')) { Update[attribute.name](el, data, attribute.value); }}); }// Parse all DOM recursively
  [].forEach.call(el.childNodes, child => Compile(child, data));
}

// Customize the event corresponding to the feature
const Update = {
  "v-text"(el, data, key) {

    // Initialize the DOM content
    el.innerText = data[key];
  },
  "v-model"(input, data, key) {

    // Initialize the Input default value
    input.value = data[key];

    // Listen for input events of the control and update data
    input.addEventListener("keyup", e => { data[key] = e.target.value; }); }};/ / = = = =

/ / data
const myData = { value: "hello world." };

/ / parsing
Compile(document.querySelector("#app"), myData);
Copy the code

Codepen. IO /nachao/pen/…

So far we have defined DOM parsing without associated data listening, let’s do it!

Full bidirectional binding

The code is as follows:

/ / publisher
function Subject() {
  this.observers = [];
  this.attach = function(callback) {
    this.observers.push(callback);
  };
  this.notify = function(value) {
    this.observers.forEach(callback= > callback(value));
  };
}

/ / subscriber
function Observer(queue, key, callback) {
  queue[key].attach(callback);
}

/ / = = = =

// Data interceptor - proxy mode
function ProxyWatcher(data, queue) {
  return new Proxy(data, {
    get: (target, key) = > target[key],
    set(target, key, value) {
      target[key] = value;

      // Notify all subscribers of this value that the data has been updatedqueue[key].notify(value); }}); }/ / = = = =

// Template parsing
function Compile(el, data) {

  // Associate custom features
  if (el.attributes) {
    [].forEach.call(el.attributes, attribute => {
      if (attribute.name.includes('v-')) { Update[attribute.name](el, data, attribute.value); }}); }// Parse all DOM recursively
  [].forEach.call(el.childNodes, child => Compile(child, data));
}

// Customize the event corresponding to the feature
const Update = {
  "v-text"(el, data, key) {

    // Initialize the DOM content
    el.innerText = data[key];

    // Create a subscription to the data and update the display as the data changes
    Observer(messageQueue, key, value => {
        el.innerText = value;
    });
  },
  "v-model"(input, data, key) {

    // Initialize the Input default value
    input.value = data[key];

    // Listen for input events of the control and update data
    input.addEventListener("keyup", e => {
      data[key] = e.target.value;
    });

    // Create a subscriptionObserver(messageQueue, key, value => { input.value = value; }); }};/ / = = = =

// Message queue
const messageQueue = {};

/ / data
const myData = ProxyWatcher({ value: "hello world." }, messageQueue);

// Add each data attribute to the observer's message queue
for (let key in myData) {
    messageQueue[key] = new Subject();
}

/ / = = = =

// Parse + correlation
Compile(document.querySelector("#app"), myData);
Copy the code

Codepen. IO /nachao/pen/…

This completes a very simple MVVM function. Of course, it is not written for the purpose of explaining principles, but for a mature framework like Vue, each core needs to be packaged into modules for more extensible definitions.

If you wrap it simply, it looks like a minimalist Vue. Codepen. IO/nachao/pen /…

The code is as follows:

// The observer function
/ / publisher
function Subject() {
  this.observers = [];
  this.attach = function(callback) {
    this.observers.push(callback);
  };
  this.notify = function(value) {
    this.observers.forEach(callback= > callback(value));
  };
}
/ / subscriber
function Observer(queue) {
  this.queue = queue
  this.add = function(key, callback) {
    this.queue[key].attach(callback); }}/ / = = = =

// Data interceptor
// Listen for data updates - proxy mode
function ProxyWatcher(data, queue) {
  return new Proxy(data, {
    get: (target, key) = > target[key],
    set(target, key, value) {
      target[key] = value;

      // Notify all subscribers of this value that the data has been updatedqueue[key].notify(value); }}); }/ / = = = =

// Template parsing
function Compile(el, vm) {

  // Associate custom features
  if (el.attributes) {
    [].forEach.call(el.attributes, attribute => {
      if (attribute.name.includes('v-')) { Update[attribute.name](el, vm.data, attribute.value, vm); }}); }// Parse all DOM recursively
  [].forEach.call(el.childNodes, child => Compile(child, vm));

  return el
}

// Customize the event corresponding to the feature
const Update = {
  "v-text"(el, data, key, vm) {

    // Initialize the DOM content
    el.innerText = data[key];

    // Create a subscription to the data and update the display as the data changes
    vm.observer.add(key, value => {
      el.innerText = value;
    });
  },
  "v-model"(input, data, key, vm) {

    // Initialize the Input default value
    input.value = data[key];

    // Create a subscription
    vm.observer.add(key, value => {
      input.value = value;
    });

    // Listen for input events of the control and update data
    input.addEventListener("keyup", e => { data[key] = e.target.value; }); }};/ / = = = =

/ / packaging
function Vue({ el, data }) {

  // initProxy
  this.messageQueue = {};
  this.observer = new Observer(this.messageQueue)
  this.data = ProxyWatcher(data, this.messageQueue);

  // initState
  for (let key in myData) {
    this.messageQueue[key] = new Subject();
  }

  // initRender
  // initEvents
  this.el = Compile(el, this);
}

/ / = = = =

/ / data
const myData = { value: "hello world." };

/ / instance
const vm = new Vue({
  el: document.querySelector("#app"),
  data: myData
});
Copy the code