Vue source depth understanding of two-way data binding principle vue

We all know vue is a MVVM development model, data-driven view of the front frame and the interior has been achieved two-way data binding, then the two-way data binding is how to achieve it?

First line and a manual by far the most simple two-way data binding

 1     <div>
 2         <input type="text" name="" id="text">
 3         <span id="show"></span>
 4     </div>
 5 
 6     <script>
 7         var text = document.getElementById('text')
 8         var span = document.getElementById('show')
 9         var obj = {}
10         Object.defineProperty(obj, 'hello', {
11             set: function(value){
12                 text.value = value
13                 span.innerText = value
14             }
15         })
16         document.addEventListener('keyup',function(e){
17             obj.hello = e.target.value
18         })
19     </script>

So that it can achieve a simple two-way data binding, and then explain what defineProperty this method it here.

grammar:

Object.defineProperty (obj, prop, descriptor) 
/ *
  of parameters:
    obj: To define the properties of the object on which the
    prop: To define or modify the name of the attribute
    descriptor: To define or modify the attribute descriptor
* /

prop is an access attributes, access attributes is a special attribute of the object, it must be defined solely by the method Object.defineProperty

Special value read or set accessor property accessor property value is actually invoke its internal characteristics of the get and set functions

Then the internal vue how to achieve two-way data binding it?

vue two-way data binding principle

vue two-way data binding is hijacked by a combination of data publisher - subscriber mode to achieve the way, we can output a first look at the definition of an object on the console vue initialization data is something.

Code:

Copy the code
var vm = new Vue({
    data: {
        obj: {
            a: 1
        }
    },
    created: function () {
        console.log(this.obj);
    }
});
Copy the code

result:

We can see the properties of a two corresponding get and set methods, why these two methods will be more of it? Since hijacking vue data is achieved by Object.defineProperty ().

Object.defineProperty () is used to do? It is possible to control the properties of an object of some unique operations, such as read and write access, can enumerate, here we'll examine two describe the property get and set its corresponding lower, if not familiar with its use, please click here read more usage .

In common, we can easily print out the attribute data of an object:

Book = {var 
  name: 'Definitive Guide VUE' 
}; 
the console.log (Book.name); // Definitive Guide VUE

If you want to execute at the same time console.log (book.name), and add a title directly to the title number, it is up to how to deal with it? Or what monitor property values ​​of an object Book to pass. This time Object.defineProperty () comes in handy, as follows:

Copy the code
Book} = {var 
var name = ''; 
Object.defineProperty (Book, 'name', { 
  SET: function (value) { 
    name = value; 
    the console.log ( 'you take a title called a' + value); 
  }, 
  GET: function () { 
    return ' "' + name + '"' 
  } 
}) 
 
Book.name = 'Definitive Guide vue'; // you take a title called the Definitive Guide vue 
console.log (Book.name ); // "vue Definitive Guide"
Copy the code

We set by Object.defineProperty () the name attribute of the object Book, rewriting its operations and get set, as the name suggests, is to get the reading function name attribute value trigger, set the name attribute is set in the value of the trigger function, so when performing Book.name = 'vue Definitive Guide' this statement, the console will print out "you took a vue of the book is the Definitive Guide," Then, when reading this property, it will output " "vue Definitive Guide", "because we do get processed in the value of the function inside. If at this time we perform the following statement, the console will output what?

console.log(Book);

result:

At first glance, is not with us in the top vue print data looks somewhat similar, indicating vue indeed for data hijacked by this method. Next we come to realize a simple version of the mvvm two-way binding code by its principles.

Ideas analysis

Mvvm achieved mainly consists of two aspects, the data update changes the view, the view changes to update the data:

The key point is how to update the data view, because the view is actually updated data can listen to through events such as input label listen 'input' events can be achieved. So we have to analyze focus, when the data changes, how to update the view.

How to update the view of data focus is to know the data has changed, just know that the data has changed, then the next things are a good deal. How do I know the data has changed, in fact, we have been given above, the answer is through Object.defineProperty () function to set a property is set, when the data changes so we just need to update some of the methods will be to trigger this function, on the inside it can achieve a data update view.

With ideas, it follows that the implementation process.

Implementation process

We already know that two-way data binding, we must first listen to hijack the data, so we need to set up a listener Observer, used to listen to all properties. If the property hair has changed, we need to tell subscribers Watcher to see if needs to be updated. Because there are a number of subscribers, so we need to have a special message subscriber Dep to collect subscribers, and unified management between the listener and subscriber Watcher's Observer. Subsequently, we also need to have the Compile a parser instruction, scanning and parsing of each node element corresponding to the relevant instruction Watcher initialized to a subscriber, and replace the template data corresponding binding or function, then when the subscriber Watcher receiving a respective attribute change, it will perform a corresponding update function, so as to update the view. So it follows that we perform the following three steps, two-way data binding:

1. Implement a listener Observer, hijacked and used to listen to all properties, if there are changes, you notify subscribers.

2. Implement a subscriber Watcher, you can receive notification of change in the property and perform the corresponding function, thereby updating the view.

3. To achieve the Compile a parser, you can scan and parse each node associated instructions, and template according to the initialization data and the corresponding subscriber initialization.

流程图如下:

1.实现一个Observer

Observer是一个数据监听器,其实现核心方法就是前文所说的Object.defineProperty( )。如果要对所有属性都进行监听的话,那么可以通过递归方法遍历所有属性值,并对其进行Object.defineProperty( )处理。如下代码,实现了一个Observer。

Copy the code
function defineReactive(data, key, val) {
    observe(val); // 递归遍历所有子属性
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            return val;
        },
        set: function(newVal) {
            val = newVal;
            console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
        }
    });
}
 
function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};
 
var library = {
    book1: {
        name: ''
    },
    book2: ''
};
observe(library);
library.book1.name = 'vue权威指南'; // 属性name已经被监听了,现在值为:“vue权威指南”
library.book2 = '没有此书籍';  // 属性book2已经被监听了,现在值为:“没有此书籍”
Copy the code

思路分析中,需要创建一个可以容纳订阅者的消息订阅器Dep,订阅器Dep主要负责收集订阅者,然后再属性变化的时候执行对应订阅者的更新函数。所以显然订阅器需要有一个容器,这个容器就是list,将上面的Observer稍微改造下,植入消息订阅器:

Copy the code
function defineReactive(data, key, val) {
    observe(val); // 递归遍历所有子属性
    var dep = new Dep(); 
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            if (是否需要添加订阅者) {
                dep.addSub(watcher); // 在这里添加一个订阅者
            }
            return val;
        },
        set: function(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
            dep.notify(); // 如果数据变化,通知所有订阅者
        }
    });
}
 
function Dep () {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
Copy the code

从代码上看,我们将订阅器Dep添加一个订阅者设计在getter里面,这是为了让Watcher初始化进行触发,因此需要判断是否要添加订阅者,至于具体设计方案,下文会详细说明的。在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。到此为止,一个比较完整Observer已经实现了,接下来我们开始设计Watcher。

2.实现Watcher

订阅者Watcher在初始化的时候需要将自己添加进订阅器Dep中,那该如何添加呢?我们已经知道监听器Observer是在get函数执行了添加订阅者Wather的操作的,所以我们只要在订阅者Watcher初始化的时候触发对应的get函数去执行添加订阅者操作即可,那要如何触发get的函数,再简单不过了,只要获取对应的属性值就可以触发了,核心原因就是因为我们使用了Object.defineProperty( )进行数据监听。这里还有一个细节点需要处理,我们只要在订阅者Watcher初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在Dep.target上缓存下订阅者,添加成功后再将其去掉就可以了。订阅者Watcher的实现如下:

Copy the code
function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    this.value = this.get();  // 将自己添加到订阅器的操作
}
 
Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        Dep.target = this;  // 缓存自己
        var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        Dep.target = null;  // 释放自己
        return value;
    }
};
Copy the code

这时候,我们需要对监听器Observer也做个稍微调整,主要是对应Watcher类原型上的get函数。需要调整地方在于defineReactive函数:

Copy the code
function defineReactive(data, key, val) {
    observe(val); // 递归遍历所有子属性
    var dep = new Dep(); 
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            if (Dep.target) {.  // 判断是否需要添加订阅者
                dep.addSub(Dep.target); // 在这里添加一个订阅者
            }
            return val;
        },
        set: function(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
            dep.notify(); // 如果数据变化,通知所有订阅者
        }
    });
}
Dep.target = null;
Copy the code

到此为止,简单版的Watcher设计完毕,这时候我们只要将Observer和Watcher关联起来,就可以实现一个简单的双向绑定数据了。因为这里没有还没有设计解析器Compile,所以对于模板数据我们都进行写死处理,假设模板上又一个节点,且id号为'name',并且双向绑定的绑定的变量也为'name',且是通过两个大双括号包起来(这里只是为了演示,暂时没什么用处),模板如下:

<body>
    <h1 id="name">{{name}}</h1>
</body>

这时候我们需要将Observer和Watcher关联起来:

Copy the code
function SelfVue (data, el, exp) {
    this.data = data;
    observe(data);
    el.innerHTML = this.data[exp];  // 初始化模板数据的值
    new Watcher(this, exp, function (value) {
        el.innerHTML = value;
    });
    return this;
}
Copy the code

然后在页面上new以下SelfVue类,就可以实现数据的双向绑定了:

Copy the code
<body>
    <h1 id="name">{{name}}</h1>
</body>
<script src="js/observer.js"></script>
<script src="js/watcher.js"></script>
<script src="js/index.js"></script>
<script type="text/javascript">
    var ele = document.querySelector('#name');
    var selfVue = new SelfVue({
        name: 'hello world'
    }, ele, 'name');
 
    window.setTimeout(function () {
        console.log('name值改变了');
        selfVue.data.name = 'canfoo';
    }, 2000);
 
</script>
Copy the code

这时候打开页面,可以看到页面刚开始显示了是'hello world',过了2s后就变成'canfoo'了。到这里,总算大功告成一半了,但是还有一个细节问题,我们在赋值的时候是这样的形式 '  selfVue.data.name = 'canfoo'  ' 而我们理想的形式是'  selfVue.name = 'canfoo'  '为了实现这样的形式,我们需要在new SelfVue的时候做一个代理处理,让访问selfVue的属性代理为访问selfVue.data的属性,实现原理还是使用Object.defineProperty( )对属性值再包一层:

Copy the code
function SelfVue (data, el, exp) {
    var self = this;
    this.data = data;
 
    Object.keys(data).forEach(function(key) {
        self.proxyKeys(key);  // 绑定代理属性
    });
 
    observe(data);
    el.innerHTML = this.data[exp];  // 初始化模板数据的值
    new Watcher(this, exp, function (value) {
        el.innerHTML = value;
    });
    return this;
}
 
SelfVue.prototype = {
    proxyKeys: function (key) {
        var self = this;
        Object.defineProperty(this, key, {
            enumerable: false,
            configurable: true,
            get: function proxyGetter() {
                return self.data[key];
            },
            set: function proxySetter(newVal) {
                self.data[key] = newVal;
            }
        });
    }
}
Copy the code

这下我们就可以直接通过'  selfVue.name = 'canfoo'  '的形式来进行改变模板数据了。如果想要迫切看到现象的童鞋赶快来获取代码!

3.实现Compile

虽然上面已经实现了一个双向数据绑定的例子,但是整个过程都没有去解析dom节点,而是直接固定某个节点进行替换数据的,所以接下来需要实现一个解析器Compile来做解析和绑定工作。解析器Compile实现步骤:

1.解析模板指令,并替换模板数据,初始化视图

2.将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器

为了解析模板,首先需要获取到dom元素,然后对含有dom元素上含有指令的节点进行处理,因此这个环节需要对dom操作比较频繁,所有可以先建一个fragment片段,将需要解析的dom节点存入fragment片段里再进行处理:

Copy the code
function nodeToFragment (el) {
    var fragment = document.createDocumentFragment();
    var child = el.firstChild;
    while (child) {
        // 将Dom元素移入fragment中
        fragment.appendChild(child);
        child = el.firstChild
    }
    return fragment;
}
Copy the code

接下来需要遍历各个节点,对含有相关指定的节点进行特殊处理,这里咱们先处理最简单的情况,只对带有 '{{变量}}' 这种形式的指令进行处理,先简道难嘛,后面再考虑更多指令情况:

Copy the code
function compileElement (el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
        var reg = /\{\{(.*)\}\}/;
        var text = node.textContent;
 
        if (self.isTextNode(node) && reg.test(text)) {  // 判断是否是符合这种形式{{}}的指令
            self.compileText(node, reg.exec(text)[1]);
        }
 
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);  // 继续递归遍历子节点
        }
    });
},
function compileText (node, exp) {
    var self = this;
    var initText = this.vm[exp];
    updateText(node, initText);  // 将初始化的数据初始化到视图中
    new Watcher(this.vm, exp, function (value) {  // 生成订阅器并绑定更新函数
        self.updateText(node, value);
    });
},
function updateText (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
}
Copy the code

获取到最外层节点后,调用compileElement函数,对所有子节点进行判断,如果节点是文本节点且匹配{{}}这种形式指令的节点就开始进行编译处理,编译处理首先需要初始化视图数据,对应上面所说的步骤1,接下去需要生成一个并绑定更新函数的订阅器,对应上面所说的步骤2。这样就完成指令的解析、初始化、编译三个过程,一个解析器Compile也就可以正常的工作了。为了将解析器Compile与监听器Observer和订阅者Watcher关联起来,我们需要再修改一下类SelfVue函数:

Copy the code
function SelfVue (options) {
    var self = this;
    this.vm = this;
    this.data = options;
 
    Object.keys(this.data).forEach(function(key) {
        self.proxyKeys(key);
    });
 
    observe(this.data);
    new Compile(options, this.vm);
    return this;
}
Copy the code
更改后,我们就不要像之前通过传入固定的元素值进行双向绑定了,可以随便命名各种变量进行双向绑定了:
Copy the code
<body>
    <div id="app">
        <h2>{{title}}</h2>
        <h1>{{name}}</h1>
    </div>
</body>
<script src="js/observer.js"></script>
<script src="js/watcher.js"></script>
<script src="js/compile.js"></script>
<script src="js/index.js"></script>
<script type="text/javascript">
 
    var selfVue = new SelfVue({
        el: '#app',
        data: {
            title: 'hello world',
            name: ''
        }
    });
 
    window.setTimeout(function () {
        selfVue.title = '你好';
    }, 2000);
 
    window.setTimeout(function () {
        selfVue.name = 'canfoo';
    }, 2500);
 
</script>
Copy the code

如上代码,在页面上可观察到,刚开始titile和name分别被初始化为 'hello world' 和空,2s后title被替换成 '你好' 3s后name被替换成 'canfoo' 了。废话不多说,再给你们来一个这个版本的代码(v2),获取代码!

到这里,一个数据双向绑定功能已经基本完成了,接下去就是需要完善更多指令的解析编译,在哪里进行更多指令的处理呢?答案很明显,只要在上文说的compileElement函数加上对其他指令节点进行判断,然后遍历其所有属性,看是否有匹配的指令的属性,如果有的话,就对其进行解析编译。这里我们再添加一个v-model指令和事件指令的解析编译,对于这些节点我们使用函数compile进行解析处理:

Copy the code
function compile (node) {
    var nodeAttrs = node.attributes;
    var self = this;
    Array.prototype.forEach.call(nodeAttrs, function(attr) {
        var attrName = attr.name;
        if (self.isDirective(attrName)) {
            var exp = attr.value;
            var dir = attrName.substring(2);
            if (self.isEventDirective(dir)) {  // 事件指令
                self.compileEvent(node, self.vm, exp, dir);
            } else {  // v-model 指令
                self.compileModel(node, self.vm, exp, dir);
            }
            node.removeAttribute(attrName);
        }
    });
}
Copy the code

上面的compile函数是挂载Compile原型上的,它首先遍历所有节点属性,然后再判断属性是否是指令属性,如果是的话再区分是哪种指令,再进行相应的处理,处理方法相对来说比较简单,这里就不再列出来,想要马上看阅读代码的同学可以马上点击这里获取。

最后我们在稍微改造下类SelfVue,使它更像vue的用法:

Copy the code
function SelfVue (options) {
    var self = this;
    this.data = options.data;
    this.methods = options.methods;
 
    Object.keys(this.data).forEach(function(key) {
        self.proxyKeys(key);
    });
 
    observe(this.data);
    new Compile(options.el, this);
    options.mounted.call(this); // 所有事情处理好后执行mounted函数
}
Copy the code

这时候我们可以来真正测试了,在页面上设置如下东西:

Copy the code
<body>
    <div id="app">
        <h2>{{title}}</h2>
        <input v-model="name">
        <h1>{{name}}</h1>
        <button v-on:click="clickMe">click me!</button>
    </div>
</body>
<script src="js/observer.js"></script>
<script src="js/watcher.js"></script>
<script src="js/compile.js"></script>
<script src="js/index.js"></script>
<script type="text/javascript">
 
     new SelfVue({
        el: '#app',
        data: {
            title: 'hello world',
            name: 'canfoo'
        },
        methods: {
            clickMe: function () {
                this.title = 'hello world';
            }
        },
        mounted: function () {
            window.setTimeout(() => {
                this.title = '你好';
            }, 1000);
        }
    });
 
</script>
Copy the code

 

Guess you like

Origin www.cnblogs.com/songyao666/p/11494923.html