? 像React,Vue這類的框架,響應式是其最核心的特性之一。通過響應式可以實現當改變數據的時候,視圖會自動變化,反之,視圖變化,數據也隨之更新。避免了繁瑣的dom操作,讓開發者在開發的時候只需要關注數據本身,而不需要關注數據如何渲染到視圖。
實現原理
2.x
? 在vue2.0中通過Object.defineProperty方法實現數據攔截,也就是為每個屬性添加get和set方法,當獲取屬性值和修改屬性值的時候會觸發get和set方法。
let vue = {}
let data = {
? ? msg: 'foo'
}
Object.defineProperty(vue, 'msg', {
? ? enumerable: true,
? ? configurable: true,
? ? get() {
? ? ? ? console.log('正在獲取msg屬性對應的值')
? ? ? ? return data.msg
? ? },
? ? set(newValue) {
? ? ? ? if(newValue === data.msg) {
? ? ? ? ? ? return
? ? ? ? }
? ? ? ? console.log('正在為msg屬性賦值')
? ? ? ? data.msg = newValue
? ? }
})
console.log(vue.msg)
vue.msg = 'bar'
? Object.defineProperty添加的數據攔截在針對數組的時候會出現問題,也就是當屬性值為一個數組的時候,如果進行push,shift等操作的時候,雖然修改了數組,但不會觸發set攔截。
? 為了解決這個問題,vue在內部重寫了原生的數組操作方法,以支持響應式。
3.x
在vue3.0版本中使用ES6新增的Proxy對象替換了Object.defineProperty,不僅簡化了添加攔截的語法,同時也可以支持數組。
let data = {
? ? msg: 'foo'
}
let vue = new Proxy(data, {
? ? get(target, key) {
? ? ? ? console.log('正在獲取msg屬性對應的值')
? ? ? ? return target[key]
? ? },
? ? set(target, key, newValue) {
? ? ? ? if(newValue === target[key]) {
? ? ? ? ? ? return
? ? ? ? }
? ? ? ? console.log('正在為msg屬性賦值')
? ? ? ? target[key] = newValue
? ? }
})
console.log(vue.msg)
vue.msg = 'bar'
依賴的開發模式
在vue實現響應式的代碼中,使用了觀察者模式。
觀察者模式
觀察者模式中,包含兩個部分:
觀察者watcher
觀察者包含一個update方法,此方法表示當事件發生變化的時候需要做的事情
class Watcher {
? ? update() {
? ? ? ? console.log('執行操作')
? ? }
}
目標dep
目標包含一個屬性和兩個方法:
subs屬性:用于存儲所有注冊的觀察者。
addSub方法: 用于添加觀察者。
notify方法: 當事件變化的時候,用于輪詢subs中所有的觀察者,并執行其update方法。
class Dep {
? ? constructor() {
? ? ? ? this.subs = []
? ? }
? ? addSub(watcher) {
? ? ? ? if (watcher.update) {
? ? ? ? ? ? this.subs.push(watcher)
? ? ? ? }
? ? }
? ? notify() {
? ? ? ? this.subs.forEach(watcher => {
? ? ? ? ? ? watcher.update()
? ? ? ? })
? ? }
}
使用方式
// 創建觀察者和目標對象
const w = new Watcher()
const d = new Dep()
// 添加觀察者
d.addSub(w)
// 觸發變化
d.notify()
發布訂閱模式
與觀察者模式很相似的是發布訂閱模式,該模式包含三個方面:
訂閱者
訂閱者類似觀察者模式中的觀察者,當事件發生變化的時候,訂閱者會執行相應的操作。
發布者
發布者類似觀察者模式中的目標,其用于發布變化。
事件中心
在事件中心中存儲著事件對應的所有訂閱者,當發布者發布事件變化后,事件中心會通知所有的訂閱者執行相應操作。
與觀察者模式相比,發布訂閱模式多了一個事件中心,其作用是隔離訂閱者和發布者之間的依賴。
?
vue中的on和emit就是實現的發布訂閱模式,因為其和響應式原理關系不大,所以此處不再詳細說明。
自實現簡版vue
簡化版的vue核心包含5大類,如下圖:
?
通過實現這5大類,就可以一窺Vue內部如何實現響應式。
vue
vue是框架的入口,負責存儲用戶變量、添加數據攔截,啟動模版編譯。
Vue類:
屬性
$options存儲初始化Vue實例時傳遞的參數
$data存儲響應式數據
$methods存儲傳入的所有函數
$el編譯的模版節點
方法
_proxyData私有方法,負責將data中所有屬性添加到Vue實例上。
_proxyMethods私有方法,遍歷傳入的函數,將非聲明周期函數添加到Vue實例上。
directive靜態方法,用于向Vue注入指令。
實現
// 所有聲明周期方法名稱
const hooks = ['beforeCreate', 'created', 'beforeMount', 'mounted',
? ? 'beforeUpdate', 'updated', 'activated', 'deactivated', 'beforeDestroy', 'destroyed']
class Vue {
? ? constructor(options) {
? ? ? ? this.$options = Object.assign(Vue.options || {}, options || {})
? ? ? ? this.$data = options.data || {}
? ? ? ? this.$methods = options.methods || {}
? ? ? ? if (options && options.el) {
? ? ? ? ? ? this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
? ? ? ? }
? ? ? ? this._proxyData(this.$data)
? ? ? ? this._proxyMethods(this.$methods)
? ? ? ? // 實現數據攔截
? ? ? ? // 啟動模版編譯
? ? }
? ? _proxyMethods(methods) {
? ? ? ? let obj = {}
? ? ? ? Object.keys(methods).forEach(key => {
? ? ? ? ? ? if (hooks.indexOf(key) === -1 && typeof methods[key] === 'function') {
? ? ? ? ? ? ? ? obj[key] = methods[key].bind(this)
? ? ? ? ? ? }
? ? ? ? })
? ? ? ? this._proxyData(obj)
? ? }
? ? _proxyData(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
? ? ? ? ? ? ? ? }
? ? ? ? ? ? })
? ? ? ? })
? ? }
? ? // 用于注冊指令的方法
? ? static directive(name, handle) {
? ? ? ? if (!Vue.options) {
? ? ? ? ? ? Vue.options = {
? ? ? ? ? ? ? ? directives: {}
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? Vue.options.directives[name] = {
? ? ? ? ? ? bind: handle,
? ? ? ? ? ? update: handle
? ? ? ? }
? ? }
}
observer
observer類負責為data對象添加數據攔截。
方法
walk輪詢對象屬性,調用defineReactive方法為每個屬性添加setter和getter。
defineReactive添加setter和getter。
實現
class Observer {
? ? constructor(data) {
? ? ? ? this.walk(data)
? ? }
? ? // 輪詢對象
? ? walk(data) {
? ? ? ? // 只有data為object對象時,才輪詢其屬性
? ? ? ? if (data && typeof data === 'object') {
? ? ? ? ? ? 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, {
? ? ? ? ? ? enumerable: true,
? ? ? ? ? ? configurable: true,
? ? ? ? ? ? get() {
? ? ? ? ? ? ? ? return val
? ? ? ? ? ? },
? ? ? ? ? ? set(newValue) {
? ? ? ? ? ? ? ? if (val === newValue) {
? ? ? ? ? ? ? ? ? ? return
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? // 如果賦值為一個對象,為對象的每一個屬性添加攔截
? ? ? ? ? ? ? ? that.walk(newValue)
? ? ? ? ? ? ? ? val = newValue
? ? ? ? ? ? }
? ? ? ? })
? ? }
}
在Vue的constructor構造函數中添加Observer:
constructor(options) {
? ? ? ? this.$options = Object.assign(Vue.options || {}, options || {})
? ? ? ? this.$data = options.data || {}
? ? ? ? this.$methods = options.methods || {}
? ? ? ? if (options && options.el) {
? ? ? ? ? ? this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
? ? ? ? }
? ? ? ? this._proxyData(this.$data)
? ? ? ? this._proxyMethods(this.$methods)
? ? ? ? // 實現數據攔截
? ? ? ? new Observer(this.$data)
? ? ? ? // 啟動模版編譯
? ? ? ? new Compiler(this)
}
directive
由于在compiler編譯模版的時候,需要用到指令解析,所以此處模擬一個指令初始化方法,用于向vue實例添加內置指令。
在此處模擬實現了四個指令:
// v-text
Vue.directive('text', function (el, binding) {
? ? const { value } = binding
? ? el.textContent = value
})
// v-model
Vue.directive('model', function (el, binding) {
? ? const { value, expression } = binding
? ? el.value = value
? ? // 實現雙向綁定
? ? el.addEventListener('input', () => {
? ? ? ? el.vm[expression] = el.value
? ? })
})
// v-html
Vue.directive('html', function (el, binding) {
? ? const { value } = binding
? ? el.innerHTML = value
})
// v-on
Vue.directive('on', function (el, binding) {
? ? const { value, argument } = binding
? ? el.addEventListener(argument, value)
})
compiler
compiler負責html模版編譯,解析模版中的插值表達式和指令等。
屬性
el保存編譯的目標元素
vm保存編譯時用到的vue上下文信息。
方法
compile負責具體的html編譯。
實現
class Compiler {
? ? constructor(vm) {
? ? ? ? this.vm = vm
? ? ? ? this.el = vm.$el
? ? ? ? // 構造函數中執行編譯
? ? ? ? this.compile(this.el)
? ? }
? ? compile(el) {
? ? ? ? if (!el) {
? ? ? ? ? ? return
? ? ? ? }
? ? ? ? const children = el.childNodes
? ? ? ? Array.from(children).forEach(node => {
? ? ? ? ? ? if (this.isElementNode(node)) {
? ? ? ? ? ? ? ? this.compileElement(node)
? ? ? ? ? ? } else if (this.isTextNode(node)) {
? ? ? ? ? ? ? ? this.compileText(node)
? ? ? ? ? ? }
? ? ? ? ? ? // 遞歸處理node下面的子節點
? ? ? ? ? ? if (node.childNodes && node.childNodes.length) {
? ? ? ? ? ? ? ? this.compile(node)
? ? ? ? ? ? }
? ? ? ? })
? ? }
? ? compileElement(node) {
? ? ? ? const directives = this.vm.$options.directives
? ? ? ? Array.from(node.attributes).forEach(attr => {
? ? ? ? ? ? // 判斷是否是指令
? ? ? ? ? ? let attrName = attr.name
? ? ? ? ? ? if (this.isDirective(attrName)) {
? ? ? ? ? ? ? ? // v-text --> text
? ? ? ? ? ? ? ? // 獲取指令的相關數據
? ? ? ? ? ? ? ? let attrNames = attrName.substr(2).split(':')
? ? ? ? ? ? ? ? let name = attrNames[0]
? ? ? ? ? ? ? ? let arg = attrNames[1]
? ? ? ? ? ? ? ? let key = attr.value
? ? ? ? ? ? ? ? // 獲取注冊的指令并執行
? ? ? ? ? ? ? ? if (directives[name]) {
? ? ? ? ? ? ? ? ? ? node.vm = this.vm
? ? ? ? ? ? ? ? ? ? // 執行指令綁定
? ? ? ? ? ? ? ? ? ? directives[name].bind(node, {
? ? ? ? ? ? ? ? ? ? ? ? name: name,
? ? ? ? ? ? ? ? ? ? ? ? value: this.vm[key],
? ? ? ? ? ? ? ? ? ? ? ? argument: arg,
? ? ? ? ? ? ? ? ? ? ? ? expression: key
? ? ? ? ? ? ? ? ? ? })
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? })
? ? }
? ? compileText(node) {
? ? ? ? // 利用正則表達式匹配插值表達式
? ? ? ? let reg = /\{\{(.+?)\}\}/
? ? ? ? const value = node.textContent
? ? ? ? if (reg.test(value)) {
? ? ? ? ? ? let key = RegExp.$1.trim()
? ? ? ? ? ? node.textContent = value.replace(reg, this.vm[key])
? ? ? ? }
? ? }
? ? // 判斷元素屬性是否是指令,簡化vue原來邏輯,現在默認只有v-開頭的屬性是指令
? ? isDirective(attrName) {
? ? ? ? return attrName.startsWith('v-')
? ? }
? ? // 判斷節點是否是文本節點
? ? isTextNode(node) {
? ? ? ? return node.nodeType === 3
? ? }
? ? // 判斷節點是否是元素節點
? ? isElementNode(node) {
? ? ? ? return node.nodeType === 1
? ? }
}
修改vue的構造函數,啟動模版編譯。
constructor(options) {
? ? ? ? this.$options = Object.assign(Vue.options || {}, options || {})
? ? ? ? this.$data = options.data || {}
? ? ? ? this.$methods = options.methods || {}
? ? ? ? if (options && options.el) {
? ? ? ? ? ? this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
? ? ? ? }
? ? ? ? this._proxyData(this.$data)
? ? ? ? this._proxyMethods(this.$methods)
? ? ? ? // 實現數據攔截
? ? ? ? new Observer(this.$data)
? ? ? ? // 啟動模版編譯
? ? ? ? new Compiler(this)
}
dep
dep負責收集某個屬性的所有觀察者,當屬性值發生變化的時候,會依次執行觀察者的update方法。
屬性
subs記錄所有的觀察者
方法
addSub添加觀察者
notify觸發執行所有觀察者的update方法
實現
class Dep {
? ? constructor() {
? ? ? ? // 存儲所有的觀察者
? ? ? ? this.subs = []
? ? }
? ? // 添加觀察者
? ? addSub(sub) {
? ? ? ? if (sub && sub.update) {
? ? ? ? ? ? this.subs.push(sub)
? ? ? ? }
? ? }
? ? // 發送通知
? ? notify() {
? ? ? ? this.subs.forEach(sub => {
? ? ? ? ? ? sub.update()
? ? ? ? })
? ? }
}
現在的問題是何時添加觀察者,何時觸發更新?
?
從上圖可以看出,應該在Observer中觸發攔截的時候對Dep進行操作,也就是get的時候添加觀察者,set時觸發更新。
修改observer的defineReactive方法:
defineReactive(data, key, val) {
? ? ? ? const that = this
? ? ? ? // 創建dep對象
? ? ? ? const dep = new Dep()
? ? ? ? // 如果val是一個對象,為對象的每一個屬性添加攔截
? ? ? ? this.walk(val)
? ? ? ? Object.defineProperty(data, key, {
? ? ? ? ? ? enumerable: true,
? ? ? ? ? ? configurable: true,
? ? ? ? ? ? get() {
? ? ? ? ? ? ? ? // 添加依賴
? ? ? ? ? ? ? ? // 在watcher中,獲取屬性值的時候,會把相應的觀察者添加到Dep.target屬性上
? ? ? ? ? ? ? ? Dep.target && dep.addSub(Dep.target)
? ? ? ? ? ? ? ? return val
? ? ? ? ? ? },
? ? ? ? ? ? set(newValue) {
? ? ? ? ? ? ? ? if (val === newValue) {
? ? ? ? ? ? ? ? ? ? return
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? // 如果賦值為一個對象,為對象的每一個屬性添加攔截
? ? ? ? ? ? ? ? that.walk(newValue)
? ? ? ? ? ? ? ? val = newValue
? ? ? ? ? ? ? ? // 觸發更新
? ? ? ? ? ? ? ? dep.notify()
? ? ? ? ? ? }
? ? ? ? })
}
watcher
watcher是觀察者對象,在vue對象的屬性發生變化的時候執行相應的更新操作。
方法
update執行具體的更新操作
實現
class Watcher {
? ? // vm: vue實例
? ? // key: 監控的屬性鍵值
? ? // cb: 回調函數,執行具體更新
? ? constructor(vm, key, cb) {
? ? ? ? this.vm = vm
? ? ? ? this.key = key
? ? ? ? this.cb = cb
? ? ? ? // 指定在這個執行環境下的watcher實例
? ? ? ? Dep.target = this
? ? ? ? // 獲取舊的數據,觸發get方法中Dep.addSub
? ? ? ? this.oldValue = vm[key]
? ? ? ? // 刪除target,等待下一次賦值
? ? ? ? Dep.target = null
? ? }
? ? update() {
? ? ? ? let newValue = this.vm[this.key]
? ? ? ? if (this.oldValue === newValue) {
? ? ? ? ? ? return
? ? ? ? }
? ? ? ? this.cb(newValue)
? ? ? ? this.oldValue = newValue
? ? }
}
由于需要數據雙向綁定,在compiler編譯模版的時候,創建Watcher實例,并指定具體如何更新頁面。
compileElement(node) {
? ? ? ? const directives = this.vm.$options.directives
? ? ? ? Array.from(node.attributes).forEach(attr => {
? ? ? ? ? ? // 判斷是否是指令
? ? ? ? ? ? let attrName = attr.name
? ? ? ? ? ? if (this.isDirective(attrName)) {
? ? ? ? ? ? ? ? // v-text --> text
? ? ? ? ? ? ? ? // 獲取指令的相關數據
? ? ? ? ? ? ? ? let attrNames = attrName.substr(2).split(':')
? ? ? ? ? ? ? ? let name = attrNames[0]
? ? ? ? ? ? ? ? let arg = attrNames[1]
? ? ? ? ? ? ? ? let key = attr.value
? ? ? ? ? ? ? ? // 獲取注冊的指令并執行
? ? ? ? ? ? ? ? if (directives[name]) {
? ? ? ? ? ? ? ? ? ? node.vm = this.vm
? ? ? ? ? ? ? ? ? ? // 執行指令綁定
? ? ? ? ? ? ? ? ? ? directives[name].bind(node, {
? ? ? ? ? ? ? ? ? ? ? ? name: name,
? ? ? ? ? ? ? ? ? ? ? ? value: this.vm[key],
? ? ? ? ? ? ? ? ? ? ? ? argument: arg,
? ? ? ? ? ? ? ? ? ? ? ? expression: key
? ? ? ? ? ? ? ? ? ? })
? ? ? ? ? ? ? ? ? ? new Watcher(this.vm, key, () => {
? ? ? ? ? ? ? ? ? ? ? ? directives[name].update(node, {
? ? ? ? ? ? ? ? ? ? ? ? ? ? name: name,
? ? ? ? ? ? ? ? ? ? ? ? ? ? value: this.vm[key],
? ? ? ? ? ? ? ? ? ? ? ? ? ? argument: arg,
? ? ? ? ? ? ? ? ? ? ? ? ? ? expression: key
? ? ? ? ? ? ? ? ? ? ? ? })
? ? ? ? ? ? ? ? ? ? })
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? })
? ? }
想持續了解更多,不妨點贊和關注唄。
Web前端技術交流q群:1137068794,
群里可以一起學習編程,進群能領到學習資料以及源代碼