這篇文章給大家分享的是有關vue 數據雙向綁定怎么實現的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
實現簡易版的數據雙向綁定

本文要實現的效果如下圖所示:

本文用到的HTML和JS主體代碼如下:
<div id="app"> <h2 v-text="msg"></h2> <input type="text" v-model="msg"> <div> <h2 v-text="msg2"></h2> <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 的綁定。
首先,我們要創造一個Vue類,這個類接收一個 options 對象,同時,我們要對 options 對象中的有效信息進行保存;
然后,我們有三個主要模塊:Observer、Compile、Wathcer,其中,Observer用來數據劫持的,Compile用來解析元素,Wathcer是觀察者??梢詫懗鋈缦麓a:(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() {}
}在這一步,我們要實現頁面的初始化,即解析出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) {}
}這樣,我們就實現頁面的初始化了。

因為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) {}
}至此,我們已經實現了:當我們在input框中輸入字符的時候,data中的數據會自動發生更新;
本小節的主要任務是:當data中的數據發生更新的時候,綁定了該數據的元素會在頁面上自動更新視圖。具體思路如下:
1) 我們將要實現一個 Wathcer 類,它有一個update方法,用來更新頁面。觀察者的代碼如下:
class Watcher{
constructor(node, updatedAttr, vm, expression){
// 將傳進來的值保存起來,這些數據都是渲染頁面時要用到的數據
this.node = node;
this.updatedAttr = updatedAttr;
this.vm = vm;
this.expression = expression;
this.update();
}
update(){
this.node[this.updatedAttr] = this.vm.$data[this.expression];
}
}2) 試想,我們該給哪些數據添加觀察者?何時給數據添加觀察者?
在解析元素的時候,當解析到v-text和v-model指令的時候,說明這個元素是需要和數據雙向綁定的,因此我們在這時往容器中添加觀察者。我們需用到這樣一個數據結構:{屬性1: [wathcer1, wathcer2...], 屬性2: [...]},如果不是很清晰,可以看下圖:

可以看到:vue實例中有一個$wathcer對象,$wathcer的每個屬性對應每個需要綁定的數據,值是一個數組,用來存放觀察了該數據的觀察者。(備注:Vue源碼中專門創造了Dep這么一個類,對應這里所說的數組,本文屬于簡易版本,就不過多介紹了)
3) 劫持數據:利用對象的訪問器屬性getter和setter做到當數據更新的時候,觸發一個動作,這個動作的主要目的就是讓所有觀察了該數據的觀察者執行update方法。
總結一下,在本小節我們需要做的工作:
實現一個Wathcer類;
在解析指令的時候(即在compile方法中)添加觀察者;
實現數據劫持(實現observe方法)。
完整代碼如下:
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(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);
}
if (node.hasAttribute("v-text")) {
let attrVal = node.getAttribute("v-text");
// node.textContent = this.$data[attrVal];
// Watcher在實例化時調用update, 替代了這行代碼
/**
* 試想Wathcer要更新節點數據的時候要用到哪些數據?
* e.g. p.innerHTML = vm.$data[msg]
* 所以要傳入的參數依次是: 當前節點node, 需要更新的節點屬性, vue實例, 綁定的數據屬性
*/
// 往容器中添加觀察者: {msg1: [Watcher, Watcher...], msg2: [...]}
if (!this.$watcher[attrVal]) {
this.$watcher[attrVal] = [];
}
this.$watcher[attrVal].push(new Watcher(node, "innerHTML", this, 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;
})
if (!this.$watcher[attrVal]) {
this.$watcher[attrVal] = [];
}
// 不同于上處用的innerHTML, 這里input用的是vaule屬性
this.$watcher[attrVal].push(new Watcher(node, "value", this, attrVal))
}
}
}
observe(data) {
Object.keys(data).forEach((key) => {
let val = data[key]; // 這個val將一直保存在內存中,每次訪問data[key],都是在訪問這個val
Object.defineProperty(data, key, {
get() {
return val; // 這里不能直接返回data[key],不然會陷入無限死循環
},
set(newVal) {
if (val !== newVal) {
val = newVal;// 同理,這里不能直接對data[key]進行設置,會陷入死循環
this.$watcher[key].forEach((w) => {
w.update();
})
}
}
})
})
}
}
class Watcher {
constructor(node, updatedAttr, vm, expression) {
// 將傳進來的值保存起來
this.node = node;
this.updatedAttr = updatedAttr;
this.vm = vm;
this.expression = expression;
this.update();
}
update() {
this.node[this.updatedAttr] = this.vm.$data[this.expression];
}
}
let vm = new Vue({
el: "#app",
data: {
msg: "hello world",
msg2: "hello xiaofei"
}
})至此,代碼就完成了。
感謝各位的閱讀!關于“vue 數據雙向綁定怎么實現”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。