大前端進階篇-Vuejs響應式原理剖析

? 像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,

群里可以一起學習編程,進群能領到學習資料以及源代碼

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。