本文适合于学习Vue源码的初级学者,阅读后,你将对Vue的数据双向绑定原理有一个大致的了解,认识Observer、Compile、Wathcer三大角色(如下图所示)以及它们所发挥的功能。
本文将一步步带你实现简易版的数据双向绑定,每一步都会详细分析这一步要解决的问题以及代码为何如此写,因此,在阅读完本文后,希望你能自己动手实现一个简易版数据双向绑定。
2. 代码实现2.1 目的分析
本文要实现的效果如下图所示:
本文用到的HTML和JS主体代码如下:
<div> <h1 v-text="msg"></h1> <input type="text" v-model="msg"> <div> <h1 v-text="msg2"></h1> <input type="text" v-model="msg2"> </div> </div>
let vm = new Vue({ el: "#app", data: { msg: "hello world", msg2: "hello xiaofei" } })
我们将按照下面三个步骤来实现:
第一步:将data中的数据同步到页面上,实现 M ==> V 的初始化;
第二步:当input框中输入值时,将新值同步到data中,实现 V ==> M 的绑定;
第三步:当data数据发生更新的时候,触发页面发生变化,实现 M ==> V 的绑定。
2.2.1 入口代码
首先,我们要创造一个Vue类,这个类接收一个 options 对象,同时,我们要对 options 对象中的有效信息进行保存;
然后,我们有三个主要模块:Observer、Compile、Wathcer,其中,Observer用来数据劫持的,Compile用来解析元素,Wathcer是观察者。可以写出如下代码:(Observer、Compile、Wathcer这三个概念,不用细究,后面会详解讲解)。
class Vue { // 接收传进来的对象 constructor(options) { // 保存有效信息 this.$el = document.querySelector(options.el); this.$data = options.data; // 容器: {属性1: [wathcer1, wathcer2...], 属性2: [...]},用来存放每个属性观察者 this.$watcher = {}; // 解析元素: 实现Compile this.compile(this.$el); // 要解析元素, 就得把元素传进去 // 劫持数据: 实现 Observer this.observe(this.$data); // 要劫持数据, 就得把数据传入 } compile() {} observe() {} }
2.2.2 页面初始化在这一步,我们要实现页面的初始化,即解析出v-text和v-model指令,并将data中的数据渲染到页面中。
这一步的关键在于实现compile方法,那么该如何解析el元素呢?思路如下:
首先要获取到el下面的所有子节点,然后遍历这些子节点,如果子节点还有子节点,那我们就需要用到递归的思想;
遍历子节点找到所有有指令的元素,并将对应的数据渲染到页面中。
代码如下:(主要看compile那部分)
class Vue { // 接收传进来的对象 constructor(options) { // 获取有用信息 this.$el = document.querySelector(options.el); this.$data = options.data; // 容器: {属性1: [wathcer1, wathcer2...], 属性2: [...]} this.$watcher = {}; // 2. 解析元素: 实现Compile this.compile(this.$el); // 要解析元素, 就得把元素传进去 // 3. 劫持数据: 实现 Observer this.observe(this.$data); // 要劫持数据, 就得把数据传入 } compile(el) { // 解析元素下的每一个子节点, 所以要获取el.children // 备注: children 返回元素集合, childNodes返回节点集合 let nodes = el.children; // 解析每个子节点的指令 for (var i = 0, length = nodes.length; i < length; i++) { let node = nodes[i]; // 如果当前节点还有子元素, 递归解析该节点 if(node.children){ this.compile(node); } // 解析带有v-text指令的元素 if (node.hasAttribute("v-text")) { let attrVal = node.getAttribute("v-text"); node.textContent = this.$data[attrVal]; // 渲染页面 } // 解析带有v-model指令的元素 if (node.hasAttribute("v-model")) { let attrVal = node.getAttribute("v-model"); node.value = this.$data[attrVal]; } } } observe(data) {} }
这样,我们就实现页面的初始化了。
2.2.3 视图影响数据因为input带有v-model指令,因此我们要实现这样一个功能:在input框中输入字符,data中绑定的数据发生相应的改变。
我们可以在input这个元素上绑定一个input事件,事件的效果就是:将data中的相应数据修改为input中的值。
这一部分的实现代码比较简单,只要看标注那个地方就明白了,代码如下:
class Vue { constructor(options) { this.$el = document.querySelector(options.el); this.$data = options.data; this.$watcher = {}; this.compile(this.$el); this.observe(this.$data); } compile(el) { let nodes = el.children; for (var i = 0, length = nodes.length; i < length; i++) { let node = nodes[i]; if(node.children){ this.compile(node); } if (node.hasAttribute("v-text")) { let attrVal = node.getAttribute("v-text"); node.textContent = this.$data[attrVal]; } if (node.hasAttribute("v-model")) { let attrVal = node.getAttribute("v-model"); node.value = this.$data[attrVal]; // 看这里!!只多了三行代码!! node.addEventListener("input", (ev)=>{ this.$data[attrVal] = ev.target.value; // 可以试着在这里执行:console.log(this.$data), // 就可以看到每次在输入框输入文字的时候,data中的msg值也发生了变化 }) } } } observe(data) {} }
2.2.4 数据影响视图至此,我们已经实现了:当我们在input框中输入字符的时候,data中的数据会自动发生更新;
本小节的主要任务是:当data中的数据发生更新的时候,绑定了该数据的元素会在页面上自动更新视图。具体思路如下:
1) 我们将要实现一个 Wathcer 类,它有一个update方法,用来更新页面。观察者的代码如下: