參考鏈接:https://www.cnblogs.com/kidney/p/6052935.html
黃軼的源碼解讀:https://github.com/DDFE/DDFE-blog/issues/7
一糕珊、雙向數(shù)據(jù)綁定和單向數(shù)據(jù)綁定概念
????????雙向數(shù)據(jù)綁定就是在單向數(shù)據(jù)綁定的基礎(chǔ)上給可輸入元素(input红选、textare等)添加了change(input)事件喇肋,來動態(tài)修改model(js)和 view(視圖)苟蹈,在單向數(shù)據(jù)綁定中右核,input輸入元素中輸入的內(nèi)容可以通過js操作dom動態(tài)獲取,js中改變的數(shù)據(jù)也需要再次操作dom反映到視圖中贺喝。雙向數(shù)據(jù)綁定通過watcher方法自動更新視圖中的數(shù)據(jù),省去了煩瑣的dom操作氮采;
二主到、訪問器屬性
var obj = {}
// 為obj對象定義一個名為hello的訪問器屬性
// 訪問器屬性是對象中的一種特殊屬性登钥,不能直接在對象中定義娶靡,只能通defineProperty方法定義
// 讀取或設(shè)置訪問器屬性的值塔鳍,實際上是調(diào)用其內(nèi)部函數(shù)get或set方法
Object.defineProperty(obj, "hello", {
get: function() {},
set: function() {}
})
obj.hello // 調(diào)用get方法呻此,并返回get方法的返回值
obj.hello = "123" // 賦值傳參焚鲜,調(diào)用set方法,參數(shù)是123
// 訪問器屬性會被優(yōu)先訪問郑兴,即訪問器屬性會覆蓋同名屬性
三情连、雙向數(shù)據(jù)綁定的簡化版
var obj = {}
Object.defineProperty(obj, "hello", {
get: function() {},
set: function(newVal) {
document.getElementById('a').value = newVal
document.getElementById('b').innerHTML = newVal
}
})
// 模擬watcher
document.addEventListener('keyup', function(e) {
obj.hello = e.target.value
})
四却舀、將vue中的值單向綁定到dom中
1)DocumentFragment文檔片斷
????????可以看做是節(jié)點容器,它可以包含多個子節(jié)點但校,將其插入到dom中時,只有它的子節(jié)點會插入到目標(biāo)節(jié)點倘是;
????????使用DocumentFragment處理節(jié)點搀崭,速度和性能遠遠優(yōu)于直接操作dom瘤睹;
????????vue進行編譯時答倡,就是將掛載目標(biāo)的所有子節(jié)點劫持(通過append方法,dom中的所有節(jié)點會被自動刪除)到DocumentFragment中绸吸,處理后再將DocumentFragment整體返回插入掛載目標(biāo)设江;
// html代碼
<div id="app">
<input type="text" id="a">
<span id="b"></span>
</div>
// js操作
var dom = nodeToFragment(document.getElementById('app'))
console.log(dom)
function nodeToFragment(node) {
var flag = document.createDocumentFragment()
var child
while (child = node.firstChild) {
flag.appendChild(child) // 將子節(jié)點劫持到文檔片斷中
}
return flag
}
document.getElementById('app').appendChild(dom) // 返回到app中
2)dom編譯和數(shù)據(jù)綁定
// html代碼
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
// js代碼
// 對dom進行編譯,將輸入框以及文本節(jié)點與data中的數(shù)據(jù)綁定
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/
// 節(jié)點類型為元素
if (node.nodeType === 1) {
var attr = node.attributes
// 解析屬性
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue // 獲取v-model綁定的屬性名
node.value = vm.data[name] // 將data的值賦給該node
node.removeAttribute('v-model')
}
}
}
// 節(jié)點類型為text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1 // 獲取匹配到的字符串
name = name.trim()
node.nodeValue = vm.data[name] // 將data的值賦給該node
}
}
}
// 將節(jié)點轉(zhuǎn)換為文檔片斷
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment()
var child
while (child = node.firstChild) {
compile(child, vm)
flag.appendChild(child) // 將子節(jié)點劫持到文檔片斷中
}
return flag
}
// vue綁定的完整操作
function Vue(options) {
this.data = options.data
var id = options.el
var dom = nodeToFragment(document.getElementById(id), this)
// 編譯完成后,將dom返回到app中
document.getElementById(id).appendChild(dom)
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})
最終結(jié)果:
五、實現(xiàn)數(shù)據(jù)與dom雙向綁定
??????? 在輸入框中輸入數(shù)據(jù)的時候练俐,首先會觸發(fā)input或者keyup事件冕臭,在相應(yīng)的事件處理程序中腺晾,我們獲取輸入框的value并賦值給vm實例的text屬性,利用defineProperty將data中的text設(shè)置為vm的訪問器屬性辜贵,會觸發(fā)set方法更新屬性的值悯蝉;
// html代碼
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
// js代碼
var obj = {}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get: function() {
return val
},
set: function(newVal) {
if (newVal === val) return
val = newVal
console.log(val)
}
})
}
// watcher
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key, obj[key])
})
}
function Vue(options) {
this.data = options.data
var data = this.data
observe(data, this)
var id = options.el
var dom = nodeToFragment(document.getElementById(id), this)
// 編譯完成后,將dom返回到app中
document.getElementById(id).appendChild(dom)
}
// 對dom進行編譯托慨,將輸入框以及文本節(jié)點與data中的數(shù)據(jù)綁定
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/
// 節(jié)點類型為元素
if (node.nodeType === 1) {
var attr = node.attributes
// 解析屬性
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue // 獲取v-model綁定的屬性名
node.addEventListener('input', function(e) {
// 給相應(yīng)的data屬性賦值鼻由,進而觸發(fā)該屬性的set方法
vm[name] = e.target.value
})
node.value = vm[name] // 將data的值賦給該node
node.removeAttribute('v-model')
}
}
}
// 節(jié)點類型為text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1 // 獲取匹配到的字符串
name = name.trim()
node.nodeValue = vm[name] // 將data的值賦給該node
}
}
}
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment()
var child
while (child = node.firstChild) {
compile(child, vm)
flag.appendChild(child) // 將子節(jié)點劫持到文檔片斷中
}
return flag
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})
結(jié)果如下:
六、實現(xiàn)數(shù)據(jù)與dom雙向綁定
????????text 文本變化了,set方法觸發(fā)了嗡靡,使用訂閱發(fā)布模式將綁定到text的文本節(jié)點同步變化跺撼,訂閱發(fā)布模式是一種一對多的關(guān)系,即多個觀察者同時監(jiān)聽一個主題對象,這個主題對象的狀態(tài)發(fā)生變化時會通知所有觀察者對象菩貌;
????????流程:發(fā)布者發(fā)出通知=》主題對象收到通知并推送給觀察者=》訂閱者執(zhí)行相應(yīng)操作
// 一個發(fā)布者publisher
var pub = {
publish: function() {
dep.notify()
}
}
// 三個訂閱者subscribers
var sub1 = {
update: function() {
console.log(1)
}
}
var sub2 = {
update: function() {
console.log(2)
}
}
var sub3 = {
update: function() {
console.log(3)
}
}
// 一個主題對象
function Dep() {
this.subs = [sub1, sub2, sub3]
}
Dep.prototype.notify = function() {
this.subs.forEach(function(sub) {
sub.update()
})
}
// 發(fā)布者發(fā)布消息仇参,主題對象執(zhí)行notif方法婆芦,進而觸發(fā)訂閱者執(zhí)行update方法
var dep = new Dep()
pub.publish() // 1,2,3
七或粮、雙向數(shù)據(jù)綁定完整代碼
????????監(jiān)聽數(shù)據(jù)的過程中,會為data中的每一個屬性生成一個主題對象dep;
????????在編譯html過程中贱除,會為每個與數(shù)據(jù)綁定相關(guān)的節(jié)點生成一個訂閱者watcher悬蔽,watcher會將自己添加到相應(yīng)屬性的dep中禾乘;
????????發(fā)出通知dep.notify()=>觸發(fā)訂閱者的update方法=>更新視圖;
function defineReactive(obj, key, val) {
var dep = new Dep()
Object.defineProperty(obj, key, {
get: function() {
// 添加訂閱者watcher到主題對象Dep
if (Dep.target) dep.addSub(Dep.target)
return val
},
set: function(newVal) {
if (newVal === val) return
val = newVal
// 作為發(fā)布者發(fā)出通知
dep.notify()
}
})
}
// watcher
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key, obj[key])
})
}
// 一個主題對象
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub)
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update()
})
}
}
function Vue(options) {
this.data = options.data
var data = this.data
observe(data, this)
var id = options.el
var dom = nodeToFragment(document.getElementById(id), this)
// 編譯完成后决记,將dom返回到app中
document.getElementById(id).appendChild(dom)
}
// 對dom進行編譯建车,將輸入框以及文本節(jié)點與data中的數(shù)據(jù)綁定
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/
// 節(jié)點類型為元素
if (node.nodeType === 1) {
var attr = node.attributes
// 解析屬性
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue // 獲取v-model綁定的屬性名
node.addEventListener('input', function(e) {
// 給相應(yīng)的data屬性賦值,進而觸發(fā)該屬性的set方法
vm[name] = e.target.value
})
node.value = vm[name] // 將data的值賦給該node
node.removeAttribute('v-model')
}
}
}
// 節(jié)點類型為text
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 Watcher(vm, node, name) {
Dep.target = this
this.name = name
this.node = node
this.vm = vm
this.update()
Dep.target = null
}
Watcher.prototype = {
update: function() {
this.get()
this.node.nodeValue = this.value
},
// 獲取data中的屬性值
get: function() {
this.value = this.vm[this.name] // 觸發(fā)相應(yīng)屬性的get
}
}
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment()
var child
while (child = node.firstChild) {
compile(child, vm)
flag.appendChild(child) // 將子節(jié)點劫持到文檔片斷中
}
return flag
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})
結(jié)果如下: