This is the NTH day of my participation in the August Changwen Challenge. For details, see: August Changwen Challenge (already 2 days).

preface

Vue is one of the most popular frameworks in the front end. It is inevitable to ask some questions about VUE in the work or in the interview. And vUE is how to achieve data binding, is one of the most frequently asked questions, today I will share a simple personal implementation of VUE. This project implements simple template compilation and bidirectional data binding.

Source code published on github, interested partners please step: VUE data two-way binding source address

Project initialization

First, to implement this simple vue, a good engineering tool is essential, and webPack is a little too cumbersome for this project, using the parcel-Bundler tool (official documentation).

Parcel is a Web application packaging tool for developers with different experiences. It takes advantage of multi-core processing to provide extreme speed and requires no configuration.

We just need to create a project and install the dependency.

mkdir simple-vue && cd simple-vue
npm init -y
npm install -D parcel-bundler
Copy the code

With this dependency installed, we create a template file index. HTML in the root directory, a SRC folder, and a main.js file in this folder as the entry file for our project.

index.html

<! </title> <meta charset=" utF-8 "> <body> <div id="app"> <input type="text"  id="ceshi"> </div> <script src="./src/main.js"></script> </body> </html>Copy the code

Add another script directive to package.json:

"dev": "parcel index.html"
Copy the code

At this point we start the project NPM run dev:

By now, our project has been preliminarily set up.

Data bidirectional binding

What is data bidirectional binding? As any of you who have written vue will know, in an instance of Vue, you define a data function that returns an object, and all the properties of that object have been processed to achieve bidirectional binding. Use v-bind or {{}} double parenthesis syntax in a template. When these attributes change, v-bind and {{}} bound text nodes are automatically assigned.

The principle of overview

Let’s start with a picture:

The basic idea of this project is based on the above diagram, and the principle of bidirectional data binding is based on the publish-subscribe pattern.

Leaving the code aside, imagine that each property in a Data object is like a YouTube account, which has a list of subscribers, or viewers. When it publishes a video, it automatically informs each viewer on the list to update their homepage and push the video. Where this property is used (v-bind, {{}}, etc.), then set it to a watcher, i.e., a viewer, and click subscribe.

With that in mind, let’s move on to the implementation.

The specific implementation

Create a vUE

First, let’s follow suit and create a vue constructor. We’ll do what the real Vue does, but this is the youth version, hee hee.

First, in our project, create a core folder to hold the various classes we define. Create a Vue. Js file below this folder to store our Vue constructor. First, we define an instance of Vue in main.js, and then write our constructor based on it:

main.js

import Vue from './core/Vue';

const app = new Vue({
  el: '#app',
  data() {
    return {
      val: '初始化val',
      info: { text: '123' },
    };
  },
});

Copy the code

In this case, we pass an object to the Vue constructor, el, which is the root node of the binding, and we do all of our DOM operations at the root node. Data is the object that we want to implement bidirectional data binding.

Let’s go back to vue.js:

Vue.js

export default class Vue { constructor(option) { this._init(option); } } Vue.prototype._init = function (option) { const vm = this; vm.$el = document.querySelector(option.el); $data = option.data(); $data = option.data(); // data object};Copy the code

Now we have a new Vue, but it has nothing to do with bidirectional data binding. What is the principle of bidirectional data binding? You might answer: because vue hijacks data objects via object.defineProperty (ve3. X via proxy), collecting dependencies when Object properties are read; Publish subscribe when the object properties are set. Great. Now that we know how it works, let’s do it now.

Hijacking a Data object

Let’s add something to the Vue constructor prototype by adding the method to hijack the object:

/ / to monitor object properties change method, subscriptions, and automatically release subscription Vue. Prototype. DefineReactive = function (obj) {if (! (obj instanceof Object)) return; For (let key in obj) {let val = obj[key]; Object.defineproperty (obj, key, {enumerable: true, // Makes it possible to enumerate freely: If true, / / a configurable the get () {the console. The log (` get: ${key} - ${val} `); return val; }, set(newVal) { if (newVal === val) return; Console. log(' set: ${key} - ${newVal} '); val = newVal; }}); }};Copy the code

Then in the _init method, call this method to hijack the data object and write a test method to see if it has successfully hijack the data object:

Vue.prototype._init = function (option) { const vm = this; vm.$el = document.querySelector(option.el); $data = option.data(); $data = option.data(); // hijks the data object vm.defineReactive(vm.$data); Const input = document.getelementById ("ceshi"); vm.$data.val; // Try to read the value input.addeventListener ("input", (e) => {vm.$data.val = e.t. value; }, false); };Copy the code

We open the console and type some text into the input box and find that some information is printed:

At this point, we have successfully hijacked the data object.

Let’s think again about the principle summary there, where each property in the data object is like a YouTube account, and now we’re hijacking data, creating a channel for each property. What does a channel have, of course, is a list of subscribers, we want to give our fans a home (also hope that the friends reading the article like and follow, give me a warm home ~). So what is this subscriber list? So let’s create one.

Create the subscriber list – Dep

Let’s create a new dep. js file in our core folder to store our Dep constructor:

Dep.js

Export default class Dep {static target = null; // Dep {static target = null; constructor() { this.subs = []; AddSub (sub) {this.subs.push(sub); RemoveSub (sub) {depend() {if (dep.target) {this.addsub (dep.target); {}} / / publish subscribe to notify () this. Subs. ForEach ((m) = > {m.u pdate (); }); }}Copy the code

In the code above, subs is our list of subscribers. There are several methods to add a subscription, remove a subscription, automatically subscribe, and publish a subscription. Target is a static variable. We’ll explain what a target is later on. Let’s go back to the method we used to hijack the object and create a subscriber list for each channel we created:

Vue.js

import Dep from "./Dep"; export default class Vue { constructor(option) { this._init(option); } } Vue.prototype._init = function (option) { const vm = this; vm.$el = document.querySelector(option.el); $data = option.data(); $data = option.data(); // hijks the data object vm.defineReactive(vm.$data); Const input = document.getelementById ("ceshi"); vm.$data.val; // Try to read the value input.addeventListener ("input", (e) => {vm.$data.val = e.t. value; }, false); }; Vue.prototype.defineReactive = function (obj) { if (! (obj instanceof Object)) return; For (let key in obj) {let val = obj[key]; // Assign const dep = new dep (); Object.defineproperty (obj, key, {enumerable: true, // Can enumerate freely: If true, / / a configurable the get () {the console. The log (` get: ${key} - ${val} `); dep.depend(); // Return val; }, set(newVal) { if (newVal === val) return; Console. log(' set: ${key} - ${newVal} '); val = newVal; dep.notify(); // Publish},}); }};Copy the code

Now that the channel has been created, it’s time to create an audience.

Create a viewer – Watcher

We create a file called watcher.js in the core folder to store our Watcher constructor.

Watcher.js

import Dep from './Dep'; export default class Watcher { constructor(obj, key, cb) { this._data = obj; // The data object this.key = key; // channel this.cb = cb; // The side effect function this.get(); } // Get () {dep. target = this; this._data[this.key]; Dep. Target = null; Update () {const newVal = this._data[this.key]; this.cb(newVal); }}Copy the code

If you look at the code above, you will see something, our old friend Target. The Watcher constructor is initialized by passing in the data object and the key attribute. The data[key] is our specific channel. Cb is our side effect function, which we call to update our homepage after the channel publishes content (only we know what the homepage looks like, so CB is provided by each subscriber). Next, I called the get method myself.

Let’s break down the logic of this get method. First, it assigns target, the static variable of the Dep constructor, to itself. What does this assignment mean in the past?

We go back to Dep and see that target is only used in the depend method.

When is depend called? Let’s go back to vue.js and find the time to call depend in the method that hijacks the data object, which is called when each channel’s get method is executed:

Now that the link is complete, let’s go back to the logic of the self-executing get function in Watcher:

  1. Dep.target = this: When creating a new Watcher, it internally calls a subscription channel method, which first stores itself (this) in dep. target.

  2. Enclosing _data while forming [this key] : The next call to the channel (i.e. This._data [this.key]) will trigger the object.defineProperty get method to notify the subscriber list to perform an automatic subscription because the channel is hijacked. Since dep. target already has this audience, it is actually added to the subscriber list by addSubs.

  1. Dep.target = null: To the final initializationDep.target.

So far, the publish-and-subscribe classes have been created and the links have been connected, so let’s write an example to verify this.

verify

Let’s start by creating a P tag in index.html and think of it as a watcher – viewer.

index.html

<! </title> <meta charset=" utF-8 "> <body> <div id="app"> <input type="text"  id="ceshi"> <p id="watcher"></p> </div> <script src="./src/main.js"></script> </body> </html>Copy the code

To improve the test method in vue.js:

Vue.js

import Dep from "./Dep"; import Watcher from "./Watcher"; export default class Vue { constructor(option) { this._init(option); } } Vue.prototype._init = function (option) { const vm = this; vm.$el = document.querySelector(option.el); $data = option.data(); $data = option.data(); // hijks the data object vm.defineReactive(vm.$data); Const input = document.getelementById ("ceshi"); input.addEventListener( "input", (e) => { vm.$data.val = e.target.value; }, false); Const watcher = document.getelementById ("watcher"); New Watcher(vm.$data, "val", (newVal) => {watcher.innerhtml = newVal; }); }; Vue.prototype.defineReactive = function (obj) { if (! (obj instanceof Object)) return; For (let key in obj) {let val = obj[key]; // Assign const dep = new dep (); Object.defineproperty (obj, key, {enumerable: true, // Can enumerate freely: If true, / / a configurable the get () {the console. The log (` get: ${key} - ${val} `); dep.depend(); // Return val; }, set(newVal) { if (newVal === val) return; Console. log(' set: ${key} - ${newVal} '); val = newVal; dep.notify(); // Publish},}); }};Copy the code

Ok, let’s try the final result here:

conclusion

So far, we have implemented a simple bi-directional data binding through a publish-subscribe pattern. But let’s think again, if there’s something missing. What about v-bind, {{}} double brackets? Don’t worry, here we implement a young Sunshine version, the next article will implement v-bind, {{}} double bracket template compilation, and listen for changes when the property is an object. I hope you can click “like” and “follow” if you see something here. Thank you for your support