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


























 
 
 












 
 
 
 