閱讀143 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Jason Miller: Preact: Into the void 0(譯)


title: Preact:Into the void 0(譯)
date: 2017-09-04 19:00:00

tags: [Preact, JSX, 虛擬DOM, 性能]

本文整理自Jason Miller在JSConf上的talk。原視頻地址:

https://www.youtube.com/watch?v=LY6y3HbDVmg

開場白

嗨,大家好,我是Jason,Github上那個developit和推特上的_developit,是一係列庫的作者(serial library author),我喜歡甜甜圈、肉汁乳酪薯條和斧頭,這意味著我是加拿大人(楓葉國的人喜歡斧頭——在加拿大把扔斧頭做成15萬用戶的大生意)。

我也喜歡“限製”。我在移動web廣泛應用前就開始開發它了,那時候還是windows mobile 5。我寫了很多UI框架,遇到了很多問題,然後努力去解決它們。這樣的理由是我發現“限製”是很有趣的“挑戰”,我有些ADHD(注意(力)缺陷多動障礙),你或許熟悉這個東西,為了高效做一些事,最好可以非常專注在上麵,這些有趣的“限製”挑戰可以幫助我營造這個環境。

點題

我寫了Preact,這個展示叫“Preact: Into the void 0”,我覺得這麼叫很聰明(這哥們太極客了,void(0)是js中返回undefined的最小腳本),這也是這個幻燈片中唯一的一個分號哦。

或許你會好奇啥是Preact,這就是Preact。我移除了源代碼映射的注釋,今天我們主要講一下這幾個圓圈圈中的部分。因為這是Preact的展示,所以我們需要一些紫色,不管那是啥,那都是我們最後要講的東西。

JSX簡介

不過在開始講這些東西前,我們需要聊聊JSX。如果你對JSX不熟悉,我不知道你之前是靠在哪個山頭。不過別擔心,JSX真的非常容易理解。JSX的核心非常簡單,等我講完時候,你完全可以用JSX重寫你Webpack配置,讓它變得更長、更複雜,這是個好事。如果你不相信我,去看看webpack2的文檔吧,都在裏麵寫著呢。

那麼什麼是JSX?JSX是一個XML風格的表達式,然後被編譯為函數調用。我們編寫左邊的那個很像HTML的尖括號語法。右邊是像babel這樣的編譯器輸出的結果,現在貌似已經有十種編譯器了。我最喜歡的JSX的點就是很喜歡這種寫法,這種帶有點DOM風格的寫法,編譯在前麵做了一些事,好讓我們可以更好地理解它。

再來看一個稍微複雜一點的寫法。這些尖括號語法被編譯為了JavaScript,其中one 和變量 world都被保留了下來,另外一個複雜的地方是,如果你的標簽名首字母大寫,那麼它在生成代碼中將會是個變量引用。

譯者注:JSX中的標簽名大小寫是有講究的,小寫代表是HTML標簽,大寫代表一個組件,具體可以看JSX的文檔。

JSX的精髓就是我們看過的那個工廠函數,它非常簡單,隻有一個接受節點名稱、屬性、children的簽名。節點名稱就是之前說的標簽名稱,它可以是字符串或者函數,屬性是可選的,它可以是個對象,剩餘的參數就是children,這就是我們編寫的形式。

你或許會想,我剛才編寫不是hyperscript嗎?你想的也不算錯,hyperscript確實跟JSX很相似,有點JSX超集的意思。看看這兩個例子吧!hyperscript支持這種附加的標簽寫法,本質上來說是CSS選擇器的寫法,去預定義元素上的屬性,而JSX卻不支持這種寫法。

JSX真正的能力是可以支持這種拓展標簽名稱。理解好JSX是很重要的,JSX是我們連接各種虛擬DOM庫的接口,JSX不是DOM,它跟DOM沒啥關聯,它隻是一種語法,它並不理解你的代碼或它被用來做的事,你甚至可以用它編寫Webpack配置,但還是別了。你可以用它編寫XML,如果你想編寫一個SOAP客戶端,而且你想用解析和序列化,你就可以使用JSX做這個。總之,我想說JSX是問題的有趣解決方案。

虛擬DOM

下個話題是虛擬DOM。

虛擬DOM僅僅隻是個代表樹狀結構的對象而已,僅僅如此,沒啥別的玄乎的!我經常把它想成一個傳遞給DOM構建器的一個配置,好讓DOM構建器不那麼理論化。

不過,首先我們要理解的是,我們如何從JSX到虛擬DOM。

我們所做的方式是調用剛才定義參數的那個h函數。這非常容易理解,我們編寫JSX,然後調用h函數,我們要做的就是定義一個h函數,生成這樣的對象。這個對象就是虛擬DOM,虛擬DOM就是一個嵌套對象。

令人驚訝的簡單,我們要做的就是這個而已!一個隻有一行代碼的函數!當然,你可以在這裏做更多的事情,如果你想扁平化children,去除空值,連接相鄰的字符串節點等。但核心是,你可以通過這個函數編寫一個虛擬DOM渲染器。

所以,讓我們做這個吧!讓我們編寫一個虛擬DOM渲染器!第一件事,我們要傳遞給我們自己一個虛擬節點,這是我們之前見過的那個對象,看右上方的滾動框。所以,第一件事是我們需要創建一個DOM對象,匹配傳遞進來的虛擬節點的類型。所以我們使用document.createElement來做這個。然後我們循環給DOM賦予屬性。接著,我們又寫了一個遞歸來循環渲染子節點。最後,我們在類型為字符串時,直接返回DOM對象。這就是我們編寫的虛擬DOM渲染器!

這裏有個稍微複雜的地方,那就是attributes。如果有人用過React,那麼你可能會怒氣衝衝地說那不是attrbutes,那是properties!事實上應該叫“props”。attributes和properties是兩個不同東東的抽象!大多HTML元素會接受數據作為attributes,它們也可以接受類似的,定型數據作為properties,通過一個叫DOM property reflection的東東。但事實上,這兩種都是不太對的,有時候我們可以使用properties,不能使用attributes,有時候又反過來。

我們需要的是將兩種寫法都寫出來!我們有一個DOM節點的引用,我們問它,你支持foo這個property嗎?如果它支持,就用property,否則就用attributes。這對自定義元素很好,因為自定義元素傾向於為property定義getter setter對。

這時你可能想問,這能運行嗎?這是個虛擬DOM,我們把它傳給編寫的渲染函數。右邊顯示的是結果,它可以運行哈哈。

DIFF算法

我們剛才編寫了一個非常簡單的虛擬DOM渲染器,也是個非常糟糕的虛擬DOM渲染器,這是版本0。說它糟糕是因為它不能DIFF,它不關注當前的DOM狀態,隻是完全替換了新dom。虛擬DOM中的DIFF算法是一個爭議和神秘的主題,爭議是有必要的,因為過程中充滿了權衡,並不是非黑即白,而神秘是沒有必要的,我試圖去揭開它神秘的麵紗!

DIFF舍棄從上到下渲染,創建新的DOM。我們將會傳遞給我們自己一個現在DOM,然後把它變為JSX中寫的樣子,隻是應用一下差異而已。

在左邊,你可以看到虛擬DOM長啥樣,隻是一個對象。在右邊,是一個真實的DOM。你可以看到,名字都差不多。你可以比較一下,然後把差異應用到右邊。

運行DIFF,隻需要三步。第一步是type,在所有事情前,我們必須要創建一個準確類型的DOM。第二步是循環遍曆children,去雙向比較它們,然後找出我們是否需要添加、移除、重排它們等。最後一步是更新attributes/props。

所以,讓我們從type 開始吧!第一件事是判斷節點是否是組件創建的。如果不是,事情就簡單了!如果同類型就更新,否則就拋棄原來的,創建一個新的。如果是組件創建的,事情會稍微複雜一點。我們需要創建一個實例,通過比較創建或更新組件的props,然後調用render方法。

譯者注:實際情況其實更加複雜,這裏需要對組件、組件實例、組件生命周期非常熟悉才能理解。在組件生命周期中,真正操控大局的是組件實例,所以這裏需要先創建一個組件實例。

children更加簡單,隻有三步。第一步是循環遍曆所有的children,把它們放到列表中,沒key的話就放到unkey列表,有key就放到keyed map中。第二步是我們把新的虛擬children轉移過去,我們在列表中發現匹配的,然後和虛擬DOM做對比,最後把它插入到當前的index中。最後一步是最簡單的,如果有kids剩餘,就刪除它們,因為它們已經用不到了。

你或許對keyed map和unkey list感興趣。我今天很想討論這個話題,我曾在stack overflow上回答過這個問題,這是我在stack overflow上回答的唯一的問題。所以,讓我們用PPT來演示它。keys是一些虛擬DOM上有意義的順序屬性,當這些虛擬DOM擁有唯一的類型。我們可以在這個例子中看到,我們擁有三個列表項——one,two,three。在第二個渲染框中,我們隻有兩個列表項,對於你和我這樣的人類而言,我們隻需要刪除two,把第三項移上去。但虛擬DOM渲染器不知道這個,沒有任何東西說明two就是第二項,它就是每次接受一個新的樹,沒有什麼事可以矯正它。這整個過程就是,看看第一項,沒變,然後啥事也沒發生,然後看看第二項,它說,不,內容不一樣啊,然後它就更改了內容。第三項直接被刪除了。默認情況下,虛擬DOM中的元素列表,它隻會push和pop,沒法移動改變中間的項。與此相反,在有key的方法中,我們給每個元素一個唯一的key,所以在第一個框中,我們看到了1,2,3,在第二個框中,我們看到了1,3。很明顯,key2被移除了,現在,我們告訴虛擬DOM應該做什麼,所以它知道當它循環到key2時,它就會刪除該項。

DIFF的最後一步是attributes,這真的很簡單,我們給我們自己老的屬性和新的屬性,從老的屬性中找到不在新屬性中的屬性,然後把它們設置為undefined。對於新的屬性,我們和老屬性對比,然後設置新屬性的值。我們解決了所有的問題,現在我們的app變得非常快!我們把所有問題都轉移到了庫中,這些庫包括:react、preact、inferno等。

性能

我想和大家討論一些性能的話題。我編寫Preact時,就想測試它的性能。

這句話是你經常在推特上看見人說的。我們經常聽到有人抱怨說DOM太慢了,DOM是性能差的根源所在。確實,DOM沒有immediate mode drawing API那麼快,它設計的目的不是這個,這是完全不同的事。DOM本質上提供了內建的accessibility。你可以使用title和字體注釋DOM,還可以得到屏幕閱讀器的支持。其他平台也可以這麼做,但是DOM做這種事情更加簡單。你根本不需要理解它是如何運作的,隻需要編寫語意化的標記即可。DOM也可以拓展,人們經常忘記這點。如果我在windows上使用推特,我想為推特添加emoji,我就安裝瀏覽器拓展,然後就hook到了每一個在推特上的輸入文本,接下來我不依賴推特的輸入字段就可以使用emoji了。推特不知道這個,也不需要知道,也不在乎這個。這就是DOM的價值之一,這種基質鞏固了所有的應用,它是一種超越我們知識範疇的拓展。這就跟那個“框架不可知論”不謀而合。你可以編寫兩個不同的插件在兩個不同的框架中,隻要它們可以渲染元素,你可以假設這些元素擁有相同的祖先元素,它們彼此之間不需要相互在乎。所以,Preact本質上來說就是個DOM渲染器,它是虛擬DOM渲染器,但它就是一種DOM庫。

接下來,我想分享我在編寫DOM庫過程中的一些經驗,第一個是使用文本節點來表示文本。這聽起來很傻,我意識到了這個。但是很驚訝的是,我們經常曲解這句話。

DOM擁有API去和文本打交道,我們卻經常忽略這些API。我們可以通過這些API去創建文本,插入文本,反轉文本等。

這是個benchmark showing,展示了textContentText.nodeValue的速度,後者很明顯更快。如果你正在編寫一個有處理文本的DOM庫或框架,那麼選擇前者會讓你發瘋。textContent做了更多工作,隻說它慢貌似不太公平。但大多情況下,我們不需要處理“更多的工作”。

下一個經驗是,避免getters,完全的!別使用它們。Text的nodeType是undefined,但是它繼承的Node的nodeType是個getter方法,性能不好。

如圖所示,splitText更快,因為這隻是檢查某個屬性是否存在,而不是調用getter。

這是一個性能測試,可以看到getters都很慢,而屬性獲取的速度卻很快。

最後一個經驗是避免Live NodeLists,不要試圖去用它們,它們特別耗性能。

這是一個例子,試圖去移除父元素的children。第一個你寫了一個倒置的循環去移除,之所以倒置是因為這是個live NodeList,它的項數不停的在變化。第二個就快多了,因為我們隻是在獲取一個屬性,不需要回頭去請求子節點。我們不需要去獲取數組的位移,我們隻是在用一個引用。

這是測試結果。

性能測試

我已經做性能優化很久了。Benchmark運行了五百萬次循環,然後計算時間。

Chrome開發工具優化了這個,使其更加可視化。

另一個工具是IRHydra。

最後一個工具是ESBench。這個工具的目的是給你一個非常簡單的用戶界麵去使用Babel和benchmark。

其他經驗

第一個是盡量明確的。不要使用一些意外情況,如果你沒有理由使用它們。這個例子中,我們檢查一個對象的屬性,它可能是0,空字符串,null,false等,第二種就清楚多了。

下一個經驗是行內幫助函數。函數可以更加通用。

下一個是短路語法。最便宜的函數調用就是你不調用它。

所有這一切都是在說一個道理:基於數據去做決定。


教程源代碼及目錄

https://github.com/lewis617/react-redux-tutorial

最後更新:2017-09-20 23:03:20

  上一篇:go  stackoverflow 上一篇關於 多語言國際化的 討論
  下一篇:go  專訪微軟SQL項目組高級產品經理:SQL Server新技術與雲化技術支持