像React,Vue這類的框架,響應式是其最核心的特性之一。經過響應式能夠實現當改變數據的時候,視圖會自動變化,反之,視圖變化,數據也隨之更新。避免了繁瑣的dom操做,讓開發者在開發的時候只須要關注數據自己,而不須要關注數據如何渲染到視圖。html
在vue2.0中經過Object.defineProperty方法實現數據攔截,也就是爲每一個屬性添加get和set方法,當獲取屬性值和修改屬性值的時候會觸發get和set方法。vue
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攔截。node
爲了解決這個問題,vue在內部重寫了原生的數組操做方法,以支持響應式。正則表達式
在vue3.0版本中使用ES6新增的Proxy對象替換了Object.defineProperty,不只簡化了添加攔截的語法,同時也能夠支持數組。express
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實現響應式的代碼中,使用了觀察者模式。數組
觀察者模式中,包含兩個部分:框架
觀察者包含一個update方法,此方法表示當事件發生變化的時候須要作的事情dom
class Watcher { update() { console.log('執行操做') } }
目標包含一個屬性和兩個方法:函數
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()
與觀察者模式很類似的是發佈訂閱模式,該模式包含三個方面:this
訂閱者相似觀察者模式中的觀察者,當事件發生變化的時候,訂閱者會執行相應的操做。
發佈者相似觀察者模式中的目標,其用於發佈變化。
在事件中心中存儲着事件對應的全部訂閱者,當發佈者發佈事件變化後,事件中心會通知全部的訂閱者執行相應操做。
與觀察者模式相比,發佈訂閱模式多了一個事件中心,其做用是隔離訂閱者和發佈者之間的依賴。
vue中的on和emit就是實現的發佈訂閱模式,由於其和響應式原理關係不大,因此此處再也不詳細說明。
簡化版的vue核心包含5大類,以下圖:
經過實現這5大類,就能夠一窺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類負責爲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) }
因爲在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負責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負責收集某個屬性的全部觀察者,當屬性值發生變化的時候,會依次執行觀察者的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是觀察者對象,在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 }) }) } } }) }