亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

React 是怎樣煉成的

lijinke666 / 1394人閱讀

摘要:唯一不足的是,這種開發(fā)方式容易造成注入等安全問題。其中,最棘手的是如何再現(xiàn)中的更新機(jī)制。換句話來說,節(jié)點是包含狀態(tài)的。對于沒有改變的節(jié)點,讓它保持原樣不動,僅僅創(chuàng)建并替換變更過的節(jié)點。是樹形結(jié)構(gòu),所以算法必須是針對樹形結(jié)構(gòu)的。

本文主要講述 React 的誕生過程和優(yōu)化思路。

內(nèi)容整理自 2014 年的 OSCON - React Architecture by vjeux,雖然從今天(2018)來看可能會有點歷史感,但仍然值得學(xué)習(xí)了解。以史為鑒,從中也可以管窺 Facebook 優(yōu)秀的工程管理文化。

字符拼接時代 - 2004

時間回到 2004 年,Mark Zuckerberg 當(dāng)時還在宿舍搗鼓最初版的 Facebook 。
這一年,大家都在用 PHP 的字符串拼接(String Concatenation)功能來開發(fā)網(wǎng)站。

$str = "
    "; foreach ($talks as $talk) { $str += "
  • " . $talk->name . "
  • "; } $str += "
";

這種網(wǎng)站開發(fā)方式在當(dāng)時看來是非常正確的,因為不管是后端開發(fā)還是前端開發(fā),甚至根本沒有開發(fā)經(jīng)驗,都可以使用這種方式搭建一個大型網(wǎng)站。

唯一不足的是,這種開發(fā)方式容易造成 XSS 注入等安全問題。如果 $talk->name 中包含惡意代碼,而又沒有做任何防護(hù)措施的話,那么攻擊者就可以注入任意 JS 代碼。于是就催生了“永遠(yuǎn)不要相信用戶的輸入”的安全守則。

最簡單的應(yīng)對方法是對用戶的任何輸入都進(jìn)行轉(zhuǎn)義(Escape)。然而這也帶來了其他麻煩,如果對字符串進(jìn)行多次轉(zhuǎn)義,那么反轉(zhuǎn)義的次數(shù)也必須是相同的,否則會無法得到原內(nèi)容。如果又不小心把 HTML 標(biāo)簽(Markup)給轉(zhuǎn)義了,那么 HTML 標(biāo)簽會直接顯示給用戶,從而導(dǎo)致很差的用戶體驗。

XHP 時代 - 2010

到了 2010 年,為了更加高效的編碼,同時也避免轉(zhuǎn)義 HTML 標(biāo)簽的錯誤,F(xiàn)acebook 開發(fā)了 XHP 。XHP 是對 PHP 的語法拓展,它允許開發(fā)者直接在 PHP 中使用 HTML 標(biāo)簽,而不再使用字符串。

$content = 
    ; foreach ($talks as $talk) { $content->appendChild(
  • {$talk->name}
  • ); }

這樣的話,所有的 HTML 標(biāo)簽都使用不同于 PHP 的語法,我們可以輕易的分辨哪些需要轉(zhuǎn)義哪些不需要轉(zhuǎn)義。

不久的后來,F(xiàn)acebook 的工程師又發(fā)現(xiàn)他們還可以創(chuàng)建自定義標(biāo)簽,而且通過組合自定義標(biāo)簽有助于構(gòu)建大型應(yīng)用。
而這恰恰是 Semantic Web 和 Web Components 概念的一種實現(xiàn)方式。

$content = ;
foreach ($talks as $talk) {
  $content->appendChild();
}

之后,F(xiàn)acebook 在 JS 中嘗試了更多的新技術(shù)方式以減小客戶端和服務(wù)端之間的延時。比如跨瀏覽器 DOM 庫和數(shù)據(jù)綁定,但是都不是很理想。

JSX - 2013

等到 2013 年,突然有一天,前端工程師 Jordan Walke 向他的經(jīng)理提出了一個大膽的想法:把 XHP 的拓展功能遷移到 JS 中。最開始大家都以為他瘋了,因為這與當(dāng)時大家都看好的 JS 框架格格不入。不過他最終還是執(zhí)著地說服了經(jīng)理,允許他用 6 個月的時間來驗證這個想法。這里不得不說 Facebook 良好的工程師管理哲學(xué)讓人敬佩,值得借鑒。

附:Lee Byron 談 Facebook 工程師文化:Why Invest in Tools

要想把 XHP 的拓展功能遷移到 JS ,首要任務(wù)是需要一個拓展來讓 JS 支持 XML 語法,該拓展稱為 JSX 。當(dāng)時,隨著 Node.js 的興起,F(xiàn)acebook 內(nèi)部對于轉(zhuǎn)換 JS 已經(jīng)有相當(dāng)多的工程實踐了。所以實現(xiàn) JSX 簡直輕而易舉,僅僅花費了大概一周的時間。

const content = (
  
    { talks.map(talk => )}
  
);
React

自此,開始了 React 的萬里長征,更大的困難還在后頭。其中,最棘手的是如何再現(xiàn) PHP 中的更新機(jī)制。

在 PHP 中,每當(dāng)有數(shù)據(jù)改變時,只需要跳到一個由 PHP 全新渲染的新頁面即可。
從開發(fā)者的角度來看的話,這種方式開發(fā)應(yīng)用是非常簡單的,因為它不需要擔(dān)心變更,且界面上用戶數(shù)據(jù)改變時所有內(nèi)容都是同步的。
只要有數(shù)據(jù)變更,就重新渲染整個頁面。

雖然簡單粗暴,但是這種方式的缺點也尤為突出,那就是它非常慢。

“You need to be right before being good”,意思是說,為了驗證遷移方案的可行性,開發(fā)者必須快速實現(xiàn)一個可用版本,暫時不考慮性能問題。

DOM

取自于 PHP 的靈感,在 JS 中實現(xiàn)重新渲染的最簡單辦法是:當(dāng)任何內(nèi)容改變時,都重新構(gòu)建整個 DOM,然后用新 DOM 取代舊 DOM 。

這種方式是可以工作的,但在有些場景下不適用。
比如它會失去當(dāng)前聚焦的元素和光標(biāo),以及文本選擇和頁面滾動位置,這些都是頁面的當(dāng)前狀態(tài)。
換句話來說,DOM 節(jié)點是包含狀態(tài)的。

既然包含狀態(tài),那么記下舊 DOM 的狀態(tài)然后在新 DOM 上還原不就行了么?
但是非常不幸,這種方式不僅實現(xiàn)起來復(fù)雜而且也無法覆蓋所有情況。

在 OSX 電腦上滾動頁面時,會伴隨著一定的滾動慣性。但是 JS 并沒有提供相應(yīng)的 API 來讀取或者寫入滾動慣性。
對包含 iframe 的頁面來說,情況則更復(fù)雜。如果它來自其他域,那么瀏覽器安全策略限制根本不會允許我們查看其內(nèi)部的內(nèi)容,更不用說還原了。
因此可以看出,DOM 不僅僅有狀態(tài),它還包含隱藏的、無法觸達(dá)的狀態(tài)

既然還原狀態(tài)行不通,那就換一種方式繞過去。
對于沒有改變的 DOM 節(jié)點,讓它保持原樣不動,僅僅創(chuàng)建并替換變更過的 DOM 節(jié)點。
這種方式實現(xiàn)了 DOM 節(jié)點復(fù)用(Reuse)。

至此,只要能夠識別出哪些節(jié)點改變了,那么就可以實現(xiàn)對 DOM 的更新。于是問題就轉(zhuǎn)化為如何比對兩個 DOM 的差異。

Diff

說到對比差異,相信大家馬上就能聯(lián)想到版本控制(Version Control)。它的原理很簡單,記錄多個代碼快照,然后使用 diff 算法比對前后兩個快照,從而生成一系列諸如“刪除 5 行”、“新增 3 行”、“替換單詞”等的改動;通過把這一系列的改動應(yīng)用到先前的代碼快照就可以得到之后的代碼快照。

而這正是 React 所需要的,只不過它的處理對象是 DOM 而不是文本文件。
難怪有人說:“I tend to think of React as Version Control for the DOM” 。

DOM 是樹形結(jié)構(gòu),所以 diff 算法必須是針對樹形結(jié)構(gòu)的。目前已知的完整樹形結(jié)構(gòu) diff 算法復(fù)雜度為 O(n^3) 。

假如頁面中有 10,000 個 DOM 節(jié)點,這個數(shù)字看起來很龐大,但其實并不是不可想象。為了計算該復(fù)雜度的數(shù)量級大小,我們還假設(shè)在一個 CPU 周期我們可以完成單次對比操作(雖然不可能完成),且 CPU 主頻為 1 GHz 。這種情況下,diff 要花費的時間如下:

整整有 17 分鐘之長,簡直無法想象!

雖然說驗證階段暫不考慮性能問題,但是我們還是可以簡單了解下該算法是如何實現(xiàn)的。

附:完整的 Tree diff 實現(xiàn)算法。

新樹上的每個節(jié)點與舊樹上的每個節(jié)點對比

如果父節(jié)點相同,繼續(xù)循環(huán)對比子樹

在上圖的樹中,依據(jù)最小操作原則,可以找到三個嵌套的循環(huán)對比。

但如果認(rèn)真思考下,其實在 Web 應(yīng)用中,很少有移動一個元素到另一個地方的場景。一個例子可能的是拖拽(Drag)并放置(Drop)元素到另一個地方,但它并不常見。

唯一的常用場景是在子元素之間移動元素,例如在列表中新增、刪除和移動元素。既然如此,那可以僅僅對比同層級的節(jié)點。

如上圖所示,僅對相同顏色的節(jié)點做 diff ,這樣能把時間復(fù)雜度降到了 O(n^2) 。

key

針對同級元素的比較,又引入了另一個問題。
同層級元素名稱不同時,可以直接識別為不匹配;相同時,卻沒那么簡單了。
假如在某個節(jié)點下,上一次渲染了三個 ,然后下一次渲染變成了兩個。此時 diff 的結(jié)果會是什么呢?

最直觀的結(jié)果是前面兩個保持不變,刪除第三個。
當(dāng)然,也可以刪除第一個同時保持最后兩個。
如果不嫌麻煩,還可以把舊的三個都刪除,然后新增兩個新元素。
這說明,對于相同標(biāo)簽名稱的節(jié)點,我們沒有足夠信息來對比前后差異。

如果再加上元素的屬性呢?比如 value ,如果前后兩次標(biāo)簽名稱和 value 屬性都相同,那么就認(rèn)為元素匹配中,無須改動。但現(xiàn)實是這行不通,因為用戶輸入時值總是在變,會導(dǎo)致元素一直被替換,導(dǎo)致失去焦點;;更糟糕的是,并不是所有 HTML 元素都有這個屬性。

那使用所有元素都有的 id 屬性呢?這是可以的,如上圖,我們可以容易的識別出前后 DOM 的差異??紤]表單情況,表單模型的輸入通常跟 id 關(guān)聯(lián),但如果使用 AJAX 來提交表單的話,我們通常不會給 input 設(shè)置 id 屬性。因此,更好的辦法是引入一個新的屬性名稱,專門用來輔助 diff 算法。這個屬性最終確定為 key 。這也是為什么在 React 中使用列表時會要求給子元素設(shè)置 key 屬性的原因。

結(jié)合 key ,再加上哈希表,diff 算法最終實現(xiàn)了 O(n) 的最優(yōu)復(fù)雜度。
至此,可以看到從 XHP 遷移到 JS 的方案可行的。接下來就可以針對各個環(huán)節(jié)進(jìn)行逐步優(yōu)化。

附:詳細(xì)的 diff 理解:不可思議的 react diff 。
持續(xù)優(yōu)化 Virtual DOM

前面說到,React 其實實現(xiàn)了對 DOM 節(jié)點的版本控制。
做過 JS 應(yīng)用優(yōu)化的人可能都知道,DOM 是復(fù)雜的,對它的操作(尤其是查詢和創(chuàng)建)是非常慢非常耗費資源的。看下面的例子,僅創(chuàng)建一個空白的 div,其實例屬性就達(dá)到 231 個。

// Chrome v63
const div = document.createElement("div");
let m = 0;
for (let k in div) {
  m++;
}
console.log(m); // 231

之所以有這么多屬性,是因為 DOM 節(jié)點被用于瀏覽器渲染管道的很多過程中。
瀏覽器首先根據(jù) CSS 規(guī)則查找匹配的節(jié)點,這個過程會緩存很多元信息,例如它維護(hù)著一個對應(yīng) DOM 節(jié)點的 id 映射表。
然后,根據(jù)樣式計算節(jié)點布局,這里又會緩存位置和屏幕定位信息,以及其他很多的元信息,瀏覽器會盡量避免重新計算布局,所以這些數(shù)據(jù)都會被緩存。
可以看出,整個渲染過程會耗費大量的內(nèi)存和 CPU 資源。

現(xiàn)在回過頭來想想 React ,其實它只在 diff 算法中用到了 DOM 節(jié)點,而且只用到了標(biāo)簽名稱和部分屬性。
如果用更輕量級的 JS 對象來代替復(fù)雜的 DOM 節(jié)點,然后把對 DOM 的 diff 操作轉(zhuǎn)移到 JS 對象,就可以避免大量對 DOM 的查詢操作。這種方式稱為 Virtual DOM 。

其過程如下:

維護(hù)一個使用 JS 對象表示的 Virtual DOM,與真實 DOM 一一對應(yīng)

對前后兩個 Virtual DOM 做 diff ,生成變更(Mutation)

把變更應(yīng)用于真實 DOM,生成最新的真實 DOM

可以看出,因為要把變更應(yīng)用到真實 DOM 上,所以還是避免不了要直接操作 DOM ,但是 React 的 diff 算法會把 DOM 改動次數(shù)降到最低。

至此,React 的兩大優(yōu)化:diff 算法和 Virtual DOM ,均已完成。再加上 XHP 時代嘗試的數(shù)據(jù)綁定,已經(jīng)算是一個可用版本了。
這個時候 Facebook 做了個重大的決定,那就是把 React 開源!

React 的開源可謂是一石激起千層浪,社區(qū)開發(fā)者都被這種全新的 Web 開發(fā)方式所吸引,React 因此迅速占領(lǐng)了 JS 開源庫的榜首。
很多大公司也把 React 應(yīng)用到生產(chǎn)環(huán)境,同時也有大批社區(qū)開發(fā)者為 React 貢獻(xiàn)了代碼。

接下來要說的兩大優(yōu)化就是來自于開源社區(qū)。

批處理(Batching)

著名瀏覽器廠商 Opera 把重排和重繪(Reflow and Repaint)列為影響頁面性能的三大原因之一。

我們說 DOM 是很慢的,除了前面說到的它的復(fù)雜和龐大,還有另一個原因就是重排和重繪。

當(dāng) DOM 被修改后,瀏覽器必須更新元素的位置和真實像素;
當(dāng)嘗試從 DOM 讀取屬性時,為了保證讀取的值是正確的,瀏覽器也會觸發(fā)重排和重繪。
因此,反復(fù)的“讀取、修改、讀取、修改...”操作,將會觸發(fā)大量的重排和重繪。

另外,由于瀏覽器本身對 DOM 操作進(jìn)行了優(yōu)化,比如把兩次很近的“修改”操作合并成一個“修改”操作。
所以如果把“讀取、修改、讀取、修改...”重新排列為“讀取、讀取...”和“修改、修改...”,會有助于減小重排和重繪的次數(shù)。但是這種刻意的、手動的級聯(lián)寫法是不安全的。

與此同時,常規(guī)的 JS 寫法又很容易觸發(fā)重排和重繪。
在減小重排和重繪的道路上,React 陷入了尷尬的處境。

最終,社區(qū)貢獻(xiàn)者 Ben Alpert 使用批處理的方式拯救了這個尷尬的處境。

在 React 中,開發(fā)者通過調(diào)用組件的 setState 方法告訴 React 當(dāng)前組件要變更了。

Ben Alpert 的做法是,調(diào)用 setState 時不立即把變更同步到 Virtual DOM,而是僅僅把對應(yīng)元素打上“待更新”的標(biāo)記。如果組件內(nèi)調(diào)用多次 setState ,那么都會進(jìn)行相同的打標(biāo)操作。

等到初始化事件被完全廣播開以后,就開始進(jìn)行從頂部到底部的重新渲染(Re-Render)過程。這就確保了 React 只對元素進(jìn)行了一次渲染。

這里要注意兩點:

此處的重新渲染是指把 setState 變更同步到 Virtual DOM ;在這之后才進(jìn)行 diff 操作生成真實的 DOM 變更。

與前文提到的“重新渲染整個 DOM ”不同的是,真實的重新渲染僅渲染被標(biāo)記的元素及其子元素,也就是說上圖中僅藍(lán)色圓圈代表的元素會被重新渲染

這也提醒開發(fā)者,應(yīng)該讓擁有狀態(tài)的組件盡量靠近葉子節(jié)點,這樣可以縮小重新渲染的范圍。

裁剪(Pruning)

隨著應(yīng)用越來越大,React 管理的組件狀態(tài)也會越來越多,這就意味著重新渲染的范圍也會越來越大。

認(rèn)真觀察上面批處理的過程可以發(fā)現(xiàn),該 Virtual DOM 右下角的三個元素其實是沒有變更的,但是因為其父節(jié)點的變更也導(dǎo)致了它們的重新渲染,多做了無用操作。

對于這種情況,React 本身已經(jīng)考慮到了,為此它提供了 bool shouldComponentUpdate(nextProps, nextState) 接口。開發(fā)者可以手動實現(xiàn)該接口來對比前后狀態(tài)和屬性,以判斷是否需要重新渲染。這樣的話,重新渲染就變成如下圖所示過程。

當(dāng)時,React 雖然提供了 shouldComponentUpdate 接口,但是并沒有提供一個默認(rèn)的實現(xiàn)方案(總是渲染),開發(fā)者必須自己手動實現(xiàn)才能達(dá)到預(yù)期效果。

其原因是,在 JS 中,我們通常使用對象來保存狀態(tài),修改狀態(tài)時是直接修改該狀態(tài)對象的。也就是說,修改前后的兩個不同狀態(tài)指向了同一個對象,所以當(dāng)直接比較兩個對象是否變更時,它們是相同的,即使?fàn)顟B(tài)已經(jīng)改變。

對此,David Nolen 提出了基于不可變數(shù)據(jù)結(jié)構(gòu)(Immutable Data Structure)的解決方案。
該方案的靈感來自于 ClojureScript ,在 ClojureScript 中,大部分的值都是不可變的。換句話說就是,當(dāng)需要更新一個值時,程序不是去修改原來的值,而是基于原來的值創(chuàng)建一個新值,然后使用新值進(jìn)行賦值。

David 使用 ClojureScript 寫了一個針對 React 的不可變數(shù)據(jù)結(jié)構(gòu)方案:Om ,為 shouldComponentUpdate 提供了默認(rèn)實現(xiàn)。

不過,由于不可變數(shù)據(jù)結(jié)構(gòu)并未被 Web 工程師廣為接受,所以當(dāng)時并未把這項功能合并進(jìn) React 。
遺憾的是,截止到目前,shouldComponentUpdate 也仍然未提供默認(rèn)實現(xiàn)。
但是 David 卻為廣大開發(fā)者開啟了一個很好的研究方向。

如果真想利用不可變數(shù)據(jù)結(jié)構(gòu)來提高 React 性能,可以參考與 React 師出同門的 Facebook Immutable.js,它是 React 好搭檔!

結(jié)束語

React 的優(yōu)化仍在繼續(xù),比如 React 16 中新引入 Fiber,它是對核心算法的一次重構(gòu),即重新設(shè)計了檢測變更的方法和時機(jī),允許渲染過程可以分段完成,而不必一次性完成。
受篇幅限制,本文不會深入介紹 Fiber ,有興趣的可以參考 React Fiber是什么 。

最后,感謝 Facebook 給開源社區(qū)帶來了如此優(yōu)秀的項目!

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/107612.html

相關(guān)文章

  • [到codewars打怪獸]利潤怎樣成的

    摘要:利潤是怎樣煉成的怪獸的屬性怪獸的技能大木博士的圖鑒你是趙老爺家的算帳二狗子,趙家老爺想要學(xué)習(xí)一些理財知識,就去詢問孔乙己。 [7 kyu]Money, Money, Money 利潤是怎樣煉成的??2016.03.15 怪獸的屬性: showImg(http://ww2.sinaimg.cn/large/006m2mhTgw1f1xxc38fbqj30qa0s2q6f.jpg);sho...

    zhangyucha0 評論0 收藏0
  • 年薪50w+的軟件測試工程師怎么成的?

    摘要:它讓傳統(tǒng)的測試工程師從簡單,重復(fù),低效可替代性強(qiáng)的手工測試,變成了有技術(shù)難度和門檻的測試開發(fā)工作,也讓我們有更多的機(jī)會拿到更高的薪資。 隨著互聯(lián)網(wǎng)行業(yè)的迅速發(fā)展,軟件測試工程師的地位越來越高,公司招聘時的薪資也越來越高,那么市場上為什么還有大量的軟件測試工程師薪資只有5-6k呢?因為他們有一...

    laznrbfe 評論0 收藏0
  • 一場穩(wěn)定、高清、流暢的大型活動直播怎么成的?

    摘要:據(jù)悉,高清直播已在阿里云的眾多游戲直播客戶中廣泛使用。這也是阿里云視頻云第四年支持雙貓晚網(wǎng)絡(luò)直播,從作戰(zhàn)室監(jiān)控的數(shù)據(jù)上來看,貓晚直播期間各項系統(tǒng)數(shù)據(jù)指標(biāo)運轉(zhuǎn)平穩(wěn),一場穩(wěn)定高清流暢的大型活動直播就就此實現(xiàn)。 雙11貓晚是家喻戶曉的綜藝晚會,在今年的雙11,阿里集團(tuán)為2500萬用戶提供了一場在線直播視覺盛宴。網(wǎng)友評價這是一場既穩(wěn)定流暢又高清的直播,當(dāng)然在這背后離不開阿里云的技術(shù)支持。 本次...

    mist14 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<