這個系列來自之前做的内部教育訓練,删減了業務相關的部分,如有錯誤,歡迎指出。
鑒于知乎文章編輯器呵呵的體驗,可能會有排版錯誤,如發現也請指正。
系列目錄:
jnoodle:前端技術演進(一):Web前端技術基礎zhuanlan.zhihu.com
jnoodle:前端技術演進(二):前端與協定zhuanlan.zhihu.com
jnoodle:前端技術演進(三):前端安全zhuanlan.zhihu.com
jnoodle:前端技術演進(四):前端三層結構與應用zhuanlan.zhihu.com
jnoodle:前端技術演進(五):現代前端互動架構zhuanlan.zhihu.com
jnoodle:前端技術演進(六):前端項目與技術實踐zhuanlan.zhihu.com
jnoodle:前端技術演進(七):前端跨棧技術zhuanlan.zhihu.com
jnoodle:前端技術演進(八):未來前端趨勢zhuanlan.zhihu.com
随着前端技術的發展,前端架構也在不斷的改變。
操作DOM時代
DOM(Document Object Model,文檔對象模型)将 HTML 文檔表達為樹結構,并定義了通路和操作 HTML 文檔的标準方法。
前端開發基本上都會涉及到HTML頁面,也就避免不了和DOM打交道。
最早期的Web前端,就是一個靜态的黃頁,網頁上的内容不能更新。
慢慢的,使用者可以在Web頁面上進行一些簡單操作了,比如送出表單,檔案上傳。但是整個頁面的部分或者整體的更新,還是靠重新整理頁面來實作的。
随着AJAX技術的出現,前端頁面上的使用者操作越來越多,越來越複雜,是以就進入了對DOM元素的直接操作時代。要對DOM元素操作,就要使用DOM API,常見的DOM API有:
- 節點查詢:getElementById、getElementsByName、getElementsByClassName、getElementsByTagName、querySelector、querySelectorAll
- 節點建立:createElement、createDocumentFragment、createTextNode、cloneNode
- 節點修改:appendChild、replaceChild、removeChild、insertBefore、innerHTML
- 節點關系:parentNode、previousSibling、childNodes
- 節點屬性:innerHTML、attributes、getAttribute、setAttribure、getComputedStyle
- 内容加載:XMLHttpRequest、ActiveX
使用DOM API可以完成前端頁面中的任何操作,但是随着網站應用的複雜化,使用原生的API非常低效。是以 jQuery 這個用來操作DOM的互動架構就誕生了。
jQuery 為什麼能成為在這個時代最流行的架構呢?主要是他幫前端開發人員解決了太多問題:
- 封裝了DOM API,提供了統一和友善的調用方式。
- 簡化了元素的選擇,可以很快的選取到想要的元素。
- 提供了AJAX接口,對XMLHttpRequest和ActiveX統一封裝。
- 統一了事件處理。
- 提供異步處理機制。
- 相容大部分主流浏覽器。
除了解決了上面這些問題,jQuery還擁有良好的生态,海量的插件拿來即用,讓前端開發比以前流暢很多。尤其是在IE6、IE7時代,沒有jQuery,意味着無窮的相容性處理。
// DOM API:
document.querySelectorAll('#container li');
// jQuery
$('#container').find('li');
随着HTML5技術的發展,jQuery提供的很多方法已經在原生的标準中實作了,慢慢的,jQuery的必要性在逐漸降低。http://youmightnotneedjquery.com/
漸漸地,SPA(Single Page Application,單頁面應用)開始被廣泛認可,整個應用的内容都在一個頁面中并完全通過異步互動來加載不同的内容,這時候使用 jQuery 直接操作DOM的方式就不容易管理了,頁面上事件的綁定會變得混亂,在這種情況下,迫切需要一個可以自動管理頁面上DOM和資料之間互動操作的架構。
MV* 模式
MVC,MVP和MVVM都是常見的軟體架構設計模式(Architectural Pattern),它通過分離關注點來改進代碼的組織方式。
單純從概念上,很難區分和感受出來這三種模式在前端架構中有什麼不同。我們通過一個例子來體會一下:有一個可以對數值進行加減操作的元件:上面顯示數值,兩個按鈕可以對數值進行加減操作,操作後的數值會更新顯示。
Model層用于封裝和應用程式的業務邏輯相關的資料以及對資料的處理方法。這裡我們把需要用到的數值變量封裝在Model中,并定義了add、sub、getVal三種操作數值方法。
var myapp = {}; // 建立這個應用對象
myapp.Model = function() {
var val = 0; // 需要操作的資料
/* 操作資料的方法 */
this.add = function(v) {
if (val < 100) val += v;
};
this.sub = function(v) {
if (val > 0) val -= v;
};
this.getVal = function() {
return val;
};
};
View作為視圖層,主要負責資料的展示。
myapp.View = function() {
/* 視圖元素 */
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');
/* 渲染資料 */
this.render = function(model) {
$num.text(model.getVal() + 'rmb');
};
};
這裡,通過Model&View完成了資料從模型層到視圖層的邏輯。但對于一個應用程式,這遠遠是不夠的,我們還需要響應使用者的操作、同步更新View和Model。
前端 MVC 模式
MVC(Model View Controller)是一種很經典的設計模式。使用者對View的操作交給了Controller處理,在Controller中響應View的事件調用Model的接口對資料進行操作,一旦Model發生變化便通知相關視圖進行更新。
Model層用來存儲業務的資料,一旦資料發生變化,模型将通知有關的視圖。
// Model
myapp.Model = function() {
var val = 0;
this.add = function(v) {
if (val < 100) val += v;
};
this.sub = function(v) {
if (val > 0) val -= v;
};
this.getVal = function() {
return val;
};
/* 觀察者模式 */
var self = this,
views = [];
this.register = function(view) {
views.push(view);
};
this.notify = function() {
for(var i = 0; i < views.length; i++) {
views[i].render(self);
}
};
};
Model和View之間使用了觀察者模式,View事先在此Model上注冊,進而觀察Model,以便更新在Model上發生改變的資料。
View和Controller之間使用了政策模式,這裡View引入了Controller的執行個體來實作特定的響應政策,比如這個栗子中按鈕的 click 事件:
// View
myapp.View = function(controller) {
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');
this.render = function(model) {
$num.text(model.getVal() + 'rmb');
};
/* 綁定事件 */
$incBtn.click(controller.increase);
$decBtn.click(controller.decrease);
};
控制器是模型和視圖之間的紐帶,MVC将響應機制封裝在Controller對象中,當使用者和應用産生互動時,控制器中的事件觸發器就開始工作了。
// Controller
myapp.Controller = function() {
var model = null,
view = null;
this.init = function() {
/* 初始化Model和View */
model = new myapp.Model();
view = new myapp.View(this);
/* View向Model注冊,當Model更新就會去通知View啦 */
model.register(view);
model.notify();
};
/* 讓Model更新數值并通知View更新視圖 */
this.increase = function() {
model.add(1);
model.notify();
};
this.decrease = function() {
model.sub(1);
model.notify();
};
};
這裡我們執行個體化View并向對應的Model執行個體注冊,當Model發生變化時就去通知View做更新。
可以明顯感覺到,MVC模式的業務邏輯主要集中在Controller,而前端的View其實已經具備了獨立處理使用者事件的能力,當每個事件都流經Controller時,這層會變得十分臃腫。而且MVC中View和Controller一般是一一對應的,捆綁起來表示一個元件,視圖與控制器間的過于緊密的連接配接讓Controller的複用性成了問題,如果想多個View共用一個Controller該怎麼辦呢?
前端 MVP 模式
MVP(Model-View-Presenter)是MVC模式的改良。和MVC的相同之處在于:Controller/Presenter負責業務邏輯,Model管理資料,View負責顯示。
在MVC裡,View是可以直接通路Model的。而MVP中的View并不能直接使用Model,而是通過為Presenter提供接口,讓Presenter去更新Model,再通過觀察者模式更新View。
與MVC相比,MVP模式通過解耦View和Model,完全分離視圖和模型使職責劃分更加清晰;由于View不依賴Model,可以将View抽離出來做成元件,它隻需要提供一系列接口提供給上層操作。
// Model
myapp.Model = function() {
var val = 0;
this.add = function(v) {
if (val < 100) val += v;
};
this.sub = function(v) {
if (val > 0) val -= v;
};
this.getVal = function() {
return val;
};
};
Model層依然是主要與業務相關的資料和對應處理資料的方法,很簡單。
// View
myapp.View = function() {
var $num = $('#num'),
$incBtn = $('#increase'),
$decBtn = $('#decrease');
this.render = function(model) {
$num.text(model.getVal() + 'rmb');
};
this.init = function() {
var presenter = new myapp.Presenter(this);
$incBtn.click(presenter.increase);
$decBtn.click(presenter.decrease);
};
};
MVP定義了Presenter和View之間的接口,使用者對View的操作都轉移到了Presenter。比如這裡的View暴露setter接口(render方法)讓Presenter調用,待Presenter通知Model更新後,Presenter調用View提供的接口更新視圖。
// Presenter
myapp.Presenter = function(view) {
var _model = new myapp.Model();
var _view = view;
_view.render(_model);
this.increase = function() {
_model.add(1);
_view.render(_model);
};
this.decrease = function() {
_model.sub(1);
_view.render(_model);
};
};
Presenter作為View和Model之間的“中間人”,除了基本的業務邏輯外,還有大量代碼需要對從View到Model和從Model到View的資料進行“手動同步”,這樣Presenter顯得很重,維護起來會比較困難。如果Presenter對視圖渲染的需求增多,它不得不過多關注特定的視圖,一旦視圖需求發生改變,Presenter也需要改動。
前端 MVVM 模式
MVVM(Model-View-ViewModel)最早由微軟提出。ViewModel指 "Model of View"——視圖的模型。
MVVM把View和Model的同步邏輯自動化了。以前Presenter負責的View和Model同步不再手動地進行操作,而是交給架構所提供的資料綁定功能進行負責,隻需要告訴它View顯示的資料對應的是Model哪一部分即可。
我們使用Vue來完成這個栗子。
在MVVM中,我們可以把Model稱為資料層,因為它僅僅關注資料本身,不關心任何行為(格式化資料由View的負責),這裡可以把它了解為一個類似json的資料對象。
// Model
var data = {
val: 0
};
和MVC/MVP不同的是,MVVM中的View通過使用模闆文法來聲明式的将資料渲染進DOM,當ViewModel對Model進行更新的時候,會通過資料綁定更新到View。
<!-- View -->
<div id="myapp">
<div>
<span>{{ val }}rmb</span>
</div>
<div>
<button v-on:click="sub(1)">-</button>
<button v-on:click="add(1)">+</button>
</div>
</div>
ViewModel大緻上就是MVC的Controller和MVP的Presenter了,也是整個模式的重點,業務邏輯也主要集中在這裡,其中的一大核心就是資料綁定。與MVP不同的是,沒有了View為Presente提供的接口,之前由Presenter負責的View和Model之間的資料同步交給了ViewModel中的資料綁定進行處理,當Model發生變化,ViewModel就會自動更新;ViewModel變化,Model也會更新。
new Vue({
el: '#myapp',
data: data,
methods: {
add(v) {
if(this.val < 100) {
this.val += v;
}
},
sub(v) {
if(this.val > 0) {
this.val -= v;
}
}
}
});
整體來看,比MVC/MVP精簡了很多,不僅僅簡化了業務與界面的依賴,還解決了資料頻繁更新(之前用jQuery操作DOM很繁瑣)的問題。因為在MVVM中,View不知道Model的存在,ViewModel和Model也察覺不到View,這種低耦合模式可以使開發過程更加容易,提高應用的可重用性。
資料綁定
在Vue中,使用了雙向綁定技術(Two-Way-Data-Binding),就是View的變化能實時讓Model發生變化,而Model的變化也能實時更新到View。其實雙向資料綁定,可以簡單地了解為一個模版引擎,但是會根據資料變更實時渲染。
有人還不要臉的申請了專利:
資料變更檢測
不同的MVVM架構中,實作雙向資料綁定的技術有所不同。目前一些主流的實作資料綁定的方式大緻有以下幾種:
手動觸發綁定
手動觸發指令綁定是比較直接的實作方式,主要思路是通過在資料對象上定義get()方法和set()方法,調用時手動觸發get ()或set()函數來擷取、修改資料,改變資料後會主動觸發get()和set()函數中View層的重新渲染功能。
髒檢測機制
Angularjs是典型的使用髒檢測機制的架構,通過檢查髒資料來進行View層操作更新。
髒檢測的基本原理是在ViewModel對象的某個屬性值發生變化時找到與這個屬性值相關的所有元素,然後再比較資料變化,如果變化則進行Directive 指令調用,對這個元素進行重新掃描渲染。
前端資料對象劫持
資料劫持是目前使用比較廣泛的方式。其基本思路是使用 Object.defineProperty 和 Object.defineProperies 對ViewModel資料對象進行屬性get ()和set()的監聽,當有資料讀取和指派操作時則掃描元素節點,運作指定對應節點的Directive指令,這樣ViewModel使用通用的等号指派就可以了。
Vue就是典型的采用資料劫持和釋出訂閱模式的架構。
- Observer 資料監聽器:負責對資料對象的所有屬性進行監聽(資料劫持),監聽到資料發生變化後通知訂閱者。
- Compiler 指令解析器:掃描模闆,并對指令進行解析,然後綁定指定事件。
- Watcher 訂閱者:關聯Observer和Compile,能夠訂閱并收到屬性變動的通知,執行指令綁定的相應操作,更新視圖。
ES6 Proxy
之前我們說過Proxy 實作資料劫持的方法:
總結來看,前端架構從直接DOM操作到MVC設計模式,然後到MVP,再到MVVM架構,前端設計模式的改進原則一直向着高效、易實作、易維護、易擴充的基本方向發展。雖然目前前端各類架構也已經成熟并開始向高版本疊代,但是還沒有結束,我們現在的程式設計對象依然沒有脫離DOM程式設計的基本套路,一次次架構的改進大大提高了開發效率,但是DOM元素運作的效率仍然沒有變。對于這個問題的解決,有的架構提出了Virtual DOM的概念。
Virtual DOM
MVVM的前端互動模式大大提高了程式設計效率,自動雙向資料綁定讓我們可以将頁面邏輯實作的核心轉移到資料層的修改操作上,而不再是在頁面中直接操作DOM。盡管MVVM改變了前端開發的邏輯方式,但是最終資料層反應到頁面上View層的渲染和改變仍是通過對應的指令進行DOM操作來完成的,而且通常一次ViewModel的變化可能會觸發頁面上多個指令操作DOM的變化,帶來大量的頁面結構層DOM操作或渲染。
比如一段僞代碼:
<ul>
<li repeat="list">{{ list.value }}</li>
</ul>
let viewModel = new VM({
data:{
list:[{value: 1},{value: 2},{value: 3}]
}
})
使用MVVM架構生成一個數字清單,此時如果需要顯示的内容變成了
[{value: 1},{value: 2},{value: 3}]
和
{value: 4}
,那麼該怎樣将這個增加的資料反映到View層上呢?可以将新的Model data 和舊的Model data 進行對比,然後記錄ViewModel的改變方式和位置,就知道了這次View 層應該怎樣去更新,這樣比直接重新渲染整個清單高效得多。
這裡其實可以了解為,ViewModel 裡的資料就是描述頁面View 内容的另一種資料結構辨別,不過需要結合特定的MVVM描述文法編譯來生成完整的DOM結構。
可以用JavaScript對象的屬性層級結構來描述上面HTML DOM對象樹的結構,當資料改變時,新生成一份改變後的Elements,并與原來的Elemnets結構進行對比,對比完成後,再決定改變哪些DOM元素。
剛才例子裡的 ulElement 對象可以了解為VirtualDOM。通常認為,Virtual DOM是一個能夠直接描述一段HTMLDOM結構的JavaScript對象,浏覽器可以根據它的結構按照一定規則建立出确定唯一的HTML DOM結構。整體來看,Virtual DOM的互動模式減少了MVVM或其他架構中對DOM的掃描或操作次數,并且在資料發生改變後隻在合适的地方根據JavaScript對象來進行
最小化的頁面DOM操作,避免大量重新渲染。
diff算法
Virtual-DOM的執行過程:
用JS對象模拟DOM樹 -> 比較兩棵虛拟DOM樹的差異 -> 把差異應用到真正的DOM樹上
在Virtual DOM中,最主要的一環就是通過對比找出兩個Virtual DOM的差異性,得到一個差異樹對象。
對于Virtual DOM的對比算法實際上是對于多叉樹結構的周遊算法。但是找到任意兩個樹之間最小的修改步驟,一般會循環遞歸對節點進行依次對比,算法複雜度達到 O(n^3),這個複雜度非常高,比如要展示1000多個節點,最悲觀要依次執行上十億次的比較。是以不同的架構采用的對比算法其實是一個略簡化的算法。
拿React來說,由于web應用中很少出現将一個元件移動到不同的層級,絕大多數情況下都是橫向移動。是以React嘗試逐層的對比兩棵樹,一旦出現不一緻,下層就不再比較了,在損失較小的情況下顯著降低了比較算法的複雜度。
前端架構的演進非常快,是以隻有知道演進的原因,才能去了解各個架構的優劣,進而根據應用的實際情況來選擇最合适的架構。對于其他技術也是如此。前端技術演進系列參考文章
以下連結可能不全,如果沒有列出的,請私信我告知,多謝。
特别感謝《現代前端技術解析》的作者張成文。
- https://book.douban.com/subject/27021790/
- https://developers.google.com/web/tools/chrome-devtools/
- https://juejin.im/post/5b148a2ce51d4506965908d2
- https://www.html5rocks.com/en/tutorials/internals/howbrowserswork
- https://hit-alibaba.github.io/interview/basic/network/HTTP.html
- http://www.ruanyifeng.com/blog/2016/08/http.html
- https://www.jianshu.com/p/80e25cb1d81a
- https://developers.google.com/web/fundamentals/performance/http2/
- https://zhuanlan.zhihu.com/p/29609078
- https://developers.google.com/web/fundamentals/security/encrypt-in-transit/why-https
- https://imtangqi.com/2016/04/07/the-notes-of-learning-illustrating-http-three/
- http://www.ruanyifeng.com/blog/2011/09/restful.html
- https://www.zcfy.cc/article/rest-apis-are-rest-in-peace-apis-long-live-graphql-3935.html
- https://graphql.cn/learn/
- http://jerryzou.com/posts/10-questions-about-graphql/
- https://www.jianshu.com/p/2ad286397f7a?open_source=weibo_search
- https://medium.freecodecamp.org/rest-apis-are-rest-in-peace-apis-long-live-graphql-d412e559d8e4
- https://xiaomingplus.com/full-stack/graphql-intro/
- https://juejin.im/post/5a3f1b8951882529c70f56e5
- https://juejin.im/post/58cdeba62f301e007e4af7e6
- https://blog.ymfe.org/%E6%B7%B7%E5%90%88%E5%BC%80%E5%8F%91%E4%B8%AD%E7%9A%84JSBridge/
- https://insights.thoughtworks.cn/eight-security-problems-in-front-end/
- https://legacy.gitbook.com/book/wizardforcel/mst-sec-lecture-notes/details
- https://cloud.tencent.com/developer/article/1136202
- http://momomoxiaoxi.com/2017/10/10/XSS/
- http://www.jsfuck.com/
- https://www.kanxue.com/book-6-59.htm
- https://www.cfca.com.cn/20180605/100003135.html
- https://frontendmasters.com/books/front-end-handbook/2018/recap.html
- https://www.html5rocks.com/zh/tutorials/webcomponents/shadowdom/
- http://www.cnblogs.com/coco1s/p/5711795.html
- https://developers.google.com/web/fundamentals/web-components/shadowdom?hl=zh-cn
- https://developers.google.com/web/fundamentals/design-and-ux/responsive/
- https://developers.google.com/web/fundamentals/design-and-ux/responsive/images
- https://javascript.ruanyifeng.com/introduction/history.html
- https://www.ibm.com/developerworks/cn/web/wa-lo-webassembly-status-and-reality/index.html
- https://segmentfault.com/a/1190000011452776
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Details_of_the_Object_Model
- https://developers.google.com/web/fundamentals/primers/promises?hl=zh-cn
- https://juejin.im/post/5acd0c8a6fb9a028da7cdfaf
- https://blog.csdn.net/caijunfen/article/details/78478438
- https://github.com/ruanyf/jstraining/blob/master/docs/history.md
- https://www.slimhill.com/webpack/
- https://zhuanlan.zhihu.com/p/30701816
- https://medium.com/jsdownunder/rollup-vs-webpack-javascript-bundling-in-2018-b35758a2268
- https://segmentfault.com/a/1190000004010453
- https://yq.aliyun.com/articles/222535
- https://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/001434501245426ad4b91f2b880464ba876a8e3043fc8ef000
- https://nodejs.org/zh-cn/docs/guides/
- https://www.cnblogs.com/yeyinfu/p/7317256.html
- https://frontendmasters.com/books/front-end-handbook/2018/2018.html
- https://github.com/ruanyf/jstraining/blob/master/docs/history.md