vue 資料雙向繫結的實現方法
1. 前言
本文適合於學習vue原始碼的初級學者,閱讀後,你將對Vue的資料雙向繫結原理有一個大致的瞭解,認識Observer、Compile、Wathcer三大角色(如下圖所示)以及它們所發揮的功能。
本文將一步步帶你實現簡易版的資料雙向繫結,每一步都會詳細分析這一步要解決的問題以及程式碼為何如此寫,因此,在閱讀完本文後,希望你能自己動手實現一個簡易版資料雙向繫結。
2. 程式碼實現
2.1 目的分析
本文要實現的效果如下圖所示:
本文用到的HTML和js主體程式碼如下:
<div id="app"> <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 實現過程
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..http://www.cppcns.com.],屬性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.$http://www.cppcns.comel);
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方法,用來更新頁面。觀察者的程式碼如下:
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,屬性2: [...]},如果不是很清晰,可以看下圖:
可以看到:vue例項中有一個$wathcer物件,$wathcer的每個屬性對應http://www.cppcns.com每個需要繫結的資料,值是一個數組,用來存放觀察了該資料的觀察者。(備註: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,屬性2: [...]}
this.$watcher = {};
// 解析元素: 實現Compile
this.compile(this.$el); // 要解析元素,所以要獲取el.children
// 拓展: children 返回元素集合,遞迴解析該節點
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",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,expression) {
// 將傳進來的值儲存起來
this.node = node;
this.updatedAttr = updatedAttr;
this.vm = vm;
this.expres程式設計客棧sion = expression;
this.update();
}
update() {
this.node[this.updatedAttr] = this.vm.$data[this.expression];
}
}
let vm = new Vue({
el: "#app",msg2: "hello xiaofei"
}
})
至此,程式碼就完成了。
3. 未來的計劃
用設計模式的知識,分析上面這份原始碼存在的問題,並和Vue原始碼進行比對,算是對Vue原始碼的解析
以上就是vue 資料雙向繫結的實現方法的詳細內容,更多關於vue 資料雙向繫結的資料請關注我們其它相關文章!