vue怎麼實現雙向綁定?這篇文章就手把手教你寫一個vue的雙向綁定,讓大家可以更好的理解雙向綁定的邏輯走向,希望對大家有幫助!
本文主要是一個寫坑和填坑的過程,讓觀眾更好的理解如何去實現雙向綁定,雙向綁定的邏輯走向,至此一步步來重頭實現一個雙向綁定。這是一個類別教學文章,只要跟著文章走,仔細觀察每個類別其實也不難實現。
開局法師?帶一隻狗! !
好像不對
重來,開局一張圖
#從圖可以看出new Vue()
分為了兩步驟走
代理程式監聽所有數據,並與Dep
進行關聯,透過Dep
通知訂閱者進行視圖更新。 【相關推薦:vuejs影片教學】
解析所有模板,並將模板中所使用的資料訂閱,並綁定一個更新函數,資料發生改變時Dep
通知訂閱者執行更新函數。
接著裡就是分析如何去實現,而且都需要寫什麼,先看一段vue的基礎程式碼,我們從頭開始分析
<div id="app"> <input v-model="message" /> <p>{{message}}</p> </div>
let app = new Vue({ el:"#app", data:{ message:"测试这是一个内容" } })
從上面程式碼我們可以看到new Vue
的操作,裡面攜帶了el
和data
屬性,這算是最基礎的屬性,而在html程式碼中我們知道<div id="app">
是vue渲染的模板根節點,所以vue要渲染頁面就要去實作一個模板解析的方法Compile
類,解析方法中還需要去處理{{ }}
和v-model
兩個指令,除了解析模板之後我們還需要去實作資料代理也就是實作Observer
類別
如下程式碼所示,這就寫完了Vue
類,夠簡單吧,如果對class
關鍵字不熟悉的,建議先去學習一下,從下面我們可能看到,這裡實例化了兩個類,一個是代理資料的類,一個是解析模板的類。
class Vue { constructor(options) { // 代理数据 new Observer(options.data) // 绑定数据 this.data = options.data // 解析模板 new Compile(options.el, this) } }
接著往下我們先寫一個Compile
類別用來解析模板,我們再來分析一波,解析模板要做什麼事
我們要解析範本不可能直接對dom繼續操作,所以我們要建立一個文件片段(虛擬dom),然後將範本DOM節點複製一份到虛擬DOM節點中,對虛擬DOM節點解析完成之後,再將虛擬DOM節點取代掉原來的DOM節點
虛擬節點複製出來之後,我們要遍歷整個節點樹進行解析,解析過程中會對DOM的atrr屬性進行遍歷找到Vue相關的指令,除此之外還要對textContent
節點內容進行解析,判斷是否存在雙花括號
將解析出來所用到的屬性進行一個訂閱
#下面我們將逐步實作
建置Compile
類,先把靜態節點和Vue實例取得出來,再定義一個虛擬dom的屬性用來儲存虛擬dom
class Compile { constructor(el, vm) { // 获取静态节点 this.el = document.querySelector(el); // vue实例 this.vm = vm // 虚拟dom this.fragment = null // 初始化方法 this.init() } }
實作初始化方法init()
,該方法主要是用於創建虛擬dom和調用解析模板的方法,解析完成之後再將DOM節點替換到頁面中
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
,主要是遍歷虛擬DOM中的所有子節點並進行解析,根據子節點類型進行不同的處理。
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) } }); } }
根據上面的程式碼我們得到需要實作兩個簡單的判斷,也就是判斷是否是html元素和文字元素,這裡透過取得nodeType
的值來進行區分,不了解的可以直接看一下傳送門:Node.nodeType,這裡也擴充了一個isVueTag
#方法,用於後面的程式碼中使用
class Compile { //...省略其他代码 // 判断是否携带 v- isVueTag(attrName) { return attrName.indexOf("v-") == 0 } // 判断是否是html元素 isElementNode(node) { return node.nodeType == 1; } // 判断是否是文字元素 isTextNode(node) { return node.nodeType == 3; } }
#實作parseHtml
方法,解析html程式碼主要是遍歷html元素上的attr屬性
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 } }) } }
处理Text元素就相对简单了,主要是将元素中的textContent
内容替换成数据即可
class Compile { //...省略其他代码 //解析文本 parseText(node, exp) { let val = this.vm.data[exp] // 解析更新文本 node.textContent = val || "" } }
至此已经完成了Compile
类的初步编写,测试结果如下,已经能够正常解析模板
下面就是我们目前所实现的流程图部分
坑点一:
modelCommand
方法中并没有实现双向绑定,只是单向绑定,后续要双向绑定时还需要继续处理坑点二:
parseText
方法上面的代码中并没有去订阅数据的改变,所以这里只会在模板解析时绑定一次数据这里主要是用于代理data中的所有数据,这里会用到一个Object.defineProperty
方法,如果不了解这个方法的先去看一下文档传送门:
文档:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/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 } }) } }
下面我们来测试一下是否成功实现了数据代理,在Vue的构造函数输出一下数据
class Vue { constructor(options) { // 代理数据 new Observer(options.data) console.log(options.data) // 绑定数据 this.data = options.data // 解析模板 new Compile(options.el, this) } }
结果如下,我们可以看出已经实现了数据代理。
对应的流程图如下所示
坑点三:
上面我们已经实现了模板解析到初始化视图,还有数据代理。而下面要实现的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
类中实现了两个方法,一个是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
全局属性上而已通过上面的代码我们已经完成了每一个类的构建,如下图所示,但是还是有几个流程是有问题的,也就是上面的坑点。所以下面要填坑
埋坑 1 和 2
完成坑点一和坑点二,在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 }) }
埋坑 3
完成坑点三,主要是为了引入管理器,通知管理器发生改变,主要是在Object.defineProperty set
方法中调用dep.notify()
方法
// 监听方法 defineReactive(data, key, val) { // 实例化管理器--------------增加这一行 let dep = new Dep() // ...省略其他代码 set(newValue) { val = newValue // 通知管理器改变--------------增加这一行 dep.notify() } }
埋坑 4
完成坑点四,主要四将订阅者加入到管理器中
defineReactive(data, key, val) { // ...省略其他代码 get() { // 将订阅者加入到管理器中--------------增加这一段 if (Dep.target) { dep.addSub(Dep.target) } return val }, // ...省略其他代码 }
完成了坑点四可能就会有靓仔疑惑了,这里是怎么加入的呢Dep.target
又是什么呢,我们不妨从头看看代码并结合下面这张图
至此我们已经实现了一个简单的双向绑定,下面测试一下
完结撒花
本文解释的并不多,所以才是类教程文章,如果读者有不懂的地方可以在评论去留言讨论
以上是手把手教你怎麼實現一個vue雙向綁定的詳細內容。更多資訊請關注PHP中文網其他相關文章!