Vue底層學習2——手擼數據響應化

全手打原創,轉載請標明出處:https://www.cnblogs.com/dreamsqin/p/14982040.html, 多謝,=。=~(如果對你有幫助的話請幫我點個贊啦)

作爲一個Web前端開發人員,使用Vue框架進行項目開發已經有一陣子,掐指一算,是時候認真探索一下Vue的底層了,以前的瞭解比較偏理論,這一次打算在弄清基本原理的前提下自己手寫Vue中的核心部分,也許這樣我纔敢說自己“深入理解”了Vue。上一篇聊了聊大家熟知的理論部分,本篇就來手擼數據響應化代碼,即數據遍歷並重寫settergetter

Object.defineProperty(obj, prop, descriptor)

它是實現數據響應式的核心,該方法可以直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。

  • obj:要定義或修改的屬性對象;
  • prop:要定義或修改的屬性名稱;
  • descriptor:要定義或修改的屬性描述,詳細說明可參見我之前寫的《Javascript基礎鞏固系列——標準庫Object對象》中屬性描述對象章節;
    下面是一個自定義setter和getter的簡單例子:
function Archiver() {
  var temperature = null;
  var archive = [];

  Object.defineProperty(this, 'temperature', {
    get: function() {
      console.log('get!');
      return temperature;
    },
    set: function(value) {
      temperature = value;
      archive.push({ val: temperature });
    }
  });

  this.getArchive = function() { return archive; };
}

var arc = new Archiver();
arc.temperature; // 訪問temperature屬性時會調用get方法,控制檯打印:'get!'
arc.temperature = 11 // 修改temperature屬性時會調用set方法,爲archive數組添加日誌條目:{val: 11};
arc.temperature = 13; // 修改temperature屬性時會調用set方法,爲archive數組添加日誌條目:{val: 13};
arc.getArchive(); //調用getArchive方法返回archive數組:[{ val: 11 }, { val: 13 }]

通過Object.defineProperty讀取和設置DOM節點內容

在上一篇做原理解析的時候有提到Vue是通過Object.defineProperty重寫data對象中各個屬性的settergetter,用於實現【響應式】和【依賴收集】。那麼我們先做一件事,通過Object.defineProperty讀取和設置DOM節點內容,以此體會一下數據驅動視圖的概念。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>defineProperty</title>
</head>
<body>
  <div id="app">
    <p id="name"></p>
  </div>

  <script>
    var obj = {};
    Object.defineProperty(obj, 'name', {
      get: function() {
      	// 訪問obj對象的name屬性時,獲取id爲name的節點內容 
        return document.querySelector('#name').innerHTML;
      },
      set: function(value) {
      	// 修改obj對象的name屬性時,設置id爲name的節點內容爲修改後的值
        document.querySelector('#name').innerHTML = value;
      }
    })
    // 數據驅動視圖變更
    obj.name = 'dreamsyang';
  </script>
</body>
</html>

運行結果如下,可以看到obj對象的name屬性值被修改後DOM節點內容也同步更新:

自建數據響應式框架

有了上面的例子做鋪墊應該對響應式有些許感覺,接下來我們自己搭建一個Vue響應式框架,需要達到的效果就是在new一個Vue實例之後實現數據初始化,達到響應式效果~爲了區別於Vue,我重新命名爲MVue

新建MVue.js文件用於MVue類封裝,參數接收配置對象

/*** MVue.js ***/
// new MVue({ data: {...} })

class MVue {
  constructor(options) {
    // 數據緩存
    this.$options = options;
    this.$data = options.data;

    // 數據遍歷
    this.observe(this.$data);
  }
}

通過observe實現data數據遍歷

/*** MVue.js ***/
// new MVue({ data: {...} })

class MVue {
  constructor(options) {...}

  observe(data) {
    // 確定data存在並且爲對象
    if (!data || typeof data !== 'object') {
      return;
    }

    // 遍歷data對象
    Object.keys(data).forEach(key => {
    	// 重寫對象屬性的getter和setter,實現數據的響應化
        this.defineReactive(data, key, data[key]);
    })
  }
}

通過defineReactive重寫gettersetter實現數據響應化

/*** MVue.js ***/
// new MVue({ data: {...} })

class MVue {
  constructor(options) {...}

  observe(data) {...}

  defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
      get: function() {
        return val;
      },
      set: function(newVal) {
        // 判斷屬性值是否發生變化
        if (newVal === val) {
          return;
        }
        val = newVal;
        // 預留視圖更新
        console.log(`${key}屬性更新了:${val}`);
      }
    })
  }
}

自建框架測試demo1

完成上述步驟後先看看目前的效果,寫個小demo測試一下:

<!-- demo1.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>demo1</title>
</head>
<body>
  <script src="MVue.js"></script>
  <script>
    const app = new MVue({
      data: {
        name: 'dreamsyang',
        infoObj: {
          location: 'chongqing',
        }
      }
    })
    app.$data.name = 'hello, dreamsyang!';
    app.$data.infoObj.location = 'oh, chongqing!';
  </script>
</body>
</html>

運行結果如下,可以看到app.$data.infoObj.location = 'oh, chongqing!'並未觸發setter中的打印,其主要原因是我們在遍歷data時不是深度遍歷:

通過遞歸實現深度遍歷

我們只需要在defineReactive執行的開始再次調用observe即可,如果val不爲對象,就會結束執行,如果爲對象就會深度遍歷。

/*** MVue.js ***/
// new MVue({ data: {...} })

class MVue {
  constructor(options) {
    // 數據緩存
    this.$options = options;
    this.$data = options.data;

    // 數據遍歷
    this.observe(this.$data);
  }

  observe(data) {
    // 確定data存在並且爲對象
    if (!data || typeof data !== 'object') {
      return;
    }

    // 遍歷data對象
    Object.keys(data).forEach(key => {
        // 重寫對象屬性的getter和setter,實現數據的響應化
        this.defineReactive(data, key, data[key]);
    })
  }

  defineReactive(obj, key, val) {
    // 解決數據嵌套,遞歸實現深度遍歷
    this.observe(val);

    Object.defineProperty(obj, key, {
      get: function() {
        return val;
      },
      set: function(newVal) {
        // 判斷屬性值是否發生變化
        if (newVal === val) {
          return;
        }
        val = newVal;
        // 預留視圖更新
        console.log(`${key}屬性更新了:${val}`);
      }
    })
  }
}

再次執行demo1結果如下,可以看到正常打印了:

參考資料

1、Object.definePropertyhttps://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
2、Vue源碼:https://github.com/vuejs/vue

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章