模擬 Vue.js 響應(yīng)式原理
一、數(shù)據(jù)驅(qū)動
準(zhǔn)備工作
- 數(shù)據(jù)驅(qū)動
- 響應(yīng)式的核心原理
- 發(fā)布訂閱模式和觀察者模式
數(shù)據(jù)驅(qū)動
- 數(shù)據(jù)響應(yīng)式、雙向綁定、數(shù)據(jù)驅(qū)動
- 數(shù)據(jù)響應(yīng)式
- 數(shù)據(jù)模型僅僅是普通的JavaScript對象,而當(dāng)我們修改數(shù)據(jù)時(shí)候,視圖會進(jìn)行更新,避免了繁瑣的DOM操作,提高開發(fā)效率
- 雙向綁定
- 數(shù)據(jù)改變,視圖改變;視圖改變,數(shù)據(jù)也隨之改變
- 我們可以使用v-model在表單元素上創(chuàng)建數(shù)據(jù)綁定
- 數(shù)據(jù)驅(qū)動是Vue最獨(dú)特的特性之一
- 開發(fā)過程中僅需要關(guān)注數(shù)據(jù)本身,不需要關(guān)心數(shù)據(jù)是如何渲染到視圖的
二、數(shù)據(jù)響應(yīng)式核心原理 -- Vue2 ------Object.defineProperty
Vue2.x
- Vue 2.x 深入響應(yīng)式原理:
https://cn.vuejs.org/v2/guide/reactivity.html - MDN - Object.defineProperty
- 瀏覽器兼容IE8以上(不兼容IE8)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>defineProperty</title>
</head>
<body>
<div id="app">
hello
</div>
<script>
// 模擬Vue中的data選項(xiàng)
let data = {
msg: 'hello'
}
// 模擬Vue 實(shí)例
let vm = {}
// 數(shù)據(jù)劫持:當(dāng)訪問或者設(shè)置vm中的成員的時(shí)候,做一些干預(yù)操作
Object.defineProperty(vm, 'msg', {
// 是否可枚舉(可遍歷)
enumerable: true,
// 可配置(可以使用delete刪除,可以通過defineProperty 重新定義)
configurable: true,
// 當(dāng)獲取值得時(shí)候執(zhí)行
get() {
console.log('get:' + data.msg);
return data.msg
},
// 當(dāng)設(shè)置值得時(shí)候執(zhí)行
set(newValue) {
console.log('set:'+newValue);
if (newValue === data.msg) return;
data.msg = newValue
// 數(shù)據(jù)更改
document.querySelector('#app').textContent = data.msg
}
})
// 測試
vm.msg = 'Hello World'
console.log(vm.msg);
</script>
</body>
</html>
- 如果有一個(gè)對象中多個(gè)屬性需要轉(zhuǎn)換getter/setter 改如何處理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>defineProperty</title>
</head>
<body>
<div id="app">
hello
</div>
<script>
// 模擬Vue中的data選項(xiàng)
let data = {
msg: 'hello',
count: 10
}
// 模擬Vue 實(shí)例
let vm = {}
proxyData(data)
function proxyData(data) {
Object.keys(data).forEach(key => {
// 數(shù)據(jù)劫持:當(dāng)訪問或者設(shè)置vm中的成員的時(shí)候,做一些干預(yù)操作
Object.defineProperty(vm, [key], {
// 是否可枚舉(可遍歷)
enumerable: true,
// 可配置(可以使用delete刪除,可以通過defineProperty 重新定義)
configurable: true,
// 當(dāng)獲取值得時(shí)候執(zhí)行
get() {
console.log('get:' + key, data[key]);
return data[key]
},
// 當(dāng)設(shè)置值得時(shí)候執(zhí)行
set(newValue) {
console.log('set:' + key, newValue);
if (newValue === data[key]) return;
data[key] = newValue
// 數(shù)據(jù)更改
document.querySelector('#app').textContent = data[key]
}
})
})
}
// 測試
vm.msg = 'Hello World'
console.log(vm.msg);
</script>
</body>
</html>
三、 數(shù)據(jù)響應(yīng)式核心原理 -- Vue3 ----- Proxy
- MDN-Proxy:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
- 直接監(jiān)聽對象,而非屬性
- ES6 中新增,IE不支持,性能由瀏覽器優(yōu)化
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>defineProperty</title>
</head>
<body>
<div id="app">
hello
</div>
<!-- vue2.x 數(shù)據(jù)響應(yīng)式核心原理 -->
<!-- <script>
// 模擬Vue中的data選項(xiàng)
let data = {
msg: 'hello',
count: 10
}
// 模擬Vue 實(shí)例
let vm = {}
proxyData(data)
function proxyData(data) {
Object.keys(data).forEach(key => {
// 數(shù)據(jù)劫持:當(dāng)訪問或者設(shè)置vm中的成員的時(shí)候,做一些干預(yù)操作
Object.defineProperty(vm, [key], {
// 是否可枚舉(可遍歷)
enumerable: true,
// 可配置(可以使用delete刪除,可以通過defineProperty 重新定義)
configurable: true,
// 當(dāng)獲取值得時(shí)候執(zhí)行
get() {
console.log('get:' + key, data[key]);
return data[key]
},
// 當(dāng)設(shè)置值得時(shí)候執(zhí)行
set(newValue) {
console.log('set:' + key, newValue);
if (newValue === data[key]) return;
data[key] = newValue
// 數(shù)據(jù)更改
document.querySelector('#app').textContent = data[key]
}
})
})
}
// 測試
vm.msg = 'Hello World'
console.log(vm.msg);
</script> -->
<!-- vue3.x 數(shù)據(jù)響應(yīng)式核心原理 -->
<script>
// 模擬Vue中的data選項(xiàng)
let data = {
msg: 'hello',
count: 10
}
// 模擬Vue 實(shí)例
let vm = new Proxy(data, {// 執(zhí)行代理行為的函數(shù)
// 當(dāng)訪問vm的成員會執(zhí)行
get(target, key) {
console.log('get,key:' + key, target[key]);
return target[key]
},
// 當(dāng)設(shè)置vm的成員會執(zhí)行
set(target, key, newValue) {
console.log('set,key:' + key, newValue);
if (newValue === target[key]) return;
target[key] = newValue
// 數(shù)據(jù)更改
document.querySelector('#app').textContent = target[key]
}
})
// 測試
vm.msg = 'Hello World'
console.log('789'+vm.msg);
</script>
</body>
</html>
四、發(fā)布訂閱模式
發(fā)布訂閱模式和觀察者模式,是兩種設(shè)計(jì)模式,在 Vue 中有各自的應(yīng)用場景,本質(zhì)相同,但存在一定的區(qū)別
- 發(fā)布/訂閱模式
- 訂閱者
- 發(fā)布者
- 信號中心
我們假定,存在一個(gè)“信號中心”,某個(gè)任務(wù)執(zhí)行完成,就向信號中心“發(fā)布”(publish)一個(gè)信號,其他任務(wù)可以向信號中心“訂閱”(subscribe)這個(gè)信號,從而知道什么時(shí)候自己可以開始執(zhí)行。這就叫做“發(fā)布/訂閱模式(publish-subscribe pattern)”
- Vue 的自定義事件
// Vue 自定義事件
let vm = new Vue()
// 注冊事件(訂閱消息)
vm.$on( 'dataChange', () => {
console.log( 'dataChange' )
} )
vm.$on( 'dataChange', () => {
console.log( 'dataChange1' )
} )
// 觸發(fā)事件
vm.$emit('dataChange')
- 兄弟組件通信過程
//eventBus.js
// 事件中心
let eventHub = new Vue()
// ComponentA.vue
// 發(fā)布者
addTodo: function () {
eventHub.$emit( 'add-todo', { text: this.newTodoText } )
this.newTodoText = ''
}
// ComponentB.vue
// 訂閱者
created: function() {
// 訂閱消息(事件)
eventHub.$on( 'add-todo', this.addTodo )
}
- 發(fā)布訂閱模式
// 事件觸發(fā)器
class EventEmitter {
constructor() {
// {'click':[fn1,fn2],'change':[fn]}
this.subs = Object.create( null )
}
// 注冊事件
$on( eventType, handler ) {
this.subs[ eventType ] = this.subs[ eventType ] || []
this.subs[ eventType ].push( handler )
}
// 觸發(fā)事件
$emit( eventType ) {
if ( this.subs[ eventType ] ) {
this.subs[ eventType ].forEach( handler => {
handler()
} )
}
}
}
// 測試
let em = new EventEmitter()
em.$on('click',()=>{
console.log('click1');
})
em.$on('click',()=>{
console.log('click2');
})
em.$emit('click')
五、觀察者模式
Vue 的響應(yīng)式機(jī)制中使用了觀察者模式,所以要了解觀察者模式是如何實(shí)現(xiàn)的,觀察者模式和發(fā)布訂閱者模式的區(qū)別是沒有事件中心,只有發(fā)布者和訂閱者,并且發(fā)布者要知道訂閱者的存在。
- 觀察者(訂閱者)-- Watcher
- update():當(dāng)事件發(fā)生時(shí),具體要做的事情,由發(fā)布者調(diào)用
- 目標(biāo)(發(fā)布者) -- Dep 當(dāng)事件發(fā)生的時(shí)候,由發(fā)布者通知訂閱者
- subs數(shù)組:存儲所有觀察者
- addSub():添加觀察者
- notify():當(dāng)事件發(fā)生,調(diào)用所有觀察者的update()方法
- 觀察者模式?jīng)]有事件中心
<script>
// 發(fā)布者- 目標(biāo)
class Dep {
constructor() {
// 記錄所有訂閱者
this.subs = []
}
// 添加觀察者
addSub( sub ) {
if ( sub && sub.update ) {
this.subs.push( sub )
}
}
notify() {
this.subs.forEach( sub => {
sub.update()
} )
}
}
// 訂閱者 - 觀察者
class Watcher {
update() {
console.log( 'update' )
}
}
// 測試
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)
dep.notify()
</script>
總結(jié)
- 觀察者模式是由具體目標(biāo)調(diào)度,比如當(dāng)事件觸發(fā),Dep就會去調(diào)用觀察者的方法,所以觀察者模式的訂閱者與發(fā)布者之間是存在依賴的
- 發(fā)布/訂閱模式是由統(tǒng)一調(diào)度中心調(diào)用,因此發(fā)布者和訂閱者不需要知道對方的存在

image.png
六、模擬Vue響應(yīng)式原理 - 分析
整體分析:
- Vue基本結(jié)構(gòu)
- 打印Vue 實(shí)例觀察
- 整體結(jié)構(gòu)

image.png
- Vue
- 把data中的成員注入到Vue實(shí)例,并且把data中的成員轉(zhuǎn)換成getter和setter,Vue 內(nèi)部會調(diào)用Observer和Compiler
- Observer
- 能夠?qū)?shù)據(jù)對象的所有屬性進(jìn)行監(jiān)聽,如果有變動可拿到最新值并通知Dep
- Compiler
- 解析每個(gè)元素中的指令,以及插值表達(dá)式,并替換成相應(yīng)的數(shù)據(jù)
- Dep 發(fā)布者
- 添加觀察者,當(dāng)數(shù)據(jù)發(fā)生變化的時(shí)候,通知所有觀察者
- Watcher 觀察者
- Watcher 內(nèi)部有update方法負(fù)責(zé)更新視圖
七、Vue
- 功能
- 負(fù)責(zé)接收初始化的參數(shù)(選項(xiàng))
- 負(fù)責(zé)把data中的屬性注入到Vue實(shí)例,轉(zhuǎn)換成getter/setter
- 負(fù)責(zé)調(diào)用observer監(jiān)聽所有屬性的變化
- 負(fù)責(zé)調(diào)用compiler 解析指令、插值表達(dá)式
-
結(jié)構(gòu)
類名:Vue
屬性:options 、el、data
方法:_proxyData()
image.png
vue.js
class Vue {
constructor(options) {
// 1、通過屬性保存選項(xiàng)的數(shù)據(jù)
this.$options = options || {}
this.$data = options.data || {}
this.$el =
typeof options.el === 'string'
? document.querySelector(options.el)
: options.el
// 2、把data中的成員轉(zhuǎn)換成getter和setter,注入到vue實(shí)例中
this._proxyData(this.$data)
// 3、調(diào)用observer對象,監(jiān)聽數(shù)據(jù)的變化
// 4、調(diào)用compiler對象,解析指令和插值表達(dá)式
}
_proxyData(data) {
// 遍歷data中的data屬性
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
if (newValue === data[key]) {
return
}
data[key] = newValue
},
})
})
// 把data的屬性注入到vue實(shí)例中
}
}
八、Observer
- 功能
- 負(fù)責(zé)把data選項(xiàng)中的屬性轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
- data中的某個(gè)屬性也是對象,把該屬性轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
- 數(shù)據(jù)變化發(fā)送通知(結(jié)合觀察者模式實(shí)現(xiàn))
- 結(jié)構(gòu)
- walk(data) 用來遍歷所有屬性
-
defineEeactive(data,key,value) 定義響應(yīng)式數(shù)據(jù),通過調(diào)用此方法把屬性轉(zhuǎn)換成getter和setter
image.png
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
// 1、判斷data是否為對象
if (!data || typeof data !== 'object') {
return
}
// 2、遍歷data對象所有屬性
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(obj, key, val) {
let that = this
// 如果val 是對象,把val內(nèi)部的屬性轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
this.walk(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return val
},
set(newValue) {
if (newValue === val) {
return
}
val = newValue
// 將新的值也轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
that.walk(newValue)
},
})
}
}
九、compiler
- 功能
- 負(fù)責(zé)編譯模板,解析指令和插值表達(dá)式
- 負(fù)責(zé)頁面的首次渲染
- 當(dāng)數(shù)據(jù)變化后重新渲染視圖
- 結(jié)構(gòu)
- el
- vm
- compile(el) 遍歷dom對象的所有節(jié)點(diǎn),并且判斷這些節(jié)點(diǎn),如果是文本節(jié)點(diǎn)解析插值表達(dá)式,如果是元素節(jié)點(diǎn)解析指令
- compileElement(node) 如果是元素節(jié)點(diǎn),解析指令
- compileText(node) 如果是文本節(jié)點(diǎn),解析插值表達(dá)式
- isDirective(attrName) 判斷當(dāng)前屬性是否是指令
- isTextNode(node) 判斷是文本節(jié)點(diǎn)
- isElementNode(node) 判斷是元素節(jié)點(diǎn)

image.png
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 編譯模板,處理文本節(jié)點(diǎn)和元素節(jié)點(diǎn)
compile(el) {
let childNodes = el.childNodes
Array.from(childNodes).forEach((node) => {
if (this.isTextNode(node)) {
//判斷是否為文本節(jié)點(diǎn)
this.compileText(node) //處理文本節(jié)點(diǎn)
} else if (this.isElementNode(node)) {
//判斷是否為元素節(jié)點(diǎn)
this.compileElement(node) //處理元素節(jié)點(diǎn)
}
// 判斷node節(jié)點(diǎn)是否有子節(jié)點(diǎn),如果有子節(jié)點(diǎn),要遞歸調(diào)用compiler
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 編譯元素節(jié)點(diǎn),處理指令
compileElement(node) {
console.log(node.attributes)
// 遍歷所有的屬性節(jié)點(diǎn)
Array.from(node.attributes).forEach((attr) => {
// 判斷是否為指令
let attrName = attr.name
if (this.isDirective(attrName)) {
// v-text ---> text
attrName = attrName.substr(2)
let key = attr.value
this.update(node, key, attrName)
}
})
}
// 調(diào)用指令所對應(yīng)的方法
update(node, key, attrName) {
let updateFn = this[attrName + 'Updater']
updateFn && updateFn(node, this.vm[key])
}
// 處理v-text指令
textUpdater(node, value) {
node.textContent = value
}
// 處理v-model
modelUpdater(node, value) {
node.value = value
}
// 編譯文本節(jié)點(diǎn),處理插值表達(dá)式
compileText(node) {
// console.dir(node)
// {{ msg }}
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
}
}
// 判斷元素屬性是否為指令 判斷是否為v- 開頭
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 判斷節(jié)點(diǎn)是否為文本節(jié)點(diǎn)
isTextNode(node) {
return node.nodeType === 3
}
// 判斷節(jié)點(diǎn)是否為元素節(jié)點(diǎn)
isElementNode(node) {
return node.nodeType === 1
}
}
十、Dep
-
Dep(Dependency)
image.png - 功能
- 收集依賴,添加觀察者
- 通知所有觀察者
- 結(jié)構(gòu)
- subs 是一個(gè)數(shù)組,用來存儲Dep中的所有的watcher
- addSub(sub) 添加watcher
-
notify() 發(fā)布通知
image.png
class Dep{
constructor(){
// 存儲所有的觀察者
this.subs = []
}
// 添加觀察者
addSub(sub){
if(sub && sub.update){
this.subs.push(sub)
}
}
// 發(fā)送通知
notify(){
this.subs.forEach(sub =>{
sub.update()
})
}
}
十一、Watcher

image.png
- 功能
- 當(dāng)數(shù)據(jù)變化觸發(fā)依賴,dep通知所有的Watcher實(shí)例更新視圖
- 自身實(shí)例化的時(shí)候往dep對象中添加自己
- 結(jié)構(gòu)
- vm 實(shí)例
- key data中的屬性名稱
- cb 回調(diào)函數(shù)
- oldValue 數(shù)據(jù)變化之前的值
-
update()
image.png
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
// data中的屬性名稱
this.key = key
// 回調(diào)函數(shù)負(fù)責(zé)更新視圖
this.cb = cb
// 要把watcher對象記錄到Dep類的靜態(tài)屬性target上
Dep.target = this
// 觸發(fā)get方法,在get方法中會調(diào)用addSub
this.oldValue = vm[key]
Dep.target = null // 防止重復(fù)添加
}
// 當(dāng)數(shù)據(jù)發(fā)生變化的時(shí)候更新視圖
update() {
let newValue = this.vm[this.key]
if (this.oldValue === newValue) {
return
}
this.cb(newValue)
}
}
十二、問題總結(jié)
-
問題
- 給屬性重新賦值成對象,是否是響應(yīng)式的(是)
-
給Vue實(shí)例新增一個(gè)成員是否是響應(yīng)式的(不是)
image.png
-
回顧整體流程
image.png
十三、完整案例
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mini Vue</title>
</head>
<body>
<div id="app">
<h1>差值表達(dá)式</h1>
<h3>{{ msg }}</h3>
<h3>{{ count }}</h3>
<h1>v-text</h1>
<div v-text="msg"></div>
<h1>v-model</h1>
<input type="text" v-model="msg" />
<input type="text" v-model="count" />
</div>
<script src="js/dep.js"></script>
<script src="js/watcher.js"></script>
<script src="js/compiler.js"></script>
<script src="js/observer.js"></script>
<script src="js/vue.js"></script>
<script>
let vm = new Vue( {
el: '#app',
data: {
msg: 'Hello Vue',
count: 100,
person: {
name: 'jack'
}
}
} )
console.log( vm.msg )
// vm.msg = { text: '123' }
</script>
</body>
</html>
vue.js
class Vue {
constructor(options) {
// 1、通過屬性保存選項(xiàng)的數(shù)據(jù)
this.$options = options || {}
this.$data = options.data || {}
this.$el =
typeof options.el === 'string'
? document.querySelector(options.el)
: options.el
// 2、把data中的成員轉(zhuǎn)換成getter和setter,注入到vue實(shí)例中
this._proxyData(this.$data)
// 3、調(diào)用observer對象,監(jiān)聽數(shù)據(jù)的變化
new Observer(this.$data)
// 4、調(diào)用compiler對象,解析指令和插值表達(dá)式
new Compiler(this)
}
_proxyData(data) {
// 遍歷data中的data屬性
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
if (newValue === data[key]) {
return
}
data[key] = newValue
},
})
})
// 把data的屬性注入到vue實(shí)例中
}
}
observer.js
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
// 1、判斷data是否為對象
if (!data || typeof data !== 'object') {
return
}
// 2、遍歷data對象所有屬性
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(obj, key, val) {
let that = this
// 收集依賴并且發(fā)送通知
let dep = new Dep()
// 如果val 是對象,把val內(nèi)部的屬性轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
this.walk(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target)
return val
},
set(newValue) {
if (newValue === val) {
return
}
val = newValue
// 將新的值也轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
that.walk(newValue)
// 發(fā)送通知
dep.notify()
},
})
}
}
compiler.js
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 編譯模板,處理文本節(jié)點(diǎn)和元素節(jié)點(diǎn)
compile(el) {
let childNodes = el.childNodes
Array.from(childNodes).forEach((node) => {
if (this.isTextNode(node)) {
//判斷是否為文本節(jié)點(diǎn)
this.compileText(node) //處理文本節(jié)點(diǎn)
} else if (this.isElementNode(node)) {
//判斷是否為元素節(jié)點(diǎn)
this.compileElement(node) //處理元素節(jié)點(diǎn)
}
// 判斷node節(jié)點(diǎn)是否有子節(jié)點(diǎn),如果有子節(jié)點(diǎn),要遞歸調(diào)用compiler
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 編譯元素節(jié)點(diǎn),處理指令
compileElement(node) {
console.log(node.attributes)
// 遍歷所有的屬性節(jié)點(diǎn)
Array.from(node.attributes).forEach((attr) => {
// 判斷是否為指令
let attrName = attr.name
if (this.isDirective(attrName)) {
// v-text ---> text
attrName = attrName.substr(2)
let key = attr.value
this.update(node, key, attrName)
}
})
}
// 調(diào)用指令所對應(yīng)的方法
update(node, key, attrName) {
let updateFn = this[attrName + 'Updater']
updateFn && updateFn.call(this, node, this.vm[key], key)
}
// 處理v-text指令
textUpdater(node, value, key) {
node.textContent = value
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// 處理v-model
modelUpdater(node, value, key) {
node.value = value
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// 雙向綁定
node.addEventListener('input',()=>{
this.vm[key] = node.value
})
}
// 編譯文本節(jié)點(diǎn),處理插值表達(dá)式
compileText(node) {
// console.dir(node)
// {{ msg }}
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
// 創(chuàng)建watcher對象,當(dāng)數(shù)據(jù)改變更新視圖
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
// 判斷元素屬性是否為指令 判斷是否為v- 開頭
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 判斷節(jié)點(diǎn)是否為文本節(jié)點(diǎn)
isTextNode(node) {
return node.nodeType === 3
}
// 判斷節(jié)點(diǎn)是否為元素節(jié)點(diǎn)
isElementNode(node) {
return node.nodeType === 1
}
}
dep.js
class Dep{
constructor(){
// 存儲所有的觀察者
this.subs = []
}
// 添加觀察者
addSub(sub){
if(sub && sub.update){
this.subs.push(sub)
}
}
// 發(fā)送通知
notify(){
this.subs.forEach(sub =>{
sub.update()
})
}
}
watcher.js
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
// data中的屬性名稱
this.key = key
// 回調(diào)函數(shù)負(fù)責(zé)更新視圖
this.cb = cb
// 要把watcher對象記錄到Dep類的靜態(tài)屬性target上
Dep.target = this
// 觸發(fā)get方法,在get方法中會調(diào)用addSub
this.oldValue = vm[key]
Dep.target = null // 防止重復(fù)添加
}
// 當(dāng)數(shù)據(jù)發(fā)生變化的時(shí)候更新視圖
update() {
let newValue = this.vm[this.key]
if (this.oldValue === newValue) {
return
}
this.cb(newValue)
}
}






