摘要:瀏覽器推遲事件直到所有的腳本都處于狀態(tài)。解析器將處理執(zhí)行這個腳本。創(chuàng)建這個腳本的解析器的文檔有正在阻塞腳本執(zhí)行腳本元素為等待解析阻塞的腳本的狀態(tài),同一時刻只能有一個這樣的腳本存在。解析器將一個或多個字符轉換為表并處理,這個過程是一個典型的。
前言
本文主要對W3C規(guī)范中關于script標簽和event loop相關的篇幅做了簡單的探討,針對一些必要的相關概念進行了適當?shù)臉俗⒑驼f明。雖然之前接觸過,但都過于零散,希望借此機會,能夠對這些概念能夠一個稍微全面一點的認識,也希望和大家進行交流。由于知識的深度和廣度以及英語水平的不足,如有錯誤,還望包涵指正。
小波折雖然之前查過W3C和WHATWG的關系,但是翻譯得差不多的時候有個問題去WHATWG提了issue,才被domenic大大告知我可能看了"假規(guī)范"- -(具體可參考鏈接1,鏈接2,F(xiàn)ork tracking),最新的規(guī)范在這,大部分還是基本一致的,新增了一些比如type=module的內容等等,還有排版呀,有的描述等等有一些變化,有興趣的可以去看看。
HTML解析瀏覽器HTML解析過程如下:
The exact processing details for these attributes are, for mostly historical reasons, somewhat non-trivial, involving a number of aspects of HTML.The implementation requirements are therefore by necessity scattered throughout the specification.
可以看到,規(guī)范也提到了規(guī)范只是一個參考,具體實現(xiàn)因人而異。在測試中,我列出以下發(fā)現(xiàn)和期待討論的話題,希望對自己和他人都能起到幫助:
Script標簽關于script標簽基本信息的一些描述這里不再過多介紹,自己有幾個比較關心的點在以下列出。
defer,async屬性對于普通腳本,defer腳本,async腳本,有如下總結:
1.對于普通的腳本,有兩點需要注意。
第一:并不是在fetch的時候完全“阻止”后續(xù)標簽的解析。我們從timeline可以看到,在第一次praseHTML的最開始,就已經將頁面所需的所有靜態(tài)資源請求send出去了(具體可參考瀏覽器預解析加載機制)。所以腳本是無法“阻止”后續(xù)標簽中引用外部資源的請求不被發(fā)送的。并且在解析這個腳本到finish load這段時間,還有很多其他操作,如擴展程序的腳本執(zhí)行,某些VM語句執(zhí)行,install這一腳本之前的腳本中的定時器等等。
第二:fetch之后接收完所有數(shù)據(jù)包最后完成finish load之后,并不是立即執(zhí)行這個腳本內的內容。而是先要判斷這個腳本在所有腳本中的順序,必須確定這個腳本之前的所有普通腳本執(zhí)行完畢后,才會執(zhí)行這個腳本。
script標簽處理模型處理模型擁有以下7種狀態(tài)屬性:
"already started" ->> "parser-inserted" ->> "non-blocking" ->> "ready to be parser-executed" ->> "the script’s type" ->> "is from an external file" ->> "the script’s script"
最后一步也就是異步將預處理腳本(見下)的結果設置為腳本的script屬性的值,無論這個值是正確的還是錯誤的,都應標記腳本為ready狀態(tài),這意味著這之后可以觸發(fā)其他行為。瀏覽器推遲load事件直到所有的腳本都處于ready狀態(tài)。
關于這些狀態(tài)的描述內容并不多,比如初始時還沒有"already started",HTML解析器解析到之后立即置為"already started",初始時沒有"parser-inserted",當HTML解析器把節(jié)點插入到父節(jié)點的時候,置為"parser-inserted",當HTML解析器在創(chuàng)建節(jié)點對象的時候默認是"non-blocking",當HTML解析器把節(jié)點插入到父節(jié)點的時候,置為"blocking"(實際是設置為false,便于理解故作此翻譯,不要打我。。),如果腳本有async屬性,那么又置為"non-blocking"避免阻塞解析,等等等等。
具體要了解的話建議查閱文檔。這里我們探討下最關鍵的部分-預處理腳本(原文為prepare a script,感覺翻譯成準備,預備都不太合適,故作此翻譯,如果有更好的翻譯還希望指正),"the script"s type","from an external file","the script"s script"都是在這一階段確定的:
預處理腳本當一個未被標記為"parser-inserted"的腳本元素經歷以下3個事件中的任意一個時,瀏覽器必須立即預處理這個腳本元素:
1.在dom節(jié)點中順序先于(先于是指按照前序進行深度優(yōu)先遍歷的時候)這個腳本的腳本被插入到dom樹之后,這個腳本元素被插入到文檔中。
2.在所有腳本元素都被插入完畢后,這個腳本元素在文檔中且有其他節(jié)點被插入到這個腳本元素中。
3.腳本元素已經在文檔中并且之前沒有src屬性,但是現(xiàn)在被設置了src屬性。
為了預處理一個腳本,瀏覽器必須進行以下步驟:
前面1-18步主要考慮的是不需要執(zhí)行或者說不符合執(zhí)行條件的時候就中斷預處理過程從而不執(zhí)行這個腳本。比如發(fā)現(xiàn)還沒有"already started",比如沒有src屬性且腳本的內容為空或者只有注釋,腳本元素沒有在文檔中,type和language屬性不符合規(guī)范,用戶禁用了JS等等。除了這些之外,還有些諸如腳本有charset則設置,沒有就用文檔本身的charset。還有有些只是規(guī)范有提及,但是沒有瀏覽器或者不是所有瀏覽器都實現(xiàn)了,比如for,event,nonce屬性等等。另外還有一些其他的考慮,這里就不一一贅述了,詳細的可以參考規(guī)范。
下面著重來看一下19-20步:
第19步:如果腳本元素沒有src屬性,則進行下列的步驟:
Let source text等于yourScriptElement.text的值。
將腳本的type屬性設置為"classic"
Let script作為用source text和settings創(chuàng)建的腳本的結果。
設置the script’s script為上一步的script。
讓腳本處于ready狀態(tài)
第20步:然后,選擇符合下列情形的第一個進行執(zhí)行:
Type 1:
the script’s type | 是否有src屬性 | 是否有defer屬性 | 是否有async屬性 | 其他條件 |
---|---|---|---|---|
"classic" | 是 | 是 | 否 | 元素已經"parser-inserted" |
將腳本元素添加到將要執(zhí)行的腳本的集合的末尾。
當腳本處于ready狀態(tài)的時候,設置腳本元素的"ready to be parser-executed"標記。解析器將處理執(zhí)行這個腳本。
Type 2:the script’s type | 是否有src屬性 | 是否有defer屬性 | 是否有async屬性 | 其他條件 |
---|---|---|---|---|
"classic" | 是 | 否 | 否 | 元素已經"parser-inserted" |
腳本元素為"等待解析阻塞的腳本"(見步驟末尾)的狀態(tài),同一時刻只能有一個這樣的腳本存在。當腳本處于ready狀態(tài)的時候,設置腳本元素的"ready to be parser-executed"標記。解析器將處理執(zhí)行這個腳本。
Type 3:the script’s type | 是否有src屬性 | 是否有defer屬性 | 是否有async屬性 | 其他條件 |
---|---|---|---|---|
"classic" | 是 | 是或者否(意思為是或者否都是一樣的) | 否 | 元素上沒有"non-blocking"標記 |
盡快當預處理腳本一開始的時候按順序將腳本元素添加到將要執(zhí)行的腳本的集合的末尾。
當腳本為ready狀態(tài)的時候,進行下面的步驟:
1.如果這個腳本現(xiàn)在不是將要執(zhí)行的腳本的集合的第一個元素,則標記腳本為ready,但是中斷剩余的步驟,不執(zhí)行這個腳本。
2.執(zhí)行腳本。
3.移除將要執(zhí)行的腳本的集合中的第一個元素。
4.如果將要執(zhí)行的腳本的集合仍然不為空且第一個元素被標記為ready,那么跳回到第2步。
Type 4:the script’s type | 是否有src屬性 | 是否有defer屬性 | 是否有async屬性 | 其他條件 |
---|---|---|---|---|
"classic" | 是 | 是或者否 | 是或者否 | 不適用 |
盡快當預處理腳本一開始的時候將腳本元素添加到將要執(zhí)行的腳本的集合的末尾。
當腳本為ready狀態(tài)的時候,執(zhí)行腳本并將它從集合中移除。
Type 5:the script’s type | 是否有src屬性 | 是否有defer屬性 | 是否有async屬性 | 其他條件 |
---|---|---|---|---|
"classic" | 否 | 是或者否 | 是或者否 | 元素已經"parser-inserted",XML或者HTML解析器的script-nesting-level比創(chuàng)建這個腳本的低或者相等。創(chuàng)建這個腳本的解析器的文檔有css正在阻塞腳本執(zhí)行 |
腳本元素為"等待解析阻塞的腳本"的狀態(tài),同一時刻只能有一個這樣的腳本存在。設置腳本元素的"ready to be parser-executed"標記。解析器將處理執(zhí)行這個腳本。
Type 6(其他情形):立即執(zhí)行這個腳本,即使有其他腳本正在執(zhí)行。
總共就這6種情形,下面有一個上面提到的概念的補充說明
等待解析阻塞的腳本:
如果一個阻塞了解析的腳本元素在它停止阻塞解析前被移動到了另一個document中,盡管如此,它仍然會阻塞解析直到造成它阻塞的原因消除。(例如,如果這個腳本元素由于有一個css阻塞了它而變成一個等待解析阻塞的腳本,但是然后這個腳本在css加載完畢前被移動到了另一個文檔中,這個腳本仍然會阻塞解析直到css加載完畢(但是阻塞的是另外那個文檔的解析了),但在這段期間,原來文檔的腳本執(zhí)行和HTML解析是暢通的)
在規(guī)范中user agents指的是實現(xiàn)了這些規(guī)范的應用。為了更好的敘述,以下我們暫且用瀏覽器來代替這一描述。
為了協(xié)調事件,用戶接口,腳本,渲染,網(wǎng)絡等等,瀏覽器必須使用event loops。對于event loops,它有兩種類型,一種是針對瀏覽器上下文(請務必先了解這一概念)而言,另一種是針對Wokrer而言。由于對Worker不太熟悉,我們這里也主要探討瀏覽器相關的東西,所以以下都不再敘述Worker相關內容。
一個event loop有一個或者多個任務隊列。一個任何隊列是一系列有序的任務集合,這樣的隊列是通過下面這些算法來工作的:
Events:通常對于專用任務而言,dispatch一個Event對象給一個特定的EventTarget對象。另外,并不是所有的事件都是通過任務隊列來dispatch(哪些不是呢,可參考區(qū)別)。
Parsing:HTML解析器將一個或多個字符轉換為token表并處理,這個過程是一個典型的task。
Callbacks:調用一個回調函數(shù)常,常適用于專有任務。
Using a resource:當fetch一個資源的時候,如果fetch發(fā)生在一個非阻塞的方法,一旦資源的部分或者全部是可用的,也會被當作一個任務執(zhí)行(即timeline中的receive data和finish loading)。
Reacting to DOM manipulation:為了響應dom變化也會導致一些元素產生task。如當一個元素被插入到文檔中的時候。(意思就是說插入之后會導致瀏覽器重新計算布局,渲染,一些監(jiān)聽節(jié)點變化的事件也會被觸發(fā),這些都是task)
每一個在瀏覽器上下文的event loop中的task都與Document對象(準確的說是實現(xiàn)了Document接口的對象,規(guī)范也提及過為了便于敘述不采用這種準確的說法,因為太長)相關聯(lián)。如果某個task被加入了某個元素的context的隊列,那么這個document對象就是這個元素的node document。如果某個task被加入了某個瀏覽器上下文的context的隊列,那么在入隊列的時候,這個document對象就是瀏覽器上下文的active document。如果某個task是通過腳本或者是針對腳本的,那么這個document對象就是通過腳本的配置對象指定的responsible document(現(xiàn)在想想responsible這個詞在這里還是挺有意思,因為純靜態(tài)頁面的document是不需要對任何東西負責的)。
當瀏覽器將一個task加入隊列的時候,它必須將這個task加入相關的event loop中的某一個任務隊列。
每一個task在定義時都會有指定的task source(一共有4種,DOM manipulation task source,user interaction task source,networking task source,history traversal task source)。所有來自一個特定的task source的task都必須被添加到一個特定的相同的event loop(例如Document對象產生的回調函數(shù),觸發(fā)在Document對象上的mouseover事件,Document中等待解析的任務等等,他們都有相同的事件源-Document),但是不同來自不同task source的task也許會被添加到不同的任務隊列。
例如,瀏覽器也許有一個針對鼠標和鍵盤的任務隊列(它們都來自user interaction這一task source)和其他的任務隊列。那么相對其他任務隊列而言,瀏覽器也許會給鼠標和鍵盤事件更高的優(yōu)先級,來保持響應與用戶的交互,但是這又不會饑餓其他任務隊列。并且絕不會將來自同一task source的事件顛倒次序執(zhí)行(意思就是task必須按照它添加時的順序去執(zhí)行)。
每一個event loop都有一個當前執(zhí)行任務。初始時為null。它被用作處理reentrancy(可重入性,類似于generator,在內聯(lián)腳本中直接使用document.write就是這樣,因為這樣是把write的參數(shù)寫到之前的input stream(就是還未解析的字節(jié)流)里面)。每一個event loop也有一個performing a microtask checkpoint 的flag,初始時為false。它被用作阻止對perform a microtask checkpoint這個算法的可重入性調用。
關于microtask:每一個event loop都有一個microtask隊列,處于microtask隊列而不是普通的task隊列中的task就叫做microtask。這里有兩種類型的microtask,一種是單一回調函數(shù)microtask,一種是復合microtask。注意,規(guī)范中只針對單一回調函數(shù)microtask有具體描述。
一個event loop在它存在的期間必須不斷重復以下步驟:
1.取出某一個任務隊列隊列頭的任務(如果存在的話)。如果與瀏覽器上下文的event loop相關聯(lián)的Documents對象不是fully active狀態(tài),那么忽略這個task。瀏覽器也許會選擇任何一個任務隊列。如果沒有task可以取的話,跳到第6步。
2.將event loop的當前運行任務設置為上一步選擇到的task。
3.運行這個task。
4.將event loop的當前運行任務設置為null。
5.將第3步中運行的task從它的任務隊列中移除。(這也說明之前取任務時進行的隊列操作是peek,而不是poll)
6.執(zhí)行一個microtask checkpoint操作。因為有點多,避免混亂我寫在這7個步驟完畢后的位置。
7.更新渲染:如果這個event loop是瀏覽器上下文的event loop而非Worker的event loop,那么執(zhí)行如下步驟:
Let now等于now()方法的返回值。(可以理解為timeline中的start time)
Let docs等于與這個event loop相關聯(lián)的Document對象集合。這個集合是隨意排序的,但是要遵循一定的原則,具體可以參照規(guī)范。簡單舉例來說,A這個Document嵌套了B和C,B嵌套了D。那么順序即可以是A,B,C,D也可以是A,B,D,C。只要保證C在B后面,B,C在A后面,D在B后面就行。
迭代docs,對于其中的每個doc。如果這里存在一個頂級的瀏覽器上下文B(頂級就是指嵌套瀏覽器上下文情況下最祖先的那個瀏覽器上下文,形象一點的描述可參考鏈接)且不會從這次更新渲染中受益,那么將docs中所有瀏覽器上下文的頂級瀏覽器上下文為B的Document對象移除。
一個頂級瀏覽器上下文是否會從渲染更新中受益取決與幾個方面,如更新頻率。舉例來說,如果瀏覽器嘗試60HZ的刷新頻率,那么這些步驟只有在每16.7ms內才是有意義的。如果瀏覽器發(fā)現(xiàn)一個頂級瀏覽器上下文無法維持這個頻率,它也許會將docs集合中的所有document對應的刷新頻率下調到30HZ,而不是偶爾下調頻率。(規(guī)范并不強制規(guī)定任何特定的模型用于何時更新渲染),類似的,如果一個頂級瀏覽器上下文是在background中(不太明白,猜測是dispaly:none之類的意思),那么瀏覽器也許會下調到4HZ,甚至更低。
另一個關于瀏覽器可能會跳過更新渲染的例子是確保某些task在某些task之后被立即執(zhí)行,這伴隨著僅僅是microtask checkpoints的交替。(或者沒有這些交替,例如requestAnimationFrame中animation幀的回調函數(shù)交替)。例如,瀏覽器也許希望合并定時器回調函數(shù),而不希望在合并的時候存在渲染更新。
如果有一個瀏覽器認為不會從渲染更新中受益的嵌套的瀏覽器上下文B,那么從docs中移除那些瀏覽器上下文為B的元素。
正如頂級瀏覽器上下文一樣,對于嵌套的瀏覽器行下午,很多因素也會影響到它是否會從更新渲染中受益。例如,瀏覽器也許希望花費較少的資源渲染第三方的內容,特別是當前不可見的內容或者是受限制的內容。在這一的例子中,瀏覽器也許會決定很少或者根本不對這些內容更新渲染。
對于docs中每個fully active的Document對象,觸發(fā)resize
對于docs中每個fully active的Document對象,觸發(fā)scroll
對于docs中每個fully active的Document對象,觸發(fā)媒體查詢和提交變化
對于docs中每個fully active的Document對象,運行CSS animations并發(fā)送事件。
對于docs中每個fully active的Document對象,運行全屏渲染步驟。
對于docs中每個fully active的Document對象,運行animations回調函數(shù)。
對于docs中每個fully active的Document對象,更新渲染或者用戶接口,和瀏覽器上下文來反應當前的狀態(tài)。
9.返回到第1步繼續(xù)執(zhí)行。
接上面提到的第6步,執(zhí)行microtask checkpoint操作如下:
當一個算法需要將一個microtask加入隊列時,它必須被追加到相關的event loop的microtask 隊列。這個microtask的task source就被叫做microtask task source。
將一個microtask移動到普通的任務隊列是很有可能的,如果發(fā)生這樣的移動的話,在它的初次運行時,將執(zhí)行spins the event loop步驟。
當瀏覽器去執(zhí)行一個microtask checkpoint的時候,如果這個performing a microtask checkpoint的falg為false,那么瀏覽器必須執(zhí)行以下步驟:
1.將這個flag置為true。
2.如果event loop的microtask隊列為空,則跳到第8步:
3.取出microtask隊列頭的元素。
4.將event loop的當前運行任務設置為上一步取出的task。
5.運行這個task。
注意:這也許會涉及調用回調函數(shù),最后會調用清理步驟,在清理步驟中也許又會執(zhí)行microtask checkpoint操作,導致無終止條件的遞歸,這就是為什么我們需要用這個flag去避免這一情況。
6.設置event loop的當前運行任務為null。
7.從microtask隊列中移除上面運行的這個task。然后返回到第2步。
8.對于每一個responsible event loop為這個event loop的環(huán)境配置對象,notify about rejected promises。
9.將flag置為false。
Timeline相關就像我們用迅雷同時下載10個文件一樣,假設我們是下行速度是1M/s,那么顯然不可能10個資源每個的下載速度都是100kb/s,因為每個資源的資源熱度是不同的,所以有的是500kb/s,而有的可能只有20kb/s,有的甚至無法下載。
對于瀏覽器而已也是類似的道理,瀏覽器的資源調度算法以及每個時間段的網(wǎng)絡情況決定了下載資源的順序,所花費的精力等等。以chrome的資源獲取優(yōu)先級算法為例,我們不難看出,在獲取到html之后,css的請求優(yōu)先級是最高的,因為對于現(xiàn)在的web頁面來說,沒有css的后果可能遠遠大于沒有其他資源。對于腳本中發(fā)起的請求如通過接口獲取數(shù)據(jù)等則為high,對于普通的js而已,優(yōu)先級為medium,普通的圖片和async腳本都為low等等等等,隨著時間的推移,這個算法肯定也會發(fā)生相應的變化來提升那個時候的應用體驗。
關于這些點在network中與之相關的莫過于Queueing和Stalled屬性了:
Queueing. The browser queues requests when:
There are higher priority requests.
There are already six TCP connections open for this origin, which is the limit. Applies to HTTP/1.0 and HTTP/1.1 only.
Stalled: The request could be stalled for any of the reasons described in Queueing.
所以瀏覽器最開始會按照html中資源出現(xiàn)的順序發(fā)送請求去獲取,但是資源的接收順序卻不一定是按照這個順序。一個請求發(fā)出去之后,后面又來了一個請求,而這個請求的優(yōu)先級比當前的要高,那么很可能就會先去接收這個優(yōu)先級更高的資源的數(shù)據(jù)。而對于優(yōu)先級相同的多個資源,則很可能采用你接收一段數(shù)據(jù),我接收一段數(shù)據(jù)這樣的方式交叉運行。也就是我們常常看到的頁面中的圖片加載的時候往往是多個圖片同時慢慢從白屏到加載完畢,而不是一個加載完畢后再加載另一個。
另外前面已經提到過了,對于普通腳本則是肯定會按照html中的順序執(zhí)行的,也就是說如果腳本a只有500kb,而在他后面的腳本b只有1kb,那么即使腳本b獲取全部字節(jié)后完成finish load也不能立即執(zhí)行,必須等到腳本a獲取全部字節(jié)后且執(zhí)行完畢后它才能執(zhí)行。而如果a和b都是async的腳本化則不必遵循這一原則,誰先獲取到誰就先執(zhí)行。為什么呢,因為async設計的本意就是為了抽離與頁面無關的邏輯的,它們之間也不應該存在連貫性和依賴性,而后面的普通腳本更不用說了,更不應依賴它們去工作。
所以后面鏈接提到的視頻中提問者說只要不操作dom和獲取dom,就應該把這些公共代碼提取出來放在head中async引入來達到性能優(yōu)化的效果,其實是不妥當?shù)摹1热鏻oadsh就符合這個要求,我們顯然不能這么做,一是因為lodash體積太大,無法保證在body尾部用到lodash的代碼所處的腳本一定晚于lodash后執(zhí)行,二是由于網(wǎng)絡原因,就是lodash是一個只有1kb的資源,也很難保證。
寫在結尾這次閱讀規(guī)范的過程,了解了很多知識,也早已超出了當初想要獲得的知識,這便是學習的樂趣。當然也有很多地方花了很長時間才弄清楚到底是表達的什么意思,也還存留一些問題到目前也仍未理解,大家有不明白或者覺得錯誤的地方希望多多交流,也希望隨著歲月,再來回頭探索的時候能夠明白。
閱讀更多the-javascript-event-loop-explained
從Chrome源碼看瀏覽器如何構建DOM樹
從Chrome源碼看瀏覽器的事件機制
瀏覽器如何構建dom樹(chrome官方文檔,另外里面有配套的視頻,非常不錯)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/81460.html
摘要:瀏覽器推遲事件直到所有的腳本都處于狀態(tài)。解析器將處理執(zhí)行這個腳本。創(chuàng)建這個腳本的解析器的文檔有正在阻塞腳本執(zhí)行腳本元素為等待解析阻塞的腳本的狀態(tài),同一時刻只能有一個這樣的腳本存在。解析器將一個或多個字符轉換為表并處理,這個過程是一個典型的。 前言 本文主要對W3C規(guī)范中關于script標簽和event loop相關的篇幅做了簡單的探討,針對一些必要的相關概念進行了適當?shù)臉俗⒑驼f明。雖然...
摘要:規(guī)范中定義了瀏覽器何時進行渲染更新,了解它有助于性能優(yōu)化。結合一些資料,對上邊規(guī)范給出一些理解有誤請指正每個線程都有自己的。列為,列為,列為。我們都知道是單線程,渲染計算和腳本運行共用同一線程網(wǎng)絡請求會有其他線程,導致腳本運行會阻塞渲染。 本文轉自blog 轉載請注明出處 異步的思考 event loops隱藏得比較深,很多人對它很陌生。但提起異步,相信每個人都知道。異步背后的靠山就是...
平日學習接觸過的網(wǎng)站積累,以每月的形式發(fā)布。2017年以前看這個網(wǎng)址:http://www.kancloud.cn/jsfron... 1. Javascript 前端生成好看的二維碼 十大經典排序算法(帶動圖演示) 為什么知乎前端圈普遍認為H5游戲和H5展示的JSer 個人整理和封裝的YU.js庫|中文詳細注釋|供新手學習使用 擴展JavaScript語法記錄 - 掉坑初期工具 漢字拼音轉換...
平日學習接觸過的網(wǎng)站積累,以每月的形式發(fā)布。2017年以前看這個網(wǎng)址:http://www.kancloud.cn/jsfron... 1. Javascript 前端生成好看的二維碼 十大經典排序算法(帶動圖演示) 為什么知乎前端圈普遍認為H5游戲和H5展示的JSer 個人整理和封裝的YU.js庫|中文詳細注釋|供新手學習使用 擴展JavaScript語法記錄 - 掉坑初期工具 漢字拼音轉換...
閱讀 1335·2019-08-30 15:44
閱讀 1385·2019-08-29 18:42
閱讀 440·2019-08-29 13:59
閱讀 777·2019-08-28 17:58
閱讀 2819·2019-08-26 12:02
閱讀 2422·2019-08-23 18:40
閱讀 2411·2019-08-23 18:13
閱讀 3112·2019-08-23 16:27