前言
實(shí)現(xiàn)Vue的數(shù)據(jù)的雙向綁定 是采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式 通過Object.defineProperty()來劫持各個屬性的setter,getter休傍,在數(shù)據(jù)變動時發(fā)布消息給訂閱者掀亩,觸發(fā)相應(yīng)的監(jiān)聽回調(diào) 需要實(shí)現(xiàn)以下幾點(diǎn):
1 實(shí)現(xiàn)一個 Observer 觀察者對data中的數(shù)據(jù)進(jìn)行監(jiān)聽置侍,若有變化,通知相應(yīng)的訂閱者
2 實(shí)現(xiàn)一個 Compile 編譯器將頁面中子節(jié)點(diǎn)拷貝到DocumentFragment對象中,然后對每個元素節(jié)點(diǎn)的指令進(jìn)行掃描與解析
3 實(shí)現(xiàn)一個 Watcher 用來連接Observer和Compile豌习,并為每個屬性綁定相應(yīng)的訂閱者苏潜,當(dāng)數(shù)據(jù)發(fā)生變化時银萍,執(zhí)行相應(yīng)的回調(diào)函數(shù),從而更新視圖恤左。
4 new MVVM的類實(shí)例對象 作為入口整合使用
準(zhǔn)備工作
下面是文件目錄贴唇,由于瀏覽器不支持 es6 的export default / import 語法所以用webpack來打包解析es6語法
實(shí)現(xiàn)vue的類
- 新創(chuàng)建一個Vue的類,接收 new實(shí)例對象傳過來的參數(shù)飞袋, el:vue實(shí)例被掛載的Dom對象戳气, _data: 接收了初始化的data數(shù)據(jù)
- 利用Object.defineProperty()的方法利用set、get 的方法對指定的屬性進(jìn)行數(shù)據(jù)代理巧鸭,也就是將示例對象上的屬性 映射到內(nèi)部_data對象屬性上去 實(shí)現(xiàn) MVVM.xxx => MVVM._data.xxx
- 接下來實(shí)現(xiàn)一個Observer 對data中的屬性進(jìn)行數(shù)據(jù)劫持將屬性與發(fā)布訂閱者綁定
- 進(jìn)行模版指令解析
vue.js
import Observer from './Observer' //觀察者
import Compiler from './Compiler' //編譯模版
class Vue {
constructor(options) {
this.$options = options
//保存掛載元素的ID
this.$el = this.$options.el
//保存實(shí)例對象中的data對象數(shù)據(jù)
this._data = this.$options.data
//遍歷屬性添加數(shù)據(jù)代理
Object.keys(this._data).forEach(key => {
this._proxy(key)
})
//通過數(shù)據(jù)劫持實(shí)現(xiàn)雙向綁定 通知發(fā)布訂閱
new Observer(this._data)
//模版指令解析
new Compiler(this.$el, this)
}
//將實(shí)例對象上的屬性 映射到內(nèi)部_data對象屬性上面 (實(shí)現(xiàn) MVVM.xxx => MVVM._data.xxx)
_proxy(key) {
//屬性描述符
Object.defineProperty(this, key, {
//讀取屬性值時調(diào)用
get() {
//返回_data 中屬性名的對應(yīng)的屬性值
return this._data[key]
},
//監(jiān)聽設(shè)置屬性調(diào)用
set(value) {
//賦值到_data屬性名對應(yīng)的屬性值
this._data[key] = value
}
})
}
}
export default Vue
實(shí)現(xiàn)Observer 觀察者
- 接收并保存vue實(shí)例對象傳遞過來的數(shù)據(jù)
- 通過Object.keys()的方法返回對象所有屬性名組成的數(shù)組瓶您,遍歷對每個屬性執(zhí)行數(shù)據(jù)劫持實(shí)現(xiàn)雙向綁定。在每次遍歷中都會new一個Dep()的實(shí)例對象纲仍,并且Dep()的實(shí)例對象與屬性是一一對應(yīng)的關(guān)系
- 在對屬性進(jìn)行數(shù)據(jù)劫持的同時觸發(fā)get()方法會去判斷當(dāng)前是否存在Watcher對象呀袱,存在就調(diào)用Dep中的listen方法將當(dāng)前的Watcher對象添加到訂閱者數(shù)組中,并返回當(dāng)前的value值
- 在通過MVVM.name = 'xxx' 賦值的過程中會觸發(fā)set()函數(shù)郑叠,簡單判斷后賦值的同時調(diào)用myDep 中的notify()方法遍歷執(zhí)行Watcher中的update()方法進(jìn)行頁面對應(yīng)視圖的更新
Observer.js
import Dep from "./Dep";
class Observer {
constructor(data) {
//保存數(shù)據(jù)
this.data = data
//對數(shù)據(jù)中所有屬性設(shè)置setter getter 通過數(shù)據(jù)劫持實(shí)現(xiàn)雙向綁定
Object.keys(data).forEach(key => {
this._bind(data, key, data[key])
})
}
_bind(data, key, value) {
//new Dep對象 保存訂閱者 遍歷調(diào)用訂閱者的update對象 (myDep實(shí)例對象與data中的屬性一一對應(yīng))
let myDep = new Dep()
//屬性描述符
Object.defineProperty(data, key, {
//獲取屬性值
get() {
//獲取屬性值 判斷是否存在wathcer實(shí)例對象 存在將其保存到Dep當(dāng)中
if (Dep.target) myDep.listen(Dep.target)
return value
},
//監(jiān)視屬性值的變化
set(newValue) {
if (newValue === value) return
//賦值
value = newValue
//觀察者改變完數(shù)據(jù) 通知發(fā)布訂閱
myDep.notify()
}
})
}
}
export default Observer
發(fā)布訂閱
- 初始化 target 保存Watcher訂閱者對象 夜赵,list 存放多個Watcher訂閱者對象
- 實(shí)現(xiàn)了一個 listen()方法用于將Watcher訂閱者對象添加到list當(dāng)中
- 實(shí)現(xiàn)了一個 notify()方法遍歷訂閱者對象調(diào)用其 update()的方法進(jìn)行頁面視圖的更新
Dep .js
class Dep {
constructor() {
this.target = null
this.list = []
}
//添加訂閱者 watcher
listen(subs) {
this.list.push(subs)
}
//調(diào)用watcher中的update方法更新視圖
notify() {
this.list.forEach(item => {
item.update()
})
}
}
export default Dep
實(shí)現(xiàn)Compile
- 通過頁面元素的id獲取當(dāng)前元素的節(jié)點(diǎn)對象并保存,保存當(dāng)前的vue的實(shí)例對象
- 創(chuàng)建通過 createFragment()方法創(chuàng)建了一個Dom碎片對象乡革, 循環(huán)將this.$el中的子節(jié)點(diǎn)依次轉(zhuǎn)移到 fragment對象中經(jīng)過compileElement()編譯元素節(jié)點(diǎn)之后return 在次添加到頁面的節(jié)點(diǎn)對象
- 通過判斷元素的節(jié)點(diǎn)類型的值再做對應(yīng)的編譯解析操作
- 當(dāng)數(shù)據(jù)類型是元素節(jié)點(diǎn)并且包含 v-model 的屬性指令時直接將綁定的屬性名對應(yīng)的屬性值賦值到元素節(jié)點(diǎn)的vlaue值實(shí)現(xiàn)視圖更新寇僧, 并且給元素添加 'input' 的監(jiān)聽事件,事件發(fā)生時將value值賦值給 對象中對應(yīng)屬性名的屬性值
- 當(dāng)數(shù)據(jù)類型為文本類型 {{ name }} 時沸版,通過正則解析判斷當(dāng)前格式獲取到 'name' 屬性名 new Watcher()實(shí)例對象初始化調(diào)用update()方法實(shí)現(xiàn)視圖更新
Compiler.js
import Watcher from "./Watcher";
const reg = /\{\{(.*)\}\}/
class Compiler {
constructor(el, vm) {
//保存根據(jù)獲取到ID對應(yīng)的頁面節(jié)點(diǎn)對象
this.$el = document.querySelector(el)
//保存當(dāng)前的MVVM對象
this.$vm = vm
//將頁面中的子節(jié)點(diǎn)轉(zhuǎn)移到fragment中
this.fragment = this.createFragment()
//將fragment 轉(zhuǎn)移回到頁面當(dāng)中
this.$el.appendChild(this.fragment)
}
createFragment() {
//創(chuàng)建Dom碎片對象
let fragment = document.createDocumentFragment(), child
//將原生節(jié)點(diǎn)拷貝到fragment
while (child = this.$el.firstChild) {
//編譯元素節(jié)點(diǎn)
this.compileElement(child)
//將節(jié)點(diǎn)加入fragment對象
fragment.appendChild(child)
}
return fragment
}
//編譯元素節(jié)點(diǎn)
compileElement(node) {
//元素節(jié)點(diǎn)
if (node.nodeType == 1) {
//獲取當(dāng)前元素節(jié)點(diǎn)的屬性值數(shù)組
let attr = node.attributes
//判斷屬性值數(shù)組中是否存在v-model
if (attr.hasOwnProperty('v-model')) {
let self = this
//獲取屬性為 v-model 的元素節(jié)點(diǎn) 并獲取節(jié)點(diǎn)值
let name = attr['v-model'].nodeValue
//初始化顯示輸入框內(nèi)容
node.value = this.$vm[name]
//給輸入框添加input事件監(jiān)聽
node.addEventListener('input', function(e){
self.$vm[name] = e.target.value
})
}
}
//文本節(jié)點(diǎn)
if (node.nodeType == 3) {
//判斷是否符合 雙大括號表達(dá)式 {{ }}
if (reg.test(node.nodeValue)) {
//獲取表達(dá)式內(nèi)容
let name = RegExp.$1
//去空格
name = name.trim()
//new一個Watcher對象更新雙大括號表達(dá)式內(nèi)容
new Watcher(node, name, this.$vm)
}
}
}
}
export default Compiler
實(shí)現(xiàn)Watcher
- 接收并保存初始化編譯模版?zhèn)鬟f過來的 節(jié)點(diǎn)元素婉宰,屬性名(‘name’) 當(dāng)前的vue實(shí)例對象
- 將自身在觸發(fā)get()方法時添加到Dep中的list數(shù)組后清空
- 初始化調(diào)用update()方法實(shí)現(xiàn)視圖更新
Watcher.js
import Dep from './Dep'
//頁面上所有訂閱數(shù)據(jù)的地方 (watcher 與頁面的表達(dá)式一一對應(yīng))
class Watcher {
constructor(node, name, vm) {
this.node = node
this.name = name
this.vm = vm
//將Dep.target 賦值為 this (Watcher的示例對象)
Dep.target = this
//調(diào)用update() 方法更新節(jié)點(diǎn)數(shù)據(jù)
this.update()
Dep.target = null
}
update() {
//將當(dāng)前的標(biāo)簽節(jié)點(diǎn)下的值改成 vm對象中對應(yīng)屬性的值
//觸發(fā)Observer中對MVVM對象監(jiān)聽的name屬性添加的get屬性 從而將當(dāng)前的{{}}的表達(dá)式注入到 Dep中
this.node.nodeValue = this.vm[this.name]
}
}
export default Watcher
vue實(shí)例對象
main.js入口文件
import Vue from './vue'
const MVVM = new Vue({
el: "#app",
data: {
name: 'hello world',
}
})
window.MVVM = MVVM
HTML代碼
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="name" />
{{ name }}
</div>
<script src="./dist/main.js"></script>
</body>
</html>
總結(jié)
至此就實(shí)現(xiàn)了一個簡單的數(shù)據(jù)雙向綁定的例子,主要的目的是為了更好的理解雙向綁定的實(shí)現(xiàn)原理與設(shè)計思想