The principle of vuejs to implement two-way binding: use data hijacking and publish-subscribe mode to hijack the setters and getters of each property through "Object.defineProperty()", publish messages to subscribers when the data changes, and trigger The corresponding listening callback is used to update the view.
The operating environment of this tutorial: windows7 system, vue2.9.6 version, DELL G3 computer.
Vue implements two-way data binding mainly using: data hijacking and publish-subscribe mode, using the Object.defineProperty()
method Perform data hijacking, and then notify the publisher (topic object) to notify all observers. After the observer receives the notification, the view will be updated.
https://jsrun.net/RMIKp/embedded/all/light
The MVVM framework mainly includes two aspects, data changes update the view, and view changes update the data.
View changes update data. If it is a label like input, you can use the oninput event..
Data changes update the view and you can use the set of Object.definProperty()
The method can detect data changes. When the data changes, this function will be triggered and the view will be updated.
We know how to implement two-way binding. First, we need to hijack and monitor the data, so we need to set up an Observer function to monitor changes in all properties.
If the attributes change, you need to tell the subscriber watcher to see if the data needs to be updated. If there are multiple subscribers, you need a Dep to collect these subscribers, and then between the listener observer and watcher unified management.
You also need a command parser, compile, to scan and parse the nodes and attributes that need to be monitored.
So, the process is roughly like this:
Implement a listener Observer to hijack and monitor all properties, and notify subscribers if changes occur.
Implement a subscriber Watcher. When receiving notification of property changes, execute the corresponding function, then update the view, and use Dep to collect these Watchers.
Implement a parser Compile, which is used to scan and parse the relevant instructions of the node, and initialize the corresponding subscriber according to the initialization template.
Observer is a data listener, and the core method is to use Object.defineProperty()
Add setter and getter methods to all properties recursively for monitoring.
var library = { book1: { name: "", }, book2: "", }; observe(library); library.book1.name = "vue权威指南"; // 属性name已经被监听了,现在值为:“vue权威指南” library.book2 = "没有此书籍"; // 属性book2已经被监听了,现在值为:“没有此书籍” // 为数据添加检测 function defineReactive(data, key, val) { observe(val); // 递归遍历所有子属性 let dep = new Dep(); // 新建一个dep Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { if (Dep.target) { // 判断是否需要添加订阅者,仅第一次需要添加,之后就不用了,详细看Watcher函数 dep.addSub(Dep.target); // 添加一个订阅者 } return val; }, set: function(newVal) { if (val == newVal) return; // 如果值未发生改变就return val = newVal; console.log( "属性" + key + "已经被监听了,现在值为:“" + newVal.toString() + "”" ); dep.notify(); // 如果数据发生变化,就通知所有的订阅者。 }, }); } // 监听对象的所有属性 function observe(data) { if (!data || typeof data !== "object") { return; // 如果不是对象就return } Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); } // Dep 负责收集订阅者,当属性发生变化时,触发更新函数。 function Dep() { this.subs = {}; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach((sub) => sub.update()); }, };
In the idea analysis, there needs to be a subscriber message subscriber Dep that can accommodate subscribers, which is used to collect subscribers and execute the corresponding update function when the attributes change.
From the code point of view, adding the subscriber Dep to the getter is to trigger the Watcher when it is initialized. Therefore, it is necessary to determine whether a subscriber is needed.
In the setter, if any data changes, all subscribers will be notified, and then the subscribers will update the corresponding functions.
At this point, a relatively complete Observer is completed. Next, start designing the Watcher.
The subscriber Watcher needs to add itself during initialization In the subscriber Dep, we already know that the Observer is a Watcher operation performed when getting, so we only need to trigger the corresponding get function when the Watcher is initialized to add the corresponding subscriber operation.
How to trigger get? Because we have already set Object.defineProperty(), we only need to get the corresponding property value to trigger it.
We only need to cache the subscriber on Dep.target when the subscriber Watcher is initialized, and remove it after the addition is successful.
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; // 缓存自己,用于判断是否添加watcher。 var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数 Dep.target = null; // 释放自己 return value; }, };
So far, the simple Watcher design is completed, and then by associating the Observer and Watcher, a simple two-way binding can be achieved.
Because the parser Compile has not been designed yet, the template data can be hard-coded first.
Convert the code into ES6 constructor writing method, preview and try.
https://jsrun.net/8SIKp/embedded/all/light
This code does not implement the compiler but directly passes in the bound variables. We only Set a data (name) on a node for binding, and then perform new MyVue on the page to achieve two-way binding.
And make value changes after two seconds. You can see that the page has also changed.
// MyVue proxyKeys(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; } }); }
The function of the above code is to proxy the key of this.data to this, so that I can easily use this.xx to get this.data.xx.
Although two-way data binding is implemented above, the entire process does not parse the DOM section store, but replaces it fixedly, so the next step is to implement a parser to do the data parsing and binding work.
Implementation steps of parser compile:
Parse the template instructions, replace the template data, and initialize the view.
将模板指定对应的节点绑定对应的更新函数,初始化相应的订阅器。
为了解析模板,首先需要解析 DOM 数据,然后对含有 DOM 元素上的对应指令进行处理,因此整个 DOM 操作较为频繁,可以新建一个 fragment 片段,将需要的解析的 DOM 存入 fragment 片段中在进行处理。
function nodeToFragment(el) { var fragment = document.createDocumentFragment(); var child = el.firstChild; while (child) { // 将Dom元素移入fragment中 fragment.appendChild(child); child = el.firstChild; } return fragment; }
接下来需要遍历各个节点,对含有相关指令和模板语法的节点进行特殊处理,先进行最简单模板语法处理,使用正则解析“{{变量}}”这种形式的语法。
function compileElement (el) { var childNodes = el.childNodes; var self = this; [].slice.call(childNodes).forEach(function(node) { var reg = /\{\{(.*)\}\}/; // 匹配{{xx}} 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; }
获取到最外层的节点后,调用 compileElement 函数,对所有的子节点进行判断,如果节点是文本节点切匹配{{}}这种形式的指令,则进行编译处理,初始化对应的参数。
然后需要对当前参数生成一个对应的更新函数订阅器,在数据发生变化时更新对应的 DOM。
这样就完成了解析、初始化、编译三个过程了。
接下来改造一个 myVue 就可以使用模板变量进行双向数据绑定了。
https://jsrun.net/K4IKp/embedded/all/light
添加完 compile 之后,一个数据双向绑定就基本完成了,接下来就是在 Compile 中添加更多指令的解析编译,比如 v-model、v-on、v-bind 等。
添加一个 v-model 和 v-on 解析:
function compile(node) { var nodeAttrs = node.attributes; var self = this; Array.prototype.forEach.call(nodeAttrs, function(attr) { var attrName = attr.name; if (isDirective(attrName)) { var exp = attr.value; var dir = attrName.substring(2); if (isEventDirective(dir)) { // 事件指令 self.compileEvent(node, self.vm, exp, dir); } else { // v-model 指令 self.compileModel(node, self.vm, exp, dir); } node.removeAttribute(attrName); // 解析完毕,移除属性 } }); } // v-指令解析 function isDirective(attr) { return attr.indexOf("v-") == 0; } // on: 指令解析 function isEventDirective(dir) { return dir.indexOf("on:") === 0; }
上面的 compile 函数是用于遍历当前 dom 的所有节点属性,然后判断属性是否是指令属性,如果是在做对应的处理(事件就去监听事件、数据就去监听数据..)
在 MyVue 中添加 mounted 方法,在所有操作都做完时执行。
class MyVue { constructor(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函数 } proxyKeys(key) { // 将this.data属性代理到this上 var self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get: function getter() { return self.data[key]; }, set: function setter(newVal) { self.data[key] = newVal; }, }); } }
然后就可以测试使用了。
https://jsrun.net/Y4IKp/embedded/all/light
总结一下流程,回头在哪看一遍这个图,是不是清楚很多了。
可以查看的代码地址:Vue2.x 的双向绑定原理及实现
相关推荐:《vue.js教程》
The above is the detailed content of What is the principle of implementing two-way binding in vuejs. For more information, please follow other related articles on the PHP Chinese website!