How does vue implement two-way binding? This article will teach you how to write avuetwo-way binding, so that everyone can better understand the logical direction of two-way binding. I hope it will be helpful to everyone!
This article is mainly a process of writing and filling pitfalls, so that the audience can better understand how to implement two-way binding and the logical direction of two-way binding. So far, step by step Let’s implement a two-way binding from scratch. This is a class tutorial article. As long as you follow the article and observe each class carefully, it is not difficult to implement.
Starting Mage? Bring a dog! !
It doesn’t seem right
Start again with a picture
It can be seen from the picturenew Vue()
It is divided into two steps
The agent monitors all data and associates it withDep
, and notifies subscribers to update the view throughDep
. [Related recommendations:vuejs video tutorial]
Parse all templates, subscribe to the data used in the templates, and bind an update function, the data occurs WhenDep
changes, notify the subscriber to execute the update function.
The next step is to analyze how to implement it and what needs to be written. Let’s first look at a piece of vue’s basic code. Let’s analyze it from the beginning
{{message}}
let app = new Vue({ el:"#app", data:{ message:"测试这是一个内容" } })
From the above code we You can see the operation of As shown in the following code, this completes the Next, let’s write a If we want to parse the template, it is impossible to continue operating on the dom directly, so we need to create a document fragment (virtual dom), and then copy the template DOM node to the virtual DOM node. After parsing the virtual DOM node, we will The DOM node replaces the original DOM node After the virtual node is copied, we need to traverse the entire node tree for parsing. During the parsing process, the atrr attribute of the DOM will be traversed to find the Vue-related command, in addition to parsing the subscribe the parsed attributes Below we will gradually implement Build the Implement the initialization method implementation The parse template method According to the above code, we conclude that two simple judgments need to be implemented, that is, whether it is an html element and a text element. Here, we obtain ## The value of #nodeType parseHtml 实现 处理Text元素就相对简单了,主要是将元素中的 至此已经完成了 下面就是我们目前所实现的流程图部分 坑点一: 坑点二: 这里主要是用于代理data中的所有数据,这里会用到一个 文档: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty 下面我们来测试一下是否成功实现了数据代理,在Vue的构造函数输出一下数据 结果如下,我们可以看出已经实现了数据代理。 对应的流程图如下所示 坑点三: 上面我们已经实现了模板解析到初始化视图,还有数据代理。而下面要实现的 管理器实现完成之后我们也就实现了流程图中的以下部分。要注意下面几点 订阅者代码相对少,但是理解起来还是有点难度的,在 坑点四: 通过上面的代码我们已经完成了每一个类的构建,如下图所示,但是还是有几个流程是有问题的,也就是上面的坑点。所以下面要填坑 埋坑 1 和 2 完成坑点一和坑点二,在 埋坑 3 完成坑点三,主要是为了引入管理器,通知管理器发生改变,主要是在 埋坑 4 完成坑点四,主要四将订阅者加入到管理器中 完成了坑点四可能就会有靓仔疑惑了,这里是怎么加入的呢 至此我们已经实现了一个简单的双向绑定,下面测试一下 完结撒花 本文解释的并不多,所以才是类教程文章,如果读者有不懂的地方可以在评论去留言讨论 The above is the detailed content of Teach you step by step how to implement a vue two-way binding. For more information, please follow other related articles on the PHP Chinese website!new Vue
, which carries theel
anddata
attributes. This is the most basic attribute, and in the html code we knowCompile
class, and the parsing method also needs To process the two instructions{{ }}
andv-model
, in addition to parsing the template, we also need to implement the data proxy, which is to implement theObserver
class
Implementing the Vue class
Vue
class. It is simple enough. If you are interested inclass
If you are not familiar with the keywords, it is recommended to learn them first. From the following we may see that two classes are instantiated here, one is the proxy data class, and the other is the parsing template class.class Vue { constructor(options) { // 代理数据 new Observer(options.data) // 绑定数据 this.data = options.data // 解析模板 new Compile(options.el, this) } }
Compile
class for parsing the template. Let’s analyze it again. What should we do to parse the template?
textContent
node content to determine whether there are double curly bracesImplement template parsing Compile class
Compile
class, first Get the static nodes and Vue instances, and then define a virtual dom attribute to store the virtual domclass Compile { constructor(el, vm) { // 获取静态节点 this.el = document.querySelector(el); // vue实例 this.vm = vm // 虚拟dom this.fragment = null // 初始化方法 this.init() } }
init()
, this method is mainly used to create virtual dom and call the method of parsing the template. After the parsing is completed, the DOM node is replaced in the page.class Compile { //...省略其他代码 init() { // 创建一个新的空白的文档片段(虚拟dom) this.fragment = document.createDocumentFragment() // 遍历所有子节点加入到虚拟dom中 Array.from(this.el.children).forEach(child => { this.fragment.appendChild(child) }) // 解析模板 this.parseTemplate(this.fragment) // 解析完成添加到页面 this.el.appendChild(this.fragment); } }
parseTemplate
mainly traverses all child nodes in the virtual DOM and parses them, and performs different processing according to the child node type.class Compile { //...省略其他代码 // 解析模板 parseTemplate(fragment) { // 获取虚拟DOM的子节点 let childNodes = fragment.childNodes || [] // 遍历节点 childNodes.forEach((node) => { // 匹配大括号正则表达式 var reg = /\{\{(.*)\}\}/; // 获取节点文本 var text = node.textContent; if (this.isElementNode(node)) { // 判断是否是html元素 // 解析html元素 this.parseHtml(node) } else if (this.isTextNode(node) && reg.test(text)) { //判断是否文本节点并带有双花括号 // 解析文本 this.parseText(node, reg.exec(text)[1]) } // 递归解析,如果还有子元素则继续解析 if (node.childNodes && node.childNodes.length != 0) { this.parseTemplate(node) } }); } }
is used to distinguish. If you don’t understand, you can directly look at the
portal: Node.nodeType. There is also an extension of theisVueTagmethod for the following code. Use
class Compile { //...省略其他代码 // 判断是否携带 v- isVueTag(attrName) { return attrName.indexOf("v-") == 0 } // 判断是否是html元素 isElementNode(node) { return node.nodeType == 1; } // 判断是否是文字元素 isTextNode(node) { return node.nodeType == 3; } }
method. Parsing the html code mainly involves traversing the attr attribute on the html element
class Compile { //...省略其他代码 // 解析html parseHtml(node) { // 获取元素属性集合 let nodeAttrs = node.attributes || [] // 元素属性集合不是数组,所以这里要转成数组之后再遍历 Array.from(nodeAttrs).forEach((attr) => { // 获取属性名称 let arrtName = attr.name; // 判断名称是否带有 v- if (this.isVueTag(arrtName)) { // 获取属性值 let exp = attr.value; //切割 v- 之后的字符串 let tag = arrtName.substring(2); if (tag == "model") { // v-model 指令处理方法 this.modelCommand(node, exp, tag) } } }); } }
modelCommand
方法,在模板解析阶段来说,我们只要把 vue实例中data的值绑定到元素上,并实现监听input方法更新数据即可。class Compile { //...省略其他代码 // 处理model指令 modelCommand(node, exp) { // 获取数据 let val = this.vm.data[exp] // 解析时绑定数据 node.value = val || "" // 监听input事件 node.addEventListener("input", (event) => { let newVlaue = event.target.value; if (val != newVlaue) { // 更新data数据 this.vm.data[exp] = newVlaue // 更新闭包数据,避免双向绑定失效 val = newVlaue } }) } }
textContent
内容替换成数据即可class Compile { //...省略其他代码 //解析文本 parseText(node, exp) { let val = this.vm.data[exp] // 解析更新文本 node.textContent = val || "" } }
Compile
类的初步编写,测试结果如下,已经能够正常解析模板
modelCommand
方法中并没有实现双向绑定,只是单向绑定,后续要双向绑定时还需要继续处理
parseText
方法上面的代码中并没有去订阅数据的改变,所以这里只会在模板解析时绑定一次数据
实现数据代理 Observer 类
Object.defineProperty
方法,如果不了解这个方法的先去看一下文档传送门:
Observer
类主要是一个递归遍历所有data中的属性然后进行数据代理的的一个方法defineReactive
中传入三个参数data
,key
,val
data
和key
都是Object.defineProperty
的参数,而val
将其作为一个闭包变量供Object.defineProperty
使用// 监听者 class Observer { constructor(data) { this.observe(data) } // 递归方法 observe(data) { //判断数据如果为空并且不是object类型则返回空字符串 if (!data || typeof data != "object") { return "" } else { //遍历data进行数据代理 Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } } // 代理方法 defineReactive(data, key, val) { // 递归子属性 this.observe(data[key]) Object.defineProperty(data, key, { configurable: true, //可配置的属性 enumerable: true, //可遍历的属性 get() { return val }, set(newValue) { val = newValue } }) } }
class Vue { constructor(options) { // 代理数据 new Observer(options.data) console.log(options.data) // 绑定数据 this.data = options.data // 解析模板 new Compile(options.el, this) } }
实现管理器 Dep 类
Dep
类主要是用于管理订阅者和通知订阅者,这里会用一个数组来记录每个订阅者,而类中也会给出一个notify
方法去调用订阅者的update
方法,实现通知订阅者更新功能。这里还定义了一个target
属性用来存储临时的订阅者,用于加入管理器时使用。class Dep { constructor() { // 记录订阅者 this.subList = [] } // 添加订阅者 addSub(sub) { // 先判断是否存在,防止重复添加订阅者 if (this.subList.indexOf(sub) == -1) { this.subList.push(sub) } } // 通知订阅者 notify() { this.subList.forEach(item => { item.update() //订阅者执行更新,这里的item就是一个订阅者,update就是订阅者提供的方法 }) } } // Dep全局属性,用来临时存储订阅者 Dep.target = null
Observer
通知Dep
主要是通过调用notify
方法Dep
通知Watcher
主要是是调用了Watcher
类中的update
方法
实现订阅者 Watcher 类
Watcher
类中实现了两个方法,一个是update
更新视图方法,一个putIn
方法(我看了好几篇文章都是定义成 get 方法,可能是因为我理解的不够好吧)。
cb
方法体,用于更新页面数据Dep
管理器中。// 订阅者 class Watcher { // vm:vue实例本身 // exp:代理数据的属性名称 // cb:更新时需要做的事情 constructor(vm, exp, cb) { this.vm = vm this.exp = exp this.cb = cb this.putIn() } update() { // 调用cb方法体,改变this指向并传入最新的数据作为参数 this.cb.call(this.vm, this.vm.data[this.exp]) } putIn() { // 把订阅者本身绑定到Dep的target全局属性上 Dep.target = this // 调用获取数据的方法将订阅者加入到管理器中 let val = this.vm.data[this.exp] // 清空全局属性 Dep.target = null } }
Watcher
类中的putIn
方法再构造函数调用后并没有加入到管理器中,而是将订阅者本身绑定到target
全局属性上而已埋坑
modelCommand
和parseText
方法中增加实例化订阅者代码,并自定义要更新时执行的方法,其实就是更新时去更新页面中的值即可modelCommand(node, exp) { // ...省略其他代码 // 实例化订阅者,更新时直接更新node的值 new Watcher(this.vm, exp, (value) => { node.value = value }) } parseText(node, exp) { // ...省略其他代码 // 实例化订阅者,更新时直接更新文本内容 new Watcher(this.vm, exp, (value) => { node.textContent = value }) }
Object.defineProperty set
方法中调用dep.notify()
方法// 监听方法 defineReactive(data, key, val) { // 实例化管理器--------------增加这一行 let dep = new Dep() // ...省略其他代码 set(newValue) { val = newValue // 通知管理器改变--------------增加这一行 dep.notify() } }
defineReactive(data, key, val) { // ...省略其他代码 get() { // 将订阅者加入到管理器中--------------增加这一段 if (Dep.target) { dep.addSub(Dep.target) } return val }, // ...省略其他代码 }
Dep.target
又是什么呢,我们不妨从头看看代码并结合下面这张图总结