提到「響應式」三個字,你們馬上想到啥?響應式佈局?響應式編程?javascript
從字面意思能夠看出,具備「響應式」特徵的事物會根據條件變化,使得目標自動做出對應變化。好比在「響應式佈局」中,頁面根據不一樣設備尺寸自動顯示不一樣樣式。html
Vue.js 中的響應式也是同樣,當數據發生變化後,使用到該數據的視圖也會相應進行自動更新。前端
接下來我根據我的理解,和你們一塊兒探索下 Vue.js 中的響應式原理,若有錯誤,歡迎指點😺~~vue
如今有個很簡單的需求,點擊頁面中 「leo」 文本後,文本內容修改成「你好,前端自習課」。java
咱們能夠直接操做 DOM,來完成這個需求:node
<span id="name">leo</span>
const node = document.querySelector('#name') node.innerText = '你好,前端自習課';
實現起來比較簡單,當咱們須要修改的數據有不少時(好比相同數據被多處引用),這樣的操做將變得複雜。react
既然說到 Vue.js,咱們就來看看 Vue.js 怎麼實現上面需求:git
<template> <div id="app"> <span @click="setName">{{ name }}</span> </div> </template> <script> export default { name: "App", data() { return { name: "leo", }; }, methods: { setName() { this.name = "你好,前端自習課"; }, }, }; </script>
觀察上面代碼,咱們經過改變數據,來自動更新視圖。當咱們有多個地方引用這個 name
時,視圖都會自動更新。github
<template> <div id="app"> <span @click="setName">{{ name }}</span> <span>{{ name }}</span> <span>{{ name }}</span> <span>{{ name }}</span> </div> </template>
當咱們使用目前主流的前端框架 Vue.js 和 React 開發業務時,只需關注頁面數據如何變化,由於數據變化後,視圖也會自動更新,這讓咱們從繁雜的 DOM 操做中解脫出來,提升開發效率。typescript
前面反覆提到「經過改變數據,來自動更新視圖」,換個說法就是「數據改變後,使用該數據的地方被動發生響應,更新視圖」。
是否是有種熟悉的感受?數據無需關注自身被多少對象引用,只需在數據變化時,通知到引用的對象便可,引用的對象做出響應。恩,有種觀察者模式的味道?
關於觀察者模式,可閱讀我以前寫的 《圖解設計模式之觀察者模式(TypeScript)》。
觀察者模式表示一種「一對多」的關係,n 個觀察者關注 1 個被觀察者,被觀察者能夠主動通知全部觀察者。接下圖:
在這張圖中,粉絲想及時收到「前端自習課」最新文章,只需關注便可,「前端自習課」有新文章,會主動推送給每一個粉絲。該過程當中,「前端自習課」是被觀察者,每位「粉絲」是觀察者。
觀察者模式核心組成包括:n 個觀察者和 1 個被觀察者。這裏實現一個簡單觀察者模式:
// 觀察目標接口 interface ISubject { addObserver: (observer: Observer) => void; // 添加觀察者 removeObserver: (observer: Observer) => void; // 移除觀察者 notify: () => void; // 通知觀察者 } // 觀察者接口 interface IObserver { update: () => void; }
// 實現被觀察者類 class Subject implements ISubject { private observers: IObserver[] = []; public addObserver(observer: IObserver): void { this.observers.push(observer); } public removeObserver(observer: IObserver): void { const idx: number = this.observers.indexOf(observer); ~idx && this.observers.splice(idx, 1); } public notify(): void { this.observers.forEach(observer => { observer.update(); }); } }
// 實現觀察者類 class Observer implements IObserver { constructor(private name: string) { } update(): void { console.log(`${this.name} has been notified.`); } }
function useObserver(){ const subject: ISubject = new Subject(); const Leo = new Observer("Leo"); const Robin = new Observer("Robin"); const Pual = new Observer("Pual"); subject.addObserver(Leo); subject.addObserver(Robin); subject.addObserver(Pual); subject.notify(); subject.removeObserver(Pual); subject.notify(); } useObserver(); // [LOG]: "Leo has been notified." // [LOG]: "Robin has been notified." // [LOG]: "Pual has been notified." // [LOG]: "Leo has been notified." // [LOG]: "Robin has been notified."
Vue.js 的數據響應式原理是基於 JS 標準內置對象方法 Object.defineProperty()
方法來實現,該方法不兼容 IE8 和 FF22 及如下版本瀏覽器,這也是爲何 Vue.js 只能在這些版本之上的瀏覽器中才能運行的緣由。
理解 Object.defineProperty()
對咱們理解 Vue.js 響應式原理很是重要。
Vue.js 3 使用proxy
方法實現響應式,二者相似,咱們只需搞懂Object.defineProperty()
,proxy
也就差很少理解了。
Object.defineProperty()
方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。
語法以下:
Object.defineProperty(obj, prop, descriptor)
obj
:要定義屬性的源對象;prop
:要定義或修改的屬性名稱或 Symbol;descriptor
:要定義或修改的屬性描述符,包括 configurable
、enumerable
、value
、writable
、get
、set
,具體的能夠去參閱文檔;
修改後的源對象。
舉個簡單🌰例子:
const leo = {}; Object.defineProperty(leo, 'age', { value: 18, writable: false }) console.log(leo.age); // 18 leo.age = 22; console.log(leo.age); // 22
咱們知道 Object.defineProperty()
方法第三個參數是屬性描述符(descriptor
),支持設置 get
和 set
描述符:
get
描述符:當訪問該屬性時,會調用此函數,默認值爲 undefined
;set
描述符:當修改該屬性時,會調用此函數,默認值爲 undefined
。一旦對象擁有了 getter/setter 方法,咱們能夠簡單將該對象稱爲響應式對象。
這兩個操做符爲咱們提供攔截數據進行操做的可能性,修改前面示例,添加 getter/setter 方法:
let leo = {}, age = 18; Object.defineProperty(leo, 'age', { get(){ // to do something console.log('監聽到請求數據'); return age; }, set(newAge){ // to do something console.log('監聽到修改數據'); age = newAge > age ? age : newAge } }) leo.age = 20; // 監聽到修改數據 console.log(leo.age); // 監聽到請求數據 // 18 leo.age = 10; // 監聽到修改數據 console.log(leo.age); // 監聽到請求數據 // 10
訪問 leo
對象的 age
屬性,會經過 get
描述符處理,而修改 age
屬性,則會經過 set
描述符處理。
經過前面兩個小節,咱們複習了「觀察者模式」和「Object.defineProperty()
」 方法,這兩個知識點在 Vue.js 響應式原理中很是重要。
接下來咱們來實現一個很簡單的數據響應式變化,需求以下:點擊「更新數據」按鈕,文本更新。
接下來咱們將實現三個類:
Dep
被觀察者類,用來生成被觀察者;Watcher
觀察者類,用來生成觀察者;Observer
類,將普通數據轉換爲響應式數據,從而實現響應式對象。用一張圖來描述三者之間關係,如今看不懂不要緊,這小節看完能夠再回顧這張圖:
這裏參照前面複習「觀察者模式」的示例,作下精簡:
// 實現被觀察者類 class Dep { constructor() { this.subs = []; } addSub(watcher) { this.subs.push(watcher); } notify(data) { this.subs.forEach(sub => sub.update(data)); } } // 實現觀察者類 class Watcher { constructor(cb) { this.cb = cb; } update(data) { this.cb(data); } }
Vue.js 響應式原理中,觀察者模式起到很是重要的做用。其中:
Dep
被觀察者類,提供用來收集觀察者( addSub
)方法和通知觀察者( notify
)方法;Watcher
觀察者類,實例化時支持傳入回調( cb
)方法,並提供更新( update
)方法;這一步須要實現 Observer
類,核心是經過 Object.defineProperty()
方法爲對象的每一個屬性設置 getter/setter,目的是將普通數據轉換爲響應式數據,從而實現響應式對象。
這裏以最簡單的單層對象爲例(下一節會介紹深層對象),如:
let initData = { text: '你好,前端自習課', desc: '每日清晨,享受一篇前端優秀文章。' };
接下來實現 Observer
類:
// 實現響應式類(最簡單單層的對象,暫不考慮深層對象) class Observer { constructor (node, data) { this.defineReactive(node, data) } // 實現數據劫持(核心方法) // 遍歷 data 中全部的數據,都添加上 getter 和 setter 方法 defineReactive(vm, obj) { //每個屬性都從新定義get、set for(let key in obj){ let value = obj[key], dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 建立觀察者 let watcher = new Watcher(v => vm.innerText = v); dep.addSub(watcher); return value; }, set(newValue) { value = newValue; // 通知全部觀察者 dep.notify(newValue); } }) } } }
上面代碼的核心是 defineReactive
方法,它遍歷原始對象中每一個屬性,爲每一個屬性實例化一個被觀察者(Dep
),而後分別調用 Object.defineProperty()
方法,爲每一個屬性添加 getter/setter。
Watcher
建立一個觀察者,並執行被觀察者的 addSub()
方法添加一個觀察者;notify()
方法通知全部觀察者,執行觀察者 update()
方法。爲了方便觀察數據變化,咱們爲「更新數據」按鈕綁定點擊事件來修改數據:
<div id="app"></div> <button id="update">更新數據</button>
測試代碼以下:
// 初始化測試數據 let initData = { text: '你好,前端自習課', desc: '每日清晨,享受一篇前端優秀文章。' }; const app = document.querySelector('#app'); // 步驟1:爲測試數據轉換爲響應式對象 new Observer(app, initData); // 步驟2:初始化頁面文本內容 app.innerText = initData.text; // 步驟3:綁定按鈕事件,點擊觸發測試 document.querySelector('#update').addEventListener('click', function(){ initData.text = `咱們必須常常保持舊的記憶和新的但願。`; console.log(`當前時間:${new Date().toLocaleString()}`) })
測試代碼中,核心在於經過實例化 Observer
,將測試數據轉換爲響應式數據,而後模擬數據變化,來觀察視圖變化。
每次點擊「更新數據」按鈕,在控制檯中都能看到「數據發生變化!」的提示,說明咱們已經能經過 setter 觀察到數據的變化狀況。
固然,你還能夠在控制檯手動修改 initData
對象中的 text
屬性,來體驗響應式變化~~
到這裏,咱們實現了很是簡單的數據響應式變化,固然 Vue.js 確定沒有這麼簡單,這個先理解,下一節看 Vue.js 響應式原理,思路就會清晰不少。
這部分代碼,我已經放到 個人 Github,地址: https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/Vue/Basics-Reactive-Demo.js
能夠再回顧下這張圖,對整個過程會更清晰:
本節代碼: https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/Vue/leo-vue-reactive/
這裏你們能夠再回顧下下面這張官網經典的圖,思考下前面講的示例。
(圖片來自:https://cn.vuejs.org/v2/guide/reactivity.html)
上一節實現了簡單的數據響應式,接下來繼續經過完善該示例,實現一個簡單的 Vue.js 響應式,測試代碼以下:
// index.js const vm = new Vue({ el: '#app', data(){ return { text: '你好,前端自習課', desc: '每日清晨,享受一篇前端優秀文章。' } } });
是否是頗有內味了,下面是咱們最終實現後項目目錄:
- mini-reactive / index.html // 入口 HTML 文件 / index.js // 入口 JS 文件 / observer.js // 實現響應式,將數據轉換爲響應式對象 / watcher.js // 實現觀察者和被觀察者(依賴收集者) / vue.js // 實現 Vue 類做爲主入口類 / compile.js // 實現編譯模版功能
知道每個文件功能之後,接下來將每一步串聯起來。
咱們首先實現入口文件,包括 index.html
/ index.js
2 個簡單文件,用來方便接下來的測試。
<!DOCTYPE html> <html lang="en"> <head> <script src="./vue.js"></script> <script src="./observer.js"></script> <script src="./compile.js"></script> <script src="./watcher.js"></script> </head> <body> <div id="app">{{text}}</div> <button id="update">更新數據</button> <script src="./index.js"></script> </body> </html>
"use strict"; const vm = new Vue({ el: '#app', data(){ return { text: '你好,前端自習課', desc: '每日清晨,享受一篇前端優秀文章。' } } }); console.log(vm.$data.text) vm.$data.text = '頁面數據更新成功!'; // 模擬數據變化 console.log(vm.$data.text)
vue.js
文件是咱們實現的整個響應式的入口文件,暴露一個 Vue
類,並掛載全局。
class Vue { constructor (options = {}) { this.$el = options.el; this.$data = options.data(); this.$methods = options.methods; // [核心流程]將普通 data 對象轉換爲響應式對象 new Observer(this.$data); if (this.$el) { // [核心流程]將解析模板的內容 new Compile(this.$el, this) } } } window.Vue = Vue;
Vue
類入參爲一個配置項 option
,使用起來跟 Vue.js 同樣,包括 $el
掛載點、 $data
數據對象和 $methods
方法列表(本文不詳細介紹)。
經過實例化 Oberser
類,將普通 data 對象轉換爲響應式對象,而後判斷是否傳入 el
參數,存在時,則實例化 Compile
類,解析模版內容。
總結下 Vue
這個類工做流程 :
observer.js 文件實現了 Observer
類,用來將普通對象轉換爲響應式對象:
class Observer { constructor (data) { this.data = data; this.walk(data); } // [核心方法]將 data 對象轉換爲響應式對象,爲每一個 data 屬性設置 getter 和 setter 方法 walk (data) { if (typeof data !== 'object') return data; Object.keys(data).forEach( key => { this.defineReactive(data, key, data[key]) }) } // [核心方法]實現數據劫持 defineReactive (obj, key, value) { this.walk(value); // [核心過程]遍歷 walk 方法,處理深層對象。 const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get () { console.log('[getter]方法執行') Dep.target && dep.addSub(Dep.target); return value }, set (newValue) { console.log('[setter]方法執行') if (value === newValue) return; // [核心過程]當設置的新值 newValue 爲對象,則繼續經過 walk 方法將其轉換爲響應式對象 if (typeof newValue === 'object') this.walk(newValue); value = newValue; dep.notify(); // [核心過程]執行被觀察者通知方法,通知全部觀察者執行 update 更新 } }) } }
相比較第四節實現的 Observer
類,這裏作了調整:
walk
核心方法,用來遍歷對象每一個屬性,分別調用數據劫持方法( defineReactive()
);defineReactive()
的 getter 中,判斷 Dep.target
存在才添加觀察者,下一節會詳細介紹 Dep.target
;defineReactive()
的 setter 中,判斷當前新值( newValue
)是否爲對象,若是是,則直接調用 this.walk()
方法將當前對象再次轉爲響應式對象,處理深層對象。經過改善後的 Observer
類,咱們就能夠實現將單層或深層嵌套的普通對象轉換爲響應式對象。
這裏實現了 Dep
被觀察者類(依賴收集者)和 Watcher
觀察者類。
class Dep { constructor() { this.subs = []; } addSub(watcher) { this.subs.push(watcher); } notify(data) { this.subs.forEach(sub => sub.update(data)); } } class Watcher { constructor (vm, key, cb) { this.vm = vm; // vm:表示當前實例 this.key = key; // key:表示當前操做的數據名稱 this.cb = cb; // cb:表示數據發生改變以後的回調 Dep.target = this; // 全局惟一 this.oldValue = this.vm.$data[key]; // 保存變化的數據做爲舊值,後續做判斷是否更新 Dep.target = null; } update () { console.log(`數據發生變化!`); let oldValue = this.oldValue; let newValue = this.vm.$data[this.key]; if (oldValue != newValue) { // 比較新舊值,發生變化才執行回調 this.cb(newValue, oldValue); }; } }
相比較第四節實現的 Watcher
類,這裏作了調整:
Dep.target
值操做;oldValue
變量,保存變化的數據做爲舊值,後續做爲判斷是否更新的依據;update()
方法中,增長當前操做對象 key
對應值的新舊值比較,若是不一樣,才執行回調。Dep.target
是當前全局惟一的訂閱者,由於同一時間只容許一個訂閱者被處理。target
指當前正在處理的目標訂閱者,當前訂閱者處理完就賦值爲 null
。這裏 Dep.target
會在 defineReactive()
的 getter 中使用到。
經過改善後的 Watcher
類,咱們操做當前操做對象 key
對應值的時候,能夠在數據有變化的狀況才執行回調,減小資源浪費。
compile.js 實現了 Vue.js 的模版編譯,如將 HTML 中的 {{text}}
模版轉換爲具體變量的值。
compile.js 介紹內容較多,考慮到篇幅問題,而且本文核心介紹響應式原理,因此這裏就暫時不介紹 compile.js 的實現,在學習的朋友能夠到我 Github 上下載該文件直接下載使用便可,地址:
https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/Vue/leo-vue-reactive/compile.js
到這裏,咱們已經將第四節的 demo 改形成簡易版 Vue.js 響應式,接下來打開 index.html 看看效果:
當 index.js 中執行到:
vm.$data.text = '咱們必須常常保持舊的記憶和新的但願。';
頁面便發生更新,頁面顯示的文本內容從「你好,前端自習課」更新成「咱們必須常常保持舊的記憶和新的但願。」。
到這裏,咱們的簡易版 Vue.js 響應式原理實現好了,能跟着文章看到這裏的朋友,給你點個大大的贊👍
本文首先經過回顧觀察者模式和 Object.defineProperty()
方法,介紹 Vue.js 響應式原理的核心知識點,而後帶你們經過一個簡單示例實現簡單響應式,最後經過改造這個簡單響應式的示例,實現一個簡單 Vue.js 響應式原理的示例。
相信看完本文的朋友,對 Vue.js 的響應式原理的理解會更深入,但願你們理清思路,再好好回味下~