Vue 響應式原理模擬
一、三個概念
1. 數據驅動
-
數據響應式
-
數據模型僅僅是普通的 JavaScript 對象,而當我們修改數據時,視圖會進行更新,避免了繁瑣的 DOM 操作,提高開發效率
-
雙向綁定
-
數據改變,視圖改變;視圖改變,數據也隨之改變
-
我們可以使用 v-model 在表單元素上創建雙向數據綁定
-
數據驅動是 Vue 最獨特的特性之一
-
開發過程中僅需要關注數據本身,不需要關心數據是如何渲染到視圖
2. 數據響應式核心原理
- Vue 2.x
// 模擬 Vue 中的 data 選項
let data = {
msg: 'hello'
}
// 模擬 Vue 的實例
let vm = {}
// 數據劫持:當訪問或者設置 vm 中的成員的時候,做一些干預操作
Object.defineProperty(vm, 'msg', {
// 可枚舉(可遍歷)
enumerable: true,
// 可配置(可以使用 delete 刪除,可以通過 defineProperty 重新定義)
configurable: true,
// 當獲取值的時候執行
get () {
console.log('get: ', data.msg)
return data.msg
},
// 當設置值的時候執行
set (newValue) {
console.log('set: ', newValue)
if (newValue === data.msg) {
return
}
data.msg = newValue
// 數據更改,更新 DOM 的值
document.querySelector('#app').textContent = data.msg
}
})
// 測試
vm.msg = 'Hello World'
console.log(vm.msg)
- Vue 3.x
// 模擬 Vue 中的 data 選項
let data = {
msg: 'hello',
count: 0
}
// 模擬 Vue 實例
let vm = new Proxy(data, {
// 當訪問 vm 的成員會執行
get (target, key) {
console.log('get, key: ', key, target[key])
return target[key]
},
// 當設置 vm 的成員會執行
set (target, key, newValue) {
console.log('set, key: ', key, newValue)
if (target[key] === newValue) {
return
}
target[key] = newValue
document.querySelector('#app').textContent = target[key]
}
})
// 測試
vm.msg = 'Hello World'
console.log(vm.msg)
3. 發佈訂閱模式和觀察者模式
Ⅰ. 發佈訂閱模式
-
發佈 / 訂閱模式
-
訂閱者
-
發佈者
-
信號中心
我們假定,存在一個 "信號中心",某個任務執行完成,就向信號中心 "發佈"(publish)一個信號,其他任務可以向信號中心 "訂閱"(subscribe)這個信號,從而知道什麼時候自己可以開始執行。這就叫做 "發佈 / 訂閱模式"(publish-subscribe pattern)
- Vue 的自定義事件
let vm = new Vue()
vm.$on('dataChange', () => {
console.log('dataChange')
})
vm.$on('dataChange', () => {
console.log('dataChange1')
})
vm.$emit('dataChange')
- 兄弟組件通信過程
// eventBus.js
// 事件中心
let eventHub = new Vue()
// ComponentA.vue
// 發佈者
addTodo: function () {
// 發佈消息(事件)
eventHub.$emit('add-todo', { text: this.newTodoText })
this.newTodoText = ''
}
// ComponentB.vue
// 訂閱者
created: function () {
// 訂閱消息(事件)
eventHub.$on('add-todo', this.addTodo)
}
- 模擬 Vue 自定義事件的實現
class EventEmitter {
constructor () {
// { eventType: [ handler1, handler2 ] }
this.subs = {}
}
// 訂閱通知
$on (eventType, handler) {
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
}
// 發佈通知
$emit (eventType) {
if (this.subs[eventType]) {
this.subs[eventType].forEach(handler => {
handler()
})
}
}
}
// 測試
var bus = new EventEmitter()
// 註冊事件
bus.$on('click', function () {
console.log('click')
})
bus.$on('click', function () {
console.log('click1')
})
// 觸發事件
bus.$emit('click')
Ⅱ. 觀察者模式
-
觀察者 (訂閱者) -- Watcher
-
update():當事件發生時,具體要做的事情
-
目標 (發佈者) -- Dep
-
subs 數組:存儲所有的觀察者
-
addSub():添加觀察者
-
notify():當事件發生,調用所有觀察者的 update() 方法
-
沒有事件中心
// 目標(發佈者)
// Dependency
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()
Ⅲ. 總結
-
觀察者模式是由具體目標調度,比如當事件觸發,Dep 就會去調用觀察者的方法,所以觀察者模式的訂閱者與發佈者之間是存在依賴的。
-
發佈 / 訂閱模式由統一調度中心調用,因此發佈者和訂閱者不需要知道對方的存在。
image.png
二、Vue 響應式原理模擬
1. Vue
-
功能
-
負責接收初始化的參數 (選項)
-
負責把 data 中的屬性注入到 Vue 實例,轉換成 getter/setter
-
負責調用 observer 監聽 data 中所有屬性的變化
-
負責調用 compiler 解析指令 / 插值表達式
-
結構
image.png
- 實現
class Vue {
constructor (options) {
// 1. 保存選項的數據
this.$options = options || {}
this.$data = options.data || {}
const el = options.el
this.$el = typeof options.el === 'string' ? document.querySelector(el) : el
// 2. 負責把 data 注入到 Vue 實例
this._proxyData(this.$data)
// 3. 負責調用 Observer 實現數據劫持
new Observer(this.$data)
// 4. 負責調用 Compiler 解析指令/插值表達式等
new Compiler(this)
}
_proxyData (data) {
// 遍歷 data 的所有屬性
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get () {
return data[key]
},
set (newValue) {
if (data[key] === newValue) {
return
}
data[key] = newValue
}
})
})
}
}
2. Observer
-
功能
-
負責把 data 選項中的屬性轉換成響應式數據
-
data 中的某個屬性也是對象,把該屬性轉換成響應式數據
-
數據變化發送通知
-
結構
image.png
- 實現
// 負責數據劫持
// 把 $data 中的成員轉換成 getter/setter
class Observer {
constructor(data) {
this.walk(data)
}
// 1. 判斷數據是否是對象,如果不是對象返回
// 2. 如果是對象,遍歷對象的所有屬性,設置爲 getter/setter
walk(data) {
if (!data || typeof data !== 'object') {
return
}
// 遍歷 data 的所有成員
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
// 定義響應式成員
defineReactive(data, key, val) {
const that = this
// 如果 val 是對象,繼續設置它下面的成員爲響應式數據
this.walk(val)
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
return val
},
set(newValue) {
if (newValue === val) {
return
}
// 如果 newValue 是對象,設置 newValue 的成員爲響應式
that.walk(newValue)
val = newValue
}
})
}
}
3. Compiler
-
功能
-
負責編譯模板,解析指令 / 插值表達式
-
負責頁面的首次渲染
-
當數據變化後重新渲染視圖
-
結構
image.png
- 實現
① compile()
// 負責解析指令/插值表達式
class Compiler {
constructor(vm) {
this.vm = vm
this.el = vm.$el
// 編譯模板
this.compile(this.el)
}
// 編譯模板
// 處理文本節點和元素節點
compile(el) {
const nodes = el.childNodes
Array.from(nodes).forEach(node => {
// 判斷是文本節點還是元素節點
if (this.isTextNode(node)) {
this.compileText(node)
} else if (this.isElementNode(node)) {
this.compileElement(node)
}
if (node.childNodes && node.childNodes.length) {
// 如果當前節點中還有子節點,遞歸編譯
this.compile(node)
}
})
}
// 判斷是否是文本節點
isTextNode(node) {
return node.nodeType === 3
}
// 判斷是否是屬性節點
isElementNode(node) {
return node.nodeType === 1
}
// 判斷是否是以 v- 開頭的指令
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 編譯文本節點
compileText(node) {
}
// 編譯屬性節點
compileElement(node) {
}
}
② compileText()
// 編譯文本節點
compileText(node) {
const reg = /\{\{(.+)\}\}/
// 獲取文本節點的內容
const value = node.textContent
if (reg.test(value)) {
// 插值表達式中的值就是我們要的屬性名稱
const key = RegExp.$1.trim()
// 把插值表達式替換成具體的值
node.textContent = value.replace(reg, this.vm[key])
}
}
③ compileElement()
-
負責編譯元素的指令
-
處理 v-text 的首次渲染
-
處理 v-model 的首次渲染
// 編譯屬性節點
compileElement(node) {
// 遍歷元素節點中的所有屬性,找到指令
Array.from(node.attributes).forEach(attr => {
// 獲取元素屬性的名稱
let attrName = attr.name
// 判斷當前的屬性名稱是否是指令
if (this.isDirective(attrName)) {
// attrName 的形式 v-text v-model
// 截取屬性的名稱,獲取 text model
attrName = attrName.substr(2)
// 獲取屬性的名稱,屬性的名稱就是我們數據對象的屬性 v-text="name",獲取的是name
const key = attr.value
// 處理不同的指令
this.update(node, key, attrName)
}
})
}
// 負責更新 DOM
// 創建 Watcher
update(node, key, dir) {
// node 節點,key 數據的屬性名稱,dir 指令的後半部分
const updaterFn = this[dir + 'Updater']
updaterFn && updaterFn(node, this.vm[key])
}
// v-text 指令的更新方法
textUpdater(node, value) {
node.textContent = value
}
// v-model 指令的更新方法
modelUpdater(node, value) {
node.value = value
}
4. Dep
-
功能
-
收集依賴,添加觀察者 (watcher)
-
通知所有觀察者
-
結構
image.png
- 實現
class Dep {
constructor() {
// 存儲所有的觀察者
this.subs = []
}
// 添加觀察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 通知所有觀察者
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 以下代碼在 Observer 類中 defineReactive 方法中添加
// 創建 dep 對象收集依賴
const dep = new Dep()
// getter 中
// get 的過程中收集依賴
Dep.target && dep.addSub(Dep.target)
// setter 中
// 當數據變化之後,發送通知
dep.notify()
5. Watcher
-
功能
-
當數據變化觸發依賴, dep 通知所有的 Watcher 實例更新視圖
-
自身實例化的時候往 dep 對象中添加自己
-
結構
image.png
- 實現
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
// data 中的屬性名稱
this.key = key
// 當數據變化的時候,調用 cb 更新視圖
this.cb = cb
// 在 Dep 的靜態屬性上記錄當前 watcher 對象,當訪問數據的時候把 watcher 添加到dep 的 subs 中
Dep.target = this
// 觸發一次 getter,讓 dep 爲當前 key 記錄 watcher
this.oldValue = vm[key]
// 清空 target
Dep.target = null
}
update() {
const newValue = this.vm[this.key]
if (this.oldValue === newValue) {
return
}
this.cb(newValue)
}
}
// 在 compiler.js(即Compiler類) 中爲每一個指令/插值表達式創建 watcher 對象,監視數據的變化
compileText(node) {
const reg = /\{\{(.+?)\}\}/
const value = node.textContent
if (reg.test(value)) {
const key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
// 編譯差值表達式中創建一個 watcher,觀察數據的變化
new Watcher(this.vm, key, newValue => {
node.textContent = newValue
})
}
}
// 因爲在 textUpdater等中要使用 this
updaterFn && updaterFn.call(this, node, this.vm[key], key)
// v-text 指令的更新方法
textUpdater(node, value, key) {
node.textContent = value
// 每一個指令中創建一個 watcher,觀察數據的變化
new Watcher(this.vm, key, value => {
node.textContent = value
})
}
// 視圖變化更新數據
// v-model 指令的更新方法
modelUpdater(node, value, key) {
node.value = value
// 每一個指令中創建一個 watcher,觀察數據的變化
new Watcher(this.vm, key, value => {
node.value = value
})
// 監聽視圖的變化
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
三、總結
1. 兩個問題你會了嗎
-
給屬性重新賦值成對象,是否是響應式的?
-
給 Vue 實例新增一個成員是否是響應式的?
2. 通過下圖回顧整體流程
image.png
3. Vue
-
記錄傳入的選項,設置 data/data/data/el
-
把 data 的成員注入到 Vue 實例
-
負責調用 Observer 實現數據響應式處理(數據劫持)
-
負責調用 Compiler 編譯指令 / 插值表達式等
4. Observer
-
數據劫持
-
負責把 data 中的成員轉換成 getter/setter
-
負責把多層屬性轉換成 getter/setter
-
如果給屬性賦值爲新對象,把新對象的成員設置爲 getter/setter
-
添加 Dep 和 Watcher 的依賴關係
-
數據變化發送通知
5. Compiler
-
負責編譯模板,解析指令 / 插值表達式
-
負責頁面的首次渲染過程
-
當數據變化後重新渲染
6. Dep
-
收集依賴,添加訂閱者 (watcher)
-
通知所有訂閱者
7. Watcher
-
自身實例化的時候往 dep 對象中添加自己
-
當數據變化 dep 通知所有的 Watcher 實例更新視圖
關於本文
作者:昆蘭
https://juejin.cn/post/6946120511713705992
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/8QWMOaGIN0yKgbl2yMuj6w