The content this article brings to you is about the detailed implementation of vue two-way binding. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.
Today’s front-end world is dominated by the three pillars of Angular, React, and Vue. If you don’t choose one camp, you basically cannot be based on the front-end. You may even have to choose two or three camps. This is the general trend.
So we must always remain curious and embrace changes. Only through constant changes can you be invincible. Being conservative can only wait for death.
I’ve been learning Vue recently, and I’ve only had a vague understanding of its two-way binding. In the past few days, I plan to study it in depth. After a few days of studying and consulting the information, I’ve gained some understanding of its principles. I know it, so I wrote a two-way binding example myself. Let’s see how to implement it step by step.
After reading this article, I believe you will have a clear understanding of Vue’s two-way binding principle. It can also help us understand Vue better.
Look at the renderings first
//代码: <div> <input> <h1>{{name}}</h1> </div> <script></script> <script></script> <script></script> <script></script> <script> const vm = new Mvue({ el: "#app", data: { name: "我是摩登" } }); </script>
Let’s talk about it before we officially start Regarding data binding, my understanding of data binding is to display the data M (model) to the view V (view). Our common architectural patterns include MVC, MVP, and MVVM patterns. Currently, front-end frameworks basically use the MVVM pattern to implement two-way binding, and Vue is no exception. However, each framework implements two-way binding in slightly different ways. Currently, there are roughly three implementation methods.
Publish and subscribe mode
Angular’s dirty checking mechanism
Data hijacking
Vue uses a combination of data hijacking and publishing and subscription to achieve two-way binding. Data hijacking is mainly achieved through Object.defineProperty
.
In this article we will not discuss the usage of Object.defineProperty in detail. We will mainly look at its storage properties get and set. Let's take a look at what happens after the object properties set through it.
var people = { name: "Modeng", age: 18 } people.age; //18 people.age = 20;
The above code is just to get/set the properties of the object, and no strange changes can be seen.
var modeng = {} var age; Object.defineProperty(modeng, 'age', { get: function () { console.log("获取年龄"); return age; }, set: function (newVal) { console.log("设置年龄"); age = newVal; } }); modeng.age = 18; console.log(modeng.age);
You will find that after the above operations, the get function will be automatically executed when we access the age attribute, and the set function will be automatically executed when setting the age attribute. This provides a very large opportunity for our two-way binding. convenient.
We know that the MVVM pattern lies in the synchronization of data and view, which means that the view will be automatically updated when the data changes, and the data will be updated when the view changes.
So what we need to do is how to detect changes in data and then notify us to update the view, and how to detect changes in the view and then update the data. Detecting views is relatively simple, it is nothing more than using event monitoring.
So how can we know that the data attributes have changed? This is using the Object.defineProperty we mentioned above. When our properties change, it will automatically trigger the set function to notify us to update the view.
Through the above description and analysis, we know that Vue implements two-way binding through data hijacking combined with the publish-subscribe model. of. We also know that data hijacking is through Object.defineProperty Method, when we know this, we need a listener Observer to listen for changes in properties. After knowing that the properties have changed, we need a Watcher Subscriber to update the view, we also need a compile directive parser to parse the directives of our node elements and initialize the view. So we need the following:
Observer Listener: Used to monitor changes in attributes to notify subscribers
Watcher Subscriber: Receive attributes Change, then update the view
Compile parser: parse instructions, initialize templates, bind subscribers
Follow this idea and we will implement it step by step.
The role of the listener is to monitor every property of the data. We also mentioned above that using the Object.defineProperty
method, when we listen to the property After changes occur, we need to notify Watcher subscribers to execute the update function to update the view. In this process, we may have many subscriber Watchers, so we need to create a container Dep for unified management.
function defineReactive(data, key, value) { //递归调用,监听所有属性 observer(value); var dep = new Dep(); Object.defineProperty(data, key, { get: function () { if (Dep.target) { dep.addSub(Dep.target); } return value; }, set: function (newVal) { if (value !== newVal) { value = newVal; dep.notify(); //通知订阅器 } } }); } function observer(data) { if (!data || typeof data !== "object") { return; } Object.keys(data).forEach(key => { defineReactive(data, key, data[key]); }); } function Dep() { this.subs = []; } Dep.prototype.addSub = function (sub) { this.subs.push(sub); } Dep.prototype.notify = function () { console.log('属性变化通知 Watcher 执行更新视图函数'); this.subs.forEach(sub => { sub.update(); }) } Dep.target = null;
Above we have created a listener Observer. Now we can try to add a listener to an object and change the properties.
var modeng = { age: 18 } observer(modeng); modeng.age = 20;
我们可以看到浏览器控制台打印出 “属性变化通知 Watcher 执行更新视图函数” 说明我们实现的监听器没毛病,既然监听器有了,我们就可以通知属性变化了,那肯定是需要 Watcher 的时候了。
Watcher 主要是接受属性变化的通知,然后去执行更新函数去更新视图,所以我们做的主要是有两步:
把 Watcher 添加到 Dep 容器中,这里我们用到了 监听器的 get 函数
接收到通知,执行更新函数。
function Watcher(vm, prop, callback) { this.vm = vm; this.prop = prop; this.callback = callback; this.value = this.get(); } Watcher.prototype = { update: function () { const value = this.vm.$data[this.prop]; const oldVal = this.value; if (value !== oldVal) { this.value = value; this.callback(value); } }, get: function () { Dep.target = this; //储存订阅器 const value = this.vm.$data[this.prop]; //因为属性被监听,这一步会执行监听器里的 get方法 Dep.target = null; return value; } }
这一步我们把 Watcher 也给弄了出来,到这一步我们已经实现了一个简单的双向绑定了,我们可以尝试把两者结合起来看下效果。
function Mvue(options, prop) { this.$options = options; this.$data = options.data; this.$prop = prop; this.$el = document.querySelector(options.el); this.init(); } Mvue.prototype.init = function () { observer(this.$data); this.$el.textContent = this.$data[this.$prop]; new Watcher(this, this.$prop, value => { this.$el.textContent = value; }); }
这里我们尝试利用一个实例来把数据与需要监听的属性传递进来,通过监听器监听数据,然后添加属性订阅,绑定更新函数。
<p>{{name}}</p> const vm = new Mvue({ el: "#app", data: { name: "我是摩登" } }, "name");
我们可以看到数据已经正常的显示在页面上,那么我们在通过控制台去修改数据,发生变化后视图也会跟着修改。
到这一步我们我们基本上已经实现了一个简单的双向绑定,但是不难发现我们这里的属性都是写死的,也没有指令模板的解析,所以下一步我们来实现一个模板解析器。
Compile 的主要作用一个是用来解析指令初始化模板,一个是用来添加添加订阅者,绑定更新函数。
因为在解析 DOM 节点的过程中我们会频繁的操作 DOM, 所以我们利用文档片段(DocumentFragment)来帮助我们去解析 DOM 优化性能。
function Compile(vm) { this.vm = vm; this.el = vm.$el; this.fragment = null; this.init(); } Compile.prototype = { init: function () { this.fragment = this.nodeFragment(this.el); }, nodeFragment: function (el) { const fragment = document.createDocumentFragment(); let child = el.firstChild; //将子节点,全部移动文档片段里 while (child) { fragment.appendChild(child); child = el.firstChild; } return fragment; } }
然后我们就需要对整个节点和指令进行处理编译,根据不同的节点去调用不同的渲染函数,绑定更新函数,编译完成之后,再把 DOM 片段添加到页面中。
Compile.prototype = { compileNode: function (fragment) { let childNodes = fragment.childNodes; [...childNodes].forEach(node => { let reg = /\{\{(.*)\}\}/; let text = node.textContent; if (this.isElementNode(node)) { this.compile(node); //渲染指令模板 } else if (this.isTextNode(node) && reg.test(text)) { let prop = RegExp.$1; this.compileText(node, prop); //渲染{{}} 模板 } //递归编译子节点 if (node.childNodes && node.childNodes.length) { this.compileNode(node); } }); }, compile: function (node) { let nodeAttrs = node.attributes; [...nodeAttrs].forEach(attr => { let name = attr.name; if (this.isDirective(name)) { let value = attr.value; if (name === "v-model") { this.compileModel(node, value); } node.removeAttribute(name); } }); }, //省略。。。 }
因为代码比较长如果全部贴出来会影响阅读,我们主要是讲整个过程实现的思路,文章结束我会把源码发出来,有兴趣的可以去查看全部代码。
到这里我们的整个的模板编译也已经完成,不过这里我们并没有实现过多的指令,我们只是简单的实现了 v-model
指令,本意是通过这篇文章让大家熟悉与认识 Vue 的双向绑定原理,并不是去创造一个新的 MVVM 实例。所以并没有考虑很多细节与设计。
现在我们实现了 Observer、Watcher、Compile,接下来就是把三者给组织起来,成为一个完整的 MVVM。
这里我们创建一个 Mvue 的类(构造函数)用来承载 Observer、Watcher、Compile 三者。
function Mvue(options) { this.$options = options; this.$data = options.data; this.$el = document.querySelector(options.el); this.init(); } Mvue.prototype.init = function () { observer(this.$data); new Compile(this); }
然后我们就去测试一下结果,看看我们实现的 Mvue 是不是真的可以运行。
<p> </p><h1>{{name}}</h1> <script></script> <script></script> <script></script> <script></script> <script> const vm = new Mvue({ el: "#app", data: { name: "完全没问题,看起来是不是很酷!" } }); </script>
我们尝试去修改数据,也完全没问题,但是有个问题就是我们修改数据时时通过 vm.$data.name 去修改数据,而不是想 Vue 中直接用 vm.name 就可以去修改,那这个是怎么做到的呢?其实很简单,Vue 做了一步数据代理操作。
我们来改造下 Mvue 添加数据代理功能,我们也是利用 Object.defineProperty
方法进行一步中间的转换操作,间接的去访问。
function Mvue(options) { this.$options = options; this.$data = options.data; this.$el = document.querySelector(options.el); //数据代理 Object.keys(this.$data).forEach(key => { this.proxyData(key); }); this.init(); } Mvue.prototype.init = function () { observer(this.$data); new Compile(this); } Mvue.prototype.proxyData = function (key) { Object.defineProperty(this, key, { get: function () { return this.$data[key] }, set: function (value) { this.$data[key] = value; } }); }
到这里我们就可以像 Vue 一样去修改我们的属性了,非常完美。完全自己动手实现,你也来试试把,体验下自己动手写代码的乐趣。
本文主要是对 Vue 双向绑定原理的学习与实现。
主要是对整个思路的学习,并没有考虑到太多的实现与设计的细节,所以还存在很多问题,并不完美。
源码地址,整个过程的全部代码,希望对你有所帮助。
The above is the detailed content of Take you to implement Vue two-way binding in detail. For more information, please follow other related articles on the PHP Chinese website!