在實現vue雙向數據綁定之前,先了解Proxy相關的概念和用法
proxy概念
Proxy 對象用於定義基本操作的自定義行為(如屬性查找、賦值、枚舉、函數調用等)。
一些術語
- handle
包含捕捉器(trap)的佔位符對象,可譯為處理器對象
- traps
提供屬性訪問的方法。這類似於操作系統中捕獲器的概念。
- target
被 Proxy 代理虛擬化的對象。
語法
const p = new Proxy(target, handler)
- target
要使用 Proxy 包裝的目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理)。
- handle
一個通常以函數作為屬性的對象,各屬性中的函數分別定義了在執行各種操作時代理 p 的行為。
使用proxy實現數據劫持
let data = {
name: YoLinDeng,
height: '176cm'
}
const p = new Proxy(data, {
get(target, prop) {
return Reflect.get(...arguments)
},
set(target, prop, newValue) {
return Reflect.set(...arguments)
}
})
關於vue中數據響應式的原理
對數據進行偵測
- 在vue2.X中,實現一個
observe類,對於對象數據,通過Object.defineProperty來劫持對象的屬性,實現getter和setter方法,這樣就可以在getter的時候知道誰(訂閲者)讀取了數據,即誰依賴了當前的數據,將它通過Dep類(訂閲器)收集統一管理,在setter的時候調用Dep類中的notify方法通知所以相關的訂閲者進行更新視圖。如果對象的屬性也是一個對象的話,則需要遞歸調用observe進行處理。 - 對於數組則需要另外處理,通過實現一個攔截器類,並將它掛載到數組數據的原型上,當調用
push/pop/shift/unshift/splice/sort/reverse修改數組數據時候,相當於調用的是攔截器中重新定義的方法,這樣在攔截器中就可以偵測到數據改變了,並通知訂閲者更新視圖。 - vue3中使用Proxy替代了Object.defineProperty,優點在於可以直接監聽對象而非屬性、可以直接監聽數組的變化、多達13種攔截方法。缺點是兼容性還不夠好。Proxy作為新標準將受到瀏覽器廠商重點持續的性能優化。
對模板字符串進行編譯
- 實現Compile解析器類,將
template中的模板字符串通過正則等方式進行處理生成對應的ast(抽象語法樹),通過調用定義的不同鈎子函數進行處理,包括開始標籤(start)並判斷是否自閉和以及解析屬性、結束標籤(end)、文本(chars)、註釋(comment) - 將通過html解析與文本解析的ast進行優化處理,在靜態節點上打標記,為後面
dom-diff算法中性能優化使用,即在對比前後vnode的時候會跳過靜態節點不作對比。 - 最後根據處理好的ast生產
render函數,在組件掛載的時候調用render函數就可以得到虛擬dom。
虛擬dom
- vnode的類型包括註釋節點、文本節點、元素節點、組件節點、函數式組件節點、克隆節點,
VNode可以描述的多種節點類型,它們本質上都是VNode類的實例,只是在實例化的時候傳入的屬性參數不同而已。 - 通過將模板字符串編譯生成虛擬dom並緩存起來,當數據發生變化時,通過對比變化前後虛擬dom,以變化後的虛擬dom為基準,更新舊的虛擬dom,使它和新的一樣。把dom-diff過程叫做
patch的過程,其主要做了三件事,分別是創建/刪除/更新節點。 - 對於子節點的更新策略,vue中為了避免雙重循環數據量大時候造成時間複雜度高帶來的性能問題,而選擇先從子節點數組中4個特殊位置進行對比,分別是:新前與舊前,新後與舊後,新後與舊前,新前與舊後。如果四種情況都沒有找到相同的節點,則再通過循環方式查找。
實現簡易的vue雙向數據綁定
vue的雙向數據綁定主要是指,數據變化更新視圖變化,視圖變化更新數據。
實現代碼如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width= , initial-scale=1.0">
<title>Document</title>
<script src="myVue.js"></script>
</head>
<body>
<div id="app">
{{name}}
<div>{{message}}</div>
<input type="text" v-model="test">
<span>{{test}}</span>
</div>
<script>
let vm = new vue({
el: '#app',
data: {
name: 'YoLinDeng',
message: '打籃球',
test: '雙向綁定數據'
}
})
// console.log(vm._data)
</script>
</body>
</html>
class vue extends EventTarget {
constructor(option) {
super()
this.option = option
this._data = this.option.data
this.el = document.querySelector(this.option.el)
this.compileNode(this.el)
this.observe(this._data)
}
// 實現監聽器方法
observe(data) {
const context = this
// 使用proxy代理,劫持數據
this._data = new Proxy(data, {
set(target, prop, newValue) {
// 自定義事件
let event = new CustomEvent(prop, {
detail: newValue
})
// 發佈自定義事件
context.dispatchEvent(event)
return Reflect.set(...arguments)
}
})
}
// 實現解析器方法,解析模板
compileNode(el) {
let child = el.childNodes
let childArr = [...child]
childArr.forEach(node => {
if (node.nodeType === 3) {
let text = node.textContent
let reg = /\{\{\s*([^\s\{\}]+)\s*\}\}/g
if (reg.test(text)) {
let $1 = RegExp.$1
this._data[$1] && (node.textContent = text.replace(reg, this._data[$1]))
// 監聽數據更改事件
this.addEventListener($1, e => {
node.textContent = text.replace(reg, e.detail)
})
}
} else if (node.nodeType === 1) { // 如果是元素節點
let attr = node.attributes
// 判斷屬性中是否含有v-model
if (attr.hasOwnProperty('v-model')) {
let keyName = attr['v-model'].nodeValue
node.value = this._data[keyName]
node.addEventListener('input', e => {
this._data[keyName] = node.value
})
}
// 遞歸調用解析器方法
this.compileNode(node)
}
})
}
}