面試官系列(5): 你為什麼使用前端框架?
往期
前言
自 Backbone 之後前端框架就如同雨後春筍般出現,我們已經習慣了用各種框架進行開發,但是前端框架出現的意義是什麼?我們為什麼要選擇前端框架進行開發呢?
提前宣告: 我們沒有對傳入的引數進行及時判斷而規避錯誤,僅僅對核心方法進行了實現.
文章目錄
- 前端框架的根本意義
- 一個完整的前端框架是怎樣的
- 基於Proxy如何實現響應式系統
1.前端框架的根本意義
1.1 前端框架的好處
最開始學習前端框架的時候(我第一個框架是 React)並不理解框架能帶來什麼,只是因為大家都在用框架,最實際的一個用途就是所有企業幾乎都在用框架,不用框架就 out 了.
隨著使用的深入我逐漸理解到框架的好處:
- 元件化: 其中以 React 的元件化最為徹底,甚至可以到函式級別的原子元件,高度的元件化可以是我們的工程易於維護、易於組合擴充。
- 天然分層: JQuery 時代的程式碼大部分情況下是麵條程式碼,耦合嚴重,現代框架不管是 MVC、MVP還是MVVM 模式都能幫助我們進行分層,程式碼解耦更易於讀寫。
- 生態: 現在主流前端框架都自帶生態,不管是資料流管理架構還是 UI 庫都有成熟的解決方案。
1.2 前端框架的根本意義
上一節我們只說了前端框架的好處,但是並沒有指出根本問題,直到我看到這篇文章(中文版)。
簡單來說,前端框架的根本意義是解決了UI 與狀態同步問題。
在 Vue 中我們如果要在todos
中新增一條,只需要app4.todos.push({ text: '新專案' })
,這時由於 Vue 內建的響應式系統會自動幫我們進行 UI 與狀態的同步工作.
<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.appendChild
、document.createElement
等 DOM 操作,我們需要一長串 DOM 操作保證狀態與 UI 的同步,其中一個環節出錯就會導致 BUG,手動操作的缺點如下:
- 頻繁操作 DOM 效能低下.
- 中間步驟過多,易產生 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 演算法的優化。