您好,登錄后才能下訂單哦!
Vue.js中實現(xiàn)響應式的原理是什么,相信很多沒有經(jīng)驗的人對此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個問題。
一、Vue.js 響應式的使用
現(xiàn)在有個很簡單的需求,點擊頁面中 “l(fā)eo” 文本后,文本內(nèi)容修改為“你好,前端自習課”。
我們可以直接操作 DOM,來完成這個需求:
<span id="name">leo</span>
const node = document.querySelector('#name') node.innerText = '你好,前端自習課';
實現(xiàn)起來比較簡單,當我們需要修改的數(shù)據(jù)有很多時(比如相同數(shù)據(jù)被多處引用),這樣的操作將變得復雜。
既然說到 Vue.js,我們就來看看 Vue.js 怎么實現(xiàn)上面需求:
<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>
觀察上面代碼,我們通過改變數(shù)據(jù),來自動更新視圖。當我們有多個地方引用這個 name 時,視圖都會自動更新。
<template> <div id="app"> <span @click="setName">{{ name }}</span> <span>{{ name }}</span> <span>{{ name }}</span> <span>{{ name }}</span> </div> </template>
當我們使用目前主流的前端框架 Vue.js 和 React 開發(fā)業(yè)務時,只需關(guān)注頁面數(shù)據(jù)如何變化,因為數(shù)據(jù)變化后,視圖也會自動更新,這讓我們從繁雜的 DOM 操作中解脫出來,提高開發(fā)效率。
二、回顧觀察者模式
前面反復提到“通過改變數(shù)據(jù),來自動更新視圖”,換個說法就是“數(shù)據(jù)改變后,使用該數(shù)據(jù)的地方被動發(fā)生響應,更新視圖”。
是不是有種熟悉的感覺?數(shù)據(jù)無需關(guān)注自身被多少對象引用,只需在數(shù)據(jù)變化時,通知到引用的對象即可,引用的對象作出響應。恩,有種觀察者模式的味道?
關(guān)于觀察者模式,可閱讀我之前寫的《圖解設計模式之觀察者模式(TypeScript)》。
1. 觀察者模式流程
觀察者模式表示一種“一對多”的關(guān)系,n 個觀察者關(guān)注 1 個被觀察者,被觀察者可以主動通知所有觀察者。接下圖:
在這張圖中,粉絲想及時收到“前端自習課”最新文章,只需關(guān)注即可,“前端自習課”有新文章,會主動推送給每個粉絲。該過程中,“前端自習課”是被觀察者,每位“粉絲”是觀察者。
2. 觀察者模式核心
觀察者模式核心組成包括:n 個觀察者和 1 個被觀察者。這里實現(xiàn)一個簡單觀察者模式:
2.1 定義接口
// 觀察目標接口 interface ISubject { addObserver: (observer: Observer) => void; // 添加觀察者 removeObserver: (observer: Observer) => void; // 移除觀察者 notify: () => void; // 通知觀察者 } // 觀察者接口 interface IObserver { update: () => void; }
2.2 實現(xiàn)被觀察者類
// 實現(xiàn)被觀察者類 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(); }); } }
2.3 實現(xiàn)觀察者類
// 實現(xiàn)觀察者類 class Observer implements IObserver { constructor(private name: string) { } update(): void { console.log(`${this.name} has been notified.`); } }
2.4 測試代碼
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."
三、回顧 Object.defineProperty()
Vue.js 的數(shù)據(jù)響應式原理是基于 JS 標準內(nèi)置對象方法 Object.defineProperty() 方法來實現(xiàn),該方法不兼容 IE8 和 FF22 及以下版本瀏覽器,這也是為什么 Vue.js 只能在這些版本之上的瀏覽器中才能運行的原因。
理解 Object.defineProperty() 對我們理解 Vue.js 響應式原理非常重要。
Vue.js 3 使用 proxy 方法實現(xiàn)響應式,兩者類似,我們只需搞懂Object.defineProperty() , proxy 也就差不多理解了。
1. 概念介紹
Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現(xiàn)有屬性,并返回此對象。
語法如下:
Object.defineProperty(obj, prop, descriptor)
入?yún)⒄f明:
obj :要定義屬性的源對象;
prop :要定義或修改的屬性名稱或 Symbol;
descriptor :要定義或修改的屬性描述符,包括 configurable、enumerable、value、writable、get、set,具體的可以去參閱文檔;
出參說明:
修改后的源對象。
舉個簡單?例子:
const leo = {}; Object.defineProperty(leo, 'age', { value: 18, writable: true }) console.log(leo.age); // 18 leo.age = 22; console.log(leo.age); // 22
2. 實現(xiàn) getter/setter
我們知道 Object.defineProperty() 方法第三個參數(shù)是屬性描述符(descriptor),支持設置 get 和 set 描述符:
get 描述符:當訪問該屬性時,會調(diào)用此函數(shù),默認值為 undefined ;
set 描述符:當修改該屬性時,會調(diào)用此函數(shù),默認值為 undefined 。
一旦對象擁有了 getter/setter 方法,我們可以簡單將該對象稱為響應式對象。
這兩個操作符為我們提供攔截數(shù)據(jù)進行操作的可能性,修改前面示例,添加 getter/setter 方法:
let leo = {}, age = 18; Object.defineProperty(leo, 'age', { get(){ // to do something console.log('監(jiān)聽到請求數(shù)據(jù)'); return age; }, set(newAge){ // to do something console.log('監(jiān)聽到修改數(shù)據(jù)'); age = newAge > age ? age : newAge } }) leo.age = 20; // 監(jiān)聽到修改數(shù)據(jù) console.log(leo.age); // 監(jiān)聽到請求數(shù)據(jù) // 18 leo.age = 10; // 監(jiān)聽到修改數(shù)據(jù) console.log(leo.age); // 監(jiān)聽到請求數(shù)據(jù) // 10
訪問 leo 對象的 age 屬性,會通過 get 描述符處理,而修改 age 屬性,則會通過 set 描述符處理。
四、實現(xiàn)簡單的數(shù)據(jù)響應式
通過前面兩個小節(jié),我們復習了“觀察者模式”和“Object.defineProperty()” 方法,這兩個知識點在 Vue.js 響應式原理中非常重要。
接下來我們來實現(xiàn)一個很簡單的數(shù)據(jù)響應式變化,需求如下:點擊“更新數(shù)據(jù)”按鈕,文本更新。
接下來我們將實現(xiàn)三個類:
Dep 被觀察者類,用來生成被觀察者;
Watcher 觀察者類,用來生成觀察者;
Observer 類,將普通數(shù)據(jù)轉(zhuǎn)換為響應式數(shù)據(jù),從而實現(xiàn)響應式對象。
用一張圖來描述三者之間關(guān)系,現(xiàn)在看不懂沒關(guān)系,這小節(jié)看完可以再回顧這張圖:
1. 實現(xiàn)精簡觀察者模式
這里參照前面復習“觀察者模式”的示例,做下精簡:
// 實現(xiàn)被觀察者類 class Dep { constructor() { this.subs = []; } addSub(watcher) { this.subs.push(watcher); } notify(data) { this.subs.forEach(sub => sub.update(data)); } } // 實現(xiàn)觀察者類 class Watcher { constructor(cb) { this.cb = cb; } update(data) { this.cb(data); } }
Vue.js 響應式原理中,觀察者模式起到非常重要的作用。其中:
Dep 被觀察者類,提供用來收集觀察者( addSub )方法和通知觀察者( notify )方法;
Watcher 觀察者類,實例化時支持傳入回調(diào)( cb )方法,并提供更新( update )方法;
2. 實現(xiàn)生成響應式的類
這一步需要實現(xiàn) Observer 類,核心是通過 Object.defineProperty() 方法為對象的每個屬性設置 getter/setter,目的是將普通數(shù)據(jù)轉(zhuǎn)換為響應式數(shù)據(jù),從而實現(xiàn)響應式對象。
這里以最簡單的單層對象為例(下一節(jié)會介紹深層對象),如:
let initData = { text: '你好,前端自習課', desc: '每日清晨,享受一篇前端優(yōu)秀文章。' };
接下來實現(xiàn) Observer 類:
// 實現(xiàn)響應式類(最簡單單層的對象,暫不考慮深層對象) class Observer { constructor (node, data) { this.defineReactive(node, data) } // 實現(xiàn)數(shù)據(jù)劫持(核心方法) // 遍歷 data 中所有的數(shù)據(jù),都添加上 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() { // 創(chuàng)建觀察者 let watcher = new Watcher(v => vvm.innerText = v); dep.addSub(watcher); return value; }, set(newValue) { value = newValue; // 通知所有觀察者 dep.notify(newValue); } }) } } }
上面代碼的核心是 defineReactive 方法,它遍歷原始對象中每個屬性,為每個屬性實例化一個被觀察者(Dep),然后分別調(diào)用 Object.defineProperty() 方法,為每個屬性添加 getter/setter。
訪問數(shù)據(jù)時,getter 執(zhí)行依賴收集(即添加觀察者),通過實例化 Watcher 創(chuàng)建一個觀察者,并執(zhí)行被觀察者的 addSub() 方法添加一個觀察者;
修改數(shù)據(jù)時,setter 執(zhí)行派發(fā)更新(即通知觀察者),通過調(diào)用被觀察者的 notify() 方法通知所有觀察者,執(zhí)行觀察者 update() 方法。
3. 測試代碼
為了方便觀察數(shù)據(jù)變化,我們?yōu)椤案聰?shù)據(jù)”按鈕綁定點擊事件來修改數(shù)據(jù):
<div id="app"></div> <button id="update">更新數(shù)據(jù)</button>
測試代碼如下:
// 初始化測試數(shù)據(jù) let initData = { text: '你好,前端自習課', desc: '每日清晨,享受一篇前端優(yōu)秀文章。' }; const app = document.querySelector('#app'); // 步驟1:為測試數(shù)據(jù)轉(zhuǎn)換為響應式對象 new Observer(app, initData); // 步驟2:初始化頁面文本內(nèi)容 app.innerText = initData.text; // 步驟3:綁定按鈕事件,點擊觸發(fā)測試 document.querySelector('#update').addEventListener('click', function(){ initData.text = `我們必須經(jīng)常保持舊的記憶和新的希望。`; console.log(`當前時間:${new Date().toLocaleString()}`) })
測試代碼中,核心在于通過實例化 Observer,將測試數(shù)據(jù)轉(zhuǎn)換為響應式數(shù)據(jù),然后模擬數(shù)據(jù)變化,來觀察視圖變化。
每次點擊“更新數(shù)據(jù)”按鈕,在控制臺中都能看到“數(shù)據(jù)發(fā)生變化!”的提示,說明我們已經(jīng)能通過 setter 觀察到數(shù)據(jù)的變化情況。
當然,你還可以在控制臺手動修改 initData 對象中的 text 屬性,來體驗響應式變化~~
到這里,我們實現(xiàn)了非常簡單的數(shù)據(jù)響應式變化,當然 Vue.js 肯定沒有這么簡單,這個先理解,下一節(jié)看 Vue.js 響應式原理,思路就會清晰很多。
這部分代碼,我已經(jīng)放到我的 Github,地址:https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/Vue/Basics-Reactive-Demo.js
可以再回顧下這張圖,對整個過程會更清晰:
observer-watcher-dep.png
五、Vue.js 響應式實現(xiàn)
本節(jié)代碼:https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/Vue/leo-vue-reactive/
這里大家可以再回顧下下面這張官網(wǎng)經(jīng)典的圖,思考下前面講的示例。
(圖片來自:https://cn.vuejs.org/v2/guide/reactivity.html)
上一節(jié)實現(xiàn)了簡單的數(shù)據(jù)響應式,接下來繼續(xù)通過完善該示例,實現(xiàn)一個簡單的 Vue.js 響應式,測試代碼如下:
// index.js const vm = new Vue({ el: '#app', data(){ return { text: '你好,前端自習課', desc: '每日清晨,享受一篇前端優(yōu)秀文章。' } } });
是不是很有內(nèi)味了,下面是我們最終實現(xiàn)后項目目錄:
- mini-reactive / index.html // 入口 HTML 文件 / index.js // 入口 JS 文件 / observer.js // 實現(xiàn)響應式,將數(shù)據(jù)轉(zhuǎn)換為響應式對象 / watcher.js // 實現(xiàn)觀察者和被觀察者(依賴收集者) / vue.js // 實現(xiàn) Vue 類作為主入口類 / compile.js // 實現(xiàn)編譯模版功能
知道每一個文件功能以后,接下來將每一步串聯(lián)起來。
1. 實現(xiàn)入口文件
我們首先實現(xiàn)入口文件,包括 index.html / index.js 2 個簡單文件,用來方便接下來的測試。
1.1 index.html
<!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">更新數(shù)據(jù)</button> <script src="./index.js"></script> </body> </html>
1.2 index.js
"use strict"; const vm = new Vue({ el: '#app', data(){ return { text: '你好,前端自習課', desc: '每日清晨,享受一篇前端優(yōu)秀文章。' } } }); console.log(vm.$data.text) vm.$data.text = '頁面數(shù)據(jù)更新成功!'; // 模擬數(shù)據(jù)變化 console.log(vm.$data.text)
2. 實現(xiàn)核心入口 vue.js
vue.js 文件是我們實現(xiàn)的整個響應式的入口文件,暴露一個 Vue 類,并掛載全局。
class Vue { constructor (options = {}) { this.$el = options.el; this.$data = options.data(); this.$methods = options.methods; // [核心流程]將普通 data 對象轉(zhuǎn)換為響應式對象 new Observer(this.$data); if (this.$el) { // [核心流程]將解析模板的內(nèi)容 new Compile(this.$el, this) } } } window.Vue = Vue;
Vue 類入?yún)橐粋€配置項 option ,使用起來跟 Vue.js 一樣,包括 $el 掛載點、 $data 數(shù)據(jù)對象和 $methods 方法列表(本文不詳細介紹)。
通過實例化 Oberser 類,將普通 data 對象轉(zhuǎn)換為響應式對象,然后判斷是否傳入 el 參數(shù),存在時,則實例化 Compile 類,解析模版內(nèi)容。
總結(jié)下 Vue 這個類工作流程 :
3. 實現(xiàn) observer.js
observer.js 文件實現(xiàn)了 Observer 類,用來將普通對象轉(zhuǎn)換為響應式對象:
class Observer { constructor (data) { this.data = data; this.walk(data); } // [核心方法]將 data 對象轉(zhuǎn)換為響應式對象,為每個 data 屬性設置 getter 和 setter 方法 walk (data) { if (typeof data !== 'object') return data; Object.keys(data).forEach( key => { this.defineReactive(data, key, data[key]) }) } // [核心方法]實現(xiàn)數(shù)據(jù)劫持 defineReactive (obj, key, value) { this.walk(value); // [核心過程]遍歷 walk 方法,處理深層對象。 const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get () { console.log('[getter]方法執(zhí)行') Dep.target && dep.addSub(Dep.target); return value }, set (newValue) { console.log('[setter]方法執(zhí)行') if (value === newValue) return; // [核心過程]當設置的新值 newValue 為對象,則繼續(xù)通過 walk 方法將其轉(zhuǎn)換為響應式對象 if (typeof newValue === 'object') this.walk(newValue); value = newValue; dep.notify(); // [核心過程]執(zhí)行被觀察者通知方法,通知所有觀察者執(zhí)行 update 更新 } }) } }
相比較第四節(jié)實現(xiàn)的 Observer 類,這里做了調(diào)整:
增加 walk 核心方法,用來遍歷對象每個屬性,分別調(diào)用數(shù)據(jù)劫持方法( defineReactive() );
在 defineReactive() 的 getter 中,判斷 Dep.target 存在才添加觀察者,下一節(jié)會詳細介紹 Dep.target;
在 defineReactive() 的 setter 中,判斷當前新值( newValue )是否為對象,如果是,則直接調(diào)用 this.walk() 方法將當前對象再次轉(zhuǎn)為響應式對象,處理深層對象。
通過改善后的 Observer 類,我們就可以實現(xiàn)將單層或深層嵌套的普通對象轉(zhuǎn)換為響應式對象。
4. 實現(xiàn) watcher.js
這里實現(xiàn)了 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:表示當前操作的數(shù)據(jù)名稱 this.cb = cb; // cb:表示數(shù)據(jù)發(fā)生改變之后的回調(diào) Dep.target = this; // 全局唯一 thisthis.oldValue = this.vm.$data[key]; // 保存變化的數(shù)據(jù)作為舊值,后續(xù)作判斷是否更新 Dep.target = null; } update () { console.log(`數(shù)據(jù)發(fā)生變化!`); let oldValue = this.oldValue; let newValue = this.vm.$data[this.key]; if (oldValue != newValue) { // 比較新舊值,發(fā)生變化才執(zhí)行回調(diào) this.cb(newValue, oldValue); }; } }
相比較第四節(jié)實現(xiàn)的 Watcher 類,這里做了調(diào)整:
在構(gòu)造函數(shù)中,增加 Dep.target 值操作;
在構(gòu)造函數(shù)中,增加 oldValue 變量,保存變化的數(shù)據(jù)作為舊值,后續(xù)作為判斷是否更新的依據(jù);
在 update() 方法中,增加當前操作對象 key 對應值的新舊值比較,如果不同,才執(zhí)行回調(diào)。
Dep.target 是當前全局唯一的訂閱者,因為同一時間只允許一個訂閱者被處理。target 指當前正在處理的目標訂閱者,當前訂閱者處理完就賦值為 null 。這里 Dep.target 會在 defineReactive() 的 getter 中使用到。
通過改善后的 Watcher 類,我們操作當前操作對象 key 對應值的時候,可以在數(shù)據(jù)有變化的情況才執(zhí)行回調(diào),減少資源浪費。
4. 實現(xiàn) compile.js
compile.js 實現(xiàn)了 Vue.js 的模版編譯,如將 HTML 中的 {{text}} 模版轉(zhuǎn)換為具體變量的值。
compile.js 介紹內(nèi)容較多,考慮到篇幅問題,并且本文核心介紹響應式原理,所以這里就暫時不介紹 compile.js 的實現(xiàn),在學習的朋友可以到我 Github 上下載該文件直接下載使用即可,地址:
https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/Vue/leo-vue-reactive/compile.js
5. 測試代碼
到這里,我們已經(jīng)將第四節(jié)的 demo 改造成簡易版 Vue.js 響應式,接下來打開 index.html 看看效果:
當 index.js 中執(zhí)行到:
vm.$data.text = '我們必須經(jīng)常保持舊的記憶和新的希望。';
看完上述內(nèi)容,你們掌握Vue.js中實現(xiàn)響應式的原理是什么的方法了嗎?如果還想學到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。