1.前端框架的根本意義
1.1 前端框架的好處
最開始學習前端框架的時候(我第一個框架是 React)並不理解框架能帶來什麼,只是因為大家都在用框架,最實際的一個用途就是所有企業幾乎都在用框架,不用框架就 out 了.
隨著使用的深入我逐漸理解到框架的好處:
- 1.組件化: 其中以 React 的組件化最為徹底,甚至可以到函數級別的原子組件,高度的組件化可以是我們的工程易於維護、易於組合拓展。
- 2.天然分層: JQuery 時代的代碼大部分情況下是麵條代碼,耦合嚴重,現代框架不管是 MVC、MVP還是MVVM 模式都能幫助我們進行分層,代碼解耦更易於讀寫。
- 3.生態: 現在主流前端框架都自帶生態,不管是數據流管理架構還是 UI 庫都有成熟的解決方案。
![你為什麼要使用前端框架Vue?](http://p2.ttnews.xyz/loading.gif)
1.2 前端框架的根本意義
簡單來說,前端框架的根本意義是解決了UI 與狀態同步問題。
在 Vue 中我們如果要在todos中添加一條,只需要app4.todos.push({ text: '新項目' }),這時由於 Vue 內置的響應式系統會自動幫我們進行 UI 與狀態的同步工作.
{{ todo.text }}
var app4 = new Vue({
el: '#app-4',
data: {
todos: [
{ text: '學習 JavaScript' },
{ text: '學習 Vue' },
{ text: '整個牛項目' }
]
}
})
//在此我向大家推薦一個前端全棧開發交流圈:619586920 突破技術瓶頸,提升思維能力
如果我們用 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並不是一個好的選擇target=https%3A%2F%2Fjuejin.im%2Fpost%2F5acd0c8a6fb9a028da7cdfaf)。
我們將用 Proxy 實現一個響應式系統。
![你為什麼要使用前端框架Vue?](http://p2.ttnews.xyz/loading.gif)
2.1 發佈訂閱中心
一個響應式系統離不開發布訂閱模式,因為我們需要一個 Dep保存訂閱者,並在 Observer 發生變化時通知保存在 Dep 中的訂閱者,讓訂閱者得知變化並更新視圖,這樣才能保證視圖與狀態的同步。
/**
* [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 的同步完成了。
3.響應式系統並不是全部
響應式系統雖然是 Vue 的核心概念,但是一個響應式系統並不夠.
響應式系統雖然得知了數據值的變化,但是當值不能完整映射 UI 時,我們依然需要進行組件級別的 reRender,這種情況並不高效,因此 Vue 在2.0版本引入了虛擬 DOM, 虛擬 DOM進行進一步的 diff 操作可以進行細粒度更高的操作,可以保證 reReander 的下限(保證不那麼慢)。
除此之外為了方便開發者,vue 內置了眾多的指令,因此我們還需要一個 vue 模板解析器.
結語
感謝您的觀看,如有不足之處,歡迎批評指正。
對前端的技術,前端全棧技術感興趣的同學關注我的頭條號,並在後臺私信發送關鍵字:“前端”即可獲取免費的前端開發攻城師學習資料
知識體系已整理好,歡迎免費領取。還有視頻分享可以免費獲取。關注我,可以獲得沒有的經驗哦!
閱讀更多 前端攻城小牛 的文章
關鍵字: 組件 數據結構 JavaScript