原文鏈接: https://www.jianshu.com/p/e7ebb1500613
一、原理:
1.vue 雙向資料系結是通過 資料劫持 結合 發布訂閱模式的方式來實作的, 也就是說資料和視圖同步,資料發生變化,視圖跟著變化,視圖變化,資料也隨之發生改變;
2.核心:關于VUE雙向資料系結,其核心是 Object.defineProperty()方法;
3.介紹一下Object.defineProperty()方法
(1)Object.defineProperty(obj, prop, descriptor) ,這個語法內有三個引數,分別為 obj (要定義其上屬性的物件) prop (要定義或修改的屬性) descriptor (具體的改變方法)
(2)簡單地說,就是用這個方法來定義一個值,當呼叫時我們使用了它里面的get方法,當我們給這個屬性賦值時,又用到了它里面的set方法;

set,get方法初步了解
二、先簡單的實作一個js的雙向資料系結來熟悉一下這個方法:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" id="a">
<span id="b"></span>
</div>
</body>
<script>
var obj = {}; //定義一個空物件
var val = 'zhao'; //賦予初始值
Object.defineProperty(obj, 'val', {//定義要修改物件的屬性
get: function () {
return val;
},
set: function (newVal) {
val = newVal;//定義val等于修改后的內容
document.getElementById('a').value = val;//讓文本框的內容等于val
document.getElementById('b').innerHTML = val;//讓span的內容等于val
}
});
document.addEventListener('keyup', function (e) {//當在文本框輸入內容時讓物件里你定義的val等于文本框的值
obj.val = e.target.value;
})
</script>
</html>
這樣我們就能實作js的雙向資料系結,也對這個方法有初步的了解;
這個例子實作的效果是:隨著文本框輸入文字的變化,span中會同步顯示相同的文字內容;這樣就實作了 model => view 以及 view => model 的雙向系結,
通過添加事件監聽keyup來觸發set方法,而set再修改了訪問器屬性的同時,也修改了dom樣式,改變了span標簽內的文本,
三、實作一個真正的雙向系結的原理
1.實作效果
先來看一下vue雙向資料系結是如何進行的,以便我們確定好思考方向

image

image
2.任務拆分
拆分任務可以讓我們的思路更加清晰:
(1)將vue中的data中的內容系結到輸入文本框和文本節點中
(2)當文本框的內容改變時,vue實體中的data也同時發生改變
(3)當data中的內容發生改變時,輸入框及文本節點的內容也發生變化
3.開始任務1——系結內容
我們先了解一下 DocuemntFragment(碎片化檔案)這個概念,你可以把他認為一個dom節點收容器,當你創造了10個節點,當每個節點都插入到檔案當中都會引發一次瀏覽器的回流,也就是說瀏覽器要回流10次,十分消耗資源,
而使用碎片化檔案,也就是說我把10個節點都先放入到一個容器當中,最后我再把容器直接插入到檔案就可以了!瀏覽器只回流了1次,
注意:還有一個很重要的特性是,如果使用appendChid方法將原dom樹中的節點添加到DocumentFragment中時,會洗掉原來的節點,
舉個例子:
可以看到,我的app中有兩個子節點,一個元素節點,一個文本節點
但是,當我通過DocumentFragment 劫持資料一下后

image

image

image
注意:我的碎片化檔案是將子節點都劫持了過來,而我的id為app的div內已經沒有內容了,
同時要主要我while的判斷條件,判斷是否有子節點,因為我每次appendChild都把node中的第一個子節點劫持走了,node中就會少一個,直到沒有的時候,child也就變成了undefined,也就終止了回圈,
來實作內容系結
我們要考慮兩個問題,一個是如何系結要input上,另一個是如何系結要文本節點中,
這樣思路就來了,我們已經獲取到了div的所以子節點了,就在DocumentFragment里面,然后對每一個節點進行處理,看是不是有跟vm實體中有關聯的內容,如果有,修改這個節點的內容,然后重新添加入DocumentFragment中,
首先,我們寫一個處理每一個節點的函式,如果有input系結v-model屬性或者有{{ xxx }}的文本節點出現,就進行內容替換,替換為vm實體中的data中的內容

image
然后,在向碎片化檔案中添加節點時,每個節點都處理一下,

image
創建Vue的實體化函式

image
效果圖如下:

image
我們成功將內容都系結到了輸入框與文本節點上!
4、實作任務2——【view => model
對于此任務,我們從輸入框考慮,輸入框的問題,輸入框如何改變data,我們通過事件監聽器keyup,input等,來獲取到最新的value,然后通過Object.defineProperty將獲取的最新的value,賦值給實體vm的text,我們把vm實體中的data下的text通過Object.defineProperty設定為訪問器屬性,這樣給vm.text賦值,就觸發了set,set函式的作用一個是更新data中的text,另一個等到任務三再說,
首先實作一個回應式監聽屬性的函式,一旦有賦新值就發生變化

image
然后,實作一個觀察者,對于一個實體 每一個屬性值都進行觀察,

image
改寫編譯函式,注意由于改成了訪問器屬性,訪問的方法也產生變化,同時添加了事件監聽器,把實體的text值隨時更新

image
實體函式中,觀察data中的所有屬性值,注意增添了observe

image
最終我們改變input中的內容能改變data中的資料,單頁面卻沒有重繪

image

image
4、實作任務3——【model => view】
通過修改vm實體的屬性 該改變輸入框的內容 與 文本節點的內容,
這里涉及到一個問題 需要我們注意,當我們修改輸入框,改變了vm實體的屬性,這是1對1的,
但是,我們可能在頁面中多處用到 data中的屬性,這是1對多的,也就是說,改變1個model的值可以改變多個view中的值,
這就需要我們引入一個新的知識點:
訂閱/發布者模式
訂閱發布模式(又稱觀察者模式)定義了一種一對多的關系,讓多個觀察者同時監聽某一個主題物件,這個主題物件的狀態發生改變時就會通知所有觀察者物件,
發布者發出通知 => 主題物件收到通知并推送給訂閱者 => 訂閱者執行相應操作
1
舉個例子:

image
之前提到的set函式的第二個作用 就是來提醒訂閱者 進行noticy操作,告訴他們:“我的text變了!” 文本節點變成了訂閱者,接到訊息后,立馬進行update操作
回顧一下,每當 new 一個 Vue,主要做了兩件事:第一個是監聽資料:observe(data),第二個是編譯 HTML:nodeToFragement(id),
在監聽資料的程序中,我們會為 data 中的每一個屬性生成一個主題物件 dep,
在編譯 HTML 的程序中,會為每個與資料系結相關的節點生成一個訂閱者 watcher,watcher 會將自己添加到相應屬性的 dep 容器中,
我們已經實作:修改輸入框內容 => 在事件回呼函式中修改屬性值 => 觸發屬性的 set 方法,
接下來我們要實作的是:發出通知 dep.notify() => 觸發訂閱者的 update 方法 => 更新視圖,
這里的關鍵邏輯是:如何將 watcher 添加到關聯屬性的 dep 中,
注意: 我把直接賦值的操作改為了 添加一個 Watcher 訂閱者

image
那么,Watcher又該做些什么呢?

image
首先,將自己賦給了一個全域變數 Dep.target;
其次,執行了 update 方法,進而執行了 get 方法,get 的方法讀取了 vm 的訪問器屬性,從而觸發了訪問器屬性的 get 方法,get 方法中將該 watcher 添加到了對應訪問器屬性的 dep 中;
再次,獲取屬性的值,然后更新視圖,
最后,將 Dep.target 設為空,因為它是全域變數,也是 watcher 與 dep 關聯的唯一橋梁,任何時刻都必須保證 Dep.target 只有一個值,

image

image
最終我們就實作了這個雙向資料系結功能,雖然很繁瑣,但我相信,你多打幾遍,一定會對你有所幫助,加油吧!!
最后小編給大家附上原始碼:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<input type="text" v-model="text" /> {{text}}
</div>
</body>
<script type="text/javascript">
// 編譯函式
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/; // 來匹配{{xxx}}中的xxx
//如果是元素節點
if(node.nodeType === 1) {
var attr = node.attributes;
//決議元素節點的所有屬性
for(let i = 0; i < attr.length; i++) {
if(attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue //看看是與哪一個資料相關
node.addEventListener('input', function(e) { //將與其相關的資料改為最新值
vm[name] = e.target.value
})
node.value = vm.data[name]; //將data中的值賦予給該node
node.removeAttribute('v-model')
}
}
}
//如果是文本節點
if(node.nodeType === 3) {
if(reg.test(node.nodeValue)) {
var name = RegExp.$1; //獲取到匹配的字串
name = name.trim();
// node.nodeValue = vm[name]; //將data中的值賦予給該node
new Watcher(vm, node, name) //系結一個訂閱者
}
}
}
// 在向碎片化檔案中添加節點時,每個節點都處理一下
function nodeToFragment(node, vm) {
var fragment = document.createDocumentFragment();
var child;
while(child = node.firstChild) {
compile(child, vm);
fragment.appendChild(child);
}
return fragment
}
// Vue建構式
// 觀察data中的所有屬性值,注意增添了observe
function Vue(options) {
this.data = options.data;
observe(this.data, this)
var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this)
//處理完所有節點后,重新把內容添加回去
document.getElementById(id).appendChild(dom)
}
// 實作一個回應式監聽屬性的函式,一旦有賦新值就發生變化
function defineReactive(obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function() {
if(Dep.target) {
dep.addSub(Dep.target)
}
return val
},
set: function(newVal) {
if(newVal === val) {
return
}
val = newVal;
console.log('新值' + val);
//一旦更新立馬通知
dep.notify();
}
})
}
// 實作一個觀察者,對于一個實體 每一個屬性值都進行觀察,
function observe(obj, vm) {
for(let key of Object.keys(obj)) {
defineReactive(vm, key, obj[key]);
}
}
// Watcher監聽者
function Watcher(vm, node, name) {
Dep.target = this;
this.vm = vm;
this.node = node;
this.name = name;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update() {
this.get();
this.node.nodeValue = this.value //更改節點內容的關鍵
},
get() {
this.value = this.vm[this.name] //觸發相應的get
}
}
// dep建構式
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub)
},
notify() {
this.subs.forEach(function(sub) {
sub.update();
})
}
}
var vm = new Vue({
el: 'app',
data: {
text: '趙剛'
}
})
</script>
</html>
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/264136.html
標籤:其他
