前言
“步入前端兩年半畜晰,自覺菜雞懶又爛∪鹂穑” 近來想著寫寫一些前端學(xué)習(xí)的心得凄鼻,左思右想。還是從 React 入筆聚假。為什么是 React块蚌?身為小白,React 龐大的技術(shù)棧確實給我很多編程思想上的啟迪膘格,也讓我了解到更多前端領(lǐng)域的知識峭范。時至今日,自己仍在探索 React 的路上瘪贱。在此纱控,感謝一路上并肩作戰(zhàn)的戰(zhàn)友和捎帶一段車程的司機們。
零菜秦、初識 React
剛開始接觸 React甜害,大概是去年這個時候。當時 React 在 Github 的 stars 飛升球昨,超過了之前如火中天的 AngularJS 尔店。一開始會覺得 React 通過 jsx 實現(xiàn) HTML 模板的做法并不比 AngularJS 的模板好用,因為按照傳統(tǒng)的前端開發(fā)模式褪尝,HTML 負責結(jié)構(gòu)層闹获,CSS 負責渲染層,JavaScript 負責行為交互層河哑,這似乎有所違背避诽。但后來對 Virtual DOM 有了進一步的了解,才發(fā)現(xiàn)這是 React 的精髓所在璃谨。
一沙庐、Virtual DOM
Virtual DOM?說到 Virtual DOM 一開始是聽說它的性能很高佳吞,是啊拱雏,渲染性能確實很高,但是只是單純使用不去思考底扳,那我又能獲得什么呢铸抑?一個庫的使用方法?了解一門前端熱門的庫衷模?思前想后鹊汛,我覺得 Virtual DOM 給我的啟迪大致有以下兩方面:性能優(yōu)化策略(diff 算法)和 顛覆傳統(tǒng) DOM 編程思想蒲赂。
1.性能優(yōu)化策略
- 傳統(tǒng)的瀏覽器渲染流程:瀏覽器中的渲染工作主要是由 渲染引擎 完成的。大致流程如下(不考慮 阻塞 等特殊情況)刁憋。
- 解析 HTML 文件滥嘴,構(gòu)建 DOM 樹;
- 解析 CSS 文件至耻,構(gòu)建 CSSOM 若皱;
- 合并 DOM 樹和 CSSOM;
- 布局(計算 DOM 節(jié)點在屏幕上的精確位置)尘颓,繪制(計算對應(yīng)節(jié)點的樣式)走触;
- 重排(DOM結(jié)構(gòu)變化),重繪(CSS變化)泥耀;
- 很明顯饺汹,通過上面的描述,可以看出每一次 DOM 結(jié)構(gòu)的改變?yōu)g覽器都會進行極大的計算量痰催,因此兜辞,Virtual DOM 做了大致以下兩個方面的處理:
- 在瀏覽器內(nèi)存中,維護著一課與頁面 DOM 結(jié)構(gòu)一致的對象樹
依賴 diff 算法極大提高了 DOM 操作的性能
簡單地說夸溶,Virtual DOM 通過批量處理 DOM 操作逸吵,可以理解為對多次 DOM 操作起了一次緩存作用,從而合并多次的 DOM 操作為一次計算缝裁。(實現(xiàn)方法是將一個事件循環(huán)中發(fā)出的 DOM 操作全部收集起來扫皱,不立即在頁面上產(chǎn)生效果,而是在事件循環(huán)的結(jié)尾捷绑,才向頁面作用)
那么韩脑,Virtual DOM 的計算過程是怎樣優(yōu)化的呢(即瀏覽器內(nèi)存中的 DOM)?不得不說粹污,React 做了一次大膽的嘗試段多。傳統(tǒng)標準的 Diff 算法 復(fù)雜度達到了 O(n^3),這就意味著要展示 1000 個節(jié)點壮吩,就要依次執(zhí)行上十億次的比較进苍。這是絕對無法滿足性能需求的。而 React 開發(fā)團隊通過制定大膽的策略鸭叙,使得 Diff 算法 復(fù)雜度降到 O(n)觉啊。但是,React 的優(yōu)化算法是基于以下三個前提的沈贝。
- Web UI 中 DOM 節(jié)點跨層級的移動操作特別少杠人,可以忽略不計
- 擁有相同類的兩個組件將會生成相似的樹形結(jié)構(gòu),擁有不同類的兩個組件將會生成不同的樹形結(jié)構(gòu)
- 對于同一層級的一組子節(jié)點,它們可以通過唯一 id 進行區(qū)分
于是搜吧,React 基于以上三個前提策略市俊,分別對 Tree diff,Component diff滤奈,Element diff 進行算法優(yōu)化,事實也證明這三個前提策略是合理且準確的撩满,它保證了整體界面構(gòu)建性能蜒程。
- Tree diff:只會對同一父節(jié)點下的所有子節(jié)點進行比較,當發(fā)現(xiàn)節(jié)點不存在伺帘,則該節(jié)點及其子節(jié)點會被完全刪除掉昭躺,不會用于進一步的比較
- Component diff:React 是基于組件構(gòu)建,如果同一類型的組件伪嫁,則按照原策略繼續(xù)比較 Virtual DOM tree领炫,如果不是,則將該組件判斷為 dirty component张咳,從而替換整個組件下的所有子節(jié)點(另外帝洪,React 提供了一個函數(shù)鉤子
shouldComponentUpdate()
來判斷該組件是否需要進行 diff) - Element diff:當節(jié)點處于同一層級時,React 提供了三種節(jié)點操作脚猾,分別為插入(INSERT——MARKUP)葱峡、移動(MOVE_EXISTING)、刪除(REMOVE_NODE)
2.抽象——編程思想的顛覆龙助?
一開始思考 React 跟 Vue 的區(qū)別的時候砰奕,就只有直觀的架構(gòu)模式 MVC,MVVM 的區(qū)別提鸟,但是 MVC 只要加個 事件監(jiān)聽 跟 數(shù)據(jù)劫持 不也可以實現(xiàn) 數(shù)據(jù)雙向綁定军援?當然,后來就想到了 Vue 比較適合中小型項目称勋,而 React 比較適合大型項目胸哥。但是,究其根本铣缠,似乎沒有明確的理由烘嘱。近來又似乎找到了答案。
- UI 層與業(yè)務(wù)邏輯的低耦:肯定很多人都有同感蝗蛙,高耦合的 DOM 編程模式蝇庭,對于項目的維護以及團隊合作都很不友好。而 Virtual DOM 對真實 DOM 進行了一層抽象捡硅,它幫助我們?nèi)ゲ僮髡鎸?DOM哮内,而我們通過操作 Virtual DOM 來控制頁面 UI。
- 當系統(tǒng)復(fù)雜度上升,模塊間的低耦也是勢在必行”狈ⅲ現(xiàn)在的前端開發(fā)更加注重用戶體驗纹因,很多傳統(tǒng)在服務(wù)端解決的功能也被遷移到客戶端,因此前端開發(fā)的復(fù)雜度勢必會逐漸上升琳拨,React 的 VIrtual DOM 可以說是一個革命性的創(chuàng)新瞭恰。
二、模塊化開發(fā)
模塊化開發(fā)在軟件工程里邊也算是一個老生長談的話題了狱庇,而在 JavaScript 這門語言中惊畏,一直都是通過
<script></script>
標簽在 HTML 文檔中引入文件,而且模塊(文件)間的依賴關(guān)系也沒有通過很多的方法去處理密任⊙掌簦或許是前期前端開發(fā)的改革注重于新功能的實現(xiàn),所以很長一段時間都花在瀏覽器 API 的優(yōu)化上面了浪讳,但是隨著前端開發(fā)工程化缰盏,模塊化開發(fā)成了一個不可以回避的問題。
1.模塊化規(guī)范
前端的模塊化規(guī)范有 AMD 和 CMD(國內(nèi)提出的)淹遵,后端的模塊化規(guī)范有 CommonJS(Nodejs 采用這種規(guī)范)口猜。
- AMD 規(guī)范(Asynchronous Module Definition):一個瀏覽器端模塊化開發(fā)的規(guī)范,主要的思想有以下三點合呐。
- 模塊將被異步加載
- 模塊加載不影響后面語句的運行
- 所有依賴某些模塊的語句均放置在回調(diào)函數(shù)中
- AMD 是 require.js 在推廣過程中對模塊定義的規(guī)范化的產(chǎn)出暮的。AMD 規(guī)范只定義了一個全局函數(shù)
define
:
define(id?, dependencies?, factory); // id:模塊名字; dependcies: 依賴模塊字面量; factory: 模塊初始化需要執(zhí)行的函數(shù)或?qū)ο?
- CMD規(guī)范(Common Module Definition):這是國內(nèi)發(fā)展出來的,該規(guī)范明確了模塊的基本書寫格式和基本交互規(guī)則淌实。AMD 是依賴關(guān)系前置冻辩,CMD 是按需加載。在 CMD 規(guī)范中拆祈,一個模塊就是一個文件恨闪,代碼書寫格式如下:
define(factory);
-
factory
為函數(shù)時,表示是模塊的構(gòu)造方法放坏。執(zhí)行該構(gòu)造方法咙咽,可以得到模塊向外提供的接口。factory 方法在執(zhí)行時淤年,默認會傳入三個參數(shù):require
钧敞、exports
和module
:
// require 是可以把其他模塊導(dǎo)入進來的一個參數(shù)
// exports 是可以把模塊內(nèi)的一些屬性和方法導(dǎo)出的一個參數(shù)
define(function(require, exports, module) {
// 模塊代碼
})
- CommonJS 規(guī)范是服務(wù)端模塊的規(guī)范,Node.js 采用了這個規(guī)范麸粮。Node.js 首先采用了 js 模塊化的概念溉苛,根據(jù) CommonJS 規(guī)范,一個單獨的文件就是一個模塊弄诲。每一個模塊都是一個單獨的作用域愚战,也就是說,在該模塊內(nèi)部定義的變量,無法被其他模塊讀取寂玲,除非定義為
global
屬性塔插。如下代碼:
// module.exports 就是模塊外部與內(nèi)部通信的橋梁
// 加載模塊使用 require 方法,該方法讀取一個文件并執(zhí)行拓哟,最后返回文件內(nèi)部的 module.exports 對象
let i = 1;
let max = 30;
module.exports = function () {
for(i -= 1; i++ < max; ) {
console.log(i);
}
max *=1.1;
}
2.React 開發(fā)中使用的模塊化
- 組件化開發(fā):想必很多前端開發(fā)的道友都會有這樣一個感觸想许,就是同一個項目中經(jīng)常有很多相似的模塊(結(jié)構(gòu)相似或樣式相似,甚至一模一樣)彰檬,那么我們就會考慮到這些模塊復(fù)用的問題伸刃。這個時候我們就會聯(lián)想到類似 Bootstrap , Foundation 等前端框架,這些框架提供了很多便利的組件逢倍,提高了開發(fā)效率。但是我們會想到一個問題就是景图,第一這些組件都是別人開發(fā)好的较雕,第二復(fù)用組件的方式就是引入 代碼庫,復(fù)制粘貼 HTML 代碼挚币。想解決這些問題亮蒋,一開始就會想到用 模板引擎 (比如 Nunjuck Template)來解決這個問題,但是我們就會想到引入分模塊引入樣式表的問題妆毕,當然也可以通過自己封裝一個類來實現(xiàn)模塊化管理(比如正則匹配url慎玖,執(zhí)行相關(guān)文件的函數(shù))。而我們看看下面 React 的組件化
jsx
文件代碼結(jié)構(gòu):
import '../welcome.scss'
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
正如前面所說的笛粘,React 是 js 主導(dǎo)的一門框架趁怔,所以我們可以在
jsx
文件中通過 js 的模塊化機制引入其他文件(比如 scss 文件),這樣就可以很好的解決組件化開發(fā)的問題(樣式表在組件內(nèi)部編寫)薪前。當然润努,js 的模塊化機制只是針對 js 的,所以類似 css示括,scss铺浇,圖片的引入,我們就得借助 Loader (比如 webpack 的 style-loader, css-loader, sass-loader, url-loader)進行轉(zhuǎn)化垛膝,把其他形式的文件轉(zhuǎn)化成 js 可以進行模塊化管理的文件鳍侣。而且我們可以實現(xiàn)一個文件一個自定義組件的效果,從而借助文件結(jié)構(gòu)更方便的管理組件結(jié)構(gòu)吼拥。ES6:一開始 React 出現(xiàn)的時候倚聚,使用的是 ES5 語法,但是這種通過 原型鏈 實現(xiàn)面向?qū)ο缶幊痰恼Z法對于跟常見的面向?qū)ο笳Z言語法還是有很多不同扔罪,因此對開發(fā)的編碼效率以及代碼的可讀性也不是很友好(個人覺得)秉沼,因此 React 官方文檔也推薦開發(fā)者使用 ES6 推出 的
class
語法糖。此外,ES6 的export
和import
Module 語法糖也幫助解決 js 文件依賴關(guān)系的模塊化管理問題唬复。當然了矗积,在接觸 React 的時候接觸到了 ES6 這些新的語法特性,就不由得去了解一下 ES6 的其他語法特性敞咧,其中很多語法特性應(yīng)用到 React 項目中棘捣,比如 箭頭表達式(Arrow functions),
let
和const
命令休建,還要對 數(shù)組方法及字符串方法 的鞏固應(yīng)用乍恐。也通過 React 的編碼規(guī)范了解了 建造者模式 等設(shè)計模式,對于 JavaScript 的學(xué)習(xí)之路有了進一步的規(guī)劃测砂。SCSS:SCSS 是 CSS 的一門預(yù)編譯語言茵烈,由于 CSS 是一門面向設(shè)計的語言,所以對于龐大的項目來說砌些,CSS 代碼的模塊化管理還有編碼規(guī)范方面問題很難解決呜投,于是就會有 SASS, LESS 等預(yù)編譯語言的出現(xiàn)存璃。SCSS 是 SASS 的一個改進版本仑荐,主要是語法方面向 CSS 靠近。使用 SCSS 模塊化管理的方法很簡單纵东,如下:
.welcome{
h1{
...
}
p{
...
}
}
// 編譯結(jié)果
.welcome h1{...}
.welcome p{...}
- 很明顯可以看出粘招,我們只要保證頂層的類與其他模塊不同,則編譯出來的 CSS 代碼就不會有模塊化沖突的問題了偎球。當然洒扎,我們通常以組件名給頂層類命名,便于管理甜橱。但是這樣就會有一個問題逊笆,不同模塊可能存在相似或相同的樣式表,那這樣編譯出來的樣式就會存在冗余的部分了岂傲,對此的解決方法难裆,目前想到的就是:
- 設(shè)置 Common.scss ,定義好全局復(fù)用的樣式效果(比如字體大小的幾款樣式镊掖,樣色乃戈,陰影效果,動畫效果)
- 組件劃分盡量劃分到不能再細分的組件亩进,這樣代碼的復(fù)用率比較高
3.包管理工具(npm)和自動化打包工具(webpack)
npm:(node package manager)症虑,顧名思義即 NodeJS 的一個包管理工具。NodeJS 除了官方提供的 核心模塊 归薛,還有大量的第三方模塊谍憔,因此 npm 的誕生就是為了更好管理和使用這些第三方模塊的匪蝙。使用的方法也很簡單,安裝需要的模塊并將此模塊作為項目的依賴模塊的信息寫入
package.json
文件中习贫,方便其他開發(fā)者直接安裝模塊逛球,也方便自己后期查看依賴模塊信息(比如版本信息)。在需要引用該模塊的文件中苫昌,通過requrie('')
語句進行調(diào)用即可颤绕。webpack:一個自動化打包工具,webpack 也就花了兩三天看看文檔祟身,寫個小 demo 測試一下而已奥务。要說感觸的話,主要是覺得
loader
,plugin
對開發(fā)過程很友好袜硫,而且打包文件進行了一系列優(yōu)化處理氯葬,比如代碼壓縮、圖片進行base64
處理等婉陷。當然溢谤,剛使用 webpack 的時候也想過這樣一些問題。
webpack 的打包原理憨攒?
webpack 打包慢的處理方式?
webpack-dev-server 的監(jiān)聽原理阀参?
第一個問題肝集,由于 webpack 是依賴于 NodeJS 的,因此模塊規(guī)范就是 CommonJS蛛壳。一個文件就是一個模塊杏瞻,
require
就是引入模塊,module.exports
就是對外暴露的接口衙荐。而打包的方式也就是常見的 管道模式(即require
的文件中的require
繼續(xù)打包)捞挥。另外,同一個模塊不會被引入兩次忧吟,因為在入口文件中砌函,每個require
都配置了一個id
,因此即使同個模塊被引入多次溜族,由于其id
一致讹俊,故不會被打包多次。第二個問題煌抒,目前想到也就以下兩種方法仍劈。
- 使用
webpack --watch
命令代替webpack
,這樣在第一次打包之后打包速度會比較快
將常用的庫等靜態(tài)資源打包成一個文件寡壮,開發(fā)時不再動態(tài)打包
第三個問題贩疙,官方文檔有給出答案讹弯。也就是建立一個小的服務(wù)器進行監(jiān)聽。
The webpack-dev-server is a little Node.js Express server, which uses the webpack-dev-middleware to serve a webpack bundle. It also has a little runtime which is connected to the server via Sock.js.
三这溅、React-router
react-router 簡單地說组民,就是 URL 與 Components 的映射,其實現(xiàn)原理深究無非就是 UI 與 URL 的同步實現(xiàn)芍躏,下圖便很清晰地解釋其實現(xiàn)原理邪乍。而 react-router 有一點是覺得比較有趣的。就是一開始以為 URL 的更新是通過
window.location
進行設(shè)置对竣,但是后來意識到window.location
對象在更新時會出現(xiàn)頁面跳轉(zhuǎn)的現(xiàn)象庇楞。于是查了一下,發(fā)現(xiàn) 單頁面應(yīng)用 的 URL 更細是通過window.history.pushState()
方法實現(xiàn)的否纬,這樣一來吕晌,history
的go()
,back()
等問題自然也能夠得到解決。后來又發(fā)現(xiàn)临燃,
window.history
對象是高版本瀏覽器才有的睛驳,于是查了一下,發(fā)現(xiàn) react-router 是基于一個開源的第三方 js 庫 history 實現(xiàn)的膜廊。主要的兼容方式為:
- 老版本瀏覽器的 history:主要通過
hash
來實現(xiàn)乏沸,對應(yīng)createHashHistory
- 高版本瀏覽器:通過 HTML5 里面的
history
,對應(yīng)createBrowerHistory
- Node 環(huán)境下:主要存儲在
memeory
里面爪瓜,對于createMemoryHistory