模擬Vue.js響應(yīng)式原理
數(shù)據(jù)驅(qū)動
- 數(shù)據(jù)響應(yīng)式
- 數(shù)據(jù)模型是普通的JavaScript對象,當(dāng)我們修改數(shù)據(jù)時,視圖會進(jìn)行相應(yīng)的更新,避免了繁瑣的DOM操作,提高開發(fā)效率
- 雙向綁定
- 數(shù)據(jù)改變,視圖發(fā)生相應(yīng)變化,視圖變化,數(shù)據(jù)發(fā)生相應(yīng)的變化
- 可以使用v-model指令在表單元素上創(chuàng)建雙向數(shù)據(jù)綁定(自定義組件也可以自己實現(xiàn)v-model)
- 數(shù)據(jù)驅(qū)動
- 數(shù)據(jù)驅(qū)動是Vue最獨特的特性之一,開發(fā)過程只需要關(guān)心數(shù)據(jù),而不需要關(guān)心數(shù)據(jù)如何被渲染到視圖
數(shù)據(jù)響應(yīng)式核心原理
Vue2.x
- 遍歷對象中的屬性,并通過
Object.defineProperty
方法,將對象中的屬性,轉(zhuǎn)換成getter/setter方法 -
Object.defineProperty
是ES5中新增的,不支持IE8以下瀏覽器
Vue3.x
- 基于ES6新增的
Proxy
來實現(xiàn) -
Proxy
代理的是整個對象,而不是對象的屬性,因此不需要對對象的屬性進(jìn)行遍歷 -
Proxy
的性能由瀏覽器優(yōu)化,要優(yōu)于Object.defineProperty
- 同樣不支持IE8以下瀏覽器
發(fā)布訂閱模式和觀察者模式
發(fā)布訂閱模式
發(fā)布訂閱模式包含發(fā)布者、訂閱者、消息中心,發(fā)布者與訂閱者相互之間不知道彼此存在,通過消息中心進(jìn)行消息轉(zhuǎn)發(fā)
發(fā)布者
訂閱者
消息中心broker
-
模擬實現(xiàn)代碼
class EventEmitter { constructor() { this.subs = Object.create(null) } // 訂閱 - 注冊事件 $on (topic, handler) { this.subs[topic] = this.subs[topic] || [] this.subs[topic].push(handler) } // 發(fā)布 - 觸發(fā)事件 $emit (topic, ...args) { this.subs[topic]?.forEach(handler => { typeof handler === 'function' && handler.apply(null, args) }); } }
觀察者模式
觀察者模式包含觀察者(訂閱者)和目標(biāo)(發(fā)布者),不存在消息中心,被觀察的目標(biāo)需要知道觀察者的存在
-
觀察者(訂閱者)Watcher
- update方法:處理函數(shù)
-
目標(biāo)(發(fā)布者)
- notify方法:通知觀察者,調(diào)用所有觀察者的update方法
- subs數(shù)組:儲存所有的觀察者
- addSub:添加觀察者
-
模擬實現(xiàn)代碼
export class Target { constructor() { this.subs = [] } addSub(sub) { sub && typeof sub.update === 'function' && this.subs.push(sub) } notify(...args) { this.subs.forEach(sub => sub.update.apply(null, args)) } } export class Watcher { update(...args) { console.log(args) } }
發(fā)布訂閱模式 vs 觀察者模式
image-20201109182709237
模擬Vue.js響應(yīng)式原理
Vue類的簡單實現(xiàn)
-
功能
- 負(fù)責(zé)接收初始化的參數(shù)(選項)
- 負(fù)責(zé)把data中的屬性轉(zhuǎn)換成getter/setter,并注入到Vue實例中
- 負(fù)責(zé)調(diào)用observer監(jiān)聽data中所有屬性的變化
- 負(fù)責(zé)調(diào)用compiler解析指令/差值表達(dá)式
-
屬性
- $options
- 保存?zhèn)魅氲倪x項
- $data
- 保存?zhèn)魅氲膁ata
- $el
- 保存掛載的DOM元素
- $options
-
方法
- _proxyData
- 把data中的屬性轉(zhuǎn)換成getter/setter,并注入到Vue實例中
- _proxyData
-
實現(xiàn)代碼
export default class Vue { // 1. 通過屬性保存選項的數(shù)據(jù) constructor(options) { this.$options = options || {} this.$data = options.data || {} this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el this._proxyData(this.$data) } // 2. 把data中的屬性轉(zhuǎn)換成getter/setter注入到vue實例中 _proxyData(data) { // Vue2.x處理方式 Object.keys(data).forEach(key => { Object.defineProperty(this, key, { enumerable: true, configurable: true, get() { return data[key] }, set(newValue) { if (data[key] === newValue) { return } data[key] = newValue }, }) }) } // 3. 調(diào)用observer對象,監(jiān)聽數(shù)據(jù)變化 new Observer(this.$data) // 4. 調(diào)用compiler對象,解析指令和差值表達(dá)式 new Compiler(this) }
Observer的簡單實現(xiàn)
-
功能
- 負(fù)責(zé)把data中的數(shù)據(jù)轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
- data中的某個屬性也是對象,把該屬性轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
- 數(shù)據(jù)變化發(fā)送通知
-
方法
- walk(data)
- 遍歷data中的所有屬性
- defineReactive(data, key, value)
- 轉(zhuǎn)換屬性為getter/setter
- walk(data)
-
實現(xiàn)代碼
export default class Observer { constructor(data) { this.walk(data) } walk(data) { // 1. 判斷data是否是對象 // 2. 遍歷data的所有屬性 if (!data || typeof data !== 'object') { return } Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } defineReactive(data, key, value) { // 為什么需要第三個參數(shù)傳遞value,而不直接使用data[key]? // 在get中使用data[key]會循環(huán)觸發(fā)getter,導(dǎo)致棧溢出 // 為什么value屬性在defineProperty執(zhí)行完成后還可以被訪問 // 外部對內(nèi)部屬性存在引用,形成了閉包 // 如果value是對象,則將對象的屬性也轉(zhuǎn)換成響應(yīng)式數(shù)據(jù) const _this = this // 創(chuàng)建Dep對象收集依賴,發(fā)送通知 let dep = new Dep() this.walk(value) Object.defineProperty(data, key, { configurable: true, enumerable: true, get() { // 收集依賴 Dep.target && dep.addSub(Dep.target) return value }, set(newValue) { if (value === newValue) { return } value = newValue // 當(dāng)屬性被重新賦值為一個對象,將對象屬性也轉(zhuǎn)換為響應(yīng)式數(shù)據(jù) _this.walk(newValue) dep.notify() } }) } }
Compiler的簡單實現(xiàn)
-
功能
- 負(fù)責(zé)編譯模板,解析指令/差值表達(dá)式
- 負(fù)責(zé)頁面的首次渲染
- 當(dāng)數(shù)據(jù)變化后,重新渲染視圖
-
屬性
- el
- DOM對象
- vm
- vue實例
- el
-
方法
compiler的方法主要用來進(jìn)行DOM操作
- compile(el)
- 編譯解析入口
- compileElement(node)
- 解析元素節(jié)點的指令
- compileText(node)
- 解析文本節(jié)點的差值表達(dá)式
- isDirective(attrName)
- 判斷屬性是否是指令
- isElementNode(node)
- 判斷是否是元素節(jié)點
- isTextNode(node)
- 判斷是否文本節(jié)點
- compile(el)
-
實現(xiàn)代碼
import Watcher from "./watcher.js" export default class Compiler { constructor(vm) { this.el = vm.$el this.vm = vm this.compile(this.el) } // 編譯模板,處理文本節(jié)點和元素節(jié)點 compile (el) { [...el.childNodes].forEach(node => { if (this.isTextNode(node)) { this.compileText(node) } if (this.isElementNode(node)) { this.compileElement(node) } // 判斷childNodes并遞歸調(diào)用compile if (node.childNodes && node.childNodes.length) { this.compile(node) } }) } // 編譯元素節(jié)點,處理指令 compileElement (node) { // 獲取并遍歷元素的屬性節(jié)點 [...node.attributes].forEach(attr => { const { name, value } = attr if (this.isDirective(name)) { this.update(node, name.slice(2), value) } }) } // 處理指令 update (node, name, value) { const fn = this[`${name}Updater`] typeof fn === 'function' && fn.call(this, node, value) } // 處理v-text指令 textUpdater(node, value) { node.textContent = this.vm[value] // 創(chuàng)建watcher對象,當(dāng)數(shù)據(jù)改變時更新視圖 new Watcher(this.vm, value, newValue => { node.textContent = newValue }) } // 處理v-model指令 modelUpdater(node, value) { node.value = this.vm[value] // 創(chuàng)建watcher對象,當(dāng)數(shù)據(jù)改變時更新視圖 new Watcher(this.vm, value, newValue => { node.value = newValue }) // 注冊表單input事件實現(xiàn)雙向綁定 node.addEventListener('input', () => { this.vm[value] = node.value }) } // 處理其他指令 // ... // 編譯文本節(jié)點,處理差值表達(dá)式 compileText (node) { // 使用正則表達(dá)式匹配差值表達(dá)式 {{ xxx }},并提取獲取表達(dá)式內(nèi)容 let reg = /\{\{(.+?)\}\}/ const text = node.textContent node.textContent = text.replace(reg, (word, key) => { key = key.trim() if (key in this.vm) { // 創(chuàng)建watcher對象,當(dāng)數(shù)據(jù)改變時更新視圖 new Watcher(this.vm, key, newValue => { node.textContent = text.replace(reg, (w, k) => this.vm[k.trim()]) }) return this.vm[key] } return word }) } // 判斷屬性是否為指令 isDirective (attrName) { return attrName.startsWith('v-') } // 判斷是否為元素節(jié)點 isElementNode (node) { return node.nodeType === 1 } // 判斷是否為文本節(jié)點 isTextNode (node) { return node.nodeType === 3 } }
Dep的簡單實現(xiàn)
-
功能
- 收集依賴,添加觀察者(watcher)
- notify通知所有觀察者
-
屬性
- subs
- 儲存所有的觀察者
- subs
-
方法
- addSubs(sub)
- 添加觀察者
- notify()
- 通知觀察者,調(diào)用所有觀察者的update方法
- addSubs(sub)
-
實現(xiàn)代碼
export default class Dep { constructor() { this.subs = [] } addSub(sub) { sub && typeof sub.update === 'function' && this.subs.push(sub) } notify(...args) { this.subs.forEach(sub => sub.update(...args)) } }
Watcher的簡單實現(xiàn)
-
功能
- 數(shù)據(jù)變化觸發(fā)依賴,接收dep通知更新視圖
- 自身實例化的時候向Dep中添加自己
-
屬性
- vm
- vue實例
- key
- 觀察的屬性名稱
- cb
- 更新時的回調(diào)處理函數(shù)
- oldValue
- 觀察的屬性數(shù)據(jù)更新之前的值
- vm
-
方法
- update()
- 更新處理函數(shù)
- update()
-
實現(xiàn)代碼
import Dep from "./dep.js" export default class Watcher { constructor(vm, key, cb) { this.vm = vm this.key = key this.cb = cb // 把watcher對象記錄到Dep類的靜態(tài)屬性target Dep.target = this // 觸發(fā)get方法,在get中調(diào)用addSub this.oldValue = vm[key] // 清空Dep.target,避免重復(fù)添加 Dep.target = null } // 數(shù)據(jù)變化時更新視圖 update(...args) { const newValue = this.vm[this.key] if (newValue !== this.oldValue) { this.cb(newValue) } } }
總結(jié)
問題
- 將屬性重新賦值成對象,是否是響應(yīng)式的?
- 是響應(yīng)式的,重新賦值成對象時會調(diào)用屬性的set方法,此時會將新賦值的內(nèi)容轉(zhuǎn)換為響應(yīng)式數(shù)據(jù)
- 為vue實例添加新的屬性時,此屬性是否是響應(yīng)式的?
- 不是響應(yīng)式的
- 可以通過Vue.set()或vm.$set()方法設(shè)置新的響應(yīng)式屬性
整體流程
?image-20201201234840455