Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

The two-way data binding principle of vue2.0 is implemented manually


May 30, 2021 Article blog


Table of contents


In a word: Object.defineProperty plus publish subscription mode

Bidirectional data binding has three core modules (dep, observer, watcher), how they are connected to each other, here's an introduction. In order to better understand the principle of two-way data binding and how they are related, first lead you to review the publishing subscription model.

One. Let's first understand what a publishing subscription pattern is

Direct code: A simple publishing subscription model to help you better understand the two-way data binding principle

//发布订阅模式
function Dep() {
    this.subs = []//收集依赖(也就是手机watcher实例),
}
Dep.prototype.addSub = function (sub) { //添加订阅者
    this.subs.push(sub); //实际上添加的是watcher这个实例
}
Dep.prototype.notify = function (sub) { //发布,这个方法的作用是遍历数组,让每个订阅者的update方法去执行
    this.subs.forEach((sub) => sub.update())
}

function Watcher(fn) {
    this.fn = fn;
}
Watcher.prototype.update = function () { //添加一个update属性让每一个实例都可以继承这个方法
    this.fn();
}
let watcher = new Watcher(function () {
    alert(1)
});//订阅
let dep = new Dep();
dep.addSub(watcher);//添加依赖,添加订阅者
dep.notify();//发布,让每个订阅者的update方法执行

What did I do when I was new Vue()?

Just for two-way data binding

<template>
      <div id="app">
        <div>obj.text的值:{{obj.text}}</div>
        <p>word的值:{{word}}</p>
        <input type="text" v-model="word">
    </div>
</template>
<script>
   new Vue({
        el: "#app",
        data: {
            obj: {
                text: "向上",
            },
            word: "学习"
        },
        methods:{
        //  ...
        }
    })
</script>

What did the Vue constructor do?

function Vue(options = {}) {
    this.$options = options;//接收参数
    var data = this._data = this.$options.data;
    observer(data);//对data中的数据进型循环递归绑定
    for (let key in data) {
       let val = data[key];
       observer(val);
       Object.defineProperty(this, key, {
           enumerable: true,
           get() {
               return this._data[key];
           },
           set(newVal) {
               this._data[key] = newVal;
           }
        })
    }
    new Compile(options.el, this)
};

In new Vue ('...') W hen constructing a function, first gets the argument options, and then assigns the data data in the argument to the _data property of the current instance (this._data . . . this.$options.data), the point is, so what is the following traversal for? First of all, when we manipulate the data is this.word get, not this._data.word, so we did a mapping, when we get the data this.word, is actually getting the value of this._data.word, we can output this in their own project to see

 The two-way data binding principle of vue2.0 is implemented manually1

1. Let's see what the observer method does

function observer(data) {
    if (typeof data !== "object") return;
    return new Observer(data);//返回一个实例
}
function Observer(data) {
    let dep = new Dep();//创建一个dep实例
    for (let key in data) {//对数据进行循环递归绑定
        let val = data[key];
        observer(val);
        Object.defineProperty(data, key, {
            enumerable: true,
            get() {
               Dep.target && dep.depend(Dep.target);//Dep.target就是Watcher的一个实例
                return val;
            },
            set(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                observer(newVal);
                dep.notify() //让所有方法执行
            }
        })
    }
}

The Observer constructor, first let dep-new Dep(), as the subsequent get and set methods that trigger data hijacking, collects dependencies and release-time calls, the main operation being to loop the data data recursively through Object.defineProperty, modifying its default read and write using getter/setter to collect dependencies and publish updates.

2. Let's take a look at what Compile did

function Compile(el, vm) {
    vm.$el = document.querySelector(el);
    let fragment = document.createDocumentFragment(); //创建文档碎片,是object类型
    while (child = vm.$el.firstChild) {
        fragment.appendChild(child);
    };//用while循环把所有节点都添加到文档碎片中,之后都是对文档碎片的操作,最后再把文档碎片添加到页面中,这里有一个很重要的特性是,如果使用appendChid方法将原dom树中的节点添加到fragment中时,会删除原来的节点。
    replace(fragment);

    function replace(fragment) {
        Array.from(fragment.childNodes).forEach((node) => {//循环所有的节点
            let text = node.textContent;
            let reg = /\{\{(.*)\}\}/;
            if (node.nodeType === 3 && reg.test(text)) {//判断当前节点是不是文本节点且符不符合{{obj.text}}的输出方式,如果满足条件说明它是双向的数据绑定,要添加订阅者(watcher)
                console.log(RegExp.$1); //obj.text
                let arr = RegExp.$1.split("."); //转换成数组的方式[obj,text],方便取值
                let val = vm;
                arr.forEach((key) => { //实现取值this.obj.text
                    val = val[key];
                });
                new Watcher(vm, RegExp.$1, function (newVal) {
                    node.textContent = text.replace(/\{\{(.*)\}\}/, newVal)
                });
                node.textContent = text.replace(/\{\{(.*)\}\}/, val); //对节点内容进行初始化的赋值
            }
            if (node.nodeType === 1) { //说明是元素节点
                let nodeAttrs = node.attributes;
                Array.from(nodeAttrs).forEach((item) => {
                    if (item.name.indexOf("v-") >= 0) {//判断是不是v-model这种指令
                        node.value = vm[item.value]//对节点赋值操作
                    }
                    //添加订阅者
                    new Watcher(vm, item.value, function (newVal) {
                        node.value = vm[item.value]
                    });
                    node.addEventListener("input", function (e) {
                        let newVal = e.target.value;
                        vm[item.value] = newVal;
                    })
                })
            }
            if (node.childNodes) { //这个节点里还有子元素,再递归
                replace(node);
            }
        })
    }

    //这是页面中的文档已经没有了,所以还要把文档碎片放到页面中
    vm.$el.appendChild(fragment);
}

Compile first explains that DocuemntFragment (document fragmentation) is a dom node collection container, when you create multiple nodes, when each node is inserted into the document will trigger a backflow, that is, the browser to return multiple times, very performance-intensive, and the use of document fragmentation is to put multiple nodes into a container, and finally insert the entire container directly on it, the browser only once.
The Compile method first traverses all nodes of the document fragmentation.
1. To determine whether it is a text node and the character does not conform to the output of the double braces of the .
2. Determine whether it is an element node and whether the property contains a directive such as v-model, if the conditions are met that it is a two-way data binding, to add subscribers (watcher), new Watcher (vm, dynamically bound variable, callback function fn) until the traversal is complete. Finally, don't forget to put the document fragments on the page

3. Dep constructor (how to collect dependencies)

var uid=0;
//发布订阅
function Dep() {
    this.id=uid++;
    this.subs = [];
}
Dep.prototype.addSub = function (sub) { //订阅
    this.subs.push(sub); //实际上添加的是watcher这个实例
}
Dep.prototype.depend = function () { // 订阅管理器
    if(Dep.target){//只有Dep.target存在时采取添加
        Dep.target.addDep(this);
    }
}
Dep.prototype.notify = function (sub) { //发布,遍历数组让每个订阅者的update方法去执行
    this.subs.forEach((sub) => sub.update())
}

Inside the Dep constructor there is an id and a subs, id-uid, which is used as a unique identity for the dep object, which is the array that holds the watcher. T he method is a subscription manager that calls the addDep method of the current watcher to add subscribers, and when the get method of Object.defineProperty is triggered, dep.target.dep.target is called to add subscribers. The dep.notify method update operation is called when the set method of Object.defineProperty is triggered when the data changes.

4. What did the Watcher constructor do?

function Watcher(vm, exp, fn) {
   this.fn = fn;
   this.vm = vm;
   this.exp = exp //
   this.newDeps = [];
   this.depIds = new Set();
   this.newDepIds = new Set();
   Dep.target = this; //this是指向当前(Watcher)的一个实例
   let val = vm;
   let arr = exp.split(".");
   arr.forEach((k) => { //取值this.obj.text
       val = val[k] //取值this.obj.text,就会触发数据劫持的get方法,把当前的订阅者(watcher实例)添加到依赖中
   });
   Dep.target = null;
}
Watcher.prototype.addDep = function (dep) {
   var id=dep.id;
   if(!this.newDepIds.has(id)){
       this.newDepIds.add(id);
       this.newDeps.push(dep);
       if(!this.depIds.has(id)){
           dep.addSub(this);
       }
   }
  
}
Watcher.prototype.update = function () { //这就是每个绑定的方法都添加一个update属性
   let val = this.vm;
   let arr = this.exp.split(".");
   arr.forEach((k) => { 
       val = val[k] //取值this.obj.text,传给fn更新操作
   });
   this.fn(val); //传一个新值
}

What did the Watcher constructor do?
1. Receive parameters that define several private properties (this.newDep, this.depIds, this.newDepIds)
2. Dep.target s this, data value-taking by parameter, which triggers Object.defineProperty's get method, which adds subscribers through the Subscriber Manager (dep.depend()) and then emptys Dep.target-null after adding;
3. AddDep on the prototype is a unique identity of id, and the judgment of several private properties prevents subscribers from being repeatedly added multiple times
4.update method is when the data is updated, dep.notify() executes, triggers the subscriber update this method, performs the release update operation.

Summarize the two-way data binding in vue 2.0, in short, Observer, Watcher, Dep three major parts;
1. First of all, use Object.defineProperty() loop recursive implementation of data hijacking, each property is assigned a subscriber collection of management array dep;
2. At compile time, create document fragmentation, add all nodes to document fragmentation, traverse all nodes of document fragmentation, and add the instance to dep's subs array if it's a new Watcher() instance
3. The final modification of the value triggers object.defineProperty() of the set method, dep.notify() is executed in the set method, and then the update method of all subscribers is called in a loop to update the view.