參考鏈接:https://www.cnblogs.com/kidney/p/6052935.html
黃軼的源碼解讀:https://github.com/DDFE/DDFE-blog/issues/7
一、雙向數據綁定和單向數據綁定概念
????????雙向數據綁定就是在單向數據綁定的基礎上給可輸入元素(input、textare等)添加了change(input)事件,來動態修改model(js)和 view(視圖),在單向數據綁定中,input輸入元素中輸入的內容可以通過js操作dom動態獲取,js中改變的數據也需要再次操作dom反映到視圖中。雙向數據綁定通過watcher方法自動更新視圖中的數據,省去了煩瑣的dom操作;
二、訪問器屬性
var obj = {}
// 為obj對象定義一個名為hello的訪問器屬性
// 訪問器屬性是對象中的一種特殊屬性,不能直接在對象中定義,只能通defineProperty方法定義
// 讀取或設置訪問器屬性的值,實際上是調用其內部函數get或set方法
Object.defineProperty(obj, "hello", {
get: function() {},
set: function() {}
})
obj.hello // 調用get方法,并返回get方法的返回值
obj.hello = "123" // 賦值傳參,調用set方法,參數是123
// 訪問器屬性會被優先訪問,即訪問器屬性會覆蓋同名屬性
三、雙向數據綁定的簡化版
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文檔片斷
????????可以看做是節點容器,它可以包含多個子節點,將其插入到dom中時,只有它的子節點會插入到目標節點;
????????使用DocumentFragment處理節點,速度和性能遠遠優于直接操作dom;
????????vue進行編譯時,就是將掛載目標的所有子節點劫持(通過append方法,dom中的所有節點會被自動刪除)到DocumentFragment中,處理后再將DocumentFragment整體返回插入掛載目標;
// 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) // 將子節點劫持到文檔片斷中
}
return flag
}
document.getElementById('app').appendChild(dom) // 返回到app中
2)dom編譯和數據綁定
// html代碼
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
// js代碼
// 對dom進行編譯,將輸入框以及文本節點與data中的數據綁定
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/
// 節點類型為元素
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')
}
}
}
// 節點類型為text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1 // 獲取匹配到的字符串
name = name.trim()
node.nodeValue = vm.data[name] // 將data的值賦給該node
}
}
}
// 將節點轉換為文檔片斷
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment()
var child
while (child = node.firstChild) {
compile(child, vm)
flag.appendChild(child) // 將子節點劫持到文檔片斷中
}
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'
}
})
最終結果:
五、實現數據與dom雙向綁定
??????? 在輸入框中輸入數據的時候,首先會觸發input或者keyup事件,在相應的事件處理程序中,我們獲取輸入框的value并賦值給vm實例的text屬性,利用defineProperty將data中的text設置為vm的訪問器屬性,會觸發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進行編譯,將輸入框以及文本節點與data中的數據綁定
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/
// 節點類型為元素
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) {
// 給相應的data屬性賦值,進而觸發該屬性的set方法
vm[name] = e.target.value
})
node.value = vm[name] // 將data的值賦給該node
node.removeAttribute('v-model')
}
}
}
// 節點類型為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) // 將子節點劫持到文檔片斷中
}
return flag
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})
結果如下:
六、實現數據與dom雙向綁定
????????text 文本變化了,set方法觸發了,使用訂閱發布模式將綁定到text的文本節點同步變化,訂閱發布模式是一種一對多的關系,即多個觀察者同時監聽一個主題對象,這個主題對象的狀態發生變化時會通知所有觀察者對象;
????????流程:發布者發出通知=》主題對象收到通知并推送給觀察者=》訂閱者執行相應操作
// 一個發布者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()
})
}
// 發布者發布消息,主題對象執行notif方法,進而觸發訂閱者執行update方法
var dep = new Dep()
pub.publish() // 1,2,3
七、雙向數據綁定完整代碼
????????監聽數據的過程中,會為data中的每一個屬性生成一個主題對象dep;
????????在編譯html過程中,會為每個與數據綁定相關的節點生成一個訂閱者watcher,watcher會將自己添加到相應屬性的dep中;
????????發出通知dep.notify()=>觸發訂閱者的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
// 作為發布者發出通知
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進行編譯,將輸入框以及文本節點與data中的數據綁定
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/
// 節點類型為元素
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) {
// 給相應的data屬性賦值,進而觸發該屬性的set方法
vm[name] = e.target.value
})
node.value = vm[name] // 將data的值賦給該node
node.removeAttribute('v-model')
}
}
}
// 節點類型為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] // 觸發相應屬性的get
}
}
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment()
var child
while (child = node.firstChild) {
compile(child, vm)
flag.appendChild(child) // 將子節點劫持到文檔片斷中
}
return flag
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})
結果如下: