面試官: 你爲何使用前端框架?

面試官系列(5): 你爲何使用前端框架?


往期


前言

自 Backbone 以後前端框架就如同雨後春筍般出現,咱們已經習慣了用各類框架進行開發,可是前端框架出現的意義是什麼?咱們爲何要選擇前端框架進行開發呢?javascript

提早聲明: 咱們沒有對傳入的參數進行及時判斷而規避錯誤,僅僅對核心方法進行了實現.html


文章目錄

  1. 前端框架的根本意義
  2. 一個完整的前端框架是怎樣的
  3. 基於Proxy如何實現響應式系統

1.前端框架的根本意義

1.1 前端框架的好處

最開始學習前端框架的時候(我第一個框架是 React)並不理解框架能帶來什麼,只是由於你們都在用框架,最實際的一個用途就是全部企業幾乎都在用框架,不用框架就 out 了.前端

隨着使用的深刻我逐漸理解到框架的好處:vue

  1. 組件化: 其中以 React 的組件化最爲完全,甚至能夠到函數級別的原子組件,高度的組件化能夠是咱們的工程易於維護、易於組合拓展。
  2. 自然分層: JQuery 時代的代碼大部分狀況下是麪條代碼,耦合嚴重,現代框架不論是 MVC、MVP仍是MVVM 模式都能幫助咱們進行分層,代碼解耦更易於讀寫。
  3. 生態: 如今主流前端框架都自帶生態,不論是數據流管理架構仍是 UI 庫都有成熟的解決方案。

1.2 前端框架的根本意義

上一節咱們只說了前端框架的好處,可是並無指出根本問題,直到我看到這篇文章(中文版)。java

簡單來講,前端框架的根本意義是解決了UI 與狀態同步問題react

在 Vue 中咱們若是要在todos中添加一條,只須要app4.todos.push({ text: '新項目' }),這時因爲 Vue 內置的響應式系統會自動幫咱們進行 UI 與狀態的同步工做.git

<div id="app-4">
  <ol>
    <li v-for="todo in todos">
      {{ todo.text }}
    </li>
  </ol>
</div>
複製代碼
var app4 = new Vue({
  el: '#app-4',
  data: {
    todos: [
      { text: '學習 JavaScript' },
      { text: '學習 Vue' },
      { text: '整個牛項目' }
    ]
  }
})
複製代碼

若是咱們用 JQuery 或者 JS 進行操做,免不了一大堆li.appendChilddocument.createElement等 DOM 操做,咱們須要一長串 DOM 操做保證狀態與 UI 的同步,其中一個環節出錯就會致使 BUG,手動操做的缺點以下:github

  1. 頻繁操做 DOM 性能低下.
  2. 中間步驟過多,易產生 bug且不易維護,並且心智要求較高不利於開發效率

不論是 vue 的數據劫持、Angular 的髒檢測仍是 React 的組件級 reRender都是幫助咱們解決 ui 與狀態同步問題的利器。面試

這也解釋了Backbone做爲前端框架鼻祖在以後落寞的緣由,Backbone只是引入了 MVC 的思想,並無解決 View 與 Modal 同步的問題,相比於現代的三大框架直接操做 Modal 就能夠同步 UI 的特性, Backbone 仍然與 JQuery 綁定,在 View 裏操做 Dom來達到同步 UI 的目的,這顯然是不符合現代前端框架設計要求的。算法


2.Vue 如何保證 UI 與狀態同步

UI 在 MVVM 中指的是 View,狀態在 MVVM 中指的是 Modal,而保證 View 和 Modal 同步的是 View-Modal。

Vue 經過一個響應式系統保證了View 與 Modal的同步,因爲要兼容IE,Vue 選擇了 Object.defineProperty做爲響應式系統的實現,可是若是不考慮 IE 用戶的話,Object.defineProperty並非一個好的選擇,具體請看面試官系列(4): 基於Proxy 數據劫持的雙向綁定優點所在

咱們將用 Proxy 實現一個響應式系統。

建議閱讀以前看一下面試官系列(4): 基於Proxy 數據劫持的雙向綁定優點所在中基於Object.defineProperty的大體實現。

2.1 發佈訂閱中心

一個響應式系統離不開發布訂閱模式,由於咱們須要一個 Dep保存訂閱者,並在 Observer 發生變化時通知保存在 Dep 中的訂閱者,讓訂閱者得知變化並更新視圖,這樣才能保證視圖與狀態的同步。

發佈訂閱模式請閱讀面試官系列(2): Event Bus的實現

/** * [subs description] 訂閱器,儲存訂閱者,通知訂閱者 * @type {Map} */
export default class Dep {
  constructor() {
    // 咱們用 hash 儲存訂閱者
    this.subs = new Map();
  }
  // 添加訂閱者
  addSub(key, sub) {
    // 取出鍵爲 key 的訂閱者
    const currentSub = this.subs.get(key);
    // 若是能取出說明有相同的 key 的訂閱者已經存在,直接添加
    if (currentSub) {
      currentSub.add(sub);
    } else {
      // 用 Set 數據結構儲存,保證惟一值
      this.subs.set(key, new Set([sub]));
    }
  }
  // 通知
  notify(key) {
  // 觸發鍵爲 key 的訂閱者們
    if (this.subs.get(key)) {
      this.subs.get(key).forEach(sub => {
        sub.update();
      });
    }
  }
}

複製代碼

2.2 監聽者的實現

咱們在訂閱器 Dep 中實現了一個notify方法來通知相應的訂閱這們,然而notify方法到底何時被觸發呢?

固然是當狀態發生變化時,即 MVVM 中的 Modal 變化時觸發通知,然而Dep 顯然沒法得知 Modal 是否發生了變化,所以咱們須要建立一個監聽者Observer來監聽 Modal, 當 Modal 發生變化的時候咱們就執行通知操做。

vue 基於Object.defineProperty來實現了監聽者,咱們用 Proxy 來實現監聽者.

Object.defineProperty監聽屬性不一樣, Proxy 能夠監聽(實際是代理)整個對象,所以就不須要遍歷對象的屬性依次監聽了,可是若是對象的屬性依然是個對象,那麼 Proxy 也沒法監聽,因此咱們實現了一個observify進行遞歸監聽便可。

/** * [Observer description] 監聽器,監聽對象,觸發後通知訂閱 * @param {[type]} obj [description] 須要被監聽的對象 */
const Observer = obj => {
  const dep = new Dep();
  return new Proxy(obj, {
    get: function(target, key, receiver) {
      // 若是訂閱者存在,直接添加訂閱
      if (Dep.target) {
        dep.addSub(key, Dep.target);
      }
      return Reflect.get(target, key, receiver);
    },
    set: function(target, key, value, receiver) {
       // 若是對象值沒有變,那麼不觸發下面的操做直接返回 
      if (Reflect.get(receiver, key) === value) {
        return;
      }
      const res = Reflect.set(target, key, observify(value), receiver);
      // 當值被觸發更改的時候,觸發 Dep 的通知方法
      dep.notify(key);
      return res;
    },
  });
};

/** * 將對象轉爲監聽對象 * @param {*} obj 要監聽的對象 */
export default function observify(obj) {
  if (!isObject(obj)) {
    return obj;
  }

  // 深度監聽
  Object.keys(obj).forEach(key => {
    obj[key] = observify(obj[key]);
  });

  return Observer(obj);
}
複製代碼

2.3 訂閱者的實現

咱們目前已經解決了兩個問題,一個是如何得知 Modal 發生了改變(利用監聽者 Observer 監聽 Modal 對象),一個是如何收集訂閱者並通知其變化(利用訂閱器收集訂閱者,並用notify通知訂閱者)。

咱們目前還差一個訂閱者(Watcher)

// 訂閱者
export default class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm; // vm 是 vue 的實例
    this.exp = exp; // 被訂閱的數據
    this.cb = cb; // 觸發更新後的回調
    this.value = this.get(); // 獲取老數據
  }
  get() {
    const exp = this.exp;
    let value;
    Dep.target = this;
    if (typeof exp === 'function') {
      value = exp.call(this.vm);
    } else if (typeof exp === 'string') {
      value = this.vm[exp];
    }
    Dep.target = null;
    return value;
  }
  // 將訂閱者放入待更新隊列等待批量更新
  update() {
    pushQueue(this);
  }
  // 觸發真正的更新操做
  run() {
    const val = this.get(); // 獲取新數據
    this.cb.call(this.vm, val, this.value);
    this.value = val;
  }
}

複製代碼

2.4 批量更新的實現

咱們在上一節中實現了訂閱者( Watcher),可是其中的update方法是將訂閱者放入了一個待更新的隊列中,而不是直接觸發,緣由以下:

所以這個隊列須要作的是異步去重,所以咱們用 Set做爲數據結構儲存 Watcher 來去重,同時用Promise模擬異步更新。

// 建立異步更新隊列
let queue = new Set()

// 用Promise模擬nextTick
function nextTick(cb) {
    Promise.resolve().then(cb)
}

// 執行刷新隊列
function flushQueue(args) {
    queue.forEach(watcher => {
            watcher.run()
        })
    // 清空
    queue = new Set()
}

// 添加到隊列
export default function pushQueue(watcher) {
    queue.add(watcher)
    // 下一個循環調用
    nextTick(flushQueue)
}

複製代碼

2.5 小結

咱們梳理一下流程, 一個響應式系統是如何作到 UI(View)與狀態(Modal)同步的?

咱們首先須要監聽 Modal, 本文中咱們用 Proxy 來監聽了 Modal 對象,所以在 Modal 對象被修改的時候咱們的 Observer 就能夠得知。

咱們得知Modal發生變化後如何通知 View 呢?要知道,一個 Modal 的改變可能觸發多個 UI 的更新,好比一個用戶的用戶名改變了,它的我的信息組件、通知組件等等組件中的用戶名都須要改變,對於這種狀況咱們很容易想到利用發佈訂閱模式來解決,咱們須要一個訂閱器(Dep)來儲存訂閱者(Watcher),當監聽到 Modal 改變時,咱們只須要通知相關的訂閱者進行更新便可。

那麼訂閱者來自哪裏呢?其實每個組件實例對應着一個訂閱者(正由於一個組件實例對應一個訂閱者,才能利用 Dep 通知到相應組件,否則亂套了,通知訂閱者就至關於間接通知了組件)。

當訂閱者得知了具體變化後它會進行相應的更新,將更新體如今 UI(View)上,至此UI 與 Modal 的同步完成了。

完整代碼已經在 github 上,目前只實現了一個響應式系統,接下來會逐步實現一個完整的迷你版 mvvm 框架,因此你能夠 star 或者 watch 來關注進度.


3.響應式系統並非所有

響應式系統雖然是 Vue 的核心概念,可是一個響應式系統並不夠.

響應式系統雖然得知了數據值的變化,可是當值不能完整映射 UI 時,咱們依然須要進行組件級別的 reRender,這種狀況並不高效,所以 Vue 在2.0版本引入了虛擬 DOM, 虛擬 DOM進行進一步的 diff 操做能夠進行細粒度更高的操做,能夠保證 reReander 的下限(保證不那麼慢)。

除此以外爲了方便開發者,vue 內置了衆多的指令,所以咱們還須要一個 vue 模板解析器.

下期預告

老掉牙問題,虛擬 DOM, 虛擬 DOM 的實現以及 diff 算法的優化。

相關文章
相關標籤/搜索