<dfn id="hx5t3"><strike id="hx5t3"><em id="hx5t3"></em></strike></dfn>

    <thead id="hx5t3"></thead><nobr id="hx5t3"><font id="hx5t3"><rp id="hx5t3"></rp></font></nobr>

    <listing id="hx5t3"></listing>

    <var id="hx5t3"></var>
    <big id="hx5t3"></big>

      
      

      <output id="hx5t3"><ruby id="hx5t3"></ruby></output>
      <menuitem id="hx5t3"><dfn id="hx5t3"></dfn></menuitem>

      <big id="hx5t3"></big>

        撒網要見魚

        撒網要見魚 查看完整檔案

        杭州編輯江南大學  |  數字媒體技術 編輯alibaba  |  偏前端 編輯 www.dailichun.com 編輯
        編輯

        你才25歲,你可以成為任何你想成為的人。

        個人動態

        撒網要見魚 發布了文章 · 2020-02-23

        【跪求】阿里釘釘前端實習內推

        此貼誠意滿滿,只求前端!
        此貼誠意滿滿,只求前端!
        此貼誠意滿滿,只求前端!

        目標

        面向2021畢業的同學

        1. 本科及以上學歷,計算機相關專業
        2. 熟練掌握HTML5/CSS3/Javascript等web前端技術
        3. 熟悉至少一種常用框架,例如React、vue等
        4. 關注新事物、新技術,有較強的學習能力,有強烈求知欲和進取心
        5. 有半年以上實際項目經驗,大廠加分

        image.png

        image.png

        內推郵箱

        lichun.dlc@alibaba-inc.com

        簡歷發我郵箱,必有回應,符合要求直接走內推?。?!

        一對一服務,享受最極速的應聘體驗!

        交個朋友

        個人網站:http://www.dailichun.com/

        也可加我微信了解更多:a546684355

        查看原文

        贊 3 收藏 1 評論 0

        撒網要見魚 發布了文章 · 2020-02-02

        釘釘開放與商業化團隊前端大量招人

        跪求前端

        急缺前端!
        急缺前端!
        急缺前端!

        • 阿里巴巴常規福利(13 薪、3-6 月年終獎、7 天以上帶薪年假等)
        • 釘釘期權,釘釘相對于阿里集團,有自己獨立的期權,想象空間大
        • 團隊妹子多,妹子多的團隊有時候也是個煩惱啊^_^

        歡迎發簡歷,合適就直接走內推環節,全程一對一溝通~

        也可直接私信,定期回復消息~

        當初我也是在社區發文后被招過去的,歡迎各位志同道合的小伙伴加入哈~

        JD(P6~P7)

        職責和挑戰

        1. 負責釘釘工作臺。工作臺是幫助企業實現數字化管理和協同的門戶,是擁有億級用戶量的產品。如何保障安全、穩定、性能和體驗是對我們的一大挑戰。
        2. 負責開放能力建設。針對紛繁的業務場景,提供合理的開放方案,既要做到深入用戶場景理解并支撐業務發展,滿足企業千人千面、千行千面的訴求,又要在技術上保障用戶的安全、穩定和體驗。需要既要有技術抽象能力、平臺架構能力,又要有業務的理解和分析能力。
        3. 開放平臺基礎建設。保障鏈路的安全和穩定。同時對如何保障用戶體驗有持續精進的熱情和追求。

        職位要求

        1. 精通HTML5、CSS3、JS(ES5/ES6)等前端開發技術
        2. 掌握主流的JS庫和開發框架,并深入理解其設計原理,例如React,Vue等
        3. 熟悉模塊化、前端編譯和構建工具,例如webpack、babel等
        4. (加分項)了解服務端或native移動應用開發,例如nodejs、Java等
        5. 對技術有強追求,有良好的溝通能力和團隊協同能力,有優秀的分析問題和解決問題的能力。

        投遞

        歡迎直接將簡歷發到郵箱哈~

        郵箱:lichun.dlc@alibaba-inc.com

        查看原文

        贊 3 收藏 2 評論 0

        撒網要見魚 收藏了文章 · 2018-07-12

        React 源碼剖析系列 - 解密 setState

        this.setState() 方法應該是每一位使用 React 的同學最先熟悉的 API。然而,你真的了解 setState 么?先看看下面這個小問題,你能否正確回答。

        引子

        class Example extends React.Component {
          constructor() {
            super();
            this.state = {
              val: 0
            };
          }
          
          componentDidMount() {
            this.setState({val: this.state.val + 1});
            console.log(this.state.val);    // 第 1 次 log
        
            this.setState({val: this.state.val + 1});
            console.log(this.state.val);    // 第 2 次 log
        
            setTimeout(() => {
              this.setState({val: this.state.val + 1});
              console.log(this.state.val);  // 第 3 次 log
        
              this.setState({val: this.state.val + 1});
              console.log(this.state.val);  // 第 4 次 log
            }, 0);
          }
        
          render() {
            return null;
          }
        };

        問上述代碼中 4 次 console.log 打印出來的 val 分別是多少?

        不賣關子,先揭曉答案,4 次 log 的值分別是:0、0、2、3。

        若結果和你心中的答案不完全相同,那下面的內容你可能會感興趣。

        同樣的 setState 調用,為何表現和結果卻大相徑庭呢?讓我們先看看 setState 到底干了什么。

        setState 干了什么

        setState 簡化調用棧

        上面這個流程圖是一個簡化的 setState 調用棧,注意其中核心的狀態判斷,在源碼(ReactUpdates.js)

        function enqueueUpdate(component) {
          // ...
        
          if (!batchingStrategy.isBatchingUpdates) {
            batchingStrategy.batchedUpdates(enqueueUpdate, component);
            return;
          }
        
          dirtyComponents.push(component);
        }

        isBatchingUpdates 為 true,則把當前組件(即調用了 setState 的組件)放入 dirtyComponents 數組中;否則 batchUpdate 所有隊列中的更新。先不管這個 batchingStrategy,看到這里大家應該已經大概猜出來了,文章一開始的例子中 4 次 setState 調用表現之所以不同,這里邏輯判斷起了關鍵作用。

        那么 batchingStrategy 究竟是何方神圣呢?其實它只是一個簡單的對象,定義了一個 isBatchingUpdates 的布爾值,和一個 batchedUpdates 方法。下面是一段簡化的定義代碼:

        var batchingStrategy = {
          isBatchingUpdates: false,
        
          batchedUpdates: function(callback, a, b, c, d, e) {
            // ...
            batchingStrategy.isBatchingUpdates = true;
            
            transaction.perform(callback, null, a, b, c, d, e);
          }
        };

        注意 batchingStrategy 中的 batchedUpdates 方法中,有一個 transaction.perform 調用。這就引出了本文要介紹的核心概念 —— Transaction(事務)。

        初識 Transaction

        熟悉 MySQL 的同學看到 Transaction 是否會心一笑?然而在 React 中 Transaction 的原理和行為和 MySQL 中并不完全相同,讓我們從源碼開始一步步開始了解。

        在 Transaction 的源碼中有一幅特別的 ASCII 圖,形象的解釋了 Transaction 的作用。

        /*
         * <pre>
         *                       wrappers (injected at creation time)
         *                                      +        +
         *                                      |        |
         *                    +-----------------|--------|--------------+
         *                    |                 v        |              |
         *                    |      +---------------+   |              |
         *                    |   +--|    wrapper1   |---|----+         |
         *                    |   |  +---------------+   v    |         |
         *                    |   |          +-------------+  |         |
         *                    |   |     +----|   wrapper2  |--------+   |
         *                    |   |     |    +-------------+  |     |   |
         *                    |   |     |                     |     |   |
         *                    |   v     v                     v     v   | wrapper
         *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
         * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
         * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
         *                    | |   | |   |   |         |   |   | |   | |
         *                    | |   | |   |   |         |   |   | |   | |
         *                    | |   | |   |   |         |   |   | |   | |
         *                    | +---+ +---+   +---------+   +---+ +---+ |
         *                    |  initialize                    close    |
         *                    +-----------------------------------------+
         * </pre>
         */

        簡單地說,一個所謂的 Transaction 就是將需要執行的 method 使用 wrapper 封裝起來,再通過 Transaction 提供的 perform 方法執行。而在 perform 之前,先執行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 執行后)再執行所有的 close 方法。一組 initialize 及 close 方法稱為一個 wrapper,從上面的示例圖中可以看出 Transaction 支持多個 wrapper 疊加。

        具體到實現上,React 中的 Transaction 提供了一個 Mixin 方便其它模塊實現自己需要的事務。而要使用 Transaction 的模塊,除了需要把 Transaction 的 Mixin 混入自己的事務實現中外,還需要額外實現一個抽象的 getTransactionWrappers 接口。這個接口是 Transaction 用來獲取所有需要封裝的前置方法(initialize)和收尾方法(close)的,因此它需要返回一個數組的對象,每個對象分別有 key 為 initialize 和 close 的方法。

        下面是一個簡單使用 Transaction 的例子

        var Transaction = require('./Transaction');
        
        // 我們自己定義的 Transaction
        var MyTransaction = function() {
          // do sth.
        };
        
        Object.assign(MyTransaction.prototype, Transaction.Mixin, {
          getTransactionWrappers: function() {
            return [{
              initialize: function() {
                console.log('before method perform');
              },
              close: function() {
                console.log('after method perform');
              }
            }];
          };
        });
        
        var transaction = new MyTransaction();
        var testMethod = function() {
          console.log('test');
        }
        transaction.perform(testMethod);
        
        // before method perform
        // test
        // after method perform

        當然在實際代碼中 React 還做了異常處理等工作,這里不詳細展開。有興趣的同學可以參考源碼中 Transaction 實現。

        說了這么多 Transaction,它到底是怎么導致上文所述 setState 的各種不同表現的呢?

        解密 setState

        那么 Transaction 跟 setState 的不同表現有什么關系呢?首先我們把 4 次 setState 簡單歸類,前兩次屬于一類,因為他們在同一次調用棧中執行;setTimeout 中的兩次 setState 屬于另一類,原因同上。讓我們分別看看這兩類 setState 的調用棧:

        componentDidMout 中 setState 的調用棧

        componentDidMout 中 setState 的調用棧

        setTimeout 中 setState 的調用棧

        setTimeout 中 setState 的調用棧

        很明顯,在 componentDidMount 中直接調用的兩次 setState,其調用棧更加復雜;而 setTimeout 中調用的兩次 setState,調用棧則簡單很多。讓我們重點看看第一類 setState 的調用棧,有沒有發現什么熟悉的身影?沒錯,就是 batchedUpdates 方法,原來早在 setState 調用前,已經處于 batchedUpdates 執行的 transaction 中!

        那這次 batchedUpdate 方法,又是誰調用的呢?讓我們往前再追溯一層,原來是 ReactMount.js 中的 _renderNewRootComponent 方法。也就是說,整個將 React 組件渲染到 DOM 中的過程就處于一個大的 Transaction 中。

        接下來的解釋就順理成章了,因為在 componentDidMount 中調用 setState 時,batchingStrategy 的 isBatchingUpdates 已經被設為 true,所以兩次 setState 的結果并沒有立即生效,而是被放進了 dirtyComponents 中。這也解釋了兩次打印 this.state.val 都是 0 的原因,新的 state 還沒有被應用到組件中。

        再反觀 setTimeout 中的兩次 setState,因為沒有前置的 batchedUpdate 調用,所以 batchingStrategy 的 isBatchingUpdates 標志位是 false,也就導致了新的 state 馬上生效,沒有走到 dirtyComponents 分支。也就是,setTimeout 中第一次 setState 時,this.state.val 為 1,而 setState 完成后打印時 this.state.val 變成了 2。第二次 setState 同理。

        擴展閱讀

        在上文介紹 Transaction 時也提到了其在 React 源碼中的多處應用,想必調試過 React 源碼的同學應該能經常見到它的身影,像 initialize、perform、close、closeAll、notifyAll 等方法出現在調用棧中,都說明當前處于一個 Transaction 中。

        既然 Transaction 這么有用,我們自己的代碼中能使用 Transaction 嗎?很可惜,答案是不能。不過針對文章一開始例子中 setTimeout 里的兩次 setState 導致兩次 render 的情況,React 偷偷給我們暴露了一個 batchedUpdates 方法,方便我們調用。

        import ReactDom, { unstable_batchedUpdates } from 'react-dom';
        
        unstable_batchedUpdates(() => {
          this.setState(val: this.state.val + 1);
          this.setState(val: this.state.val + 1);
        });

        當然因為這個不是公開的 API,后續存在廢棄的風險,大家在業務系統里慎用喲!

        注釋

        1. test-react 文中測試代碼已放在 Github 上,需要自己實驗探索的同學可以 clone 下來自己斷點調試。

        2. 為了避免引入更多的概念,上文中所說到的 batchingStrategy 均指 ReactDefaultBatchingStrategy,該 strategy 在 React 初始化時由 ReactDefaultInjection 注入到 ReactUpdates 中作為默認的 strategy。在 server 渲染時,則會注入不同的 strategy,有興趣的同學請自行探索。

        查看原文

        撒網要見魚 評論了文章 · 2018-05-29

        從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理

        前言

        見解有限,如有描述不當之處,請幫忙及時指出,如有錯誤,會及時修正。

        ----------超長文+多圖預警,需要花費不少時間。----------

        如果看完本文后,還對進程線程傻傻分不清,不清楚瀏覽器多進程、瀏覽器內核多線程、JS單線程、JS運行機制的區別。那么請回復我,一定是我寫的還不夠清晰,我來改。。。

        ----------正文開始----------

        最近發現有不少介紹JS單線程運行機制的文章,但是發現很多都僅僅是介紹某一部分的知識,而且各個地方的說法還不統一,容易造成困惑。
        因此準備梳理這塊知識點,結合已有的認知,基于網上的大量參考資料,
        從瀏覽器多進程到JS單線程,將JS引擎的運行機制系統的梳理一遍。

        展現形式:由于是屬于系統梳理型,就沒有由淺入深了,而是從頭到尾的梳理知識體系,
        重點是將關鍵節點的知識點串聯起來,而不是僅僅剖析某一部分知識。

        內容是:從瀏覽器進程,再到瀏覽器內核運行,再到JS引擎單線程,再到JS事件循環機制,從頭到尾系統的梳理一遍,擺脫碎片化,形成一個知識體系

        目標是:看完這篇文章后,對瀏覽器多進程,JS單線程,JS事件循環機制這些都能有一定理解,
        有一個知識體系骨架,而不是似懂非懂的感覺。

        另外,本文適合有一定經驗的前端人員,新手請規避,避免受到過多的概念沖擊??梢韵却嫫饋?,有了一定理解后再看,也可以分成多批次觀看,避免過度疲勞。

        大綱

        • 區分進程和線程
        • 瀏覽器是多進程的

          • 瀏覽器都包含哪些進程?
          • 瀏覽器多進程的優勢
          • 重點是瀏覽器內核(渲染進程)
          • Browser進程和瀏覽器內核(Renderer進程)的通信過程
        • 梳理瀏覽器內核中線程之間的關系

          • GUI渲染線程與JS引擎線程互斥
          • JS阻塞頁面加載
          • WebWorker,JS的多線程?
          • WebWorker與SharedWorker
        • 簡單梳理下瀏覽器渲染流程

          • load事件與DOMContentLoaded事件的先后
          • css加載是否會阻塞dom樹渲染?
          • 普通圖層和復合圖層
        • 從Event Loop談JS的運行機制

          • 事件循環機制進一步補充
          • 單獨說說定時器
          • setTimeout而不是setInterval
        • 事件循環進階:macrotask與microtask
        • 寫在最后的話

        區分進程和線程

        線程和進程區分不清,是很多新手都會犯的錯誤,沒有關系。這很正常。先看看下面這個形象的比喻:

        - 進程是一個工廠,工廠有它的獨立資源
        
        - 工廠之間相互獨立
        
        - 線程是工廠中的工人,多個工人協作完成任務
        
        - 工廠內有一個或多個工人
        
        - 工人之間共享空間

        再完善完善概念:

        - 工廠的資源 -> 系統分配的內存(獨立的一塊內存)
        
        - 工廠之間的相互獨立 -> 進程之間相互獨立
        
        - 多個工人協作完成任務 -> 多個線程在進程中協作完成任務
        
        - 工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成
        
        - 工人之間共享空間 -> 同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)

        然后再鞏固下:

        如果是windows電腦中,可以打開任務管理器,可以看到有一個后臺進程列表。對,那里就是查看進程的地方,而且可以看到每個進程的內存資源信息以及cpu占有率。

        所以,應該更容易理解了:進程是cpu資源分配的最小單位(系統會給它分配內存)

        最后,再用較為官方的術語描述一遍:

        • 進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)
        • 線程是cpu調度的最小單位(線程是建立在進程的基礎上的一次程序運行單位,一個進程中可以有多個線程)

        tips

        • 不同進程之間也可以通信,不過代價較大
        • 現在,一般通用的叫法:單線程與多線程,都是指在一個進程內的單和多。(所以核心還是得屬于一個進程才行)

        瀏覽器是多進程的

        理解了進程與線程了區別后,接下來對瀏覽器進行一定程度上的認識:(先看下簡化理解)

        • 瀏覽器是多進程的
        • 瀏覽器之所以能夠運行,是因為系統給它的進程分配了資源(cpu、內存)
        • 簡單點理解,每打開一個Tab頁,就相當于創建了一個獨立的瀏覽器進程。

        關于以上幾點的驗證,請再第一張圖

        圖中打開了Chrome瀏覽器的多個標簽頁,然后可以在Chrome的任務管理器中看到有多個進程(分別是每一個Tab頁面有一個獨立的進程,以及一個主進程)。
        感興趣的可以自行嘗試下,如果再多打開一個Tab頁,進程正常會+1以上

        注意:在這里瀏覽器應該也有自己的優化機制,有時候打開多個tab頁后,可以在Chrome任務管理器中看到,有些進程被合并了
        (所以每一個Tab標簽對應一個進程并不一定是絕對的)

        瀏覽器都包含哪些進程?

        知道了瀏覽器是多進程后,再來看看它到底包含哪些進程:(為了簡化理解,僅列舉主要進程)

        1. Browser進程:瀏覽器的主進程(負責協調、主控),只有一個。作用有

          • 負責瀏覽器界面顯示,與用戶交互。如前進,后退等
          • 負責各個頁面的管理,創建和銷毀其他進程
          • 將Renderer進程得到的內存中的Bitmap,繪制到用戶界面上
          • 網絡資源的管理,下載等
        2. 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建
        3. GPU進程:最多一個,用于3D繪制等
        4. 瀏覽器渲染進程(瀏覽器內核)(Renderer進程,內部是多線程的):默認每個Tab頁面一個進程,互不影響。主要作用為

          • 頁面渲染,腳本執行,事件處理等

        強化記憶:在瀏覽器中打開一個網頁相當于新起了一個進程(進程內有自己的多線程)

        當然,瀏覽器有時會將多個進程合并(譬如打開多個空白標簽頁后,會發現多個空白標簽頁被合并成了一個進程),如圖

        另外,可以通過Chrome的更多工具 -> 任務管理器自行驗證

        瀏覽器多進程的優勢

        相比于單進程瀏覽器,多進程有如下優點:

        • 避免單個page crash影響整個瀏覽器
        • 避免第三方插件crash影響整個瀏覽器
        • 多進程充分利用多核優勢
        • 方便使用沙盒模型隔離插件等進程,提高瀏覽器穩定性

        簡單點理解:如果瀏覽器是單進程,那么某個Tab頁崩潰了,就影響了整個瀏覽器,體驗有多差;同理如果是單進程,插件崩潰了也會影響整個瀏覽器;而且多進程還有其它的諸多優勢。。。

        當然,內存等資源消耗也會更大,有點空間換時間的意思。

        重點是瀏覽器內核(渲染進程)

        重點來了,我們可以看到,上面提到了這么多的進程,那么,對于普通的前端操作來說,最終要的是什么呢?答案是渲染進程

        可以這樣理解,頁面的渲染,JS的執行,事件的循環,都在這個進程內進行。接下來重點分析這個進程

        請牢記,瀏覽器的渲染進程是多線程的(這點如果不理解,請回頭看進程和線程的區分

        終于到了線程這個概念了?,好親切。那么接下來看看它都包含了哪些線程(列舉一些主要常駐線程):

        1. GUI渲染線程

          • 負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,布局和繪制等。
          • 當界面需要重繪(Repaint)或由于某種操作引發回流(reflow)時,該線程就會執行
          • 注意,GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(相當于被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閑時立即被執行。
        2. JS引擎線程

          • 也稱為JS內核,負責處理Javascript腳本程序。(例如V8引擎)
          • JS引擎線程負責解析Javascript腳本,運行代碼。
          • JS引擎一直等待著任務隊列中任務的到來,然后加以處理,一個Tab頁(renderer進程)中無論什么時候都只有一個JS線程在運行JS程序
          • 同樣注意,GUI渲染線程與JS引擎線程是互斥的,所以如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞。
        3. 事件觸發線程

          • 歸屬于瀏覽器而不是JS引擎,用來控制事件循環(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開線程協助)
          • 當JS引擎執行代碼塊如setTimeOut時(也可來自瀏覽器內核的其他線程,如鼠標點擊、AJAX異步請求等),會將對應任務添加到事件線程中
          • 當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理
          • 注意,由于JS的單線程關系,所以這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閑時才會去執行)

        4. 定時觸發器線程

          • 傳說中的setIntervalsetTimeout所在線程
          • 瀏覽器定時計數器并不是由JavaScript引擎計數的,(因為JavaScript引擎是單線程的, 如果處于阻塞線程狀態就會影響記計時的準確)
          • 因此通過單獨線程來計時并觸發定時(計時完畢后,添加到事件隊列中,等待JS引擎空閑后執行)
          • 注意,W3C在HTML標準中規定,規定要求setTimeout中低于4ms的時間間隔算為4ms。
        5. 異步http請求線程

          • 在XMLHttpRequest在連接后是通過瀏覽器新開一個線程請求
          • 將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件,將這個回調再放入事件隊列中。再由JavaScript引擎執行。

        看到這里,如果覺得累了,可以先休息下,這些概念需要被消化,畢竟后續將提到的事件循環機制就是基于事件觸發線程的,所以如果僅僅是看某個碎片化知識,
        可能會有一種似懂非懂的感覺。要完成的梳理一遍才能快速沉淀,不易遺忘。放張圖鞏固下吧:

        再說一點,為什么JS引擎是單線程的?額,這個問題其實應該沒有標準答案,譬如,可能僅僅是因為由于多線程的復雜性,譬如多線程操作一般要加鎖,因此最初設計時選擇了單線程。。。

        Browser進程和瀏覽器內核(Renderer進程)的通信過程

        看到這里,首先,應該對瀏覽器內的進程和線程都有一定理解了,那么接下來,再談談瀏覽器的Browser進程(控制進程)是如何和內核通信的,
        這點也理解后,就可以將這部分的知識串聯起來,從頭到尾有一個完整的概念。

        如果自己打開任務管理器,然后打開一個瀏覽器,就可以看到:任務管理器中出現了兩個進程(一個是主控進程,一個則是打開Tab頁的渲染進程),
        然后在這前提下,看下整個的過程:(簡化了很多)

        • Browser進程收到用戶請求,首先需要獲取頁面內容(譬如通過網絡下載資源),隨后將該任務通過RendererHost接口傳遞給Render進程
        • Renderer進程的Renderer接口收到消息,簡單解釋后,交給渲染線程,然后開始渲染

          • 渲染線程接收請求,加載網頁并渲染網頁,這其中可能需要Browser進程獲取資源和需要GPU進程來幫助渲染
          • 當然可能會有JS線程操作DOM(這樣可能會造成回流并重繪)
          • 最后Render進程將結果傳遞給Browser進程
        • Browser進程接收到結果并將結果繪制出來

        這里繪一張簡單的圖:(很簡化)

        看完這一整套流程,應該對瀏覽器的運作有了一定理解了,這樣有了知識架構的基礎后,后續就方便往上填充內容。

        這塊再往深處講的話就涉及到瀏覽器內核源碼解析了,不屬于本文范圍。

        如果這一塊要深挖,建議去讀一些瀏覽器內核源碼解析文章,或者可以先看看參考下來源中的第一篇文章,寫的不錯

        梳理瀏覽器內核中線程之間的關系

        到了這里,已經對瀏覽器的運行有了一個整體的概念,接下來,先簡單梳理一些概念

        GUI渲染線程與JS引擎線程互斥

        由于JavaScript是可操縱DOM的,如果在修改這些元素屬性同時渲染界面(即JS線程和UI線程同時運行),那么渲染線程前后獲得的元素數據就可能不一致了。

        因此為了防止渲染出現不可預期的結果,瀏覽器設置GUI渲染線程與JS引擎為互斥的關系,當JS引擎執行時GUI線程會被掛起,
        GUI更新則會被保存在一個隊列中等到JS引擎線程空閑時立即被執行。

        JS阻塞頁面加載

        從上述的互斥關系,可以推導出,JS如果執行時間過長就會阻塞頁面。

        譬如,假設JS引擎正在進行巨量的計算,此時就算GUI有更新,也會被保存到隊列中,等待JS引擎空閑后執行。
        然后,由于巨量計算,所以JS引擎很可能很久很久后才能空閑,自然會感覺到巨卡無比。

        所以,要盡量避免JS執行時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞的感覺。

        WebWorker,JS的多線程?

        前文中有提到JS引擎是單線程的,而且JS執行時間過長會阻塞頁面,那么JS就真的對cpu密集型計算無能為力么?

        所以,后來HTML5中支持了Web Worker。

        MDN的官方解釋是:

        Web Worker為Web內容在后臺線程中運行腳本提供了一種簡單的方法。線程可以執行任務而不干擾用戶界面
        
        一個worker是使用一個構造函數創建的一個對象(e.g. Worker()) 運行一個命名的JavaScript文件 
        
        這個文件包含將在工作線程中運行的代碼; workers 運行在另一個全局上下文中,不同于當前的window
        
        因此,使用 window快捷方式獲取當前全局的范圍 (而不是self) 在一個 Worker 內將返回錯誤

        這樣理解下:

        • 創建Worker時,JS引擎向瀏覽器申請開一個子線程(子線程是瀏覽器開的,完全受主線程控制,而且不能操作DOM)
        • JS引擎線程與worker線程間通過特定的方式通信(postMessage API,需要通過序列化對象來與線程交互特定的數據)

        所以,如果有非常耗時的工作,請單獨開一個Worker線程,這樣里面不管如何翻天覆地都不會影響JS引擎主線程,
        只待計算出結果后,將結果通信給主線程即可,perfect!

        而且注意下,JS引擎是單線程的,這一點的本質仍然未改變,Worker可以理解是瀏覽器給JS引擎開的外掛,專門用來解決那些大量計算問題。

        其它,關于Worker的詳解就不是本文的范疇了,因此不再贅述。

        WebWorker與SharedWorker

        既然都到了這里,就再提一下SharedWorker(避免后續將這兩個概念搞混)

        • WebWorker只屬于某個頁面,不會和其他頁面的Render進程(瀏覽器內核進程)共享

          • 所以Chrome在Render進程中(每一個Tab頁就是一個render進程)創建一個新的線程來運行Worker中的JavaScript程序。
        • SharedWorker是瀏覽器所有頁面共享的,不能采用與Worker同樣的方式實現,因為它不隸屬于某個Render進程,可以為多個Render進程共享使用

          • 所以Chrome瀏覽器為SharedWorker單獨創建一個進程來運行JavaScript程序,在瀏覽器中每個相同的JavaScript只存在一個SharedWorker進程,不管它被創建多少次。

        看到這里,應該就很容易明白了,本質上就是進程和線程的區別。SharedWorker由獨立的進程管理,WebWorker只是屬于render進程下的一個線程

        簡單梳理下瀏覽器渲染流程

        本來是直接計劃開始談JS運行機制的,但想了想,既然上述都一直在談瀏覽器,直接跳到JS可能再突兀,因此,中間再補充下瀏覽器的渲染流程(簡單版本)

        為了簡化理解,前期工作直接省略成:(要展開的或完全可以寫另一篇超長文)

        - 瀏覽器輸入url,瀏覽器主進程接管,開一個下載線程,
        然后進行 http請求(略去DNS查詢,IP尋址等等操作),然后等待響應,獲取內容,
        隨后將內容通過RendererHost接口轉交給Renderer進程
        
        - 瀏覽器渲染流程開始

        瀏覽器器內核拿到內容后,渲染大概可以劃分成以下幾個步驟:

        1. 解析html建立dom樹
        2. 解析css構建render樹(將CSS代碼解析成樹形的數據結構,然后結合DOM合并成render樹)
        3. 布局render樹(Layout/reflow),負責各元素尺寸、位置的計算
        4. 繪制render樹(paint),繪制頁面像素信息
        5. 瀏覽器會將各層的信息發送給GPU,GPU會將各層合成(composite),顯示在屏幕上。

        所有詳細步驟都已經略去,渲染完畢后就是load事件了,之后就是自己的JS邏輯處理了

        既然略去了一些詳細的步驟,那么就提一些可能需要注意的細節把。

        這里重繪參考來源中的一張圖:(參考來源第一篇)

        load事件與DOMContentLoaded事件的先后

        上面提到,渲染完畢后會觸發load事件,那么你能分清楚load事件與DOMContentLoaded事件的先后么?

        很簡單,知道它們的定義就可以了:

        • 當 DOMContentLoaded 事件觸發時,僅當DOM加載完成,不包括樣式表,圖片。

        (譬如如果有async加載的腳本就不一定完成)

        • 當 onload 事件觸發時,頁面上所有的DOM,樣式表,腳本,圖片都已經加載完成了。

        (渲染完畢了)

        所以,順序是:DOMContentLoaded -> load

        css加載是否會阻塞dom樹渲染?

        這里說的是頭部引入css的情況

        首先,我們都知道:css是由單獨的下載線程異步下載的。

        然后再說下幾個現象:

        • css加載不會阻塞DOM樹解析(異步加載時DOM照常構建)
        • 但會阻塞render樹渲染(渲染時需等css加載完畢,因為render樹需要css信息)

        這可能也是瀏覽器的一種優化機制。

        因為你加載css的時候,可能會修改下面DOM節點的樣式,
        如果css加載不阻塞render樹渲染的話,那么當css加載完之后,
        render樹可能又得重新重繪或者回流了,這就造成了一些沒有必要的損耗。
        所以干脆就先把DOM樹的結構先解析完,把可以做的工作做完,然后等你css加載完之后,
        在根據最終的樣式來渲染render樹,這種做法性能方面確實會比較好一點。

        普通圖層和復合圖層

        渲染步驟中就提到了composite概念。

        可以簡單的這樣理解,瀏覽器渲染的圖層一般包含兩大類:普通圖層以及復合圖層

        首先,普通文檔流內可以理解為一個復合圖層(這里稱為默認復合層,里面不管添加多少元素,其實都是在同一個復合圖層中)

        其次,absolute布局(fixed也一樣),雖然可以脫離普通文檔流,但它仍然屬于默認復合層。

        然后,可以通過硬件加速的方式,聲明一個新的復合圖層,它會單獨分配資源
        (當然也會脫離普通文檔流,這樣一來,不管這個復合圖層中怎么變化,也不會影響默認復合層里的回流重繪)

        可以簡單理解下:GPU中,各個復合圖層是單獨繪制的,所以互不影響,這也是為什么某些場景硬件加速效果一級棒

        可以Chrome源碼調試 -> More Tools -> Rendering -> Layer borders中看到,黃色的就是復合圖層信息

        如下圖??梢则炞C上述的說法

        如何變成復合圖層(硬件加速)

        將該元素變成一個復合圖層,就是傳說中的硬件加速技術

        • 最常用的方式:translate3d、translateZ
        • opacity屬性/過渡動畫(需要動畫執行的過程中才會創建合成層,動畫沒有開始或結束后元素還會回到之前的狀態)
        • will-chang屬性(這個比較偏僻),一般配合opacity與translate使用(而且經測試,除了上述可以引發硬件加速的屬性外,其它屬性并不會變成復合層),

        作用是提前告訴瀏覽器要變化,這樣瀏覽器會開始做一些優化工作(這個最好用完后就釋放)

        • <video><iframe><canvas><webgl>等元素
        • 其它,譬如以前的flash插件

        absolute和硬件加速的區別

        可以看到,absolute雖然可以脫離普通文檔流,但是無法脫離默認復合層。
        所以,就算absolute中信息改變時不會改變普通文檔流中render樹,
        但是,瀏覽器最終繪制時,是整個復合層繪制的,所以absolute中信息的改變,仍然會影響整個復合層的繪制。
        (瀏覽器會重繪它,如果復合層中內容多,absolute帶來的繪制信息變化過大,資源消耗是非常嚴重的)

        而硬件加速直接就是在另一個復合層了(另起爐灶),所以它的信息改變不會影響默認復合層
        (當然了,內部肯定會影響屬于自己的復合層),僅僅是引發最后的合成(輸出視圖)

        復合圖層的作用?

        一般一個元素開啟硬件加速后會變成復合圖層,可以獨立于普通文檔流中,改動后可以避免整個頁面重繪,提升性能

        但是盡量不要大量使用復合圖層,否則由于資源消耗過度,頁面反而會變的更卡

        硬件加速時請使用index

        使用硬件加速時,盡可能的使用index,防止瀏覽器默認給后續的元素創建復合層渲染

        具體的原理時這樣的:
        **webkit CSS3中,如果這個元素添加了硬件加速,并且index層級比較低,
        那么在這個元素的后面其它元素(層級比這個元素高的,或者相同的,并且releative或absolute屬性相同的),
        會默認變為復合層渲染,如果處理不當會極大的影響性能**

        簡單點理解,其實可以認為是一個隱式合成的概念:如果a是一個復合圖層,而且b在a上面,那么b也會被隱式轉為一個復合圖層,這點需要特別注意

        另外,這個問題可以在這個地址看到重現(原作者分析的挺到位的,直接上鏈接):

        http://web.jobbole.com/83575/

        從Event Loop談JS的運行機制

        到此時,已經是屬于瀏覽器頁面初次渲染完畢后的事情,JS引擎的一些運行機制分析。

        注意,這里不談可執行上下文,VO,scop chain等概念(這些完全可以整理成另一篇文章了),這里主要是結合Event Loop來談JS代碼是如何執行的。

        讀這部分的前提是已經知道了JS引擎是單線程,而且這里會用到上文中的幾個概念:(如果不是很理解,可以回頭溫習)

        • JS引擎線程
        • 事件觸發線程
        • 定時觸發器線程

        然后再理解一個概念:

        • JS分為同步任務和異步任務
        • 同步任務都在主線程上執行,形成一個執行棧
        • 主線程之外,事件觸發線程管理著一個任務隊列,只要異步任務有了運行結果,就在任務隊列之中放置一個事件。
        • 一旦執行棧中的所有同步任務執行完畢(此時JS引擎空閑),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中,開始執行。

        看圖:

        看到這里,應該就可以理解了:為什么有時候setTimeout推入的事件不能準時執行?因為可能在它推入到事件列表時,主線程還不空閑,正在執行其它代碼,
        所以自然有誤差。

        事件循環機制進一步補充

        這里就直接引用一張圖片來協助理解:(參考自Philip Roberts的演講《Help, I'm stuck in an event-loop》)

        上圖大致描述就是:

        • 主線程運行時會產生執行棧,

        棧中的代碼調用某些api時,它們會在事件隊列中添加各種事件(當滿足觸發條件后,如ajax請求完畢)

        • 而棧中的代碼執行完畢,就會讀取事件隊列中的事件,去執行那些回調
        • 如此循環
        • 注意,總是要等待棧中的代碼執行完畢后才會去讀取事件隊列中的事件

        單獨說說定時器

        上述事件循環機制的核心是:JS引擎線程和事件觸發線程

        但事件上,里面還有一些隱藏細節,譬如調用setTimeout后,是如何等待特定時間后才添加到事件隊列中的?

        是JS引擎檢測的么?當然不是了。它是由定時器線程控制(因為JS引擎自己都忙不過來,根本無暇分身)

        為什么要單獨的定時器線程?因為JavaScript引擎是單線程的, 如果處于阻塞線程狀態就會影響記計時的準確,因此很有必要單獨開一個線程用來計時。

        什么時候會用到定時器線程?當使用setTimeoutsetInterval,它需要定時器線程計時,計時完成后就會將特定的事件推入事件隊列中。

        譬如:

        setTimeout(function(){
            console.log('hello!');
        }, 1000);

        這段代碼的作用是當1000毫秒計時完畢后(由定時器線程計時),將回調函數推入事件隊列中,等待主線程執行

        setTimeout(function(){
            console.log('hello!');
        }, 0);
        
        console.log('begin');

        這段代碼的效果是最快的時間內將回調函數推入事件隊列中,等待主線程執行

        注意:

        • 執行結果是:先beginhello!
        • 雖然代碼的本意是0毫秒后就推入事件隊列,但是W3C在HTML標準中規定,規定要求setTimeout中低于4ms的時間間隔算為4ms。

        (不過也有一說是不同瀏覽器有不同的最小時間設定)

        • 就算不等待4ms,就算假設0毫秒就推入事件隊列,也會先執行begin(因為只有可執行棧內空了后才會主動讀取事件隊列)

        setTimeout而不是setInterval

        用setTimeout模擬定期計時和直接用setInterval是有區別的。

        因為每次setTimeout計時到后就會去執行,然后執行一段時間后才會繼續setTimeout,中間就多了誤差
        (誤差多少與代碼執行時間有關)

        而setInterval則是每次都精確的隔一段時間推入一個事件
        (但是,事件的實際執行時間不一定就準確,還有可能是這個事件還沒執行完畢,下一個事件就來了)

        而且setInterval有一些比較致命的問題就是:

        • 累計效應(上面提到的),如果setInterval代碼在(setInterval)再次添加到隊列之前還沒有完成執行,

        就會導致定時器代碼連續運行好幾次,而之間沒有間隔。
        就算正常間隔執行,多個setInterval的代碼執行時間可能會比預期?。ㄒ驗榇a執行需要一定時間)

        • 譬如像iOS的webview,或者Safari等瀏覽器中都有一個特點,在滾動的時候是不執行JS的,如果使用了setInterval,會發現在滾動結束后會執行多次由于滾動不執行JS積攢回調,如果回調執行時間過長,就會非常容器造成卡頓問題和一些不可知的錯誤(這一塊后續有補充,setInterval自帶的優化,不會重復添加回調)
        • 而且把瀏覽器最小化顯示等操作時,setInterval并不是不執行程序,

        它會把setInterval的回調函數放在隊列中,等瀏覽器窗口再次打開時,一瞬間全部執行時

        所以,鑒于這么多但問題,目前一般認為的最佳方案是:用setTimeout模擬setInterval,或者特殊場合直接用requestAnimationFrame

        補充:JS高程中有提到,JS引擎會對setInterval進行優化,如果當前事件隊列中有setInterval的回調,不會重復添加。不過,仍然是有很多問題。。。

        事件循環進階:macrotask與microtask

        這段參考了參考來源中的第2篇文章(英文版的),(加了下自己的理解重新描述了下),
        強烈推薦有英文基礎的同學直接觀看原文,作者描述的很清晰,示例也很不錯,如下:

        https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

        上文中將JS事件循環機制梳理了一遍,在ES5的情況是夠用了,但是在ES6盛行的現在,仍然會遇到一些問題,譬如下面這題:

        console.log('script start');
        
        setTimeout(function() {
            console.log('setTimeout');
        }, 0);
        
        Promise.resolve().then(function() {
            console.log('promise1');
        }).then(function() {
            console.log('promise2');
        });
        
        console.log('script end');

        嗯哼,它的正確執行順序是這樣子的:

        script start
        script end
        promise1
        promise2
        setTimeout

        為什么呢?因為Promise里有了一個一個新的概念:microtask

        或者,進一步,JS中分為兩種任務類型:macrotaskmicrotask,在ECMAScript中,microtask稱為jobs,macrotask可稱為task

        它們的定義?區別?簡單點可以按如下理解:

        • macrotask(又稱之為宏任務),可以理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調并放到執行棧中執行)

          • 每一個task會從頭到尾將這個任務執行完畢,不會執行其它
          • 瀏覽器為了能夠使得JS內部task與DOM任務能夠有序的執行,會在一個task執行結束后,在下一個 task 執行開始前,對頁面進行重新渲染
        (`task->渲染->task->...`)
        
        • microtask(又稱為微任務),可以理解是在當前 task 執行結束后立即執行的任務

          • 也就是說,在當前task任務后,下一個task之前,在渲染之前
          • 所以它的響應速度相比setTimeout(setTimeout是task)會更快,因為無需等渲染
          • 也就是說,在某一個macrotask執行完后,就會將在它執行期間產生的所有microtask都執行完畢(在渲染前)

        分別很么樣的場景會形成macrotask和microtask呢?

        • macrotask:主代碼塊,setTimeout,setInterval等(可以看到,事件隊列中的每一個事件都是一個macrotask)
        • microtask:Promise,process.nextTick等

        __補充:在node環境下,process.nextTick的優先級高于Promise__,也就是可以簡單理解為:在宏任務結束后會先執行微任務隊列中的nextTickQueue部分,然后才會執行微任務中的Promise部分。

        參考:http://www.tvxinternet.com/q/1010000011914016

        再根據線程來理解下:

        • macrotask中的事件都是放在一個事件隊列中的,而這個隊列由事件觸發線程維護
        • microtask中的所有微任務都是添加到微任務隊列(Job Queues)中,等待當前macrotask執行完畢后執行,而這個隊列由JS引擎線程維護

        (這點由自己理解+推測得出,因為它是在主線程下無縫執行的)

        所以,總結下運行機制:

        • 執行一個宏任務(棧中沒有就從事件隊列中獲?。?/li>
        • 執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
        • 宏任務執行完畢后,立即執行當前微任務隊列中的所有微任務(依次執行)
        • 當前宏任務執行完畢,開始檢查渲染,然后GUI線程接管渲染
        • 渲染完畢后,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲?。?/li>

        如圖:

        另外,請注意下Promisepolyfill與官方版本的區別:

        • 官方版本中,是標準的microtask形式
        • polyfill,一般都是通過setTimeout模擬的,所以是macrotask形式
        • 請特別注意這兩點區別

        注意,有一些瀏覽器執行結果不一樣(因為它們可能把microtask當成macrotask來執行了),
        但是為了簡單,這里不描述一些不標準的瀏覽器下的場景(但記住,有些瀏覽器可能并不標準)

        20180126補充:使用MutationObserver實現microtask

        MutationObserver可以用來實現microtask
        (它屬于microtask,優先級小于Promise,
        一般是Promise不支持時才會這樣做)

        它是HTML5中的新特性,作用是:監聽一個DOM變動,
        當DOM對象樹發生任何變動時,Mutation Observer會得到通知

        像以前的Vue源碼中就是利用它來模擬nextTick的,
        具體原理是,創建一個TextNode并監聽內容變化,
        然后要nextTick的時候去改一下這個節點的文本內容,
        如下:(Vue的源碼,未修改)

        var counter = 1
        var observer = new MutationObserver(nextTickHandler)
        var textNode = document.createTextNode(String(counter))
        
        observer.observe(textNode, {
            characterData: true
        })
        timerFunc = () => {
            counter = (counter + 1) % 2
            textNode.data = String(counter)
        }

        對應Vue源碼鏈接

        不過,現在的Vue(2.5+)的nextTick實現移除了MutationObserver的方式(據說是兼容性原因),
        取而代之的是使用MessageChannel
        (當然,默認情況仍然是Promise,不支持才兼容的)。

        MessageChannel屬于宏任務,優先級是:MessageChannel->setTimeout,
        所以Vue(2.5+)內部的nextTick與2.4及之前的實現是不一樣的,需要注意下。

        這里不展開,可以看下https://juejin.im/post/5a1af88f5188254a701ec230

        寫在最后的話

        看到這里,不知道對JS的運行機制是不是更加理解了,從頭到尾梳理,而不是就某一個碎片化知識應該是會更清晰的吧?

        同時,也應該注意到了JS根本就沒有想象的那么簡單,前端的知識也是無窮無盡,層出不窮的概念、N多易忘的知識點、各式各樣的框架、
        底層原理方面也是可以無限的往下深挖,然后你就會發現,你知道的太少了。。。

        另外,本文也打算先告一段落,其它的,如JS詞法解析,可執行上下文以及VO等概念就不繼續在本文中寫了,后續可以考慮另開新的文章。

        最后,喜歡的話,就請給個贊吧!

        附錄

        博客

        初次發布2018.01.21于我個人博客上面

        http://www.dailichun.com/2018/01/21/js_singlethread_eventloop.html

        招聘軟廣

        阿里巴巴釘釘商業化團隊大量hc,高薪股權。機會好,技術成長空間足,業務也有很大的發揮空間!

        還在猶豫什么,來吧?。?!

        社招(P6~P7)

        職責和挑戰

        1. 負責釘釘工作臺。工作臺是幫助企業實現數字化管理和協同的門戶,是擁有億級用戶量的產品。如何保障安全、穩定、性能和體驗是對我們的一大挑戰。
        2. 負責開放能力建設。針對紛繁的業務場景,提供合理的開放方案,既要做到深入用戶場景理解并支撐業務發展,滿足企業千人千面、千行千面的訴求,又要在技術上保障用戶的安全、穩定和體驗。需要既要有技術抽象能力、平臺架構能力,又要有業務的理解和分析能力。
        3. 開放平臺基礎建設。保障鏈路的安全和穩定。同時對如何保障用戶體驗有持續精進的熱情和追求。

        職位要求

        1. 精通HTML5、CSS3、JS(ES5/ES6)等前端開發技術
        2. 掌握主流的JS庫和開發框架,并深入理解其設計原理,例如React,Vue等
        3. 熟悉模塊化、前端編譯和構建工具,例如webpack、babel等
        4. (加分項)了解服務端或native移動應用開發,例如nodejs、Java等
        5. 對技術有強追求,有良好的溝通能力和團隊協同能力,有優秀的分析問題和解決問題的能力。

        前端實習

        面向2021畢業的同學

        1. 本科及以上學歷,計算機相關專業
        2. 熟練掌握HTML5/CSS3/Javascript等web前端技術
        3. 熟悉至少一種常用框架,例如React、vue等
        4. 關注新事物、新技術,有較強的學習能力,有強烈求知欲和進取心
        5. 有半年以上實際項目經驗,大廠加分

        image.png

        image.png

        內推郵箱

        lichun.dlc@alibaba-inc.com

        簡歷發我郵箱,必有回應,符合要求直接走內推?。?!

        一對一服務,有問必答!

        也可加我微信了解更多:a546684355

        參考資料

        查看原文

        撒網要見魚 收藏了文章 · 2018-03-23

        【全文】狼叔:如何正確的學習Node.js

        說明

        2017-12-14 我發了一篇文章《沒用過Node.js,就別瞎逼逼》是因為有人在知乎上黑Node.js。那篇文章的反響還是相當不錯的,甚至連著名的hax賀老都很認同,下班時讀那篇文章,竟然坐車的還坐過站了。大家可以很明顯的感到Node.js的普及度還不夠,還存很多誤解。甚至說很多小白用戶也得不到很好的學習。大神都功成身退,書也跟不上,大部分都是2013年左右的,Node.js版本都是基于v0.10左右的,現在已經v9了。想想也是有點可惜,使用如此廣泛的Node.js被大家默認,卻沒人來科普。

        反思之后,我就想準備一個科普的Live,于是就有了《狼叔:如何正確學習 Node.js?》,相信能夠對很多喜歡Node.js的朋友有所幫助。Live已完成目前1200多人,230人評價,平均4.8+,還算是一個比較成功的Live?,F整理出來,希望對更多朋友有用。

        【全文】狼叔:如何正確的學習Node.js

        Live 簡介

        現在,越來越多的科技公司和開發者開始使用 Node.js 開發各種應用。Node.js除了能夠輔助大前端開發外,還可以編寫Web應用,封裝Api,組裝RPC服務等,甚至是開發VSCode編輯器一樣的PC客戶端。和其它技術相比, Node.js 簡單易學,性能好、部署容易,能夠輕松處理高并發場景下的大量服務器請求。Node.js 周邊的生態也非常強大,NPM(Node包管理)上有超過60萬個模塊,日下超過載量3億次。但編寫 Node.js 代碼對新人和其它語言背景的開發者來說,不是一件容易的事,在入門之前需要弄懂不少復雜的概念。

        我身邊也有很多人問我:如何學習 Node.js ?作為一名 Node.js 布道者,我做過很多 Node.js 普及和推廣的工作,對它的基本概念和核心模塊都很熟悉; 此外,我還在撰寫一本名為《更了不起的 Node.js 》的書,已經寫了 2 年,積累了很豐富的資料,本次 Live 也將為你提供對 Node.js 更全面的解讀。

        本次 Live 主要包括以下內容,目錄

        Part 0 :Node.js簡介

        a)Node.js簡介
        b)什么是Node.js?
        c)基本原理

        Part 1前言:學習 Node.js 的三個境界

        Part 2準備:如何學習Node.js

        • 2.1 Node 用途那么多,我該從哪里學起?
        • 2.2 Node Web 框架那么多,我該怎么選?
        • 2.3 關于 Node 的書幾乎都過時了,我該買哪本?

        Part 3延伸:大前端變化那么快,如何才能做到每日精進?

        Part 4實踐:從招聘角度來看, Node.js 開發需要具備哪些技能?

        Part 5答疑:回答大家的問題

        本次Live主要是科普,適用新用戶和比較迷茫的Node朋友,希望大家多多理解和支持。

        Part 0 :Node.js簡介

        a)Node.js簡介
        b)什么是Node.js?
        c)基本原理

        a)Node.js簡介

        Node.js 誕生于 2009 年,由 Joyent 的員工 Ryan Dahl 開發而成,之后 Joyent 公司一直扮演著 Node.js 孵化者的角色。由于諸多原因,Ryan 在2012年離開社區,隨后在2015年由于 Node 貢獻者對 es6 新特性集成問題的分歧,導致分裂出iojs,并由 iojs 發布1.0、2.0和3.0版本。由于 iojs 的分裂最終促成了2015年Node基金會的成立,并順利發布了4.0版本。Node.js基金會的創始成員包括 Google、Joyent、IBM、Paypal、微軟、Fidelity 和 Linux基金會,創始成員將共同掌管過去由 Joyent 一家企業掌控的 Node.js 開源項目。此后,Node.js基金會發展非常好,穩定的發布5、6、7、8等版本,截止發稿最新版本已經是8.6,長期支持版本是6.11。

        Node.js 不是一門語言也不是框架,它只是基于 Google V8 引擎的 JavaScript 運行時環境,同時結合 Libuv 擴展了 JavaScript 功能,使之支持 io、fs 等只有語言才有的特性,使得 JavaScript 能夠同時具有 DOM 操作(瀏覽器)和 I/O、文件讀寫、操作數據庫(服務器端)等能力,是目前最簡單的全棧式語言。

        早在2007年,Jeff Atwood 就提出了著名的 Atwood定律

        任何能夠用 JavaScript 實現的應用系統,最終都必將用 JavaScript 實現

        目前 Node.js 在大部分領域都占有一席之地,尤其是 I/O 密集型的,比如 Web 開發,微服務,前端構建等。不少大型網站都是使用 Node.js 作為后臺開發語言的,用的最多的就是使用Node.js做前端渲染和架構優化,比如 淘寶 雙十一、去哪兒網 的 PC 端核心業務等。另外,有不少知名的前端庫也是使用 Node.js 開發的,比如,Webpack 是一個強大的打包器,React/Vue 是成熟的前端組件化框架。

        Node.js通常被用來開發低延遲的網絡應用,也就是那些需要在服務器端環境和前端實時收集和交換數據的應用(API、即時聊天、微服務)。阿里巴巴、騰訊、Qunar、百度、PayPal、道瓊斯、沃爾瑪和 LinkedIn 都采用了 Node.js 框架搭建應用。

        另外, Node.js 編寫的包管理器 npm 已成為開源包管理了領域最好的生態,直接到2017年10月份,有模塊超過47萬,每周下載量超過32億次,每個月有超過700萬開發者使用npm。

        當然了,Node.js 也有一些缺點。Node.js 經常被人們吐槽的一點就是:回調太多難于控制(俗稱回調地獄)和 CPU 密集任務處理的不是很好。但是,目前異步流程技術已經取得了非常不錯的進步,從Callback、Promise 到 Async函數,可以輕松的滿足所有開發需求。至于 CPU 密集任務處理并非不可解,方案有很多,比如通過系統底層語言 Rust 來擴展 Node.js,但這樣會比較麻煩。筆者堅信在合適的場景使用合適的東西,尤其是在微服務架構下,一切都是服務,可以做到語言無關。如果大家想使 JavaScript 做 CPU 密集任務,推薦 Node.js 的兄弟項目 fibjs,基于纖程(fiber,可以簡單理解為更輕量級的線程),效率非常高,兼容npm,同時沒有異步回調煩惱。

        b)什么是Node.js?

        按照 Node.js官方網站主頁 的說法:

        Node.js? is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js' package ecosystem, npm, is the largest ecosystem of open source libraries in the world.

        從這段介紹來看,解讀要點如下

        • Node.js 不是 JavaScript 應用,不是語言(JavaScript 是語言),不是像 Rails(Ruby)、 Laravel(PHP) 或 Django(Python) 一樣的框架,也不是像 Nginx 一樣的 Web 服務器。Node.js 是 JavaScript 運行時環境
        • 構建在 Chrome's V8 這個著名的 JavaScript 引擎之上,Chrome V8 引擎以 C/C++ 為主,相當于使用JavaScript 寫法,轉成 C/C++ 調用,大大的降低了學習成本
        • 事件驅動(event-driven),非阻塞 I/O 模型(non-blocking I/O model),簡單點講就是每個函數都是異步的,最后由 Libuv 這個 C/C++ 編寫的事件循環處理庫來處理這些 I/O 操作,隱藏了非阻塞 I/O 的具體細節,簡化并發編程模型,讓你可以輕松的編寫高性能的Web應用,所以它是輕量(lightweight)且高效(efficient)的
        • 使用 npm 作為包管理器,目前 npm 是開源庫里包管理最大的生態,功能強大,截止到2017年12月,模塊數量超過 60 萬+

        大多數人都認為 Node.js 只能寫網站后臺或者前端工具,這其實是不全面的,Node.js的目標是讓并發編程更簡單,主要應用在以網絡編程為主的 I/O 密集型應用。它是開源的,跨平臺,并且高效(尤其是I/O處理),包括IBM、Microsoft、Yahoo、SAP、PayPal、沃爾瑪及GoDaddy都是 Node.js 的用戶。

        c)基本原理

        下面是一張 Node.js 早期的架構圖,來自 Node.js 之父 Ryan Dahl 的演講稿,在今天依然不過時,它簡要的介紹了 Node.js 是基于 Chrome V8引擎構建的,由事件循環(Event Loop)分發 I/O 任務,最終工作線程(Work Thread)將任務丟到線程池(Thread Pool)里去執行,而事件循環只要等待執行結果就可以了。

        14912763353044.png

        核心概念

        • Chrome V8 是 Google 發布的開源 JavaScript 引擎,采用 C/C++ 編寫,在 Google 的 Chrome 瀏覽器中被使用。Chrome V8 引擎可以獨立運行,也可以用來嵌入到 C/C++ 應用程序中執行。
        • Event Loop 事件循環(由 libuv 提供)
        • Thread Pool 線程池(由 libuv 提供)

        梳理一下

        • Chrome V8 是 JavaScript 引擎
        • Node.js 內置 Chrome V8 引擎,所以它使用的 JavaScript 語法
        • JavaScript 語言的一大特點就是單線程,也就是說,同一個時間只能做一件事
        • 單線程就意味著,所有任務需要排隊,前一個任務結束,才會執行后一個任務。如果前一個任務耗時很長,后一個任務就不得不一直等著。
        • 如果排隊是因為計算量大,CPU 忙不過來,倒也算了,但是很多時候 CPU 是閑著的,因為 I/O 很慢,不得不等著結果出來,再往下執行
        • CPU 完全可以不管 I/O 設備,掛起處于等待中的任務,先運行排在后面的任務
        • 將等待中的 I/O 任務放到 Event Loop 里
        • 由 Event Loop 將 I/O 任務放到線程池里
        • 只要有資源,就盡力執行

        我們再換一個維度看一下

        14992384974942.png

        核心

        • Chrome V8 解釋并執行 JavaScript 代碼(這就是為什么瀏覽器能執行 JavaScript 原因)
        • libuv 由事件循環和線程池組成,負責所有 I/O 任務的分發與執行

        在解決并發問題上,異步是最好的解決方案,可以拿排隊和叫號機來理解

        • 排隊:在排隊的時候,你除了等之外什么都干不了
        • 叫號機:你要做的是先取號碼,等輪到你的時候,系統會通知你,這中間,你可以做任何你想做的事兒

        Node.js 其實就是幫我們構建類似的機制。我們在寫代碼的時候,實際上就是取號的過程,由 Event Loop 來接受處理,而真正執行操作的是具體的線程池里的 I/O 任務。之所以說 Node.js 是單線程,就是因為在接受任務的時候是單線程的,它無需進程/線程切換上下文的成本,非常高效,但它在執行具體任務的時候是多線程的。

        Node.js 公開宣稱的目標是 “旨在提供一種簡單的構建可伸縮網絡程序的方法”,毫無疑問,它確實做到了。這種做法將并發編程模型簡化了,Event Loop和具體線程池等細節被 Node.js 封裝了,繼而將異步調用 Api 寫法暴露給開發者。真是福禍相依,一方面簡化了并發編程,另一方面在寫法上埋下了禍根,這種做法的好處是能讓更多人輕而易舉的寫出高性能的程序!

        在Node.js Bindings層做的事兒就是將 Chrome V8 等暴露的 C/C++ 接口轉成JavaScript Api,并且結合這些 Api 編寫了 Node.js 標準庫,所有這些 Api 統稱為 Node.js SDK,后面模塊章節會有更詳細的討論。

        微軟在2016年宣布在MIT許可協議下開放 Chakra 引擎,并以 ChakraCore 為名在 Github 上開放了源代碼,ChakraCore 是一個完整的 JavaScript 虛擬機,它擁有著和 Chakra 幾乎相同的功能與特性。微軟向 Node.js 主分支提交代碼合并請求,讓 Node.js 用上 ChakraCore引擎,即 nodejs/node-chakracore 項目。實際上微軟是通過創建名為 V8 shim 的庫的賦予了 ChakraCore 處理谷歌 Chrome V8 引擎指令的能力,其原理示意圖如下

        15018598977763.jpg

        目前,Node.js 同時支持這2種 JavaScript 引擎,二者性能和特性上各有千秋,ChakraCore 在特性上感覺更潮一些,曾經是第一個支持 Async函數 的引擎,但目前 Node.js 還是以 Chrome V8 引擎為主, ChakraCore 版本需要單獨安裝,大家了解一下就好。

        Part 1前言:學習 Node.js 的三個境界

        我總結的編程3種境界

        • 打日志:console.log
        • 斷點調試:斷點調試:node debugger 或node inspector 或vscode
        • 測試驅動開發(tdd | bdd)

        大家可以自測一下,自己在哪個水平?如果是第三個階段,那么本場Live可能不太適合你。哈哈哈

        Part 2準備:如何學習Node.js

        Node不是語言,不是框架,只是基于V8運行時環境。結合libuv能夠通過js語法獲得更好的等價于c/c++的性能。

        它很簡單,異步是解決并發的最佳實踐。本節主要講如何學習Node.js,是本次Live非常核心的內容,大家要注意聽。

        基礎學習

        1)js語法必須會

        1. js基本語法,都是c語系的,有其他語言背景學習起來相對更簡單
        2. 常見用法,比如正則,比如數據結構,尤其是數組的幾種用法。比如bind/call/apply等等
        3. 面向對象寫法。js是基于對象的,所以它的oo寫起來非常詭異。參見紅皮書JavaScript高級編程,很多框架都是自己實現oo基礎框架,比如ext-core等。

        犀牛書,《JavaScript權威指南》,沒事就多翻翻,看少多少遍都不為過。

        2)個人學習和技術選型都要循序漸進

        1. 先能寫,采用面向過程寫法,簡單理解就是定義一堆function,然后調用,非常簡單
        2. 然后再追求更好的寫法,可以面向對象。對于規?;木幊虂碚f,oo是有它的優勢的,一般java、c#,ruby這些語言里都有面向對象,所以后端更習慣,但對于語言經驗不那么強的前端來說算高級技巧。
        3. 等oo玩膩了,可以有更好的追求:函數式編程,無論編程思維,還是用法上都對已有的編程思維是個挑戰。我很喜歡函數式,但不太會在團隊里使用,畢竟oo階段還沒完全掌握,風險會比較大。但如果團隊水平都非常高了,團隊穩定是可以用的。

        可以看出我的思路,先能寫,然后再追求更好的寫法,比如面向對象。等團隊水平到一定程度了,并且穩定的時候,可以考慮更加極致的函數式寫法。

        團隊是這樣選型的,個人學習也這樣,最好是循序漸進,步子邁大了不好。

        3)各種高級的JavaScript友好語言

        JavaScript友好語言指的是能夠使用其他語法實現,但最終編譯成js的語言。自從Node.js出現后,這種黑科技層出不窮。比如比較有名的coffee、typescript、babel(es)等。

        CoffeeScript雖然也是JavaScript友好語言,但其語法借鑒ruby,崇尚極簡,對于類型和OO機制上還是偏弱,而且這么多年也沒發展起來,仍然是比較小眾的活著。未來比例會越來越少的。

        顯然TypeScript會越來越好,TypeScript 的強大之處是要用過才知道的。

        • 1)規?;幊?,像Java那種,靜態類型,面向對象,前端只有TypeScript能做到
        • 2)親爹是微軟安德斯·海爾斯伯格,不知道此人的請看borland傳奇去
        • 3)開源,未來很好
        • 4)組合拳:TypeScript + VSCode = 神器

        當下前端發展速度極快,以指數級的曲線增長。以前可能1年都不一定有一項新技術,現在可能每個月都有。大前端,Node全棧,架構演進等等都在快速變化??梢哉f,前端越復雜,有越多的不確定性,TypeScript的機會就越大。

        4)再論面向對象

        面向對象想用好也不容易的,而且js里有各種實現,真是讓人眼花繚亂。

        • 基于原型的寫法,縱觀JavaScript高級編程,就是翻來覆去的講這個,這個很基礎,但不好是很好用??梢圆挥?,但不可以不會。
        • 自己寫面向對象機制是最好的,但不是每個人都有這個能力的。好在es6規范出了更好一點的面向對象,通過class、extends、super關鍵字來定義類,已經明顯好很多了,雖然還很弱,但起碼勉強能用起來了。從面向過程走過來的同學,推薦這種寫法,簡單易用。但要注意面向對象要有面向對象的寫法,要理解抽象,繼承,封裝,多態4個基本特征。如果想用好,你甚至還需要看一些設計模式相關的書。好在有《JavaScript設計模式》一書。Koa2里已經在用這種寫法了。
        • js是腳本語言,解釋即可執行。所以它的最大缺點是沒有類型系統,這在規?;幊汤锸欠浅NkU的,一個函數,傳參就能玩死人。于是現在流行使用flow和typescript來做類型校驗。flow只是工具,比較輕量級。而typescript是es6超級,給es6補充了類型系統和更完善的面向對象機制,所以大部分人都會對ts有好感,很有可能是未來的趨勢。

        對于es6高級特性,我是比較保守的,一般node長期支持版本lts支持的我都讓用,一些更新的特性我一般不讓使用。根本lts版本保持一致就好。

        我的團隊現在是采用es6的面向對象寫法開發,后面會一點一點轉到typescript上的。熟練oo轉到ts是非常容易的。

        安裝Node.js環境

        3m安裝法

        • nvm(node version manager)【需要使用npm安裝,替代品是yrm(支持yarn),nvs對window支持很好】
        • nrm(node registry manager)【需要使用npm安裝,替代品是yrm(支持yarn)】
        • npm(node packages manager)【內置,替代品是n或nvs(對win也支持)】

        nvm

        node版本發布非???,而且多版本共存可能性較大,推薦使用nvm來安裝node

        $ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
        
        $ echo 'export NVM_DIR="$HOME/.nvm"' >> ~/.zshrc
        $ echo '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm' >> ~/.zshrc
        $ source ~/.zshrc
        
        $ nvm install 0.10
        $ nvm install 4
        $ nvm install 6
        $ nvm install 8

        nrm

        https://registry.npmjs.com 是node官方的源(registry),服務器在國外,下載速度較慢,推薦安裝nrm來切換源,國內的cnpm和taobao的源都非???,當然,如果你想自建源也是支持的。

        $ npm install --global nrm --registry=https://registry.npm.taobao.org
        $ nrm use cnpm

        npm

        nrm切換完源之后,你安裝npm模塊的速度會更快。

        $ npm install --global yarn

        npm基本命令

        名稱描述簡寫
        npm install xxx安裝xxx模塊,但不記錄到package.json里npm i xxx
        npm install --save xxx安裝xxx模塊,并且記錄到package.json里,字段對應的dependency,是產品環境必須依賴的模塊npm i -s xxx
        npm install --save-de xxx安裝xxx模塊,并且記錄到package.json里,字段對應的dev-dependency,是開發環境必須依賴的模塊,比如測試類的(mocha、chai、sinon、zombie、supertest等)都在npm i -D xxx
        npm install --global xxx全局安裝xxx模塊,但不記錄到package.json里,如果模塊里package.json有bin配置,會自動鏈接,作為cli命令npm i -g xxx

        常用軟件

        • 1)oh my zsh是我最習慣的shell,終端下非常好用

        配合iterm2分屏 + spectacle全屏,幾乎無敵

        • 2)brew是mac裝軟件非常好的方式,和apt-get、rpm等都非常類似

        安裝4個必備軟件

        • brew install git 最流行的SCM源碼版本控制軟件
        • brew install wget 下載、扒站神器
        • brew install ack 搜索代碼神器
        • brew install autojump 終端下多目錄跳轉神器
        • 3)vim

        我雖然不算vim黨,但也深愛著。janus是一個非常好用的vim集成開發環境。比如ctrl-p、nerdtree等插件都集成了,對我這種懶人足夠了。

        IDE和編輯器

        關于Node.js的IDE和編輯器有很多選擇,對比如下

        名稱是否收費斷點調試功能
        Webstorm收費支持是IDE,在代碼提示、重構等方面功能非常強大,支持的各種語言、框架、模板也非常多,支持斷點調試,好處是特別智能,缺點也是特別智能
        Sublime/TextMate收費不支持編輯器里非常好用的,textmate主要針對mac用戶,sublime是跨平臺的,相信很多前端開發都熟悉
        Vim/Emace免費不支持命令行下的編輯器,非常強大,難度也稍大,但更為酷炫,而且對于服務器部署開發來說是值得一學的
        VSCode/Atom免費支持Atom比較早,功能強大,缺點稍卡頓,VSCode是微軟出的,速度快,對于Node.js 調試,重構,代碼提示等方面支持都非常好

        Visual Studio Code是一個運行于 Mac、Windows和 Linux 之上的,針對于編寫現代 Web 和云應用的跨平臺源代碼編輯器。它功能強大,便于調試,加上它本身也是基于 Node.js 模塊 electron 構建的,尤其要推薦大家使用。

        Visual Studio Code(以下簡稱vsc)

        • vsc是一個比較潮比較新的編輯器(跨平臺Mac OS X、Windows和 Linux )
        • vsc功能和textmate、sublime、notepad++,ultraedit等比較,毫不遜色
        • vsc尤其是在nodejs(調試)和typescript、go上支持尤其好
        • vsc提供了自定義 Debugger Adapter 和 VSCode Debug Protocol 從而實現自己的調試器

        值得一學,我推薦VSCode編輯器!

        更多調試方法,參見https://github.com/i5ting/nod...

        Node.js應用場景

        《Node.js in action》一書里說,Node.js 所針對的應用程序有一個專門的簡稱:DIRT。它表示數據密集型實時(data-intensive real-time)程序。因為 Node.js 自身在 I/O 上非常輕量,它善于將數據從一個管道混排或代理到另一個管道上,這能在處理大量請求時持有很多開放的連接,并且只占用一小部分內存。它的設計目標是保證響應能力,跟瀏覽器一樣。

        這話不假,但在今天來看,DIRT 還是范圍小了。其實 DIRT 本質上說的 I/O 處理的都算,但隨著大前端的發展,Node.js 已經不再只是 I/O 處理相關,而是更加的“Node”!

        Node.js 使用場景主要分為4大類

        屏幕快照 2017-05-17 07.25.05.png

        • 1)跨平臺:覆蓋你能想到的面向用戶的所有平臺,傳統的PC Web端,以及PC客戶端 nw.js/electron 、移動端 cordova、HTML5、react-native、weex,硬件 ruff.io
        • 2)Web應用開發:網站、Api、RPC服務等
        • 3)前端:三大框架 React VueAngular 輔助開發,以及工程化演進過程(使用Gulp /Webpack 構建 Web 開發工具)
        • 4)工具:npm上各種工具模塊,包括各種前端預編譯、構建工具 Grunt / Gulp、腳手架,命令行工具,各種奇技淫巧等

        下面列出具體的 Node.js 的使用場景,以模塊維度劃分

        分類描述相關模塊
        網站類似于 cnodejs.org 這樣傳統的網站Express / Koa
        Api同時提供給移動端,PC,H5 等前端使用的 HTTP Api 接口Restify / HApi
        Api代理為前端提供的,主要對后端Api接口進行再處理,以便更多的適應前端開發Express / Koa
        IM即時聊天實時應用,很多是基于 WebSocket協議的Socket.io / sockjs
        反向代理提供類似于 nginx 反向代理功能,但對前端更友好anyproxy / node-http-proxy / hiproxy
        前端構建工具輔助前端開發,尤其是各種預編譯,構建相關的工具,能夠極大的提高前端開發效率Grunt / Gulp / Bower / Webpack / Fis3 / YKit
        命令行工具使用命令行是非??岬姆绞?,前端開發自定義了很多相關工具,無論是shell命令,node腳本,還是各種腳手架等,幾乎每個公司小組都會自己的命令行工具集Cordova / Shell.js
        操作系統有實現,但估計不太會有人用NodeOS
        跨平臺打包工具使用 Web 開發技術開發PC客戶端是目前最流行的方式,會有更多前端開發工具是采用這種方式的PC端的electron、nw.js,比如釘釘PC客戶端、微信小程序IDE、微信客戶端,移動的Cordova,即老的Phonegap,還有更加有名的一站式開發框架Ionicframework
        P2P區塊鏈開發、BT客戶端webtorrent / ipfs
        編輯器AtomVSCode 都是基于 electron 模塊的electron
        物聯網與硬件ruff.io和很多硬件都支持node sdkruff

        Node.js 應用場景非常豐富,比如 Node.js 可以開發操作系統,但一般我都不講的,就算說了也沒多大意義,難道大家真的會用嗎?一般,我習慣將 Node.js 應用場景氛圍7個部分。

        1)初衷,server端,不想成了前端開發的基礎設施
        2)命令行輔助工具,甚至可以是運維
        3)移動端:cordova,pc端:nw.js和electron
        4)組件化,構建,代理
        5)架構,前后端分離、api proxy
        6)性能優化、反爬蟲與爬蟲
        7) 全棧最便捷之路

        編號場景說明
        1反向代理Node.js可以作為nginx這樣的反向代理,雖然線上我們很少這樣做,但它確確實實可以這樣做。比如node-http-proxy和anyproxy等,其實使用Node.js做這種請求轉發是非常簡單的,在后面的http章節里,有單獨的講解。
        2爬蟲有大量的爬蟲模塊,比如node-crawler等,寫起來比python要簡單一些,尤其搭配jsdom(node版本的jQuery)類庫的,對前端來說尤其友好
        3命令行工具所有輔助開發,運維,提高效率等等可以用cli做的,使用node來開發都非常合適,是編寫命令行工具最簡單的方式,java8以后也參考了node的命令行實現
        4微服務與RPCnode里有各種rpc支持,比如node編寫的dnode,seneca,也有跨語言支持的grpc,足夠應用了
        5微信公眾號開發相關sdk,框架非常多,是快速開發的利器
        6前端流行SSR && PWASSR是服務器端渲染,PWA是漸進式Web應用,都是今年最火的技術。如果大家用過,一定對Node.js不陌生。比如React、Vuejs都是Node.js實現的ssr。至于pwa的service-worker也是Node.js實現的。那么為啥不用其他語言實現呢?不是其他語言不能實現,而是使用Node.js簡單、方便、學習成本低,輕松獲得高性能,如果用其他語言,我至少還得裝環境

        可以說目前大家能夠看到的、用到的軟件都有 Node.js 身影,當下最流行的軟件寫法也大都是基于 Node.js 的,比如 PC 客戶端 luin/medis 采用 electron 打包,寫法采用 React + Redux。我自己一直的實踐的【Node全?!?,也正是基于這種趨勢而形成的。在未來,Node.js 的應用場景會更加的廣泛,更多參見 sindresorhus/awesome-nodejs。

        Node核心:異步流程控制

        Node.js是為異步而生的,它自己把復雜的事兒做了(高并發,低延時),交給用戶的只是有點難用的Callback寫法。也正是坦誠的將異步回調暴露出來,才有更好的流程控制方面的演進。也正是這些演進,讓Node.js從DIRT(數據敏感實時應用)擴展到更多的應用場景,今天的Node.js已經不只是能寫后端的JavaScript,已經涵蓋了所有涉及到開發的各個方面,而Node全棧更是熱門種的熱門。

        直面問題才能有更好的解決方式,Node.js的異步是整個學習Node.js過程中重中之重。

        • 1) 異步流程控制學習重點
        • 2)Api寫法:Error-first Callback 和 EventEmitter
        • 3)中流砥柱:Promise
        • 4)終極解決方案:Async/Await

        1) 異步流程控制學習重點

        我整理了一張圖,更直觀一些。從09年到現在,8年多的時間里,整個Node.js社區做了大量嘗試,其中曲折足足夠寫一本書的了。大家先簡單了解一下。

        Screen Shot 2017-04-05 at 08.43.08.png

        • 紅色代表Promise,是使用最多的,無論async還是generator都可用
        • 藍色是Generator,過度貨
        • 綠色是Async函數,趨勢

        結論:Promise是必須會的,那你為什么不順勢而為呢?

        推薦:使用Async函數 + Promise組合,如下圖所示。

        其實,一般使用是不需要掌握上圖中的所有技術的。對于初學者來說,先夠用,再去深究細節。所以,精簡一下,只了解3個就足夠足夠用了。

        Screen Shot 2017-04-05 at 08.43.34.png

        結論

        1. Node.js SDK里callback寫法必須會的。
        2. Node.js學習重點: Async函數與Promise

          1. 中流砥柱:Promise
          2. 終極解決方案:Async/Await

        所以下面我們會分個小部分進行講解。

        2)Api寫法:Error-first Callback 和 EventEmitter

        a)Error-first Callback
        定義錯誤優先的回調寫法只需要注意2條規則即可:

        • 回調函數的第一個參數返回的error對象,如果error發生了,它會作為第一個err參數返回,如果沒有,一般做法是返回null。
        • 回調函數的第二個參數返回的是任何成功響應的結果數據。如果結果正常,沒有error發生,err會被設置為null,并在第二個參數就出返回成功結果數據。

        下面讓我們看一下調用函數示例,Node.js 文檔里最常采用下面這樣的回調方式:

        function(err, res) {
          // process the error and result
        }

        這里的 callback 指的是帶有2個參數的函數:"err"和 "res"。語義上講,非空的“err”相當于程序異常;而空的“err”相當于可以正常返回結果“res”,無任何異常。

        b)EventEmitter

        事件模塊是 Node.js 內置的對觀察者模式“發布/訂閱”(publish/subscribe)的實現,通過EventEmitter屬性,提供了一個構造函數。該構造函數的實例具有 on 方法,可以用來監聽指定事件,并觸發回調函數。任意對象都可以發布指定事件,被 EventEmitter 實例的 on 方法監聽到。

        在node 6之后,可以直接使用require('events')

        var EventEmitter = require('events')
        var util = require('util')
        
        var MyEmitter = function () {
         
        }
        
        util.inherits(MyEmitter, EventEmitter)
        
        const myEmitter = new MyEmitter();
        
        myEmitter.on('event', (a, b) => {
          console.log(a, b, this);
            // Prints: a b {}
        });
        
        myEmitter.emit('event', 'a', 'b');

        和jquery、vue里的Event是非常類似的。而且前端自己也有EventEmitter。

        c)如何更好的查Node.js文檔

        API是應用程序接口Application Programming Interface的簡稱。從Node.js異步原理,我們可以知道,核心在于 Node.js SDK 中API調用,然后交由EventLoop(Libuv)去執行,所以我們一定要熟悉Node.js的API操作。

        Node.js的API都是異步的,同步的函數是奢求,要查API文檔,在高并發場景下慎用。

        筆者推薦使用 DashZeal 查看離線文檔,經常查看離線文檔,對Api理解會深入很多,比IDE輔助要好,可以有效避免離開IDE就不會寫代碼的窘境。

        api.png

        3)中流砥柱:Promise

        回調地獄

        Node.js 因為采用了錯誤優先的回調風格寫法,導致sdk里導出都是回調函數。如果組合調用的話,就會特別痛苦,經常會出現回調里嵌套回調的問題,大家都非常厭煩這種寫法,稱之為Callback Hell,即回調地獄。一個經典的例子來自著名的Promise模塊q文檔里。

        step1(function (value1) {
            step2(value1, function(value2) {
                step3(value2, function(value3) {
                    step4(value3, function(value4) {
                        // Do something with value4
                    });
                });
            });
        });

        這里只是做4步,嵌套了4層回調,如果更多步驟呢?很多新手淺嘗輒止,到這兒就望而卻步,粉轉黑。這明顯不夠成熟,最起碼你要看看它的應對解決方案吧!

        Node.js 約定所有Api都采用錯誤優先的回調方式,這部分場景都是大家直接調用接口,無太多變化。而Promise是對回調地獄的思考,或者說是改良方案。目前使用非常普遍,可以說是在async函數普及之前唯一一個通用性規范,甚至 Node.js 社區都在考慮 Promise 化,可見其影響之大。

        Promise最早也是在commonjs社區提出來的,當時提出了很多規范。比較接受的是promise/A規范。后來人們在這個基礎上,提出了promise/A+規范,也就是實際上現在的業內推行的規范。ES6 也是采用的這種規范。

        Promise意味著[許愿|承諾]一個還沒有完成的操作,但在未來會完成的。與Promise最主要的交互方法是通過將函數傳入它的then方法從而獲取得Promise最終的值或Promise最終最拒絕(reject)的原因。要點有三個:

        • 遞歸,每個異步操作返回的都是promise對象
        • 狀態機:三種狀態轉換,只在promise對象內部可以控制,外部不能改變狀態
        • 全局異常處理

        1)定義

        var promise = new Promise(function(resolve, reject) {
          // do a thing, possibly async, then…
        
          if (/* everything turned out fine */) {
            resolve("Stuff worked!");
          }
          else {
            reject(Error("It broke"));
          }
        });

        每個Promise定義都是一樣的,在構造函數里傳入一個匿名函數,參數是resolve和reject,分別代表成功和失敗時候的處理。

        2)調用

        promise.then(function(text){
            console.log(text)// Stuff worked!
            return Promise.reject(new Error('我是故意的'))
        }).catch(function(err){
            console.log(err)
        })

        它的主要交互方式是通過then函數,如果Promise成功執行resolve了,那么它就會將resolve的值傳給最近的then函數,作為它的then函數的參數。如果出錯reject,那就交給catch來捕獲異常就好了。

        Promise 的最大優勢是標準化,各類異步工具庫都按照統一規范實現,即使是async函數也可以無縫集成。所以用 Promise 封裝 API 通用性強,用起來簡單,學習成本低。在async函數普及之前,絕大部分應用都是采用Promise來做異步流程控制的,所以掌握Promise是Node.js學習過程中必須要掌握的重中之重。

        Bluebird是 Node.js 世界里性能最好的Promise/a+規范的實現模塊,Api非常齊全,功能強大,是原生Promise外的不二選擇。

        好處如下:

        • 避免Node.js內置Promise實現 問題,使用與所有版本兼容
        • 避免Node.js 4曾經出現的內存泄露問題
        • 內置更多擴展,timeout、 promisifyAll等,對Promise/A+規范提供了強有力的補充

        限于時間關系,這里就不一一列舉了,還是那句話,在學習Node.js過程中,對于Promise了解多深入都不過分。

        推薦學習資料

        4)終極解決方案:Async/Await

        Async/Await是異步操作的終極解決方案,Koa 2在node 7.6發布之后,立馬發布了正式版本,并且推薦使用async函數來編寫Koa中間件。

        這里給出一段Koa 2應用里的一段代碼

        exports.list = async (ctx, next) => {
          try {
            let students = await Student.getAllAsync();
          
            await ctx.render('students/index', {
              students : students
            })
          } catch (err) {
            return ctx.api_error(err);
          }
        };

        它做了3件事兒

        • 通過await Student.getAllAsync();來獲取所有的students信息。
        • 通過await ctx.render渲染頁面
        • 由于是同步代碼,使用try/catch做的異常處理

        是不是非常簡單,現在Eggjs里也都是這樣同步的代碼。

        4.1 正常寫法

        const pkgConf = require('pkg-conf');
        
        async function main(){
            const config = await pkgConf('unicorn');
        
            console.log(config.rainbow);
            //=> true
        }
        
        main();

        變態寫法

        const pkgConf = require('pkg-conf');
        
        (async () => {
            const config = await pkgConf('unicorn');
        
            console.log(config.rainbow);
            //=> true
        })();

        4.2 await + Promise

        const Promise = require('bluebird');
        const fs = Promise.promisifyAll(require("fs"));
        
        async function main(){
            const contents = await fs.readFileAsync("myfile.js", "utf8")
            console.log(contents);
        }
        
        main();

        4.3 await + co + generator

        const co = require('co');
        const Promise = require('bluebird');
        const fs = Promise.promisifyAll(require("fs"));
        
        async function main(){
           const contents = co(function* () {
              var result = yield fs.readFileAsync("myfile.js", "utf8")
              return result;
           })
        
            console.log(contents);
        }
        
        main();

        要點

        • co的返回值是promise,所以await可以直接接co。
        • co的參數是genrator
        • 在generator里可以使用yield,而yield后面接的有5種可能,故而把這些可以yield接的方式成為yieldable,即可以yield接的。

          • Promises
          • Thunks (functions)
          • array (parallel execution)
          • objects (parallel execution)
          • Generators 和 GeneratorFunctions

        由上面3中基本用法可以推出Async函數要點如下:

        • Async函數語義上非常好
        • Async不需要執行器,它本身具備執行能力,不像Generator需要co模塊
        • Async函數的異常處理采用try/catch和Promise的錯誤處理,非常強大
        • Await接Promise,Promise自身就足夠應對所有流程了,包括async函數沒有純并行處理機制,也可以采用Promise里的all和race來補齊
        • Await釋放Promise的組合能力,外加co和Promise的then,幾乎沒有不支持的場景

        綜上所述

        • Async函數是趨勢,如果Chrome 52. v8 5.1已經支持Async函數(https://github.com/nodejs/CTC...,Node.js支持還會遠么?
        • Async和Generator函數里都支持promise,所以promise是必須會的。
        • Generator和yield異常強大,不過不會成為主流,所以學會基本用法和promise就好了,沒必要所有的都必須會。
        • co作為Generator執行器是不錯的,它更好的是當做Promise 包裝器,通過Generator支持yieldable,最后返回Promise,是不是有點無恥?

        小結

        這部分共講了4個小點,都是極其直接的必須掌握的知識點。

        • 1) 異步流程控制學習重點
        • 2)Api寫法:Error-first Callback 和 EventEmitter
        • 3)中流砥柱:Promise
        • 4)終極解決方案:Async/Await

        這里再提一下關于Node.js源碼閱讀問題,很多人api都還沒完熟練就去閱讀源碼,這是非常不贊成的,不帶著問題去讀源碼是比較容易迷失在大量代碼中的。效果并不好。

        先用明白,然后再去閱讀Node.js源碼,然后探尋libuv并發機制。很多人買了樸大的《深入淺出Node.js》一書,看了之后還是不太會用,不是書寫的不好,而是步驟不對。

        • Node in action和了不起的Node.js是入門的絕好書籍,非常簡單,各個部分都講了,但不深入,看了之后,基本就能用起來了
        • 當你用了一段之后,你會對Node.js的運行機制好奇,為啥呢?這時候去讀樸大的《深入淺出Node.js》一書就能夠解惑。原因很簡單,九淺一深一書是偏向底層實現原理的書,從操作系統,并發原理,node源碼層層解讀。如果是新手讀,難免會比較郁悶。
        • 實踐類的可以看看雷宗民(老雷)和趙坤(nswbmw)寫的書

        我一般給大家的推薦是把Node in action讀上5遍10遍,入門干活足夠了。剩下的就是反復實踐,多寫代碼和npm模塊就好。

        目前所有的書籍幾乎都有點過時了,大部分都是Node.js v0.10左右的版本的,我得新書是基于Node.js 8版本的,預計2018年3月或4月出版。別催我,真沒法更快了。

        目錄

        博文視點的美女編輯在苦逼的整理中,預計出版在3月之后(不要催我,我也沒法說),20章,800頁+,定價預計在130+。

        Web編程要點

        一般,后端開發指的是 Web 應用開發中和視圖渲染無關的部分,主要是和數據庫交互為主的重業務型邏輯處理。但現在架構升級后,Node.js 承擔了前后端分離重任之后,有了更多玩法。從帶視圖的傳統Web應用面向Api接口應用,到通過 RPC 調用封裝對數據庫的操作,到提供前端 Api 代理和網關,服務組裝等,統稱為后端開發,不再是以往只有和數據庫打交道的部分才算后端。這樣,就可以讓前端工程師對開發過程可控,更好的進行調優和性能優化。

        對 Node.js 來說,一直沒有在后端取得其合理的占有率,原因是多方面的,暫列幾條。

        • 1)利益分配,已有實現大多是Java或者其他語言,基本是沒法撼動的,重寫的成本是巨大的,另外,如果用Node寫了,那么那些寫Java的人怎么辦?搶人飯碗,這是要拼命的。
        • 2)Node相對年輕,大家對Node的理解不夠,回調和異步流程控制略麻煩,很多架構師都不愿意花時間去學習。盡管在Web應用部分處理起來非常簡單高效,但在遇到問題時并不容易排查定位,對開發者水平要求略高。
        • 3)開發者技能單一,很多是從前端轉過來的,對數據庫,架構方面知識欠缺,對系統設計也知之不多,這是很危險的,有種麻桿打狼兩頭害怕的感覺。
        • 4)Node在科普、培訓、布道等方面做的并不好,國外使用的非常多,國內卻很少人知道,不如某些語言做得好。

        盡管如此,Node.js 還是盡人皆知,卷入各種是非風口,也算是在大前端浪潮中大紅大紫。原因它的定位非常明確,補足以 JavaScript 為核心的全棧體系中服務器部分。開發也是人,能夠同時掌握并精通多門語言的人畢竟不多,而且程序員的美德是“懶”,能使用 JavaScript 一門語言完成所有事兒,為什么要學更多呢?

        對于 Web 應用大致分2種,帶視圖的傳統Web應用面向Api接口應用,我們先看一下 Node.js Web 應用開發框架的演進時間線大致如下:

        • 2010年 TJ Holowaychuk 寫的 Express
        • 2011年 Derby.js 開始開發,8月5日,WalmartLabs 的一位成員 Eran Hammer 提交了 Hapi 的第一次git記錄。Hapi 原本是 Postmile 的一部分,并且最開始是基于 Express 構建的。后來它發展成自己自己的框架,
        • 2012年1月21日,專注于 Rest api 的 Restify 發布1.0版本,同構的 Meteor 開始投入開發,最像Rails 的 Sails 也開始了開發
        • 2013年 TJ Holowaychuk 開始玩 es6 generator,編寫 co 這個 Generator 執行器,并開始了Koa 項目。2013 年下半年李成銀開始 ThinkJS,參考 ThinkPHP
        • 2014年4月9日,Express 發布4.0,進入4.x時代持續到今天,MEAN.js 開始隨著 MEAN 架構的提出開始開發,意圖大一統,另外 Total.js 開始起步,最像PHP里 Laravel 或 Python 里的 Django 或 ASP.NET MVC的框架,代表著 Node.js 的成熟,開始從其他語言里的成熟框架借鑒
        • 2015年8月22日,下一代 Web 框架 Koa 發布1.0,可以在Node.js v0.12下面,通過co 和 generator實現同步邏輯,那時候 co 還是基于 thunkfy 的,在2015年10月30日,ThinkJS發布了首個基于 Es2015+ 特性開發的 v2.0 版本
        • 2016 年 09 月,螞蟻金服的 Eggjs,在 JSConf China 2016 上亮相并宣布開源
        • 2017年2月,下一代Web框架 Koa 發布v2.0正式版

        我們可以根據框架的特性進行分類

        框架名稱特性點評
        Express簡單、實用,路由中間件等五臟俱全最著名的Web框架
        Derby.js && Meteor同構前后端都放到一起,模糊了開發便捷,看上去更簡單,實際上上對開發來說要求更高
        Sails、Total面向其他語言,Ruby、PHP等借鑒業界優秀實現,也是 Node.js 成熟的一個標志
        MEAN.js面向架構類似于腳手架,又期望同構,結果只是蹭了熱點
        Hapi和Restfy面向Api && 微服務移動互聯網時代Api的作用被放大,故而獨立分類。尤其是對于微服務開發更是利器
        ThinkJS面向新特性借鑒ThinkPHP,并慢慢走出自己的一條路,對于Async函數等新特性支持,無出其右,新版v3.0是基于Koa v2.0的作為內核的
        Koa專注于異步流程改進下一代Web框架
        Egg基于Koa,在開發上有極大便利企業級Web開發框架

        對于框架選型

        • 業務場景、特點,不必為了什么而什么,避免本末倒置
        • 自身團隊能力、喜好,有時候技術選型決定團隊氛圍的,需要平衡激進與穩定
        • 出現問題的時候,有人能夠做到源碼級定制。Node.js 已經有8年歷史,但模塊完善程度良莠不齊,如果不慎踩到一個坑里,需要團隊在無外力的情況能夠搞定,否則會影響進度
        Tips:個人學習求新,企業架構求穩,無非喜好與場景而已

        Node.js 本來就為了做后端而設計的,這里我們再看看利益問題。Node.js 向后端延伸,必然會觸動后端開發的利益。那么 Proxy 層的事兒,前后端矛盾的交界處,后端不想變,前端又求變,那么長此以往,Api接口會變得越來越惡心。后端是愿意把Api的事兒叫前端的,對后端來說,只要你不動我的數據庫和服務就可以。

        但是 Node.js 能不能做這部分呢?答案是能的,這個是和 Java、PHP 類似的,一般是和數據庫連接到一起,處理帶有業務邏輯的。目前國內大部分都是以 Java、PHP 等為主,所以要想吃到這部分并不容易。

        • 小公司,創業公司,新孵化的項目更傾向于 Node.js ,簡單,快速,高效
        • 微服務架構下的某些服務,使用 Node.js 開發,是比較合理的

        國內這部分一直沒有做的很好,所以 Node.js 在大公司還沒有很好的被應用,安全問題、生態問題、歷史遺留問題等,還有很多人對 Node.js 的誤解。

        • 單線程很脆弱,這是事實,但單線程不等于不能多核并發,而且你還有集群呢
        • 運維,其實很簡單,比其他語言之簡單,日志采集、監控也非常簡單
        • 模塊穩定性,對于 MongoDB、MySQL、Redis 等還是相當不錯,但其他的數據庫支持可能沒那么好。
        • 安全問題是個偽命題,所有框架面臨的都是一樣的。

        這些對于提供Api服務來說已經足夠了,本書后面有大量篇幅講如何使用 Koa 框架來構建Api服務。

        Web編程核心

        • 異步流程控制(前面講過了)
        • 基本框架 Koa或Express,新手推薦Express,畢竟資料多,上手更容易。如果有一定經驗,推薦Koa,其實這些都是為了了解Web編程原理,尤其是中間件機制理解。
        • 數據庫 mongodb或mysql都行,mongoose和Sequelize、bookshelf,TypeOrm等都非常不錯。對于事物,不是Node.js的鍋,是你選的數據庫的問題。另外一些偏門,想node連sqlserver等估計還不成熟,我是不會這樣用的。
        • 模板引擎, ejs,jade,nunjucks。理解原理最好。尤其是extend,include等高級用法,理解布局,復用的好處。其實前后端思路都是一樣的。

        迷茫時學習Node.js最好的方法

        Node.js 編寫的包管理器 npm 已成為開源包管理了領域最好的生態,直接到2017年10月份,有模塊超過47萬,每周下載量超過32億次,每個月有超過700萬開發者使用npm?,F在早已經超過60萬個模塊了。

        這里就不一一舉例了,給出一個迷茫時學習Node.js最好的方法吧!

        某天,我在3w咖啡整理書稿,然后小弟梁過來了,聊聊他的現狀,一副很不好的樣子,在天津我曾帶過他大半年,總不能不管,我給他的建議是:“每天看10個npm模塊”

        對于學習Node.js迷茫的人來說,這是最好的方式,當你不知道如何做的時候,就要向前(錢)看,你要知道積累哪些技能對以后有好處。對于學習Node.js必經之路,一定是要掌握很多模塊用法,并從中汲取技巧、思路、設計思想的。與其不知道學什么,為什么不每天積累幾個技巧呢?

        推薦一個repo即 https://github.com/parro-it/a... 小型庫集合,一天看十個不是夢!

        更多討論 https://zhuanlan.zhihu.com/p/...

        非科班出身如何Node.js

        有朋友提問

        狼叔,關注你和cnode很久了,最近有點迷茫,想請你指點下。
        我的情況是這樣的,非科班出身,從事前端工作4年,公司使用的技術棧是vue2、vue-router、vuex、webpack,目前的能力處理工作還是比較輕松,但是也很明確自己有很多不足,只是對于如何提升比較迷茫。
        不足:
        1、非科班出身,計算機基礎薄弱
        2、對當前使用的技術了解不夠深入,很多東西只停留在會用的層面
        3、對服務端了解較少,想學node,卻不知道如何系統的學習

        解答困惑:

        1、計算機基礎薄弱該如何完善自己的知識體系?

        答:追逐長尾,所見所聞不懂的都去學就好啦。我是這樣過來的,頭幾年每天14個小時+,很累,不過效果還可以。os,算法,數據結構,設計模式,編譯原理,基本也就這些重點。做到每天都有進步就好,別貪多求快。數學和英文當然也是越狠越好的!

        2、如何在技術上做更深入的探索?

        答:技術人只關注技術,想法創意通常比較少。最簡單的辦法就是抓自己的癢,比我大學時和朋友們翻譯過grails文檔,所以對翻譯有情節。為了翻譯,我用node寫了無數工具嘗試,反復對比各種翻譯工具,理解它們背后的設計。包括markdown里嵌html標簽標識中英文,然后gulp編譯成獨立文檔。甚至一度想上線賣服務。這種折騰真的很爽,甚至耽誤了不少翻譯。有時要警惕長尾,不要忘了自己的初衷

        3、如何系統的學習node?

        答:階段

        1/要會用,能完成工作任務
        2/寫點提高效率的工具
        3/參與開源項目,甚至是node源碼

        應對方法

        1/《node in action》看五遍,然后就去寫吧,別管代碼質量如何,能寫敢寫
        2/多用些模塊,理解它們,如果有機會就自己寫一下,萬一有很多人用你,我小弟寫過一個地區選擇加載的json數據,star數不少呢
        3/給別人貢獻代碼,要去學別人的習慣,網上有git標準工作流和提pr方法,你要做的是精研該模塊代碼,關注issue,其他就是等機會。另外樸靈的深入淺出多讀幾遍,試著讀node源碼,你的理解會更好。推薦看看我寫的《通過開源項目去學習》https://github.com/i5ting/Stu...
        4/跳出node范圍,重新審視node的應用場景,對未來你的技術選項和決策大有裨益

        • 2.1 Node 用途那么多,我該從哪里學起?

        答:如果有機會就直接上Web應用,如果沒有機會就從前端構建,工具等方面開始做,慢慢引入更潮更酷的前端技術,自然就把Node引入進來了。不要急。

        • 2.2 Node Web 框架那么多,我該怎么選?

        答:初學者推薦Express,如果有一定經驗,推薦Koa。當然真正項目里還是推薦Eggjs和Thinkjs這樣的框架。

        • 2.3 關于 Node 的書幾乎都過時了,我該買哪本?

        答:
        1)Node in action和了不起的Node.js是入門的絕好書籍,非常簡單,各個部分都講了,但不深入,看了之后,基本就能用起來了
        2)當你用了一段之后,你會對Node.js的運行機制好奇,為啥呢?這時候去讀樸大的《深入淺出Node.js》一書就能夠解惑。原因很簡單,九淺一深一書是偏向底層實現原理的書,從操作系統,并發原理,node源碼層層解讀。如果是新手讀,難免會比較郁悶。
        3)實踐類的可以看看雷宗民(老雷)和趙坤(nswbmw)寫的書

        如果你不著急,也可以等我的那本《更了不起的Node.js》,時間待定。

        Part 3延伸:大前端變化那么快,如何才能做到每日精進?

        有朋友問現在Android開發和web前端開發哪個前景更好?我的回答是明顯是前端更好,看一下移動端發展過程

        native < hybrid < rn/weex < h5

        目前rn和weex的開發逐漸變得主流,組件化寫法已經由前端主導了。以前ios和android程序員占比很高,但現在就留1到2個寫插件,真是差別很大。

        Web開發對移動端的沖擊非常大。當然現在Web技術也開發PC client了,比如vscode是通過electron打包的,效果還是相當不錯的。

        前端可以說是最近幾年開發里最火的部分,原因很多,最主要是開發方式的變更,以今時今日的眼光來看,稱之為現代Web開發是不為過的。

        先給出現代Web開發的概覽圖

        15117552681353.jpg

        每次演講我會都問大家是不是前端,回答“是”的人非常多,我會開玩笑的恭喜大家:“現在的前端就是錢端”,確實,現在前端發展異常的快,而且沒有趨向于類比java里ssh框架的那種穩定,所以未來很長一段時間,還會增長,持續混亂,這對前端來說是把雙刃劍,一方面有很強的壓迫感,不學習就跟不上時代,另一方它也是機遇,能夠帶給更多機會,包括money。

        大家都疑惑的一個問題是如何在這樣巨變的時代能夠通過學習來應變,我可以很負責的告訴大家,沒有捷徑,但通過掌握 Node.js 能夠讓你降低這個學習曲線而已,畢竟Node.js是大前端的基礎設施。大家可以看一下,前端的開發過程,模塊化,構建,輔助工具,調優,架構調整,可以說Node.js是無處不在的。

        其實,輔助大前端開發只是Node.js的一個非常無心插柳的衍生功能,通過掌握Node.js能夠讓你能做的更多、獲得的更多,甚至可以說有更多自我實現的快樂,這也是我那本書書名字里“更了不起的”要去闡述的內容。

        綜上種種,就是我一直提倡以 JavaScript 語言為中心的 Node全棧 概念的緣由,JavaScript 覆蓋所有前端,Node.js 擅長做 I/O 密集型的后端,外加輔助開發的各種基礎設施,無疑是工作、學習和成為快速掌握全棧技術最好的途徑。你會的越多,你能做的就更多,你的人生也將會有不一樣的精彩篇章。

        全棧核心

        • 后端不會的 UI(界面相關)
        • 前端不會的 DB(業務相關)

        只要打通這2個要點,其他就比較容易了。最怕的是哪樣都接觸點,然后就號稱自己是全棧,建議大家不要這樣做,這就好比在簡歷里寫精通一樣,基本上都會被問到尷尬。全棧是一種信仰,不是拿來吹牛逼的,而可以解決更多問題,讓自己的知識體系不留空白,享受自我實現的極致快樂。

        我的全棧之路

        想問一下狼叔最近的業務一直都是簡單的用express搭一個后端服務,沒有其他更加深入node的業務了,這種時候應該如何自己給自己創應用場景呢

        沒有目標就向錢看,有目標就向前看
        • 從 java 開始,蹭課,背著機箱到深圳,3個月胖20斤
        • 堅持翻譯英文文檔,看 《Thinking in Java》
        • 畢業后開始 bi,整理 bi 文檔
        • 學長明林清,傳授 jQuery,愿意學,別人就更愿意分析
        • 接手《內蒙廣電數據分析與科學決策系統》,打通前、后端
        • 廣東聯通,自己造輪子,寫 jQuery 插件,DRY
        • 做云計算,學習 AIX,寫有《凌云志》
        • 分手、離職,去做 iOS,從 cordova 開始搞 H5,研究各種移動端框架,自己寫框架,轉原生
        • 面試也是學習的利器,輕松進新浪
        • 總結了大量 iOS 經驗,想寫書,結果寫了一堆寫書的工具
        既然無法逃避,就熱愛它,最后變成興趣
        • 去網秦做技術總監,做首席,管架構,帶人,寫開源項目
        • 創業,當 CTO,結婚,做公眾號運營,寫書,最苦的時候沒錢吃飯,又不能找媳婦要,只能在 StuQ 上講點課
        • 加入去哪兒網,任職前端架構師
        • 加入阿里巴巴,前端技術專家
        人生不只有代碼,但它能讓我快樂,終生受益

        也曾懵懂,也曾迷茫,但我這人比較傻,一直信奉:“一次只做1件事兒,盡力做到極致”,短時間看這是比較傻的,但一旦你堅持下去,你就會發現技術其實是門手藝,厚積薄發。

        我沒辦法說自己最擅長什么,但在什么場景下用什么技術是我擅長的?;蛘哒f,應變是我最大的本事。很多框架,新技術我都沒見過,用過,但花一點點過一下,就能拿已有的知識快速的理解它,這其實是長期學習的好處。

        現在越來越忙,寫代碼的時間越來越少,技術又越發展越快,我能做好的就是每日精進,仗著這點已有的知識儲備跟年輕人比賽。我不覺得累,相反我很享受這種感覺,沒有被時代淘汰,是一件多么幸福的事兒。

        從后端轉

        做后端的人

        • 對數據庫是比較熟悉,無論 mongodb,還是 mysql、postgres
        • 對前端理解比較弱,會基本的 html,css,模板引擎等比較熟悉
        4階段循序漸進,build 與工具齊飛

        前端開發4階段,我的感覺是按照順序,循序漸進就好。

        從前端轉

        從前端往后端轉,api 接口非常容易學會,像 express、koa 這類框架大部分人一周就能學會,最難的是對 db、er 模型的理解,說直白點,還是業務需求落地的理解

        我們來想想一般的前端有什么技能?

        • html
        • css(兼容瀏覽器)
        • js 會點(可能更多的是會點 jquery)
        • ps 切圖
        • firebug 和 chrome debuger 會的人都不太多
        • 用過幾個框架,大部分人是僅僅會用
        • 英語一般
        • svn/git 會一點

        那么他們如果想在前端領域做的更深有哪些難點呢?

        • 基礎:oo,dp,命令,shell,構建等
        • 編程思想上的理解(mvc、ioc,規約等)
        • 區分概念
        • 外圍驗收,如 H5 和 hybird 等
        • 追趕趨勢,如何學習新東西

        以上皆是痛點,所以比較好的辦法應該是這樣的。

        • 玩轉 npm、gulp 這樣的前端工具類(此時還是前端)
        • 使用 node 做前后端分離(此時還是前端)
        • express、koa 這類框架
        • jade、ejs 等模板引擎
        • nginx
        • 玩轉【后端】異步流程處理(promise/es6的(generator|yield)/es7(async|await))
        • 玩轉【后端】mongodb、mysql 對應的 Node 模塊

        從我們的經驗看,這樣是比較靠譜的。先做最簡單前后端分離,里面沒有任何和db相關,前端可以非常容易的學會,基本2周就已經非常熟練了。一般半年后,讓他們接觸【異步流程處理】和【數據庫】相關內容,學習后端代碼,就可以全棧了。

        從移動端轉

        看一下移動端發展過程

        native < hybrid < rn/weex < h5

        目前rn和weex的開發逐漸變得主流,組件化寫法已經由前端主導了。以前ios和android程序員占比很高,但現在就留1到2個寫插件,真是差別很大。狼叔一直固執的以為未來是h5的。

        現在的 Native 開發是姥姥不疼舅舅不愛,非常尷尬,很明顯連培訓出的人就業不要工資混經驗就很明顯了。另外領導們也都在惦記,能不能用 H5 寫?這還算是保守的,如果直接激進的就直接上 RN 了,那么 Native開發的程序員就變了

        一個寫插件的程序員...招誰惹誰了。。。。

        要么忍,要么轉,沒辦法,認命吧,溫水里舒服了幾年,也該學點東西了

        • hybrid 或組件化開發,你總要會一樣
        • 無論哪種,你都離前端很近,因為 H5 或組件化都是從前端走出來的
        • 組件化在前端領域先行,無論借鑒還是學習都不可避免
        • 如果沒時間就直接上組件化,如果有時間就好好學學前端完整體系,最終也還是要學組件化

        原生開發就是 iOS 用 OC/Swift,Android 用 java 或 scala 等,就算偶爾嵌入 webview,能玩js的機會也非常好少

        所以移動端轉全棧的方法,最好是從 cordova(以前叫 phonegap)開始做 hybrid 開發。

        • 只要關注 www 目錄里的 H5 即可,比較簡單
        • 如果 H5 不足以完成的情況下,可以編寫 cordova 插件,即通過插件讓 js 調用原生 sdk 里功能
        • cordova 的 cli 可以通過 npm 安裝,學習 npm 的好方法
        • 學習 gulp 構建工具

        只要入了 H5 的坑,其實就非常好辦了。

        • 然后 h5、zeptojs、iscroll、fastclick 等
        • 然后微信常用的,如 weui、vux(vue+weui)、jmui(react+weui)
        • 然后可以玩點框架,比如 jquery mobile,sencha touch
        • 然后可以玩點高級貨,ionicframework(基于 angularjs、cordova)
        • 然后前端4階段,依次打怪升級
        • 然后 node

        這個基本上是我走的路,從2010年寫iOS、做phonegap(當時是0.9.3)、一路走到現在的總結吧!

        以前技術發展還不是那么明顯,寫 Java 的時候 Apache 的開源用的比較多,那時開源的代碼托管sourceforge,google code 也都湊合用,自從 GitGitHub 出現時候,代碼社交興起,極大的促進了開源的活躍,使得大量明星項目脫引而出。這是好事,如果沒有開源,中國的軟件水平真是要落后好多年。那么問題也來了,如何能夠在技術快速發展的今天,個人成長也能更好呢?

        學習的3種層次,跟人學最快,其次是跟書(或者博客)學,最差的是自悟。但是牛人不能遇到,遇到了也未必有精力教你,書本或者博客,別人有,但不一定寫出來,就算是寫了,可能只是點到為止。至于自悟,如果沒有深厚的積累的,還是有相當大難度的。

        對于開發來說代碼是一切基礎,在掌握了一定計算機基礎后,其差別就在于代碼質量和眼界。編程沒有捷徑,能夠做到每日精進就是極好的?,F在開源代碼非常多,要能夠從中獲取自己所需的知識,也是一種本領!如果能夠堅持著每日精進,根本不需要向其他人學習的。

        15011322589471.jpg

        大家可以在 Github 隨便打開一個前端項目,里面有一半以上都是 Node.js 相關信息,各種包管理、測試、ci、輔助模塊,如果大家對這些基礎信息掌握的非常好,那么學習一個新的框架,你要比別人快好多,最重要的是學了一次,到處使用。

        很多人問我怎么才能成為一個 Node.js 大神?我的回答是“在cnode論壇上堅持寫文章和開源項目2年,足矣,輕松進阿里騰訊,不用你找他們,他們自會找你的”。

        從今天起,開始重視開源項目,重視 node,做到每日精進

        Part 4實踐:從招聘角度來看, Node.js 開發需要具備哪些技能?

        招人標準

        先說下我的招人標準,做技術總監時上指下派只要看好技術能力和態度即可,做CTO時要考慮團隊文化,人品和能否在公司長留,所以不同的人面試要看的點是不一樣的,我曾面過很多Node.js程序員,也見過很多面試題,匯總一下,大致有以下9個點:

        1. 基本的Node.js幾個特性,比如事件驅動、非阻塞I/O、Stream等
        2. 異步流程控制相關,Promise是必問的
        3. 掌握1種以上Web框架,比如Express、Koa、Thinkjs、Restfy、Hapi等,會問遇到過哪些問題、以及前端優化等常識
        4. 數據庫相關,尤其是SQL、緩存、Mongodb等
        5. 對于常見Node.js模塊、工具的使用,觀察一個人是否愛學習、折騰
        6. 是否熟悉linux,是否獨立部署過服務器,有+分
        7. js語法和es6、es7,延伸CoffeeScript、TypeScript等,看看你是否關注新技術,有+分
        8. 對前端是否了解,有+分
        9. 是否參與過或寫過開源項目,技術博客、有+分

        補充一句:只看技能沒人品的人,千萬別招,白臉狼

        主動執行,輔助團隊
        掌握一門后端語言;熟悉用戶體驗相關知識;了解軟件工程。
        精通瀏覽器工作原理,熟悉HTTP協議,熟悉設計模式。
        掌握改善無障礙訪問的方法;掌握數據采集分析能力;熟悉可維護性問題。
        通過開發、使用、推廣效率工具讓自己與團隊的效率得到提高;
        提煉可復用組件,為類庫貢獻高質量代碼.
        積極完善知識庫;
        跨團隊分享技術和專業相關知識。
        輔導新人技能成長;
        協助主管做招聘和團隊管理工作。

        大家是選大公司還是小公司?

        我再知乎上回復的《在跳槽后的第三個月,收到世界500強的offer,我該怎么辦?》

        1)互聯網公司優先,流量大,人才多,機會也多,流程規范一些

        2)今天的世界500強不比從前了,普華永道應該是四大之一,不知道信息化怎么樣,你只要和你現在的公司對比就好了。

        3)問問自己想要什么,錢,經歷,還是時間

        如果你很年輕,現在很安逸,我建議你換。如果不是很想動,那就學會所有能接觸到的知識再換。

        我是降薪來的阿里,原因有三,一是有事可為,老板重用你給你機會,二是集團內部是open的,偏偏我知識面足夠可以看出它們的好處,算是另一種補償吧,三是對個人品牌是一個升級,狼叔的職業生涯到此已經足夠了,進可攻退可守,也算另一種自由吧!

        錢多是個優勢而已,還有氛圍,文化,信仰

        • 牛人多
        • 業務需要
        • 成熟后有更多精力
        • 內部競爭,優勝劣汰
        • 財務相對自由,可以追求信仰了

        前幾天還和 @蘇千 聊,我和 @死月絲卡蕾特 相繼入職阿里,還有cnode社區著名程序員也即將入職阿里,當時大家開玩笑說:

        “前端的終極歸宿是阿里,不是在阿里,就是在去阿里的路上”

        另外要說的一點是pc和h5站在使用Node.js做api中間層,其最佳實踐已成型,量也是極大的。以前前端玩得還是比較弱,如果能夠緩存+控制頁面數據,獲得一個性能極大的提升也是極好的。2018年,爭取拿這個做主題演講上qcon或archsummit大會。

        每天忙得很開心,這就是我現在狀態。其實,我的折騰還是在于想做些事情。如果你也想跟我一起做事,請將簡歷郵件給我 langshu.ssl@alibaba-inc.com,團隊大量招人,也可以幫忙推薦給集團其他部門。

        大公司的做事方式

        • 按照規矩做事,不要碰紅線
        • 工時一般壓的都不會太緊,都是可以商量的,但態度一定要好
        • 閑的時候自己學點東西,上班要學習相關,下班學不相干的。別犯傻。
        • 多創造點額外價值,讓你的領導更喜歡你
        • 理解能力要強,不要讓你的領導說二遍。

        小公司

        • 簡單粗暴,快速出東西,領導最關心的是進度
        • 執行力要強,遇佛殺佛,有鬼殺鬼
        • 代碼質量其實沒太多人管,但自己要注意養成好習慣
        • 沒有機會自己創造機會。創造機會之前是贏得信任。

        狼叔經常說的一句:“少抱怨,多思考,未來更美好”,大部分人都喜歡積極的人,遇到問題不怕不躲不避,要相信自己能夠解決,最多是時間問題。

        還有一句是:“沒目標向錢看,有目標向前看”。歷史上很多這樣的例子,在2010年左右iOS開發剛起步,會拖拽弄個界面的就五位數工資,比做JAVA的幾年的都要多。這世界就是這樣不公平。但是你也不能不思進取,這樣也是極其危險。在2016年左右,其實iOS開發就遇到了市場飽和的問題,很多培訓出來的人都找不到工作,各家公司都在考慮換react-native或weex或者h5。

        所以,當你有機會進入一個很有前途的方向,你要努力學好,并準備好下一個階段的應變。相反,如果當成找一個養老的地方,早晚會遇到尷尬的。比如現在很多iOS程序員被迫去學react/vue等,境遇也不太好的,更有甚者直接被開除。

        優酷-高級前端開發

        職位描述

        1. 支撐企業級應用后臺開發,使用 React 等前端框架搭建后臺頁面,實現交互需求及后端服務對接;
        2. 以模塊化的思想設計開發通用前端組件,并能夠針對OTT,移動端進行針對性優化;
        3. 在理解前端開發流程的基礎上,結合前端實際建立或優化提升工作效率的工具;
        4. 在理解產品業務的基礎上,提升產品的用戶體驗,技術驅動業務的發展;
        5. 關注前端前沿技術研究,通過新技術服務團隊和業務;
        6. 使用 Weex 技術開發產品需求。"

        職位要求

        1. 精通前端技術,包括HTML/CSS/JavaScript/Node.JS等;
        2. 掌握Bootstrap,jQuery,AngularJS,React等框架,并有項目實踐;
        3. 熟悉前端模塊化、編譯和構建工具,如grunt,gulp,webpack等;
        4. 至少熟悉一門非前端的語言(如Java/PHP/C/C++/Python/Ruby),有項目實踐更佳;
        5. 具備跨終端的前端開發能力,在Web(PC+Mobile)/Node.js/Native App三個方向上至少精通一個方向,具備多個的更佳,鼓勵在Native和Web技術融合上的探索;
        6. 具有較強的學習能力,對前端技術有持續的熱情,個性樂觀開朗,邏輯性強,善于和產品,UED,后端等方向同學合作。

        PixelLab

        PixelLab是與淘寶GM Lab聯合成立的專注于視頻算法方向的研發部門,主要涉及視頻的空間感知、軌跡跟蹤、圖像分割、遮擋檢測以及照片級渲染等相關技術。用于實現視頻內的內容植入與后期特效的研發,屬于視頻MR的場景,主要應用于廣告植入平臺的研發,方向靠譜老板人好,歡迎推薦。主要需要的崗位包括了圖像算法、3D視覺算法,渲染算法,WebGL以及并行計算等幾大方向,因為算法類招聘實在難,所以將JD的鏈接帖出來希望同事們有適合的人可以內推一下。

        崗位要求:
        1) 本科及以上學歷,5年以上工作開發經驗;
        2) 扎實的計算幾何基礎,熟悉常見數學工具;
        3) 熟練WebGL, Canvas渲染開發,熟練Shader編寫, 熟悉Three.js, OSG.js者優先;
        4) 熟練運用JavaScript語言與HTML5、CSS3等技術;
        5) 熟悉主流移動瀏覽器的技術特點,有移動端H5, WebGL項目經驗者優先;
        6) 有移動端WebGL開發經驗者優先;
        6) 學習能力強、應變能力強,優秀的溝通能力和協調能力,執行能力強,具備較強的團隊合作精神。

        螞蟻金服

        崗位要求:

        • 大學本科學歷,2年以上開發經驗,能熟練使用常見類庫或框架,編寫高質量的前端代碼;
        • 熟悉NodeJS,有NodeJS開發經驗,熟悉Expresskoa等框架;
        • 熟練掌握React、Redux及相關框架和技術,有單頁面應用開發經驗;
        • 精通ES6,gulp,webpack等規范和技術;
        • 善于 Web 性能優化與前端效果的實現;
        • 良好的團隊合作精神和積極主動的溝通意識,具有很強的學習能力和對新技術的追求精神,樂于分享;
        • 有大型網站開發經驗者優先。

        我們的前端專業建設方向

        • 基于ReactJS的主題可配置組件化平臺
        • 基于Nodejs的UED中臺業務(瀏覽器端web頁面監控等)
        • 基于Docker的nodejs云容器平臺
        • 基于Webpack的前端工程化體系建設
        • 基于eggjs的react同構框架
        • 基于G2的業務數據可視化組件庫
        • 大規模圖形識別/圖像處理/AR/VR//語音交互等研究性領域探索

        聯系方式

        目前北京,杭州,廣州,上海,深圳,成都都在招聘,如果你也想跟我一起共事,請將簡歷郵件給我 langshu.ssl@alibaba-inc.com,團隊大量招人,也可以幫忙推薦給集團其他部門。

        有機會和winter,勾股,玉伯,蘇千,樸靈、死馬、偏右,徐飛,阮一峰,天豬,裕波等大神一起工作哦。

        悄悄地說,其實其他公司我也可以幫推薦。

        結束語

        年輕時死磕,年長點讓小弟死磕,現在抓個專家一起吃飯,沒有什么是一頓飯解決不了的,不行就二頓

        工程師的能力不是編碼和死磕,而是解決問題

        • 年輕死磕是為了長本事,30歲以前都可以這樣做
        • 帶團隊后,要懂得任務下放,讓更多人幫你,別帶人越多越累
        • 30歲之后是打牌階段,技能積累足夠用,這時要注重社交,打組合拳才能玩的更好

        強調30歲不是我創造的,大部分人都會認為30歲后事情,壓力會明顯大得多,比如家庭,孩子,房子,車子,票子,甚至是管理,權利,欲望等等吧。我感受最深的是身體不如從前和記憶力明顯下降。

        狼叔說: “少抱怨,多思考,未來更美好”

        大部分人體會不到堅持的樂趣,不會玩,所以抱怨多。其實玩出樂趣也是一種能力,尤其是像寫代碼這種看似無聊的事兒。最開始可能只想賺點錢,后面變成熱愛,這樣才美好。只要堅持每日精進開心就好了。

        另外,時間也要好處處理,狼叔總會提菜根譚上的一句話:【閑時要有吃緊的心思,忙里要有偷閑的樂趣】。

        每個人的一生中都有很多坎,類似于瓶頸,唯有苦難和堅持才能沖破,堅持會產生自信,苦難會創造機會。一個經過苦難還有自信的人,一定會有更美好的未來。

        如果大家還有問題,可以去cnode社區發帖at我,也可以在Node全棧公眾號提問。

        Part 5答疑:回答大家的問題

        答疑有點多,這里就不一一貼出來,如果是新用戶和比較迷茫的Node朋友去Live里聽吧。

        查看原文

        撒網要見魚 發布了文章 · 2018-03-12

        從輸入URL到頁面加載的過程?如何由一道題完善自己的前端知識體系!

        前言

        見解有限,如有描述不當之處,請幫忙指出,如有錯誤,會及時修正。

        為什么要梳理這篇文章?

        最近恰好被問到這方面的問題,嘗試整理后發現,這道題的覆蓋面可以非常廣,很適合作為一道承載知識體系的題目。

        關于這道題目的吐槽暫且不提(這是一道被提到無數次的題,得到不少人的贊同,也被很多人反感),本文的目的是如何借助這道題梳理自己的前端知識體系!

        竊認為,每一個前端人員,如果要往更高階發展,必然會將自己的知識體系梳理一遍,沒有牢固的知識體系,無法往更高處走!

        展現形式:本文并不是將所有的知識點列一遍,而是偏向于分析+梳理

        內容:在本文中只會梳理一些比較重要的前端向知識點,其它的可能會被省略

        目標:本文的目標是梳理一個較為完整的前端向知識體系

        本文是個人階段性梳理知識體系的成果,然后加以修繕后發布成文章,因此并不確保適用于所有人員,但是,個人認為本文還是有一定參考價值的

        另外,如有不同見解,可以一起討論

        ----------超長文預警,需要花費大量時間。----------

        本文適合有一定經驗的前端人員,新手請規避。

        本文內容超多,建議先了解主干,然后分成多批次閱讀。

        本文是前端向,以前端領域的知識為重點

        大綱

        • 對知識體系進行一次預評級
        • 為什么說知識體系如此重要?
        • 梳理主干流程
        • 從瀏覽器接收url到開啟網絡請求線程

          • 多進程的瀏覽器
          • 多線程的瀏覽器內核
          • 解析URL
          • 網絡請求都是單獨的線程
          • 更多
        • 開啟網絡線程到發出一個完整的http請求

          • DNS查詢得到IP
          • tcp/ip請求
          • 五層因特網協議棧
        • 從服務器接收到請求到對應后臺接收到請求

          • 負載均衡
          • 后臺的處理
        • 后臺和前臺的http交互

          • http報文結構
          • cookie以及優化
          • gzip壓縮
          • 長連接與短連接
          • http 2.0
          • https
        • 單獨拎出來的緩存問題,http的緩存

          • 強緩存與弱緩存
          • 緩存頭部簡述
          • 頭部的區別
        • 解析頁面流程

          • 流程簡述
          • HTML解析,構建DOM
          • 生成CSS規則
          • 構建渲染樹
          • 渲染
          • 簡單層與復合層
          • Chrome中的調試
          • 資源外鏈的下載
          • loaded和domcontentloaded
        • CSS的可視化格式模型

          • 包含塊(Containing Block)
          • 控制框(Controlling Box)
          • BFC(Block Formatting Context)
          • IFC(Inline Formatting Context)
          • 其它
        • JS引擎解析過程

          • JS的解釋階段
          • JS的預處理階段
          • JS的執行階段
          • 回收機制
        • 其它
        • 總結

        對知識體系進行一次預評級

        看到這道題目,不借助搜索引擎,自己的心里是否有一個答案?

        這里,以目前的經驗(了解過一些處于不同階段的相關前端人員的情況),大概有以下幾種情況:(以下都是以點見面,實際上不同階段人員一般都會有其它的隱藏知識點的)

        level1:

        完全沒什么概念的,支支吾吾的回答,一般就是這種水平(大致形象點描述):

        • 瀏覽器發起請求,服務端返回數據,然后前端解析成網頁,執行腳本。。。

        這類人員一般都是:

        • 萌新(剛接觸前端的,包括0-6個月都有可能有這種回答)
        • 沉淀人員(就是那種可能已經接觸了前端幾年,但是仍然處于初級階段的那種。。。)

        當然了,后者一般還會偶爾提下http、后臺、瀏覽器渲染,js引擎等等關鍵字,但基本都是一詳細的問就不知道了。。。

        level2:

        已經有初步概念,但是可能沒有完整梳理過,導致無法形成一個完整的體系,或者是很多細節都不會展開,大概是這樣子的:(可能符合若干條)

        • 知道瀏覽器輸入url后會有http請求這個概念
        • 有后臺這個概念,大致知道前后端的交互,知道前后端只要靠http報文通信
        • 知道瀏覽器接收到數據后會進行解析,有一定概念,但是具體流程不熟悉(如render樹構建流程,layout、paint,復合層與簡單層,常用優化方案等不是很熟悉)
        • 對于js引擎的解析流程有一定概念,但是細節不熟悉(如具體的形參,函數,變量提升,執行上下文以及VO、AO、作用域鏈,回收機制等概念不是很熟悉)
        • 如可能知道一些http規范初步概念,但是不熟悉(如http報文結構,常用頭部,緩存機制,http2.0,https等特性,跨域與web安全等不是很熟悉)

        到這里,看到這上面一大堆的概念后,心里應該也會有點底了。。。

        實際上,大部分的前端人員可能都處于level2,但是,跳出這個階段并不容易,一般需要積累,不斷學習,才能水到渠成

        這類人員一般都是:

        • 工作1-3年左右的普通人員(占大多數,而且大多數人員工作3年左右并沒有實質上的提升)
        • 工作3年以上的老人(這部分人大多都業務十分嫻熟,一個當好幾個用,但是,基礎比較薄弱,可能沒有嘗試寫過框架、組件、腳手架等)

        大部分的初中級都陷在這個階段,如果要突破,不斷學習,積累,自然能水到渠成,打通任督二脈

        level3:

        基本能到這一步的,不是高階就是接近高階,因為很多概念并不是靠背就能理解的,而要理解這么多,需形成體系,一般都需要積累,非一日之功。

        一般包括什么樣的回答呢?(這里就以自己的簡略回答進行舉例),一般這個階段的人員都會符合若干條(不一定全部,當然可能還有些是這里遺漏的):

        • 首先略去那些鍵盤輸入、和操作系統交互、以及屏幕顯示原理、網卡等硬件交互之類的(前端向中,很多硬件原理暫時略去。。。)
        • 對瀏覽器模型有整體概念,知道瀏覽器是多進程的,瀏覽器內核是多線程的,清楚進程與線程之間得區別,以及輸入url后會開一個新的網絡線程
        • 對從開啟網絡線程到發出一個完整的http請求中間的過程有所了解(如dns查詢,tcp/ip鏈接,五層因特網協議棧等等,以及一些優化方案,如dns-prefetch
        • 對從服務器接收到請求到對應后臺接收到請求有一定了解(如負載均衡,安全攔截以及后臺代碼處理等)
        • 對后臺和前臺的http交互熟悉(包括http報文結構,場景頭部,cookie,跨域,web安全,http緩存,http2.0,https等)
        • 對瀏覽器接收到http數據包后的解析流程熟悉(包括解析html,詞法分析然后解析成dom樹、解析css生成css規則樹、合并成render樹,然后layout、painting渲染、里面可能還包括復合圖層的合成、GPU繪制、外鏈處理、加載順序等)
        • 對JS引擎解析過程熟悉(包括JS的解釋,預處理,執行上下文,VO,作用域鏈,this,回收機制等)

        可以看到,上述包括了一大堆的概念,僅僅是偏前端向,而且沒有詳細展開,就已經如此之多的概念了,所以,個人認為如果沒有自己的見解,沒有形成自己的知識體系,僅僅是看看,背背是沒用的,過一段時間就會忘光了。

        再說下一般這個階段的都可能是什么樣的人吧。(不一定準確,這里主要是靠少部分現實以及大部分推測得出)

        • 工作2年以上的前端(基本上如果按正常進度的話,至少接觸前端兩年左右才會開始走向高階,當然,現在很多都是上學時就開始學了的,還有部分是天賦異稟,不好預估。。。)
        • 或者是已經十分熟悉其它某門語言,再轉前端的人(基本上是很快就可以將前端水準提升上去)

        一般符合這個條件的都會有各種隱藏屬性(如看過各大框架、組件的源碼,寫過自己的組件、框架、腳手架,做過大型項目,整理過若干精品博文等)

        level4:

        由于本人層次尚未達到,所以大致說下自己的見解吧。

        一般這個層次,很多大佬都并不僅僅是某個技術棧了,而是成為了技術專家,技術leader之類的角色。所以僅僅是回答某個技術問題已經無法看出水準了,
        可能更多的要看架構,整體把控,大型工程構建能力等等

        不過,對于某些執著于技術的大佬,大概會有一些回答吧:(猜的)

        • 從鍵盤談起到系統交互,從瀏覽器到CPU,從調度機制到系統內核,從數據請求到二進制、匯編,從GPU繪圖到LCD顯示,然后再分析系統底層的進程、內存等等

        總之,從軟件到硬件,到材料,到分子,原子,量子,薛定諤的貓,人類起源,宇宙大爆炸,平行宇宙?感覺都毫無違和感。。。

        這點可以參考下本題的原始出處:

        http://fex.baidu.com/blog/2014/05/what-happen/

        為什么說知識體系如此重要?

        為什么說知識體系如此重要呢?這里舉幾個例子

        假設有被問到這樣一道題目(隨意想到的一個):

        • 如何理解getComputedStyle

        在尚未梳理知識體系前,大概會這樣回答:

        • 普通版本:getComputedStyle會獲取當前元素所有最終使用的CSS屬性值(最終計算后的結果),通過window.getComputedStyle等價于document.defaultView.getComputedStyle調用
        • 詳細版本:window.getComputedStyle(elem, null).getPropertyValue("height")可能的值為100px,而且,就算是css上寫的是inherit,getComputedStyle也會把它最終計算出來的。不過注意,如果元素的背景色透明,那么getComputedStyle獲取出來的就是透明的這個背景(因為透明本身也是有效的),而不會是父節點的背景。所以它不一定是最終顯示的顏色。

        就這個API來說,上述的回答已經比較全面了。

        但是,其實它是可以繼續延伸的。

        譬如現在會這樣回答:

        • getComputedStyle會獲取當前元素所有最終使用的CSS屬性值,window.document.defaultView.等價...
        • getComputedStyle會引起回流,因為它需要獲取祖先節點的一些信息進行計算(譬如寬高等),所以用的時候慎用,回流會引起性能問題。然后合適的話會將話題引導回流,重繪,瀏覽器渲染原理等等。當然也可以列舉一些其它會引發回流的操作,如offsetXXX,scrollXXX,clientXXX,currentStyle等等

        再舉一個例子:

        • visibility: hiddendisplay: none的區別

        可以如下回答:

        • 普通回答,一個隱藏,但占據位置,一個隱藏,不占據位置
        • 進一步,display由于隱藏后不占據位置,所以造成了dom樹的改變,會引發回流,代價較大
        • 再進一步,當一個頁面某個元素經常需要切換display時如何優化,一般會用復合層優化,或者要求低一點用absolute讓其脫離普通文檔流也行。然后可以將話題引到普通文檔流,absolute文檔流,復合圖層的區別,
        • 再進一步可以描述下瀏覽器渲染原理以及復合圖層和普通圖層的繪制區別(復合圖層單獨分配資源,獨立繪制,性能提升,但是不能過多,還有隱式合成等等)

        上面這些大概就是知識系統化后的回答,會更全面,容易由淺入深,而且一有機會就可以往更底層挖

        前端向知識的重點

        此部分的內容是站在個人視角分析的,并不是說就一定是正確答案

        首先明確,計算機方面的知識是可以無窮無盡的挖的,而本文的重點是梳理前端向的重點知識

        對于前端向(這里可能沒有提到node.js之類的,更多的是指客戶端前端),這里將知識點按重要程度劃分成以下幾大類:

        • 核心知識,必須掌握的,也是最基礎的,譬如瀏覽器模型,渲染原理,JS解析過程,JS運行機制等,作為骨架來承載知識體系
        • 重點知識,往往每一塊都是一個知識點,而且這些知識點都很重要,譬如http相關,web安全相關,跨域處理等
        • 拓展知識,這一塊可能更多的是了解,稍微實踐過,但是認識上可能沒有上面那么深刻,譬如五層因特網協議棧,hybrid模式,移動原生開發,后臺相關等等(當然,在不同領域,可能有某些知識就上升到重點知識層次了,譬如hybrid開發時,懂原生開發是很重要的)

        為什么要按上面這種方式劃分?

        這大概與個人的技術成長有關。

        記得最開始學前端知識時,是一點一點的積累,一個知識點一個知識點的攻克。

        就這樣,雖然在很長一段時間內積累了不少的知識,但是,總是無法將它串聯到一起。每次梳理時都是很分散的,無法保持思路連貫性。

        直到后來,在將瀏覽器渲染原理、JS運行機制、JS引擎解析流程梳理一遍后,感覺就跟打通了任督二脈一樣,有了一個整體的架構,以前的知識點都連貫起來了。

        梳理出了一個知識體系,以后就算再學新的知識,也會盡量往這個體系上靠攏,環環相扣,更容易理解,也更不容易遺忘

        梳理主干流程

        回到這道題上,如何回答呢?先梳理一個骨架

        知識體系中,最重要的是骨架,脈絡。有了骨架后,才方便填充細節。所以,先梳理下主干流程:

        1. 從瀏覽器接收url到開啟網絡請求線程(這一部分可以展開瀏覽器的機制以及進程與線程之間的關系)
        
        2. 開啟網絡線程到發出一個完整的http請求(這一部分涉及到dns查詢,tcp/ip請求,五層因特網協議棧等知識)
        
        3. 從服務器接收到請求到對應后臺接收到請求(這一部分可能涉及到負載均衡,安全攔截以及后臺內部的處理等等)
        
        4. 后臺和前臺的http交互(這一部分包括http頭部、響應碼、報文結構、cookie等知識,可以提下靜態資源的cookie優化,以及編碼解碼,如gzip壓縮等)
        
        5. 單獨拎出來的緩存問題,http的緩存(這部分包括http緩存頭部,etag,catch-control等)
        
        6. 瀏覽器接收到http數據包后的解析流程(解析html-詞法分析然后解析成dom樹、解析css生成css規則樹、合并成render樹,然后layout、painting渲染、復合圖層的合成、GPU繪制、外鏈資源的處理、loaded和domcontentloaded等)
        
        7. CSS的可視化格式模型(元素的渲染規則,如包含塊,控制框,BFC,IFC等概念)
        
        8. JS引擎解析過程(JS的解釋階段,預處理階段,執行階段生成執行上下文,VO,作用域鏈、回收機制等等)
        
        9. 其它(可以拓展不同的知識模塊,如跨域,web安全,hybrid模式等等內容)

        梳理出主干骨架,然后就需要往骨架上填充細節內容

        從瀏覽器接收url到開啟網絡請求線程

        這一部分展開的內容是:瀏覽器進程/線程模型,JS的運行機制

        多進程的瀏覽器

        瀏覽器是多進程的,有一個主控進程,以及每一個tab頁面都會新開一個進程(某些情況下多個tab會合并進程)

        進程可能包括主控進程,插件進程,GPU,tab頁(瀏覽器內核)等等

        • Browser進程:瀏覽器的主進程(負責協調、主控),只有一個
        • 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建
        • GPU進程:最多一個,用于3D繪制
        • 瀏覽器渲染進程(內核):默認每個Tab頁面一個進程,互不影響,控制頁面渲染,腳本執行,事件處理等(有時候會優化,如多個空白tab會合并成一個進程)

        如下圖:

        多線程的瀏覽器內核

        每一個tab頁面可以看作是瀏覽器內核進程,然后這個進程是多線程的,它有幾大類子線程

        • GUI線程
        • JS引擎線程
        • 事件觸發線程
        • 定時器線程
        • 網絡請求線程

        可以看到,里面的JS引擎是內核進程中的一個線程,這也是為什么常說JS引擎是單線程的

        解析URL

        輸入URL后,會進行解析(URL的本質就是統一資源定位符)

        URL一般包括幾大部分:

        • protocol,協議頭,譬如有http,ftp等
        • host,主機域名或IP地址
        • port,端口號
        • path,目錄路徑
        • query,即查詢參數
        • fragment,即#后的hash值,一般用來定位到某個位置

        網絡請求都是單獨的線程

        每次網絡請求時都需要開辟單獨的線程進行,譬如如果URL解析到http協議,就會新建一個網絡線程去處理資源下載

        因此瀏覽器會根據解析出得協議,開辟一個網絡線程,前往請求資源(這里,暫時理解為是瀏覽器內核開辟的,如有錯誤,后續修復)

        更多

        由于篇幅關系,這里就大概介紹一個主干流程,關于瀏覽器的進程機制,更多可以參考以前總結的一篇文章(因為內容實在過多,里面包括JS運行機制,進程線程的詳解)

        從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理

        開啟網絡線程到發出一個完整的http請求

        這一部分主要內容包括:dns查詢,tcp/ip請求構建,五層因特網協議棧等等

        仍然是先梳理主干,有些詳細的過程不展開(因為展開的話內容過多)

        DNS查詢得到IP

        如果輸入的是域名,需要進行dns解析成IP,大致流程:

        • 如果瀏覽器有緩存,直接使用瀏覽器緩存,否則使用本機緩存,再沒有的話就是用host
        • 如果本地沒有,就向dns域名服務器查詢(當然,中間可能還會經過路由,也有緩存等),查詢到對應的IP

        注意,域名查詢時有可能是經過了CDN調度器的(如果有cdn存儲功能的話)

        而且,需要知道dns解析是很耗時的,因此如果解析域名過多,會讓首屏加載變得過慢,可以考慮dns-prefetch優化

        這一塊可以深入展開,具體請去網上搜索,這里就不占篇幅了(網上可以看到很詳細的解答)

        tcp/ip請求

        http的本質就是tcp/ip請求

        需要了解3次握手規則建立連接以及斷開連接時的四次揮手

        tcp將http長報文劃分為短報文,通過三次握手與服務端建立連接,進行可靠傳輸

        三次握手的步驟:(抽象派)

        客戶端:hello,你是server么?
        服務端:hello,我是server,你是client么
        客戶端:yes,我是client

        建立連接成功后,接下來就正式傳輸數據

        然后,待到斷開連接時,需要進行四次揮手(因為是全雙工的,所以需要四次揮手)

        四次揮手的步驟:(抽象派)

        主動方:我已經關閉了向你那邊的主動通道了,只能被動接收了
        被動方:收到通道關閉的信息
        被動方:那我也告訴你,我這邊向你的主動通道也關閉了
        主動方:最后收到數據,之后雙方無法通信

        tcp/ip的并發限制

        瀏覽器對同一域名下并發的tcp連接是有限制的(2-10個不等)

        而且在http1.0中往往一個資源下載就需要對應一個tcp/ip請求

        所以針對這個瓶頸,又出現了很多的資源優化方案

        get和post的區別

        get和post雖然本質都是tcp/ip,但兩者除了在http層面外,在tcp/ip層面也有區別。

        get會產生一個tcp數據包,post兩個

        具體就是:

        • get請求時,瀏覽器會把headersdata一起發送出去,服務器響應200(返回數據),
        • post請求時,瀏覽器先發送headers,服務器響應100 continue,

        瀏覽器再發送data,服務器響應200(返回數據)。

        再說一點,這里的區別是specification(規范)層面,而不是implementation(對規范的實現)

        五層因特網協議棧

        其實這個概念挺難記全的,記不全沒關系,但是要有一個整體概念

        其實就是一個概念: 從客戶端發出http請求到服務器接收,中間會經過一系列的流程。

        簡括就是:

        從應用層的發送http請求,到傳輸層通過三次握手建立tcp/ip連接,再到網絡層的ip尋址,再到數據鏈路層的封裝成幀,最后到物理層的利用物理介質傳輸。

        當然,服務端的接收就是反過來的步驟

        五層因特網協議棧其實就是:

        1.應用層(dns,http) DNS解析成IP并發送http請求
        
        2.傳輸層(tcp,udp) 建立tcp連接(三次握手)
        
        3.網絡層(IP,ARP) IP尋址
        
        4.數據鏈路層(PPP) 封裝成幀
        
        5.物理層(利用物理介質傳輸比特流) 物理傳輸(然后傳輸的時候通過雙絞線,電磁波等各種介質)

        當然,其實也有一個完整的OSI七層框架,與之相比,多了會話層、表示層。

        OSI七層框架:物理層、數據鏈路層、網絡層、傳輸層、會話層、表示層、應用層

        表示層:主要處理兩個通信系統中交換信息的表示方式,包括數據格式交換,數據加密與解密,數據壓縮與終端類型轉換等
        
        會話層:它具體管理不同用戶和進程之間的對話,如控制登陸和注銷過程

        從服務器接收到請求到對應后臺接收到請求

        服務端在接收到請求時,內部會進行很多的處理

        這里由于不是專業的后端分析,所以只是簡單的介紹下,不深入

        負載均衡

        對于大型的項目,由于并發訪問量很大,所以往往一臺服務器是吃不消的,所以一般會有若干臺服務器組成一個集群,然后配合反向代理實現負載均衡

        當然了,負載均衡不止這一種實現方式,這里不深入...

        簡單的說:

        用戶發起的請求都指向調度服務器(反向代理服務器,譬如安裝了nginx控制負載均衡),然后調度服務器根據實際的調度算法,分配不同的請求給對應集群中的服務器執行,然后調度器等待實際服務器的HTTP響應,并將它反饋給用戶

        后臺的處理

        一般后臺都是部署到容器中的,所以一般為:

        • 先是容器接受到請求(如tomcat容器)
        • 然后對應容器中的后臺程序接收到請求(如java程序)
        • 然后就是后臺會有自己的統一處理,處理完后響應響應結果

        概括下:

        • 一般有的后端是有統一的驗證的,如安全攔截,跨域驗證
        • 如果這一步不符合規則,就直接返回了相應的http報文(如拒絕請求等)
        • 然后當驗證通過后,才會進入實際的后臺代碼,此時是程序接收到請求,然后執行(譬如查詢數據庫,大量計算等等)
        • 等程序執行完畢后,就會返回一個http響應包(一般這一步也會經過多層封裝)
        • 然后就是將這個包從后端發送到前端,完成交互

        后臺和前臺的http交互

        前后端交互時,http報文作為信息的載體

        所以http是一塊很重要的內容,這一部分重點介紹它

        http報文結構

        報文一般包括了:通用頭部,請求/響應頭部,請求/響應體

        通用頭部

        這也是開發人員見過的最多的信息,包括如下:

        Request Url: 請求的web服務器地址
        
        Request Method: 請求方式
        (Get、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE)
        
        Status Code: 請求的返回狀態碼,如200代表成功
        
        Remote Address: 請求的遠程服務器地址(會轉為IP)

        譬如,在跨域拒絕時,可能是method為options,狀態碼為404/405等(當然,實際上可能的組合有很多)

        其中,Method的話一般分為兩批次:

        HTTP1.0定義了三種請求方法: GET, POST 和 HEAD方法。
        以及幾種Additional Request Methods:PUT、DELETE、LINK、UNLINK
        
        HTTP1.1定義了八種請求方法:GET、POST、HEAD、OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

        HTTP 1.0定義參考:https://tools.ietf.org/html/rfc1945

        HTTP 1.1定義參考:https://tools.ietf.org/html/rfc2616

        這里面最常用到的就是狀態碼,很多時候都是通過狀態碼來判斷,如(列舉幾個最常見的):

        200——表明該請求被成功地完成,所請求的資源發送回客戶端
        304——自從上次請求后,請求的網頁未修改過,請客戶端使用本地緩存
        400——客戶端請求有錯(譬如可以是安全模塊攔截)
        401——請求未經授權
        403——禁止訪問(譬如可以是未登錄時禁止)
        404——資源未找到
        500——服務器內部錯誤
        503——服務不可用
        ...

        再列舉下大致不同范圍狀態的意義

        1xx——指示信息,表示請求已接收,繼續處理
        2xx——成功,表示請求已被成功接收、理解、接受
        3xx——重定向,要完成請求必須進行更進一步的操作
        4xx——客戶端錯誤,請求有語法錯誤或請求無法實現
        5xx——服務器端錯誤,服務器未能實現合法的請求

        總之,當請求出錯時,狀態碼能幫助快速定位問題,完整版本的狀態可以自行去互聯網搜索

        請求/響應頭部

        請求和響應頭部也是分析時常用到的

        常用的請求頭部(部分):

        Accept: 接收類型,表示瀏覽器支持的MIME類型
        (對標服務端返回的Content-Type)
        Accept-Encoding:瀏覽器支持的壓縮類型,如gzip等,超出類型不能接收
        Content-Type:客戶端發送出去實體內容的類型
        Cache-Control: 指定請求和響應遵循的緩存機制,如no-cache
        If-Modified-Since:對應服務端的Last-Modified,用來匹配看文件是否變動,只能精確到1s之內,http1.0中
        Expires:緩存控制,在這個時間內不會請求,直接使用緩存,http1.0,而且是服務端時間
        Max-age:代表資源在本地緩存多少秒,有效時間內不會請求,而是使用緩存,http1.1中
        If-None-Match:對應服務端的ETag,用來匹配文件內容是否改變(非常精確),http1.1中
        Cookie: 有cookie并且同域訪問時會自動帶上
        Connection: 當瀏覽器與服務器通信時對于長連接如何進行處理,如keep-alive
        Host:請求的服務器URL
        Origin:最初的請求是從哪里發起的(只會精確到端口),Origin比Referer更尊重隱私
        Referer:該頁面的來源URL(適用于所有類型的請求,會精確到詳細頁面地址,csrf攔截常用到這個字段)
        User-Agent:用戶客戶端的一些必要信息,如UA頭部等

        常用的響應頭部(部分):

        Access-Control-Allow-Headers: 服務器端允許的請求Headers
        Access-Control-Allow-Methods: 服務器端允許的請求方法
        Access-Control-Allow-Origin: 服務器端允許的請求Origin頭部(譬如為*)
        Content-Type:服務端返回的實體內容的類型
        Date:數據從服務器發送的時間
        Cache-Control:告訴瀏覽器或其他客戶,什么環境可以安全的緩存文檔
        Last-Modified:請求資源的最后修改時間
        Expires:應該在什么時候認為文檔已經過期,從而不再緩存它
        Max-age:客戶端的本地資源應該緩存多少秒,開啟了Cache-Control后有效
        ETag:請求變量的實體標簽的當前值
        Set-Cookie:設置和頁面關聯的cookie,服務器通過這個頭部把cookie傳給客戶端
        Keep-Alive:如果客戶端有keep-alive,服務端也會有響應(如timeout=38)
        Server:服務器的一些相關信息

        一般來說,請求頭部和響應頭部是匹配分析的。

        譬如,請求頭部的Accept要和響應頭部的Content-Type匹配,否則會報錯

        譬如,跨域請求時,請求頭部的Origin要匹配響應頭部的Access-Control-Allow-Origin,否則會報跨域錯誤

        譬如,在使用緩存時,請求頭部的If-Modified-Since、If-None-Match分別和響應頭部的Last-Modified、ETag對應

        還有很多的分析方法,這里不一一贅述

        請求/響應實體

        http請求時,除了頭部,還有消息實體,一般來說

        請求實體中會將一些需要的參數都放入進入(用于post請求)。

        譬如實體中可以放參數的序列化形式(a=1&b=2這種),或者直接放表單對象(Form Data對象,上傳時可以夾雜參數以及文件),等等

        而一般響應實體中,就是放服務端需要傳給客戶端的內容

        一般現在的接口請求時,實體中就是對于的信息的json格式,而像頁面請求這種,里面就是直接放了一個html字符串,然后瀏覽器自己解析并渲染。

        CRLF

        CRLF(Carriage-Return Line-Feed),意思是回車換行,一般作為分隔符存在

        請求頭和實體消息之間有一個CRLF分隔,響應頭部和響應實體之間用一個CRLF分隔

        一般來說(分隔符類別):

        CRLF->Windows-style
        LF->Unix Style
        CR->Mac Style

        如下圖是對某請求的http報文結構的簡要分析

        cookie以及優化

        cookie是瀏覽器的一種本地存儲方式,一般用來幫助客戶端和服務端通信的,常用來進行身份校驗,結合服務端的session使用。

        場景如下(簡述):

        在登陸頁面,用戶登陸了
        
        此時,服務端會生成一個session,session中有對于用戶的信息(如用戶名、密碼等)
        
        然后會有一個sessionid(相當于是服務端的這個session對應的key)
        
        然后服務端在登錄頁面中寫入cookie,值就是:jsessionid=xxx
        
        然后瀏覽器本地就有這個cookie了,以后訪問同域名下的頁面時,自動帶上cookie,自動檢驗,在有效時間內無需二次登陸。

        上述就是cookie的常用場景簡述(當然了,實際情況下得考慮更多因素)

        一般來說,cookie是不允許存放敏感信息的(千萬不要明文存儲用戶名、密碼),因為非常不安全,如果一定要強行存儲,首先,一定要在cookie中設置httponly(這樣就無法通過js操作了),另外可以考慮rsa等非對稱加密(因為實際上,瀏覽器本地也是容易被攻克的,并不安全)

        另外,由于在同域名的資源請求時,瀏覽器會默認帶上本地的cookie,針對這種情況,在某些場景下是需要優化的。

        譬如以下場景:

        客戶端在域名A下有cookie(這個可以是登陸時由服務端寫入的)
        
        然后在域名A下有一個頁面,頁面中有很多依賴的靜態資源(都是域名A的,譬如有20個靜態資源)
        
        此時就有一個問題,頁面加載,請求這些靜態資源時,瀏覽器會默認帶上cookie
        
        也就是說,這20個靜態資源的http請求,每一個都得帶上cookie,而實際上靜態資源并不需要cookie驗證
        
        此時就造成了較為嚴重的浪費,而且也降低了訪問速度(因為內容更多了)

        當然了,針對這種場景,是有優化方案的(多域名拆分)。具體做法就是:

        • 將靜態資源分組,分別放到不同的域名下(如static.base.com
        • page.base.com(頁面所在域名)下請求時,是不會帶上static.base.com域名的cookie的,所以就避免了浪費

        說到了多域名拆分,這里再提一個問題,那就是:

        • 在移動端,如果請求的域名數過多,會降低請求速度(因為域名整套解析流程是很耗費時間的,而且移動端一般帶寬都比不上pc)
        • 此時就需要用到一種優化方案:dns-prefetch(讓瀏覽器空閑時提前解析dns域名,不過也請合理使用,勿濫用)

        關于cookie的交互,可以看下圖總結

        gzip壓縮

        首先,明確gzip是一種壓縮格式,需要瀏覽器支持才有效(不過一般現在瀏覽器都支持),
        而且gzip壓縮效率很好(高達70%左右)

        然后gzip一般是由apache、tomcat等web服務器開啟

        當然服務器除了gzip外,也還會有其它壓縮格式(如deflate,沒有gzip高效,且不流行)

        所以一般只需要在服務器上開啟了gzip壓縮,然后之后的請求就都是基于gzip壓縮格式的,
        非常方便。

        長連接與短連接

        首先看tcp/ip層面的定義:

        • 長連接:一個tcp/ip連接上可以連續發送多個數據包,在tcp連接保持期間,如果沒有數據包發送,需要雙方發檢測包以維持此連接,一般需要自己做在線維持(類似于心跳包)
        • 短連接:通信雙方有數據交互時,就建立一個tcp連接,數據發送完成后,則斷開此tcp連接

        然后在http層面:

        • http1.0中,默認使用的是短連接,也就是說,瀏覽器沒進行一次http操作,就建立一次連接,任務結束就中斷連接,譬如每一個靜態資源請求時都是一個單獨的連接
        • http1.1起,默認使用長連接,使用長連接會有這一行Connection: keep-alive,在長連接的情況下,當一個網頁打開完成后,客戶端和服務端之間用于傳輸http的tcp連接不會關閉,如果客戶端再次訪問這個服務器的頁面,會繼續使用這一條已經建立的連接

        注意: keep-alive不會永遠保持,它有一個持續時間,一般在服務器中配置(如apache),另外長連接需要客戶端和服務器都支持時才有效

        http 2.0

        http2.0不是https,它相當于是http的下一代規范(譬如https的請求可以是http2.0規范的)

        然后簡述下http2.0與http1.1的顯著不同點:

        • http1.1中,每請求一個資源,都是需要開啟一個tcp/ip連接的,所以對應的結果是,每一個資源對應一個tcp/ip請求,由于tcp/ip本身有并發數限制,所以當資源一多,速度就顯著慢下來
        • http2.0中,一個tcp/ip請求可以請求多個資源,也就是說,只要一次tcp/ip請求,就可以請求若干個資源,分割成更小的幀請求,速度明顯提升。

        所以,如果http2.0全面應用,很多http1.1中的優化方案就無需用到了(譬如打包成精靈圖,靜態資源多域名拆分等)

        然后簡述下http2.0的一些特性:

        • 多路復用(即一個tcp/ip連接可以請求多個資源)
        • 首部壓縮(http頭部壓縮,減少體積)
        • 二進制分幀(在應用層跟傳送層之間增加了一個二進制分幀層,改進傳輸性能,實現低延遲和高吞吐量)
        • 服務器端推送(服務端可以對客戶端的一個請求發出多個響應,可以主動通知客戶端)
        • 請求優先級(如果流被賦予了優先級,它就會基于這個優先級來處理,由服務器決定需要多少資源來處理該請求。)

        https

        https就是安全版本的http,譬如一些支付等操作基本都是基于https的,因為http請求的安全系數太低了。

        簡單來看,https與http的區別就是: 在請求前,會建立ssl鏈接,確保接下來的通信都是加密的,無法被輕易截取分析

        一般來說,如果要將網站升級成https,需要后端支持(后端需要申請證書等),然后https的開銷也比http要大(因為需要額外建立安全鏈接以及加密等),所以一般來說http2.0配合https的體驗更佳(因為http2.0更快了)

        一般來說,主要關注的就是SSL/TLS的握手流程,如下(簡述):

        1. 瀏覽器請求建立SSL鏈接,并向服務端發送一個隨機數–Client random和客戶端支持的加密方法,比如RSA加密,此時是明文傳輸。 
        
        2. 服務端從中選出一組加密算法與Hash算法,回復一個隨機數–Server random,并將自己的身份信息以證書的形式發回給瀏覽器
        (證書里包含了網站地址,非對稱加密的公鑰,以及證書頒發機構等信息)
        
        3. 瀏覽器收到服務端的證書后
            
            - 驗證證書的合法性(頒發機構是否合法,證書中包含的網址是否和正在訪問的一樣),如果證書信任,則瀏覽器會顯示一個小鎖頭,否則會有提示
            
            - 用戶接收證書后(不管信不信任),瀏覽會生產新的隨機數–Premaster secret,然后證書中的公鑰以及指定的加密方法加密`Premaster secret`,發送給服務器。
            
            - 利用Client random、Server random和Premaster secret通過一定的算法生成HTTP鏈接數據傳輸的對稱加密key-`session key`
            
            - 使用約定好的HASH算法計算握手消息,并使用生成的`session key`對消息進行加密,最后將之前生成的所有信息發送給服務端。 
            
        4. 服務端收到瀏覽器的回復
        
            - 利用已知的加解密方式與自己的私鑰進行解密,獲取`Premaster secret`
            
            - 和瀏覽器相同規則生成`session key`
            
            - 使用`session key`解密瀏覽器發來的握手消息,并驗證Hash是否與瀏覽器發來的一致
            
            - 使用`session key`加密一段握手消息,發送給瀏覽器
            
        5. 瀏覽器解密并計算握手消息的HASH,如果與服務端發來的HASH一致,此時握手過程結束,

        之后所有的https通信數據將由之前瀏覽器生成的session key并利用對稱加密算法進行加密

        這里放一張圖(來源:阮一峰-圖解SSL/TLS協議

        單獨拎出來的緩存問題,http的緩存

        前后端的http交互中,使用緩存能很大程度上的提升效率,而且基本上對性能有要求的前端項目都是必用緩存的

        強緩存與弱緩存

        緩存可以簡單的劃分成兩種類型:強緩存200 from cache)與協商緩存304

        區別簡述如下:

        • 強緩存(200 from cache)時,瀏覽器如果判斷本地緩存未過期,就直接使用,無需發起http請求
        • 協商緩存(304)時,瀏覽器會向服務端發起http請求,然后服務端告訴瀏覽器文件未改變,讓瀏覽器使用本地緩存

        對于協商緩存,使用Ctrl + F5強制刷新可以使得緩存無效

        但是對于強緩存,在未過期時,必須更新資源路徑才能發起新的請求(更改了路徑相當于是另一個資源了,這也是前端工程化中常用到的技巧)

        緩存頭部簡述

        上述提到了強緩存和協商緩存,那它們是怎么區分的呢?

        答案是通過不同的http頭部控制

        先看下這幾個頭部:

        If-None-Match/E-tag、If-Modified-Since/Last-Modified、Cache-Control/Max-Age、Pragma/Expires

        這些就是緩存中常用到的頭部,這里不展開。僅列舉下大致使用。

        屬于強緩存控制的:

        (http1.1)Cache-Control/Max-Age
        (http1.0)Pragma/Expires

        注意:Max-Age不是一個頭部,它是Cache-Control頭部的值

        屬于協商緩存控制的:

        (http1.1)If-None-Match/E-tag
        (http1.0)If-Modified-Since/Last-Modified

        可以看到,上述有提到http1.1http1.0,這些不同的頭部是屬于不同http時期的

        再提一點,其實HTML頁面中也有一個meta標簽可以控制緩存方案-Pragma

        <META HTTP-EQUIV="Pragma" CONTENT="no-cache">

        不過,這種方案還是比較少用到,因為支持情況不佳,譬如緩存代理服務器肯定不支持,所以不推薦

        頭部的區別

        首先明確,http的發展是從http1.0到http1.1

        而在http1.1中,出了一些新內容,彌補了http1.0的不足。

        http1.0中的緩存控制:

        • Pragma:嚴格來說,它不屬于專門的緩存控制頭部,但是它設置no-cache時可以讓本地強緩存失效(屬于編譯控制,來實現特定的指令,主要是因為兼容http1.0,所以以前又被大量應用)
        • Expires:服務端配置的,屬于強緩存,用來控制在規定的時間之前,瀏覽器不會發出請求,而是直接使用本地緩存,注意,Expires一般對應服務器端時間,如Expires:Fri, 30 Oct 1998 14:19:41
        • If-Modified-Since/Last-Modified:這兩個是成對出現的,屬于協商緩存的內容,其中瀏覽器的頭部是If-Modified-Since,而服務端的是Last-Modified,它的作用是,在發起請求時,如果If-Modified-SinceLast-Modified匹配,那么代表服務器資源并未改變,因此服務端不會返回資源實體,而是只返回頭部,通知瀏覽器可以使用本地緩存。Last-Modified,顧名思義,指的是文件最后的修改時間,而且只能精確到1s以內

        http1.1中的緩存控制:

        • Cache-Control:緩存控制頭部,有no-cache、max-age等多種取值
        • Max-Age:服務端配置的,用來控制強緩存,在規定的時間之內,瀏覽器無需發出請求,直接使用本地緩存,注意,Max-Age是Cache-Control頭部的值,不是獨立的頭部,譬如Cache-Control: max-age=3600,而且它值得是絕對時間,由瀏覽器自己計算
        • If-None-Match/E-tag:這兩個是成對出現的,屬于協商緩存的內容,其中瀏覽器的頭部是If-None-Match,而服務端的是E-tag,同樣,發出請求后,如果If-None-MatchE-tag匹配,則代表內容未變,通知瀏覽器使用本地緩存,和Last-Modified不同,E-tag更精確,它是類似于指紋一樣的東西,基于FileEtag INode Mtime Size生成,也就是說,只要文件變,指紋就會變,而且沒有1s精確度的限制。

        Max-Age相比Expires?

        Expires使用的是服務器端的時間

        但是有時候會有這樣一種情況-客戶端時間和服務端不同步

        那這樣,可能就會出問題了,造成了瀏覽器本地的緩存無用或者一直無法過期

        所以一般http1.1后不推薦使用Expires

        Max-Age使用的是客戶端本地時間的計算,因此不會有這個問題

        因此推薦使用Max-Age。

        注意,如果同時啟用了Cache-ControlExpires,Cache-Control優先級高。

        E-tag相比Last-Modified?

        Last-Modified

        • 表明服務端的文件最后何時改變的
        • 它有一個缺陷就是只能精確到1s,
        • 然后還有一個問題就是有的服務端的文件會周期性的改變,導致緩存失效

        E-tag

        • 是一種指紋機制,代表文件相關指紋
        • 只有文件變才會變,也只要文件變就會變,
        • 也沒有精確時間的限制,只要文件一遍,立馬E-tag就不一樣了

        如果同時帶有E-tagLast-Modified,服務端會優先檢查E-tag

        各大緩存頭部的整體關系如下圖

        解析頁面流程

        前面有提到http交互,那么接下來就是瀏覽器獲取到html,然后解析,渲染

        這部分很多都參考了網上資源,特別是圖片,參考了來源中的文章

        流程簡述

        瀏覽器內核拿到內容后,渲染步驟大致可以分為以下幾步:

        1. 解析HTML,構建DOM樹
        
        2. 解析CSS,生成CSS規則樹
        
        3. 合并DOM樹和CSS規則,生成render樹
        
        4. 布局render樹(Layout/reflow),負責各元素尺寸、位置的計算
        
        5. 繪制render樹(paint),繪制頁面像素信息
        
        6. 瀏覽器會將各層的信息發送給GPU,GPU會將各層合成(composite),顯示在屏幕上

        如下圖:

        HTML解析,構建DOM

        整個渲染步驟中,HTML解析是第一步。

        簡單的理解,這一步的流程是這樣的:瀏覽器解析HTML,構建DOM樹。

        但實際上,在分析整體構建時,卻不能一筆帶過,得稍微展開。

        解析HTML到構建出DOM當然過程可以簡述如下:

        Bytes → characters → tokens → nodes → DOM

        譬如假設有這樣一個HTML頁面:(以下部分的內容出自參考來源,修改了下格式)

        <html>
          <head>
            <meta name="viewport" content="width=device-width,initial-scale=1">
            <link href="style.css" rel="stylesheet">
            <title>Critical Path</title>
          </head>
          <body>
            <p>Hello <span>web performance</span> students!</p>
            <div><img data-original="awesome-photo.jpg"></div>
          </body>
        </html>

        瀏覽器的處理如下:

        列舉其中的一些重點過程:

        1. Conversion轉換:瀏覽器將獲得的HTML內容(Bytes)基于他的編碼轉換為單個字符
        
        2. Tokenizing分詞:瀏覽器按照HTML規范標準將這些字符轉換為不同的標記token。每個token都有自己獨特的含義以及規則集
        
        3. Lexing詞法分析:分詞的結果是得到一堆的token,此時把他們轉換為對象,這些對象分別定義他們的屬性和規則
        
        4. DOM構建:因為HTML標記定義的就是不同標簽之間的關系,這個關系就像是一個樹形結構一樣
        例如:body對象的父節點就是HTML對象,然后段略p對象的父節點就是body對象

        最后的DOM樹如下:

        生成CSS規則

        同理,CSS規則樹的生成也是類似。簡述為:

        Bytes → characters → tokens → nodes → CSSOM

        譬如style.css內容如下:

        body { font-size: 16px }
        p { font-weight: bold }
        span { color: red }
        p span { display: none }
        img { float: right }

        那么最終的CSSOM樹就是:

        構建渲染樹

        當DOM樹和CSSOM都有了后,就要開始構建渲染樹了

        一般來說,渲染樹和DOM樹相對應的,但不是嚴格意義上的一一對應

        因為有一些不可見的DOM元素不會插入到渲染樹中,如head這種不可見的標簽或者display: none

        整體來說可以看圖:

        渲染

        有了render樹,接下來就是開始渲染,基本流程如下:

        圖中重要的四個步驟就是:

        1. 計算CSS樣式
        
        2. 構建渲染樹
        
        3. 布局,主要定位坐標和大小,是否換行,各種position overflow z-index屬性
        
        4. 繪制,將圖像繪制出來

        然后,圖中的線與箭頭代表通過js動態修改了DOM或CSS,導致了重新布局(Layout)或渲染(Repaint)

        這里Layout和Repaint的概念是有區別的:

        • Layout,也稱為Reflow,即回流。一般意味著元素的內容、結構、位置或尺寸發生了變化,需要重新計算樣式和渲染樹
        • Repaint,即重繪。意味著元素發生的改變只是影響了元素的一些外觀之類的時候(例如,背景色,邊框顏色,文字顏色等),此時只需要應用新樣式繪制這個元素就可以了

        回流的成本開銷要高于重繪,而且一個節點的回流往往回導致子節點以及同級節點的回流,
        所以優化方案中一般都包括,盡量避免回流。

        什么會引起回流?

        1.頁面渲染初始化
        
        2.DOM結構改變,比如刪除了某個節點
        
        3.render樹變化,比如減少了padding
        
        4.窗口resize
        
        5.最復雜的一種:獲取某些屬性,引發回流,
        很多瀏覽器會對回流做優化,會等到數量足夠時做一次批處理回流,
        但是除了render樹的直接變化,當獲取一些屬性時,瀏覽器為了獲得正確的值也會觸發回流,這樣使得瀏覽器優化無效,包括
            (1)offset(Top/Left/Width/Height)
             (2) scroll(Top/Left/Width/Height)
             (3) cilent(Top/Left/Width/Height)
             (4) width,height
             (5) 調用了getComputedStyle()或者IE的currentStyle

        回流一定伴隨著重繪,重繪卻可以單獨出現

        所以一般會有一些優化方案,如:

        • 減少逐項更改樣式,最好一次性更改style,或者將樣式定義為class并一次性更新
        • 避免循環操作dom,創建一個documentFragment或div,在它上面應用所有DOM操作,最后再把它添加到window.document
        • 避免多次讀取offset等屬性。無法避免則將它們緩存到變量
        • 將復雜的元素絕對定位或固定定位,使得它脫離文檔流,否則回流代價會很高

        注意:改變字體大小會引發回流

        再來看一個示例:

        var s = document.body.style;
        
        s.padding = "2px"; // 回流+重繪
        s.border = "1px solid red"; // 再一次 回流+重繪
        s.color = "blue"; // 再一次重繪
        s.backgroundColor = "#ccc"; // 再一次 重繪
        s.fontSize = "14px"; // 再一次 回流+重繪
        // 添加node,再一次 回流+重繪
        document.body.appendChild(document.createTextNode('abc!'));

        簡單層與復合層

        上述中的渲染中止步于繪制,但實際上繪制這一步也沒有這么簡單,它可以結合復合層和簡單層的概念來講。

        這里不展開,進簡單介紹下:

        • 可以認為默認只有一個復合圖層,所有的DOM節點都是在這個復合圖層下的
        • 如果開啟了硬件加速功能,可以將某個節點變成復合圖層
        • 復合圖層之間的繪制互不干擾,由GPU直接控制
        • 而簡單圖層中,就算是absolute等布局,變化時不影響整體的回流,但是由于在同一個圖層中,仍然是會影響繪制的,因此做動畫時性能仍然很低。而復合層是獨立的,所以一般做動畫推薦使用硬件加速

        更多參考:

        普通圖層和復合圖層

        Chrome中的調試

        Chrome的開發者工具中,Performance中可以看到詳細的渲染過程:


        資源外鏈的下載

        上面介紹了html解析,渲染流程。但實際上,在解析html時,會遇到一些資源連接,此時就需要進行單獨處理了

        簡單起見,這里將遇到的靜態資源分為一下幾大類(未列舉所有):

        • CSS樣式資源
        • JS腳本資源
        • img圖片類資源

        遇到外鏈時的處理

        當遇到上述的外鏈時,會單獨開啟一個下載線程去下載資源(http1.1中是每一個資源的下載都要開啟一個http請求,對應一個tcp/ip鏈接)

        遇到CSS樣式資源

        CSS資源的處理有幾個特點:

        • CSS下載時異步,不會阻塞瀏覽器構建DOM樹
        • 但是會阻塞渲染,也就是在構建render時,會等到css下載解析完畢后才進行(這點與瀏覽器優化有關,防止css規則不斷改變,避免了重復的構建)
        • 有例外,media query聲明的CSS是不會阻塞渲染的

        遇到JS腳本資源

        JS腳本資源的處理有幾個特點:

        • 阻塞瀏覽器的解析,也就是說發現一個外鏈腳本時,需等待腳本下載完成并執行后才會繼續解析HTML
        • 瀏覽器的優化,一般現代瀏覽器有優化,在腳本阻塞時,也會繼續下載其它資源(當然有并發上限),但是雖然腳本可以并行下載,解析過程仍然是阻塞的,也就是說必須這個腳本執行完畢后才會接下來的解析,并行下載只是一種優化而已
        • defer與async,普通的腳本是會阻塞瀏覽器解析的,但是可以加上defer或async屬性,這樣腳本就變成異步了,可以等到解析完畢后再執行

        注意,defer和async是有區別的: defer是延遲執行,而async是異步執行。

        簡單的說(不展開):

        • async是異步執行,異步下載完畢后就會執行,不確保執行順序,一定在onload前,但不確定在DOMContentLoaded事件的前或后
        • defer是延遲執行,在瀏覽器看起來的效果像是將腳本放在了body后面一樣(雖然按規范應該是在DOMContentLoaded事件前,但實際上不同瀏覽器的優化效果不一樣,也有可能在它后面)

        遇到img圖片類資源

        遇到圖片等資源時,直接就是異步下載,不會阻塞解析,下載完畢后直接用圖片替換原有src的地方

        loaded和domcontentloaded

        簡單的對比:

        • DOMContentLoaded 事件觸發時,僅當DOM加載完成,不包括樣式表,圖片(譬如如果有async加載的腳本就不一定完成)
        • load 事件觸發時,頁面上所有的DOM,樣式表,腳本,圖片都已經加載完成了

        CSS的可視化格式模型

        這一部分內容很多參考《精通CSS-高級Web標準解決方案》以及參考來源

        前面提到了整體的渲染概念,但實際上文檔樹中的元素是按什么渲染規則渲染的,是可以進一步展開的,此部分內容即: CSS的可視化格式模型

        先了解:

        • CSS中規定每一個元素都有自己的盒子模型(相當于規定了這個元素如何顯示)
        • 然后可視化格式模型則是把這些盒子按照規則擺放到頁面上,也就是如何布局
        • 換句話說,盒子模型規定了怎么在頁面里擺放盒子,盒子的相互作用等等

        說到底: CSS的可視化格式模型就是規定了瀏覽器在頁面中如何處理文檔樹

        關鍵字:

        包含塊(Containing Block)
        控制框(Controlling Box)
        BFC(Block Formatting Context)
        IFC(Inline Formatting Context)
        定位體系
        浮動
        ...

        另外,CSS有三種定位機制:普通流,浮動,絕對定位,如無特別提及,下文中都是針對普通流中的

        包含塊(Containing Block)

        一個元素的box的定位和尺寸,會與某一矩形框有關,這個框就稱之為包含塊。

        元素會為它的子孫元素創建包含塊,但是,并不是說元素的包含塊就是它的父元素,元素的包含塊與它的祖先元素的樣式等有關系

        譬如:

        • 根元素是最頂端的元素,它沒有父節點,它的包含塊就是初始包含塊
        • static和relative的包含塊由它最近的塊級、單元格或者行內塊祖先元素的內容框(content)創建
        • fixed的包含塊是當前可視窗口
        • absolute的包含塊由它最近的position 屬性為absolute、relative或者fixed的祖先元素創建

          • 如果其祖先元素是行內元素,則包含塊取決于其祖先元素的direction特性
          • 如果祖先元素不是行內元素,那么包含塊的區域應該是祖先元素的內邊距邊界

        控制框(Controlling Box)

        塊級元素和塊框以及行內元素和行框的相關概念

        塊框:

        • 塊級元素會生成一個塊框(Block Box),塊框會占據一整行,用來包含子box和生成的內容
        • 塊框同時也是一個塊包含框(Containing Box),里面要么只包含塊框,要么只包含行內框(不能混雜),如果塊框內部有塊級元素也有行內元素,那么行內元素會被匿名塊框包圍

        關于匿名塊框的生成,示例:

        <DIV>
        Some text
        <P>More text
        </DIV>

        div生成了一個塊框,包含了另一個塊框p以及文本內容Some text,此時Some text文本會被強制加到一個匿名的塊框里面,被div生成的塊框包含(其實這個就是IFC中提到的行框,包含這些行內框的這一行匿名塊形成的框,行框和行內框不同)

        換句話說:

        如果一個塊框在其中包含另外一個塊框,那么我們強迫它只能包含塊框,因此其它文本內容生成出來的都是匿名塊框(而不是匿名行內框)

        行內框:

        • 一個行內元素生成一個行內框
        • 行內元素能排在一行,允許左右有其它元素

        關于匿名行內框的生成,示例:

        <P>Some <EM>emphasized</EM> text</P>

        P元素生成一個塊框,其中有幾個行內框(如EM),以及文本Some , text,此時會專門為這些文本生成匿名行內框

        display屬性的影響

        display的幾個屬性也可以影響不同框的生成:

        • block,元素生成一個塊框
        • inline,元素產生一個或多個的行內框
        • inline-block,元素產生一個行內級塊框,行內塊框的內部會被當作塊塊來格式化,而此元素本身會被當作行內級框來格式化(這也是為什么會產生BFC
        • none,不生成框,不再格式化結構中,當然了,另一個visibility: hidden則會產生一個不可見的框

        總結:

        • 如果一個框里,有一個塊級元素,那么這個框里的內容都會被當作塊框來進行格式化,因為只要出現了塊級元素,就會將里面的內容分塊幾塊,每一塊獨占一行(出現行內可以用匿名塊框解決)
        • 如果一個框里,沒有任何塊級元素,那么這個框里的內容會被當成行內框來格式化,因為里面的內容是按照順序成行的排列

        BFC(Block Formatting Context)

        FC(格式上下文)?

        FC即格式上下文,它定義框內部的元素渲染規則,比較抽象,譬如

        FC像是一個大箱子,里面裝有很多元素
        
        箱子可以隔開里面的元素和外面的元素(所以外部并不會影響FC內部的渲染)
        
        內部的規則可以是:如何定位,寬高計算,margin折疊等等

        不同類型的框參與的FC類型不同,譬如塊級框對應BFC,行內框對應IFC

        注意,并不是說所有的框都會產生FC,而是符合特定條件才會產生,只有產生了對應的FC后才會應用對應渲染規則

        BFC規則:

        在塊格式化上下文中
        
        每一個元素左外邊與包含塊的左邊相接觸(對于從右到左的格式化,右外邊接觸右邊)
        
        即使存在浮動也是如此(所以浮動元素正常會直接貼近它的包含塊的左邊,與普通元素重合)
        
        除非這個元素也創建了一個新的BFC

        總結幾點BFC特點:

        1. 內部box在垂直方向,一個接一個的放置
        2. box的垂直方向由margin決定,屬于同一個BFC的兩個box間的margin會重疊
        3. BFC區域不會與float box重疊(可用于排版)
        4. BFC就是頁面上的一個隔離的獨立容器,容器里面的子元素不會影響到外面的元素。反之也如此
        5. 計算BFC的高度時,浮動元素也參與計算(不會浮動坍塌)

        如何觸發BFC?

        1. 根元素
        2. float屬性不為none
        3. positionabsolutefixed
        4. displayinline-block, flex, inline-flex,table,table-cell,table-caption
        5. overflow不為visible

        這里提下,display: table,它本身不產生BFC,但是它會產生匿名框(包含display: table-cell的框),而這個匿名框產生BFC

        更多請自行網上搜索

        IFC(Inline Formatting Context)

        IFC即行內框產生的格式上下文

        IFC規則

        在行內格式化上下文中
        
        框一個接一個地水平排列,起點是包含塊的頂部。
        
        水平方向上的 margin,border 和 padding 在框之間得到保留
        
        框在垂直方向上可以以不同的方式對齊:它們的頂部或底部對齊,或根據其中文字的基線對齊

        行框

        包含那些框的長方形區域,會形成一行,叫做行框

        行框的寬度由它的包含塊和其中的浮動元素決定,高度的確定由行高度計算規則決定

        行框的規則:

        如果幾個行內框在水平方向無法放入一個行框內,它們可以分配在兩個或多個垂直堆疊的行框中(即行內框的分割)
        
        行框在堆疊時沒有垂直方向上的分割且永不重疊
        
        行框的高度總是足夠容納所包含的所有框。不過,它可能高于它包含的最高的框(例如,框對齊會引起基線對齊)
        
        行框的左邊接觸到其包含塊的左邊,右邊接觸到其包含塊的右邊。

        結合補充下IFC規則:

        浮動元素可能會處于包含塊邊緣和行框邊緣之間
        
        盡管在相同的行內格式化上下文中的行框通常擁有相同的寬度(包含塊的寬度),它們可能會因浮動元素縮短了可用寬度,而在寬度上發生變化
        
        同一行內格式化上下文中的行框通常高度不一樣(如,一行包含了一個高的圖形,而其它行只包含文本)
        
        當一行中行內框寬度的總和小于包含它們的行框的寬,它們在水平方向上的對齊,取決于 `text-align` 特性
        
        空的行內框應該被忽略
        
        即不包含文本,保留空白符,margin/padding/border非0的行內元素,
        以及其他常規流中的內容(比如,圖片,inline blocks 和 inline tables),
        并且不是以換行結束的行框,
        必須被當作零高度行框對待

        總結:

        • 行內元素總是會應用IFC渲染規則
        • 行內元素會應用IFC規則渲染,譬如text-align可以用來居中等
        • 塊框內部,對于文本這類的匿名元素,會產生匿名行框包圍,而行框內部就應用IFC渲染規則
        • 行內框內部,對于那些行內元素,一樣應用IFC渲染規則
        • 另外,inline-block,會在元素外層產生IFC(所以這個元素是可以通過text-align水平居中的),當然,它內部則按照BFC規則渲染

        相比BFC規則來說,IFC可能更加抽象(因為沒有那么條理清晰的規則和觸發條件)

        但總的來說,它就是行內元素自身如何顯示以及在框內如何擺放的渲染規則,這樣描述應該更容易理解

        其它

        當然還有有一些其它內容:

        • 譬如常規流,浮動,絕對定位等區別
        • 譬如浮動元素不包含在常規流中
        • 譬如相對定位,絕對定位,Fixed定位等區別
        • 譬如z-index的分層顯示機制等

        這里不一一展開,更多請參考:

        http://bbs.csdn.net/topics/340204423

        JS引擎解析過程

        前面有提到遇到JS腳本時,會等到它的執行,實際上是需要引擎解析的,這里展開描述(介紹主干流程)

        JS的解釋階段

        首先得明確: JS是解釋型語音,所以它無需提前編譯,而是由解釋器實時運行

        引擎對JS的處理過程可以簡述如下:

        1. 讀取代碼,進行詞法分析(Lexical analysis),然后將代碼分解成詞元(token)
        
        2. 對詞元進行語法分析(parsing),然后將代碼整理成語法樹(syntax tree)
        
        3. 使用翻譯器(translator),將代碼轉為字節碼(bytecode)
        
        4. 使用字節碼解釋器(bytecode interpreter),將字節碼轉為機器碼

        最終計算機執行的就是機器碼。

        為了提高運行速度,現代瀏覽器一般采用即時編譯(JIT-Just In Time compiler

        即字節碼只在運行時編譯,用到哪一行就編譯哪一行,并且把編譯結果緩存(inline cache

        這樣整個程序的運行速度能得到顯著提升。

        而且,不同瀏覽器策略可能還不同,有的瀏覽器就省略了字節碼的翻譯步驟,直接轉為機器碼(如chrome的v8)

        總結起來可以認為是: 核心的JIT編譯器將源碼編譯成機器碼運行

        JS的預處理階段

        上述將的是解釋器的整體過程,這里再提下在正式執行JS前,還會有一個預處理階段
        (譬如變量提升,分號補全等)

        預處理階段會做一些事情,確保JS可以正確執行,這里僅提部分:

        分號補全

        JS執行是需要分號的,但為什么以下語句卻可以正常運行呢?

        console.log('a')
        console.log('b')

        原因就是JS解釋器有一個Semicolon Insertion規則,它會按照一定規則,在適當的位置補充分號

        譬如列舉幾條自動加分號的規則:

        • 當有換行符(包括含有換行符的多行注釋),并且下一個token沒法跟前面的語法匹配時,會自動補分號。
        • 當有}時,如果缺少分號,會補分號。
        • 程序源代碼結束時,如果缺少分號,會補分號。

        于是,上述的代碼就變成了

        console.log('a');
        console.log('b');

        所以可以正常運行

        當然了,這里有一個經典的例子:

        function b() {
            return
            {
                a: 'a'
            };
        }

        由于分號補全機制,所以它變成了:

        function b() {
            return;
            {
                a: 'a'
            };
        }

        所以運行后是undefined

        變量提升

        一般包括函數提升和變量提升

        譬如:

        a = 1;
        b();
        function b() {
            console.log('b');
        }
        var a;

        經過變量提升后,就變成:

        function b() {
            console.log('b');
        }
        var a;
        a = 1;
        b();

        這里沒有展開,其實展開也可以牽涉到很多內容的

        譬如可以提下變量聲明,函數聲明,形參,實參的優先級順序,以及es6中let有關的臨時死區等

        JS的執行階段

        此階段的內容中的圖片來源:深入理解JavaScript系列(10):JavaScript核心(晉級高手必讀篇)

        解釋器解釋完語法規則后,就開始執行,然后整個執行流程中大致包含以下概念:

        • 執行上下文,執行堆棧概念(如全局上下文,當前活動上下文)
        • VO(變量對象)和AO(活動對象)
        • 作用域鏈
        • this機制等

        這些概念如果深入講解的話內容過多,因此這里僅提及部分特性

        執行上下文簡單解釋

        • JS有執行上下文
        • 瀏覽器首次載入腳本,它將創建全局執行上下文,并壓入執行棧棧頂(不可被彈出)
        • 然后每進入其它作用域就創建對應的執行上下文并把它壓入執行棧的頂部
        • 一旦對應的上下文執行完畢,就從棧頂彈出,并將上下文控制權交給當前的棧。
        • 這樣依次執行(最終都會回到全局執行上下文)

        譬如,如果程序執行完畢,被彈出執行棧,然后有沒有被引用(沒有形成閉包),那么這個函數中用到的內存就會被垃圾處理器自動回收

        然后執行上下文與VO,作用域鏈,this的關系是:

        每一個執行上下文,都有三個重要屬性:

        • 變量對象(Variable object,VO)
        • 作用域鏈(Scope chain)
        • this

        VO與AO

        VO是執行上下文的屬性(抽象概念),但是只有全局上下文的變量對象允許通過VO的屬性名稱來間接訪問(因為在全局上下文里,全局對象自身就是變量對象)

        AO(activation object),當函數被調用者激活,AO就被創建了

        可以理解為:

        • 在函數上下文中:VO === AO
        • 在全局上下文中:VO === this === global

        總的來說,VO中會存放一些變量信息(如聲明的變量,函數,arguments參數等等)

        作用域鏈

        它是執行上下文中的一個屬性,原理和原型鏈很相似,作用很重要。

        譬如流程簡述:

        在函數上下文中,查找一個變量foo
        
        如果函數的VO中找到了,就直接使用
        
        否則去它的父級作用域鏈中(__parent__)找
        
        如果父級中沒找到,繼續往上找
        
        直到全局上下文中也沒找到就報錯

        this指針

        這也是JS的核心知識之一,由于內容過多,這里就不展開,僅提及部分

        注意:this是執行上下文環境的一個屬性,而不是某個變量對象的屬性

        因此:

        • this是沒有一個類似搜尋變量的過程
        • 當代碼中使用了this,這個 this的值就直接從執行的上下文中獲取了,而不會從作用域鏈中搜尋
        • this的值只取決中進入上下文時的情況

        所以經典的例子:

        var baz = 200;
        var bar = {
            baz: 100,
            foo: function() {
                console.log(this.baz);
            }
        };
        var foo = bar.foo;
        
        // 進入環境:global
        foo(); // 200,嚴格模式中會報錯,Cannot read property 'baz' of undefined
        
        // 進入環境:global bar
        bar.foo(); // 100

        就要明白了上面this的介紹,上述例子很好理解

        更多參考:

        深入理解JavaScript系列(13):This? Yes,this!

        回收機制

        JS有垃圾處理器,所以無需手動回收內存,而是由垃圾處理器自動處理。

        一般來說,垃圾處理器有自己的回收策略。

        譬如對于那些執行完畢的函數,如果沒有外部引用(被引用的話會形成閉包),則會回收。(當然一般會把回收動作切割到不同的時間段執行,防止影響性能)

        常用的兩種垃圾回收規則是:

        • 標記清除
        • 引用計數

        Javascript引擎基礎GC方案是(simple GC):mark and sweep(標記清除),簡單解釋如下:

        1. 遍歷所有可訪問的對象。
        2. 回收已不可訪問的對象。

        譬如:(出自javascript高程)

        當變量進入環境時,例如,在函數中聲明一個變量,就將這個變量標記為“進入環境”。

        從邏輯上講,永遠不能釋放進入環境的變量所占用的內存,因為只要執行流進入相應的環境,就可能會用到它們。

        而當變量離開環境時,則將其標記為“離開環境”。

        垃圾回收器在運行的時候會給存儲在內存中的所有變量都加上標記(當然,可以使用任何標記方式)。

        然后,它會去掉環境中的變量以及被環境中的變量引用的變量的標記(閉包,也就是說在環境中的以及相關引用的變量會被去除標記)。

        而在此之后再被加上標記的變量將被視為準備刪除的變量,原因是環境中的變量已經無法訪問到這些變量了。

        最后,垃圾回收器完成內存清除工作,銷毀那些帶標記的值并回收它們所占用的內存空間。

        關于引用計數,簡單點理解:

        跟蹤記錄每個值被引用的次數,當一個值被引用時,次數+1,減持時-1,下次垃圾回收器會回收次數為0的值的內存(當然了,容易出循環引用的bug)

        GC的缺陷

        和其他語言一樣,javascript的GC策略也無法避免一個問題: GC時,停止響應其他操作

        這是為了安全考慮。

        而Javascript的GC在100ms甚至以上

        對一般的應用還好,但對于JS游戲,動畫對連貫性要求比較高的應用,就麻煩了。

        這就是引擎需要優化的點: 避免GC造成的長時間停止響應。

        GC優化策略

        這里介紹常用到的:分代回收(Generation GC)

        目的是通過區分“臨時”與“持久”對象:

        • 多回收“臨時對象”區(young generation
        • 少回收“持久對象”區(tenured generation
        • 減少每次需遍歷的對象,從而減少每次GC的耗時。

        像node v8引擎就是采用的分代回收(和java一樣,作者是java虛擬機作者。)

        更多可以參考:

        V8 內存淺析

        其它

        可以提到跨域

        譬如發出網絡請求時,會用AJAX,如果接口跨域,就會遇到跨域問題

        可以參考:

        ajax跨域,這應該是最全的解決方案了

        可以提到web安全

        譬如瀏覽器在解析HTML時,有XSSAuditor,可以延伸到web安全相關領域

        可以參考:

        AJAX請求真的不安全么?談談Web安全與AJAX的關系。

        更多

        如可以提到viewport概念,講講物理像素,邏輯像素,CSS像素等概念

        如熟悉Hybrid開發的話可以提及一下Hybrid相關內容以及優化

        ...

        總結

        上述這么多內容,目的是:梳理出自己的知識體系

        本文由于是前端向,所以知識梳理時有重點,很多其它的知識點都簡述或略去了,重點介紹的模塊總結:

        • 瀏覽器的進程/線程模型、JS運行機制(這一塊的詳細介紹鏈接到了另一篇文章)
        • http規范(包括報文結構,頭部,優化,http2.0,https等)
        • http緩存(單獨列出來,因為它很重要)
        • 頁面解析流程(HTML解析,構建DOM,生成CSS規則,構建渲染樹,渲染流程,復合層的合成,外鏈的處理等)
        • JS引擎解析過程(包括解釋階段,預處理階段,執行階段,包括執行上下文、VO、作用域鏈、this、回收機制等)
        • 跨域相關,web安全單獨鏈接到了具體文章,其它如CSS盒模型,viewport等僅是提及概念

        關于本文的價值?

        本文是個人階段性梳理知識體系的成果,然后加以修繕后發布成文章,因此并不確保適用于所有人員

        但是,個人認為本文還是有一定參考價值的

        寫在最后的話

        還是那句話:知識要形成體系

        梳理出知識體系后,有了一個骨架,知識點不易遺忘,而且學習新知識時也會更加迅速,更重要的是容易舉一反三,可以由一個普通的問題,深挖拓展到底層原理

        前端知識是無窮無盡的,本文也僅僅是簡單梳理出一個承載知識體系的骨架而已,更多的內容仍然需要不斷學習,積累

        另外,本文結合從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理這篇文章,更佳噢!

        附錄

        博客

        初次發布2018.03.12于我個人博客上面

        http://www.dailichun.com/2018/03/12/whenyouenteraurl.html

        招聘軟廣

        阿里巴巴釘釘商業化團隊大量hc,高薪股權。機會好,技術成長空間足,業務也有很大的發揮空間!

        還在猶豫什么,來吧?。?!

        社招(P6~P7)

        職責和挑戰

        1. 負責釘釘工作臺。工作臺是幫助企業實現數字化管理和協同的門戶,是擁有億級用戶量的產品。如何保障安全、穩定、性能和體驗是對我們的一大挑戰。
        2. 負責開放能力建設。針對紛繁的業務場景,提供合理的開放方案,既要做到深入用戶場景理解并支撐業務發展,滿足企業千人千面、千行千面的訴求,又要在技術上保障用戶的安全、穩定和體驗。需要既要有技術抽象能力、平臺架構能力,又要有業務的理解和分析能力。
        3. 開放平臺基礎建設。保障鏈路的安全和穩定。同時對如何保障用戶體驗有持續精進的熱情和追求。

        職位要求

        1. 精通HTML5、CSS3、JS(ES5/ES6)等前端開發技術
        2. 掌握主流的JS庫和開發框架,并深入理解其設計原理,例如React,Vue等
        3. 熟悉模塊化、前端編譯和構建工具,例如webpack、babel等
        4. (加分項)了解服務端或native移動應用開發,例如nodejs、Java等
        5. 對技術有強追求,有良好的溝通能力和團隊協同能力,有優秀的分析問題和解決問題的能力。

        前端實習

        面向2021畢業的同學

        1. 本科及以上學歷,計算機相關專業
        2. 熟練掌握HTML5/CSS3/Javascript等web前端技術
        3. 熟悉至少一種常用框架,例如React、vue等
        4. 關注新事物、新技術,有較強的學習能力,有強烈求知欲和進取心
        5. 有半年以上實際項目經驗,大廠加分

        image.png

        image.png

        內推郵箱

        lichun.dlc@alibaba-inc.com

        簡歷發我郵箱,必有回應,符合要求直接走內推?。?!

        一對一服務,有問必答!

        也可加我微信了解更多:a546684355

        參考資料

        查看原文

        贊 580 收藏 1050 評論 42

        撒網要見魚 發布了文章 · 2018-03-12

        從輸入URL到頁面加載的過程?如何由一道題完善自己的前端知識體系!

        前言

        見解有限,如有描述不當之處,請幫忙指出,如有錯誤,會及時修正。

        為什么要梳理這篇文章?

        最近恰好被問到這方面的問題,嘗試整理后發現,這道題的覆蓋面可以非常廣,很適合作為一道承載知識體系的題目。

        關于這道題目的吐槽暫且不提(這是一道被提到無數次的題,得到不少人的贊同,也被很多人反感),本文的目的是如何借助這道題梳理自己的前端知識體系!

        竊認為,每一個前端人員,如果要往更高階發展,必然會將自己的知識體系梳理一遍,沒有牢固的知識體系,無法往更高處走!

        展現形式:本文并不是將所有的知識點列一遍,而是偏向于分析+梳理

        內容:在本文中只會梳理一些比較重要的前端向知識點,其它的可能會被省略

        目標:本文的目標是梳理一個較為完整的前端向知識體系

        本文是個人階段性梳理知識體系的成果,然后加以修繕后發布成文章,因此并不確保適用于所有人員,但是,個人認為本文還是有一定參考價值的

        另外,如有不同見解,可以一起討論

        ----------超長文預警,需要花費大量時間。----------

        本文適合有一定經驗的前端人員,新手請規避。

        本文內容超多,建議先了解主干,然后分成多批次閱讀。

        本文是前端向,以前端領域的知識為重點

        大綱

        • 對知識體系進行一次預評級
        • 為什么說知識體系如此重要?
        • 梳理主干流程
        • 從瀏覽器接收url到開啟網絡請求線程

          • 多進程的瀏覽器
          • 多線程的瀏覽器內核
          • 解析URL
          • 網絡請求都是單獨的線程
          • 更多
        • 開啟網絡線程到發出一個完整的http請求

          • DNS查詢得到IP
          • tcp/ip請求
          • 五層因特網協議棧
        • 從服務器接收到請求到對應后臺接收到請求

          • 負載均衡
          • 后臺的處理
        • 后臺和前臺的http交互

          • http報文結構
          • cookie以及優化
          • gzip壓縮
          • 長連接與短連接
          • http 2.0
          • https
        • 單獨拎出來的緩存問題,http的緩存

          • 強緩存與弱緩存
          • 緩存頭部簡述
          • 頭部的區別
        • 解析頁面流程

          • 流程簡述
          • HTML解析,構建DOM
          • 生成CSS規則
          • 構建渲染樹
          • 渲染
          • 簡單層與復合層
          • Chrome中的調試
          • 資源外鏈的下載
          • loaded和domcontentloaded
        • CSS的可視化格式模型

          • 包含塊(Containing Block)
          • 控制框(Controlling Box)
          • BFC(Block Formatting Context)
          • IFC(Inline Formatting Context)
          • 其它
        • JS引擎解析過程

          • JS的解釋階段
          • JS的預處理階段
          • JS的執行階段
          • 回收機制
        • 其它
        • 總結

        對知識體系進行一次預評級

        看到這道題目,不借助搜索引擎,自己的心里是否有一個答案?

        這里,以目前的經驗(了解過一些處于不同階段的相關前端人員的情況),大概有以下幾種情況:(以下都是以點見面,實際上不同階段人員一般都會有其它的隱藏知識點的)

        level1:

        完全沒什么概念的,支支吾吾的回答,一般就是這種水平(大致形象點描述):

        • 瀏覽器發起請求,服務端返回數據,然后前端解析成網頁,執行腳本。。。

        這類人員一般都是:

        • 萌新(剛接觸前端的,包括0-6個月都有可能有這種回答)
        • 沉淀人員(就是那種可能已經接觸了前端幾年,但是仍然處于初級階段的那種。。。)

        當然了,后者一般還會偶爾提下http、后臺、瀏覽器渲染,js引擎等等關鍵字,但基本都是一詳細的問就不知道了。。。

        level2:

        已經有初步概念,但是可能沒有完整梳理過,導致無法形成一個完整的體系,或者是很多細節都不會展開,大概是這樣子的:(可能符合若干條)

        • 知道瀏覽器輸入url后會有http請求這個概念
        • 有后臺這個概念,大致知道前后端的交互,知道前后端只要靠http報文通信
        • 知道瀏覽器接收到數據后會進行解析,有一定概念,但是具體流程不熟悉(如render樹構建流程,layout、paint,復合層與簡單層,常用優化方案等不是很熟悉)
        • 對于js引擎的解析流程有一定概念,但是細節不熟悉(如具體的形參,函數,變量提升,執行上下文以及VO、AO、作用域鏈,回收機制等概念不是很熟悉)
        • 如可能知道一些http規范初步概念,但是不熟悉(如http報文結構,常用頭部,緩存機制,http2.0,https等特性,跨域與web安全等不是很熟悉)

        到這里,看到這上面一大堆的概念后,心里應該也會有點底了。。。

        實際上,大部分的前端人員可能都處于level2,但是,跳出這個階段并不容易,一般需要積累,不斷學習,才能水到渠成

        這類人員一般都是:

        • 工作1-3年左右的普通人員(占大多數,而且大多數人員工作3年左右并沒有實質上的提升)
        • 工作3年以上的老人(這部分人大多都業務十分嫻熟,一個當好幾個用,但是,基礎比較薄弱,可能沒有嘗試寫過框架、組件、腳手架等)

        大部分的初中級都陷在這個階段,如果要突破,不斷學習,積累,自然能水到渠成,打通任督二脈

        level3:

        基本能到這一步的,不是高階就是接近高階,因為很多概念并不是靠背就能理解的,而要理解這么多,需形成體系,一般都需要積累,非一日之功。

        一般包括什么樣的回答呢?(這里就以自己的簡略回答進行舉例),一般這個階段的人員都會符合若干條(不一定全部,當然可能還有些是這里遺漏的):

        • 首先略去那些鍵盤輸入、和操作系統交互、以及屏幕顯示原理、網卡等硬件交互之類的(前端向中,很多硬件原理暫時略去。。。)
        • 對瀏覽器模型有整體概念,知道瀏覽器是多進程的,瀏覽器內核是多線程的,清楚進程與線程之間得區別,以及輸入url后會開一個新的網絡線程
        • 對從開啟網絡線程到發出一個完整的http請求中間的過程有所了解(如dns查詢,tcp/ip鏈接,五層因特網協議棧等等,以及一些優化方案,如dns-prefetch
        • 對從服務器接收到請求到對應后臺接收到請求有一定了解(如負載均衡,安全攔截以及后臺代碼處理等)
        • 對后臺和前臺的http交互熟悉(包括http報文結構,場景頭部,cookie,跨域,web安全,http緩存,http2.0,https等)
        • 對瀏覽器接收到http數據包后的解析流程熟悉(包括解析html,詞法分析然后解析成dom樹、解析css生成css規則樹、合并成render樹,然后layout、painting渲染、里面可能還包括復合圖層的合成、GPU繪制、外鏈處理、加載順序等)
        • 對JS引擎解析過程熟悉(包括JS的解釋,預處理,執行上下文,VO,作用域鏈,this,回收機制等)

        可以看到,上述包括了一大堆的概念,僅僅是偏前端向,而且沒有詳細展開,就已經如此之多的概念了,所以,個人認為如果沒有自己的見解,沒有形成自己的知識體系,僅僅是看看,背背是沒用的,過一段時間就會忘光了。

        再說下一般這個階段的都可能是什么樣的人吧。(不一定準確,這里主要是靠少部分現實以及大部分推測得出)

        • 工作2年以上的前端(基本上如果按正常進度的話,至少接觸前端兩年左右才會開始走向高階,當然,現在很多都是上學時就開始學了的,還有部分是天賦異稟,不好預估。。。)
        • 或者是已經十分熟悉其它某門語言,再轉前端的人(基本上是很快就可以將前端水準提升上去)

        一般符合這個條件的都會有各種隱藏屬性(如看過各大框架、組件的源碼,寫過自己的組件、框架、腳手架,做過大型項目,整理過若干精品博文等)

        level4:

        由于本人層次尚未達到,所以大致說下自己的見解吧。

        一般這個層次,很多大佬都并不僅僅是某個技術棧了,而是成為了技術專家,技術leader之類的角色。所以僅僅是回答某個技術問題已經無法看出水準了,
        可能更多的要看架構,整體把控,大型工程構建能力等等

        不過,對于某些執著于技術的大佬,大概會有一些回答吧:(猜的)

        • 從鍵盤談起到系統交互,從瀏覽器到CPU,從調度機制到系統內核,從數據請求到二進制、匯編,從GPU繪圖到LCD顯示,然后再分析系統底層的進程、內存等等

        總之,從軟件到硬件,到材料,到分子,原子,量子,薛定諤的貓,人類起源,宇宙大爆炸,平行宇宙?感覺都毫無違和感。。。

        這點可以參考下本題的原始出處:

        http://fex.baidu.com/blog/2014/05/what-happen/

        為什么說知識體系如此重要?

        為什么說知識體系如此重要呢?這里舉幾個例子

        假設有被問到這樣一道題目(隨意想到的一個):

        • 如何理解getComputedStyle

        在尚未梳理知識體系前,大概會這樣回答:

        • 普通版本:getComputedStyle會獲取當前元素所有最終使用的CSS屬性值(最終計算后的結果),通過window.getComputedStyle等價于document.defaultView.getComputedStyle調用
        • 詳細版本:window.getComputedStyle(elem, null).getPropertyValue("height")可能的值為100px,而且,就算是css上寫的是inherit,getComputedStyle也會把它最終計算出來的。不過注意,如果元素的背景色透明,那么getComputedStyle獲取出來的就是透明的這個背景(因為透明本身也是有效的),而不會是父節點的背景。所以它不一定是最終顯示的顏色。

        就這個API來說,上述的回答已經比較全面了。

        但是,其實它是可以繼續延伸的。

        譬如現在會這樣回答:

        • getComputedStyle會獲取當前元素所有最終使用的CSS屬性值,window.document.defaultView.等價...
        • getComputedStyle會引起回流,因為它需要獲取祖先節點的一些信息進行計算(譬如寬高等),所以用的時候慎用,回流會引起性能問題。然后合適的話會將話題引導回流,重繪,瀏覽器渲染原理等等。當然也可以列舉一些其它會引發回流的操作,如offsetXXX,scrollXXX,clientXXX,currentStyle等等

        再舉一個例子:

        • visibility: hiddendisplay: none的區別

        可以如下回答:

        • 普通回答,一個隱藏,但占據位置,一個隱藏,不占據位置
        • 進一步,display由于隱藏后不占據位置,所以造成了dom樹的改變,會引發回流,代價較大
        • 再進一步,當一個頁面某個元素經常需要切換display時如何優化,一般會用復合層優化,或者要求低一點用absolute讓其脫離普通文檔流也行。然后可以將話題引到普通文檔流,absolute文檔流,復合圖層的區別,
        • 再進一步可以描述下瀏覽器渲染原理以及復合圖層和普通圖層的繪制區別(復合圖層單獨分配資源,獨立繪制,性能提升,但是不能過多,還有隱式合成等等)

        上面這些大概就是知識系統化后的回答,會更全面,容易由淺入深,而且一有機會就可以往更底層挖

        前端向知識的重點

        此部分的內容是站在個人視角分析的,并不是說就一定是正確答案

        首先明確,計算機方面的知識是可以無窮無盡的挖的,而本文的重點是梳理前端向的重點知識

        對于前端向(這里可能沒有提到node.js之類的,更多的是指客戶端前端),這里將知識點按重要程度劃分成以下幾大類:

        • 核心知識,必須掌握的,也是最基礎的,譬如瀏覽器模型,渲染原理,JS解析過程,JS運行機制等,作為骨架來承載知識體系
        • 重點知識,往往每一塊都是一個知識點,而且這些知識點都很重要,譬如http相關,web安全相關,跨域處理等
        • 拓展知識,這一塊可能更多的是了解,稍微實踐過,但是認識上可能沒有上面那么深刻,譬如五層因特網協議棧,hybrid模式,移動原生開發,后臺相關等等(當然,在不同領域,可能有某些知識就上升到重點知識層次了,譬如hybrid開發時,懂原生開發是很重要的)

        為什么要按上面這種方式劃分?

        這大概與個人的技術成長有關。

        記得最開始學前端知識時,是一點一點的積累,一個知識點一個知識點的攻克。

        就這樣,雖然在很長一段時間內積累了不少的知識,但是,總是無法將它串聯到一起。每次梳理時都是很分散的,無法保持思路連貫性。

        直到后來,在將瀏覽器渲染原理、JS運行機制、JS引擎解析流程梳理一遍后,感覺就跟打通了任督二脈一樣,有了一個整體的架構,以前的知識點都連貫起來了。

        梳理出了一個知識體系,以后就算再學新的知識,也會盡量往這個體系上靠攏,環環相扣,更容易理解,也更不容易遺忘

        梳理主干流程

        回到這道題上,如何回答呢?先梳理一個骨架

        知識體系中,最重要的是骨架,脈絡。有了骨架后,才方便填充細節。所以,先梳理下主干流程:

        1. 從瀏覽器接收url到開啟網絡請求線程(這一部分可以展開瀏覽器的機制以及進程與線程之間的關系)
        
        2. 開啟網絡線程到發出一個完整的http請求(這一部分涉及到dns查詢,tcp/ip請求,五層因特網協議棧等知識)
        
        3. 從服務器接收到請求到對應后臺接收到請求(這一部分可能涉及到負載均衡,安全攔截以及后臺內部的處理等等)
        
        4. 后臺和前臺的http交互(這一部分包括http頭部、響應碼、報文結構、cookie等知識,可以提下靜態資源的cookie優化,以及編碼解碼,如gzip壓縮等)
        
        5. 單獨拎出來的緩存問題,http的緩存(這部分包括http緩存頭部,etag,catch-control等)
        
        6. 瀏覽器接收到http數據包后的解析流程(解析html-詞法分析然后解析成dom樹、解析css生成css規則樹、合并成render樹,然后layout、painting渲染、復合圖層的合成、GPU繪制、外鏈資源的處理、loaded和domcontentloaded等)
        
        7. CSS的可視化格式模型(元素的渲染規則,如包含塊,控制框,BFC,IFC等概念)
        
        8. JS引擎解析過程(JS的解釋階段,預處理階段,執行階段生成執行上下文,VO,作用域鏈、回收機制等等)
        
        9. 其它(可以拓展不同的知識模塊,如跨域,web安全,hybrid模式等等內容)

        梳理出主干骨架,然后就需要往骨架上填充細節內容

        從瀏覽器接收url到開啟網絡請求線程

        這一部分展開的內容是:瀏覽器進程/線程模型,JS的運行機制

        多進程的瀏覽器

        瀏覽器是多進程的,有一個主控進程,以及每一個tab頁面都會新開一個進程(某些情況下多個tab會合并進程)

        進程可能包括主控進程,插件進程,GPU,tab頁(瀏覽器內核)等等

        • Browser進程:瀏覽器的主進程(負責協調、主控),只有一個
        • 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建
        • GPU進程:最多一個,用于3D繪制
        • 瀏覽器渲染進程(內核):默認每個Tab頁面一個進程,互不影響,控制頁面渲染,腳本執行,事件處理等(有時候會優化,如多個空白tab會合并成一個進程)

        如下圖:

        多線程的瀏覽器內核

        每一個tab頁面可以看作是瀏覽器內核進程,然后這個進程是多線程的,它有幾大類子線程

        • GUI線程
        • JS引擎線程
        • 事件觸發線程
        • 定時器線程
        • 網絡請求線程

        可以看到,里面的JS引擎是內核進程中的一個線程,這也是為什么常說JS引擎是單線程的

        解析URL

        輸入URL后,會進行解析(URL的本質就是統一資源定位符)

        URL一般包括幾大部分:

        • protocol,協議頭,譬如有http,ftp等
        • host,主機域名或IP地址
        • port,端口號
        • path,目錄路徑
        • query,即查詢參數
        • fragment,即#后的hash值,一般用來定位到某個位置

        網絡請求都是單獨的線程

        每次網絡請求時都需要開辟單獨的線程進行,譬如如果URL解析到http協議,就會新建一個網絡線程去處理資源下載

        因此瀏覽器會根據解析出得協議,開辟一個網絡線程,前往請求資源(這里,暫時理解為是瀏覽器內核開辟的,如有錯誤,后續修復)

        更多

        由于篇幅關系,這里就大概介紹一個主干流程,關于瀏覽器的進程機制,更多可以參考以前總結的一篇文章(因為內容實在過多,里面包括JS運行機制,進程線程的詳解)

        從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理

        開啟網絡線程到發出一個完整的http請求

        這一部分主要內容包括:dns查詢,tcp/ip請求構建,五層因特網協議棧等等

        仍然是先梳理主干,有些詳細的過程不展開(因為展開的話內容過多)

        DNS查詢得到IP

        如果輸入的是域名,需要進行dns解析成IP,大致流程:

        • 如果瀏覽器有緩存,直接使用瀏覽器緩存,否則使用本機緩存,再沒有的話就是用host
        • 如果本地沒有,就向dns域名服務器查詢(當然,中間可能還會經過路由,也有緩存等),查詢到對應的IP

        注意,域名查詢時有可能是經過了CDN調度器的(如果有cdn存儲功能的話)

        而且,需要知道dns解析是很耗時的,因此如果解析域名過多,會讓首屏加載變得過慢,可以考慮dns-prefetch優化

        這一塊可以深入展開,具體請去網上搜索,這里就不占篇幅了(網上可以看到很詳細的解答)

        tcp/ip請求

        http的本質就是tcp/ip請求

        需要了解3次握手規則建立連接以及斷開連接時的四次揮手

        tcp將http長報文劃分為短報文,通過三次握手與服務端建立連接,進行可靠傳輸

        三次握手的步驟:(抽象派)

        客戶端:hello,你是server么?
        服務端:hello,我是server,你是client么
        客戶端:yes,我是client

        建立連接成功后,接下來就正式傳輸數據

        然后,待到斷開連接時,需要進行四次揮手(因為是全雙工的,所以需要四次揮手)

        四次揮手的步驟:(抽象派)

        主動方:我已經關閉了向你那邊的主動通道了,只能被動接收了
        被動方:收到通道關閉的信息
        被動方:那我也告訴你,我這邊向你的主動通道也關閉了
        主動方:最后收到數據,之后雙方無法通信

        tcp/ip的并發限制

        瀏覽器對同一域名下并發的tcp連接是有限制的(2-10個不等)

        而且在http1.0中往往一個資源下載就需要對應一個tcp/ip請求

        所以針對這個瓶頸,又出現了很多的資源優化方案

        get和post的區別

        get和post雖然本質都是tcp/ip,但兩者除了在http層面外,在tcp/ip層面也有區別。

        get會產生一個tcp數據包,post兩個

        具體就是:

        • get請求時,瀏覽器會把headersdata一起發送出去,服務器響應200(返回數據),
        • post請求時,瀏覽器先發送headers,服務器響應100 continue,

        瀏覽器再發送data,服務器響應200(返回數據)。

        再說一點,這里的區別是specification(規范)層面,而不是implementation(對規范的實現)

        五層因特網協議棧

        其實這個概念挺難記全的,記不全沒關系,但是要有一個整體概念

        其實就是一個概念: 從客戶端發出http請求到服務器接收,中間會經過一系列的流程。

        簡括就是:

        從應用層的發送http請求,到傳輸層通過三次握手建立tcp/ip連接,再到網絡層的ip尋址,再到數據鏈路層的封裝成幀,最后到物理層的利用物理介質傳輸。

        當然,服務端的接收就是反過來的步驟

        五層因特網協議棧其實就是:

        1.應用層(dns,http) DNS解析成IP并發送http請求
        
        2.傳輸層(tcp,udp) 建立tcp連接(三次握手)
        
        3.網絡層(IP,ARP) IP尋址
        
        4.數據鏈路層(PPP) 封裝成幀
        
        5.物理層(利用物理介質傳輸比特流) 物理傳輸(然后傳輸的時候通過雙絞線,電磁波等各種介質)

        當然,其實也有一個完整的OSI七層框架,與之相比,多了會話層、表示層。

        OSI七層框架:物理層、數據鏈路層、網絡層、傳輸層、會話層、表示層、應用層

        表示層:主要處理兩個通信系統中交換信息的表示方式,包括數據格式交換,數據加密與解密,數據壓縮與終端類型轉換等
        
        會話層:它具體管理不同用戶和進程之間的對話,如控制登陸和注銷過程

        從服務器接收到請求到對應后臺接收到請求

        服務端在接收到請求時,內部會進行很多的處理

        這里由于不是專業的后端分析,所以只是簡單的介紹下,不深入

        負載均衡

        對于大型的項目,由于并發訪問量很大,所以往往一臺服務器是吃不消的,所以一般會有若干臺服務器組成一個集群,然后配合反向代理實現負載均衡

        當然了,負載均衡不止這一種實現方式,這里不深入...

        簡單的說:

        用戶發起的請求都指向調度服務器(反向代理服務器,譬如安裝了nginx控制負載均衡),然后調度服務器根據實際的調度算法,分配不同的請求給對應集群中的服務器執行,然后調度器等待實際服務器的HTTP響應,并將它反饋給用戶

        后臺的處理

        一般后臺都是部署到容器中的,所以一般為:

        • 先是容器接受到請求(如tomcat容器)
        • 然后對應容器中的后臺程序接收到請求(如java程序)
        • 然后就是后臺會有自己的統一處理,處理完后響應響應結果

        概括下:

        • 一般有的后端是有統一的驗證的,如安全攔截,跨域驗證
        • 如果這一步不符合規則,就直接返回了相應的http報文(如拒絕請求等)
        • 然后當驗證通過后,才會進入實際的后臺代碼,此時是程序接收到請求,然后執行(譬如查詢數據庫,大量計算等等)
        • 等程序執行完畢后,就會返回一個http響應包(一般這一步也會經過多層封裝)
        • 然后就是將這個包從后端發送到前端,完成交互

        后臺和前臺的http交互

        前后端交互時,http報文作為信息的載體

        所以http是一塊很重要的內容,這一部分重點介紹它

        http報文結構

        報文一般包括了:通用頭部,請求/響應頭部,請求/響應體

        通用頭部

        這也是開發人員見過的最多的信息,包括如下:

        Request Url: 請求的web服務器地址
        
        Request Method: 請求方式
        (Get、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE)
        
        Status Code: 請求的返回狀態碼,如200代表成功
        
        Remote Address: 請求的遠程服務器地址(會轉為IP)

        譬如,在跨域拒絕時,可能是method為options,狀態碼為404/405等(當然,實際上可能的組合有很多)

        其中,Method的話一般分為兩批次:

        HTTP1.0定義了三種請求方法: GET, POST 和 HEAD方法。
        以及幾種Additional Request Methods:PUT、DELETE、LINK、UNLINK
        
        HTTP1.1定義了八種請求方法:GET、POST、HEAD、OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

        HTTP 1.0定義參考:https://tools.ietf.org/html/rfc1945

        HTTP 1.1定義參考:https://tools.ietf.org/html/rfc2616

        這里面最常用到的就是狀態碼,很多時候都是通過狀態碼來判斷,如(列舉幾個最常見的):

        200——表明該請求被成功地完成,所請求的資源發送回客戶端
        304——自從上次請求后,請求的網頁未修改過,請客戶端使用本地緩存
        400——客戶端請求有錯(譬如可以是安全模塊攔截)
        401——請求未經授權
        403——禁止訪問(譬如可以是未登錄時禁止)
        404——資源未找到
        500——服務器內部錯誤
        503——服務不可用
        ...

        再列舉下大致不同范圍狀態的意義

        1xx——指示信息,表示請求已接收,繼續處理
        2xx——成功,表示請求已被成功接收、理解、接受
        3xx——重定向,要完成請求必須進行更進一步的操作
        4xx——客戶端錯誤,請求有語法錯誤或請求無法實現
        5xx——服務器端錯誤,服務器未能實現合法的請求

        總之,當請求出錯時,狀態碼能幫助快速定位問題,完整版本的狀態可以自行去互聯網搜索

        請求/響應頭部

        請求和響應頭部也是分析時常用到的

        常用的請求頭部(部分):

        Accept: 接收類型,表示瀏覽器支持的MIME類型
        (對標服務端返回的Content-Type)
        Accept-Encoding:瀏覽器支持的壓縮類型,如gzip等,超出類型不能接收
        Content-Type:客戶端發送出去實體內容的類型
        Cache-Control: 指定請求和響應遵循的緩存機制,如no-cache
        If-Modified-Since:對應服務端的Last-Modified,用來匹配看文件是否變動,只能精確到1s之內,http1.0中
        Expires:緩存控制,在這個時間內不會請求,直接使用緩存,http1.0,而且是服務端時間
        Max-age:代表資源在本地緩存多少秒,有效時間內不會請求,而是使用緩存,http1.1中
        If-None-Match:對應服務端的ETag,用來匹配文件內容是否改變(非常精確),http1.1中
        Cookie: 有cookie并且同域訪問時會自動帶上
        Connection: 當瀏覽器與服務器通信時對于長連接如何進行處理,如keep-alive
        Host:請求的服務器URL
        Origin:最初的請求是從哪里發起的(只會精確到端口),Origin比Referer更尊重隱私
        Referer:該頁面的來源URL(適用于所有類型的請求,會精確到詳細頁面地址,csrf攔截常用到這個字段)
        User-Agent:用戶客戶端的一些必要信息,如UA頭部等

        常用的響應頭部(部分):

        Access-Control-Allow-Headers: 服務器端允許的請求Headers
        Access-Control-Allow-Methods: 服務器端允許的請求方法
        Access-Control-Allow-Origin: 服務器端允許的請求Origin頭部(譬如為*)
        Content-Type:服務端返回的實體內容的類型
        Date:數據從服務器發送的時間
        Cache-Control:告訴瀏覽器或其他客戶,什么環境可以安全的緩存文檔
        Last-Modified:請求資源的最后修改時間
        Expires:應該在什么時候認為文檔已經過期,從而不再緩存它
        Max-age:客戶端的本地資源應該緩存多少秒,開啟了Cache-Control后有效
        ETag:請求變量的實體標簽的當前值
        Set-Cookie:設置和頁面關聯的cookie,服務器通過這個頭部把cookie傳給客戶端
        Keep-Alive:如果客戶端有keep-alive,服務端也會有響應(如timeout=38)
        Server:服務器的一些相關信息

        一般來說,請求頭部和響應頭部是匹配分析的。

        譬如,請求頭部的Accept要和響應頭部的Content-Type匹配,否則會報錯

        譬如,跨域請求時,請求頭部的Origin要匹配響應頭部的Access-Control-Allow-Origin,否則會報跨域錯誤

        譬如,在使用緩存時,請求頭部的If-Modified-Since、If-None-Match分別和響應頭部的Last-Modified、ETag對應

        還有很多的分析方法,這里不一一贅述

        請求/響應實體

        http請求時,除了頭部,還有消息實體,一般來說

        請求實體中會將一些需要的參數都放入進入(用于post請求)。

        譬如實體中可以放參數的序列化形式(a=1&b=2這種),或者直接放表單對象(Form Data對象,上傳時可以夾雜參數以及文件),等等

        而一般響應實體中,就是放服務端需要傳給客戶端的內容

        一般現在的接口請求時,實體中就是對于的信息的json格式,而像頁面請求這種,里面就是直接放了一個html字符串,然后瀏覽器自己解析并渲染。

        CRLF

        CRLF(Carriage-Return Line-Feed),意思是回車換行,一般作為分隔符存在

        請求頭和實體消息之間有一個CRLF分隔,響應頭部和響應實體之間用一個CRLF分隔

        一般來說(分隔符類別):

        CRLF->Windows-style
        LF->Unix Style
        CR->Mac Style

        如下圖是對某請求的http報文結構的簡要分析

        cookie以及優化

        cookie是瀏覽器的一種本地存儲方式,一般用來幫助客戶端和服務端通信的,常用來進行身份校驗,結合服務端的session使用。

        場景如下(簡述):

        在登陸頁面,用戶登陸了
        
        此時,服務端會生成一個session,session中有對于用戶的信息(如用戶名、密碼等)
        
        然后會有一個sessionid(相當于是服務端的這個session對應的key)
        
        然后服務端在登錄頁面中寫入cookie,值就是:jsessionid=xxx
        
        然后瀏覽器本地就有這個cookie了,以后訪問同域名下的頁面時,自動帶上cookie,自動檢驗,在有效時間內無需二次登陸。

        上述就是cookie的常用場景簡述(當然了,實際情況下得考慮更多因素)

        一般來說,cookie是不允許存放敏感信息的(千萬不要明文存儲用戶名、密碼),因為非常不安全,如果一定要強行存儲,首先,一定要在cookie中設置httponly(這樣就無法通過js操作了),另外可以考慮rsa等非對稱加密(因為實際上,瀏覽器本地也是容易被攻克的,并不安全)

        另外,由于在同域名的資源請求時,瀏覽器會默認帶上本地的cookie,針對這種情況,在某些場景下是需要優化的。

        譬如以下場景:

        客戶端在域名A下有cookie(這個可以是登陸時由服務端寫入的)
        
        然后在域名A下有一個頁面,頁面中有很多依賴的靜態資源(都是域名A的,譬如有20個靜態資源)
        
        此時就有一個問題,頁面加載,請求這些靜態資源時,瀏覽器會默認帶上cookie
        
        也就是說,這20個靜態資源的http請求,每一個都得帶上cookie,而實際上靜態資源并不需要cookie驗證
        
        此時就造成了較為嚴重的浪費,而且也降低了訪問速度(因為內容更多了)

        當然了,針對這種場景,是有優化方案的(多域名拆分)。具體做法就是:

        • 將靜態資源分組,分別放到不同的域名下(如static.base.com
        • page.base.com(頁面所在域名)下請求時,是不會帶上static.base.com域名的cookie的,所以就避免了浪費

        說到了多域名拆分,這里再提一個問題,那就是:

        • 在移動端,如果請求的域名數過多,會降低請求速度(因為域名整套解析流程是很耗費時間的,而且移動端一般帶寬都比不上pc)
        • 此時就需要用到一種優化方案:dns-prefetch(讓瀏覽器空閑時提前解析dns域名,不過也請合理使用,勿濫用)

        關于cookie的交互,可以看下圖總結

        gzip壓縮

        首先,明確gzip是一種壓縮格式,需要瀏覽器支持才有效(不過一般現在瀏覽器都支持),
        而且gzip壓縮效率很好(高達70%左右)

        然后gzip一般是由apache、tomcat等web服務器開啟

        當然服務器除了gzip外,也還會有其它壓縮格式(如deflate,沒有gzip高效,且不流行)

        所以一般只需要在服務器上開啟了gzip壓縮,然后之后的請求就都是基于gzip壓縮格式的,
        非常方便。

        長連接與短連接

        首先看tcp/ip層面的定義:

        • 長連接:一個tcp/ip連接上可以連續發送多個數據包,在tcp連接保持期間,如果沒有數據包發送,需要雙方發檢測包以維持此連接,一般需要自己做在線維持(類似于心跳包)
        • 短連接:通信雙方有數據交互時,就建立一個tcp連接,數據發送完成后,則斷開此tcp連接

        然后在http層面:

        • http1.0中,默認使用的是短連接,也就是說,瀏覽器沒進行一次http操作,就建立一次連接,任務結束就中斷連接,譬如每一個靜態資源請求時都是一個單獨的連接
        • http1.1起,默認使用長連接,使用長連接會有這一行Connection: keep-alive,在長連接的情況下,當一個網頁打開完成后,客戶端和服務端之間用于傳輸http的tcp連接不會關閉,如果客戶端再次訪問這個服務器的頁面,會繼續使用這一條已經建立的連接

        注意: keep-alive不會永遠保持,它有一個持續時間,一般在服務器中配置(如apache),另外長連接需要客戶端和服務器都支持時才有效

        http 2.0

        http2.0不是https,它相當于是http的下一代規范(譬如https的請求可以是http2.0規范的)

        然后簡述下http2.0與http1.1的顯著不同點:

        • http1.1中,每請求一個資源,都是需要開啟一個tcp/ip連接的,所以對應的結果是,每一個資源對應一個tcp/ip請求,由于tcp/ip本身有并發數限制,所以當資源一多,速度就顯著慢下來
        • http2.0中,一個tcp/ip請求可以請求多個資源,也就是說,只要一次tcp/ip請求,就可以請求若干個資源,分割成更小的幀請求,速度明顯提升。

        所以,如果http2.0全面應用,很多http1.1中的優化方案就無需用到了(譬如打包成精靈圖,靜態資源多域名拆分等)

        然后簡述下http2.0的一些特性:

        • 多路復用(即一個tcp/ip連接可以請求多個資源)
        • 首部壓縮(http頭部壓縮,減少體積)
        • 二進制分幀(在應用層跟傳送層之間增加了一個二進制分幀層,改進傳輸性能,實現低延遲和高吞吐量)
        • 服務器端推送(服務端可以對客戶端的一個請求發出多個響應,可以主動通知客戶端)
        • 請求優先級(如果流被賦予了優先級,它就會基于這個優先級來處理,由服務器決定需要多少資源來處理該請求。)

        https

        https就是安全版本的http,譬如一些支付等操作基本都是基于https的,因為http請求的安全系數太低了。

        簡單來看,https與http的區別就是: 在請求前,會建立ssl鏈接,確保接下來的通信都是加密的,無法被輕易截取分析

        一般來說,如果要將網站升級成https,需要后端支持(后端需要申請證書等),然后https的開銷也比http要大(因為需要額外建立安全鏈接以及加密等),所以一般來說http2.0配合https的體驗更佳(因為http2.0更快了)

        一般來說,主要關注的就是SSL/TLS的握手流程,如下(簡述):

        1. 瀏覽器請求建立SSL鏈接,并向服務端發送一個隨機數–Client random和客戶端支持的加密方法,比如RSA加密,此時是明文傳輸。 
        
        2. 服務端從中選出一組加密算法與Hash算法,回復一個隨機數–Server random,并將自己的身份信息以證書的形式發回給瀏覽器
        (證書里包含了網站地址,非對稱加密的公鑰,以及證書頒發機構等信息)
        
        3. 瀏覽器收到服務端的證書后
            
            - 驗證證書的合法性(頒發機構是否合法,證書中包含的網址是否和正在訪問的一樣),如果證書信任,則瀏覽器會顯示一個小鎖頭,否則會有提示
            
            - 用戶接收證書后(不管信不信任),瀏覽會生產新的隨機數–Premaster secret,然后證書中的公鑰以及指定的加密方法加密`Premaster secret`,發送給服務器。
            
            - 利用Client random、Server random和Premaster secret通過一定的算法生成HTTP鏈接數據傳輸的對稱加密key-`session key`
            
            - 使用約定好的HASH算法計算握手消息,并使用生成的`session key`對消息進行加密,最后將之前生成的所有信息發送給服務端。 
            
        4. 服務端收到瀏覽器的回復
        
            - 利用已知的加解密方式與自己的私鑰進行解密,獲取`Premaster secret`
            
            - 和瀏覽器相同規則生成`session key`
            
            - 使用`session key`解密瀏覽器發來的握手消息,并驗證Hash是否與瀏覽器發來的一致
            
            - 使用`session key`加密一段握手消息,發送給瀏覽器
            
        5. 瀏覽器解密并計算握手消息的HASH,如果與服務端發來的HASH一致,此時握手過程結束,

        之后所有的https通信數據將由之前瀏覽器生成的session key并利用對稱加密算法進行加密

        這里放一張圖(來源:阮一峰-圖解SSL/TLS協議

        單獨拎出來的緩存問題,http的緩存

        前后端的http交互中,使用緩存能很大程度上的提升效率,而且基本上對性能有要求的前端項目都是必用緩存的

        強緩存與弱緩存

        緩存可以簡單的劃分成兩種類型:強緩存200 from cache)與協商緩存304

        區別簡述如下:

        • 強緩存(200 from cache)時,瀏覽器如果判斷本地緩存未過期,就直接使用,無需發起http請求
        • 協商緩存(304)時,瀏覽器會向服務端發起http請求,然后服務端告訴瀏覽器文件未改變,讓瀏覽器使用本地緩存

        對于協商緩存,使用Ctrl + F5強制刷新可以使得緩存無效

        但是對于強緩存,在未過期時,必須更新資源路徑才能發起新的請求(更改了路徑相當于是另一個資源了,這也是前端工程化中常用到的技巧)

        緩存頭部簡述

        上述提到了強緩存和協商緩存,那它們是怎么區分的呢?

        答案是通過不同的http頭部控制

        先看下這幾個頭部:

        If-None-Match/E-tag、If-Modified-Since/Last-Modified、Cache-Control/Max-Age、Pragma/Expires

        這些就是緩存中常用到的頭部,這里不展開。僅列舉下大致使用。

        屬于強緩存控制的:

        (http1.1)Cache-Control/Max-Age
        (http1.0)Pragma/Expires

        注意:Max-Age不是一個頭部,它是Cache-Control頭部的值

        屬于協商緩存控制的:

        (http1.1)If-None-Match/E-tag
        (http1.0)If-Modified-Since/Last-Modified

        可以看到,上述有提到http1.1http1.0,這些不同的頭部是屬于不同http時期的

        再提一點,其實HTML頁面中也有一個meta標簽可以控制緩存方案-Pragma

        <META HTTP-EQUIV="Pragma" CONTENT="no-cache">

        不過,這種方案還是比較少用到,因為支持情況不佳,譬如緩存代理服務器肯定不支持,所以不推薦

        頭部的區別

        首先明確,http的發展是從http1.0到http1.1

        而在http1.1中,出了一些新內容,彌補了http1.0的不足。

        http1.0中的緩存控制:

        • Pragma:嚴格來說,它不屬于專門的緩存控制頭部,但是它設置no-cache時可以讓本地強緩存失效(屬于編譯控制,來實現特定的指令,主要是因為兼容http1.0,所以以前又被大量應用)
        • Expires:服務端配置的,屬于強緩存,用來控制在規定的時間之前,瀏覽器不會發出請求,而是直接使用本地緩存,注意,Expires一般對應服務器端時間,如Expires:Fri, 30 Oct 1998 14:19:41
        • If-Modified-Since/Last-Modified:這兩個是成對出現的,屬于協商緩存的內容,其中瀏覽器的頭部是If-Modified-Since,而服務端的是Last-Modified,它的作用是,在發起請求時,如果If-Modified-SinceLast-Modified匹配,那么代表服務器資源并未改變,因此服務端不會返回資源實體,而是只返回頭部,通知瀏覽器可以使用本地緩存。Last-Modified,顧名思義,指的是文件最后的修改時間,而且只能精確到1s以內

        http1.1中的緩存控制:

        • Cache-Control:緩存控制頭部,有no-cache、max-age等多種取值
        • Max-Age:服務端配置的,用來控制強緩存,在規定的時間之內,瀏覽器無需發出請求,直接使用本地緩存,注意,Max-Age是Cache-Control頭部的值,不是獨立的頭部,譬如Cache-Control: max-age=3600,而且它值得是絕對時間,由瀏覽器自己計算
        • If-None-Match/E-tag:這兩個是成對出現的,屬于協商緩存的內容,其中瀏覽器的頭部是If-None-Match,而服務端的是E-tag,同樣,發出請求后,如果If-None-MatchE-tag匹配,則代表內容未變,通知瀏覽器使用本地緩存,和Last-Modified不同,E-tag更精確,它是類似于指紋一樣的東西,基于FileEtag INode Mtime Size生成,也就是說,只要文件變,指紋就會變,而且沒有1s精確度的限制。

        Max-Age相比Expires?

        Expires使用的是服務器端的時間

        但是有時候會有這樣一種情況-客戶端時間和服務端不同步

        那這樣,可能就會出問題了,造成了瀏覽器本地的緩存無用或者一直無法過期

        所以一般http1.1后不推薦使用Expires

        Max-Age使用的是客戶端本地時間的計算,因此不會有這個問題

        因此推薦使用Max-Age。

        注意,如果同時啟用了Cache-ControlExpires,Cache-Control優先級高。

        E-tag相比Last-Modified?

        Last-Modified

        • 表明服務端的文件最后何時改變的
        • 它有一個缺陷就是只能精確到1s,
        • 然后還有一個問題就是有的服務端的文件會周期性的改變,導致緩存失效

        E-tag

        • 是一種指紋機制,代表文件相關指紋
        • 只有文件變才會變,也只要文件變就會變,
        • 也沒有精確時間的限制,只要文件一遍,立馬E-tag就不一樣了

        如果同時帶有E-tagLast-Modified,服務端會優先檢查E-tag

        各大緩存頭部的整體關系如下圖

        解析頁面流程

        前面有提到http交互,那么接下來就是瀏覽器獲取到html,然后解析,渲染

        這部分很多都參考了網上資源,特別是圖片,參考了來源中的文章

        流程簡述

        瀏覽器內核拿到內容后,渲染步驟大致可以分為以下幾步:

        1. 解析HTML,構建DOM樹
        
        2. 解析CSS,生成CSS規則樹
        
        3. 合并DOM樹和CSS規則,生成render樹
        
        4. 布局render樹(Layout/reflow),負責各元素尺寸、位置的計算
        
        5. 繪制render樹(paint),繪制頁面像素信息
        
        6. 瀏覽器會將各層的信息發送給GPU,GPU會將各層合成(composite),顯示在屏幕上

        如下圖:

        HTML解析,構建DOM

        整個渲染步驟中,HTML解析是第一步。

        簡單的理解,這一步的流程是這樣的:瀏覽器解析HTML,構建DOM樹。

        但實際上,在分析整體構建時,卻不能一筆帶過,得稍微展開。

        解析HTML到構建出DOM當然過程可以簡述如下:

        Bytes → characters → tokens → nodes → DOM

        譬如假設有這樣一個HTML頁面:(以下部分的內容出自參考來源,修改了下格式)

        <html>
          <head>
            <meta name="viewport" content="width=device-width,initial-scale=1">
            <link href="style.css" rel="stylesheet">
            <title>Critical Path</title>
          </head>
          <body>
            <p>Hello <span>web performance</span> students!</p>
            <div><img data-original="awesome-photo.jpg"></div>
          </body>
        </html>

        瀏覽器的處理如下:

        列舉其中的一些重點過程:

        1. Conversion轉換:瀏覽器將獲得的HTML內容(Bytes)基于他的編碼轉換為單個字符
        
        2. Tokenizing分詞:瀏覽器按照HTML規范標準將這些字符轉換為不同的標記token。每個token都有自己獨特的含義以及規則集
        
        3. Lexing詞法分析:分詞的結果是得到一堆的token,此時把他們轉換為對象,這些對象分別定義他們的屬性和規則
        
        4. DOM構建:因為HTML標記定義的就是不同標簽之間的關系,這個關系就像是一個樹形結構一樣
        例如:body對象的父節點就是HTML對象,然后段略p對象的父節點就是body對象

        最后的DOM樹如下:

        生成CSS規則

        同理,CSS規則樹的生成也是類似。簡述為:

        Bytes → characters → tokens → nodes → CSSOM

        譬如style.css內容如下:

        body { font-size: 16px }
        p { font-weight: bold }
        span { color: red }
        p span { display: none }
        img { float: right }

        那么最終的CSSOM樹就是:

        構建渲染樹

        當DOM樹和CSSOM都有了后,就要開始構建渲染樹了

        一般來說,渲染樹和DOM樹相對應的,但不是嚴格意義上的一一對應

        因為有一些不可見的DOM元素不會插入到渲染樹中,如head這種不可見的標簽或者display: none

        整體來說可以看圖:

        渲染

        有了render樹,接下來就是開始渲染,基本流程如下:

        圖中重要的四個步驟就是:

        1. 計算CSS樣式
        
        2. 構建渲染樹
        
        3. 布局,主要定位坐標和大小,是否換行,各種position overflow z-index屬性
        
        4. 繪制,將圖像繪制出來

        然后,圖中的線與箭頭代表通過js動態修改了DOM或CSS,導致了重新布局(Layout)或渲染(Repaint)

        這里Layout和Repaint的概念是有區別的:

        • Layout,也稱為Reflow,即回流。一般意味著元素的內容、結構、位置或尺寸發生了變化,需要重新計算樣式和渲染樹
        • Repaint,即重繪。意味著元素發生的改變只是影響了元素的一些外觀之類的時候(例如,背景色,邊框顏色,文字顏色等),此時只需要應用新樣式繪制這個元素就可以了

        回流的成本開銷要高于重繪,而且一個節點的回流往往回導致子節點以及同級節點的回流,
        所以優化方案中一般都包括,盡量避免回流。

        什么會引起回流?

        1.頁面渲染初始化
        
        2.DOM結構改變,比如刪除了某個節點
        
        3.render樹變化,比如減少了padding
        
        4.窗口resize
        
        5.最復雜的一種:獲取某些屬性,引發回流,
        很多瀏覽器會對回流做優化,會等到數量足夠時做一次批處理回流,
        但是除了render樹的直接變化,當獲取一些屬性時,瀏覽器為了獲得正確的值也會觸發回流,這樣使得瀏覽器優化無效,包括
            (1)offset(Top/Left/Width/Height)
             (2) scroll(Top/Left/Width/Height)
             (3) cilent(Top/Left/Width/Height)
             (4) width,height
             (5) 調用了getComputedStyle()或者IE的currentStyle

        回流一定伴隨著重繪,重繪卻可以單獨出現

        所以一般會有一些優化方案,如:

        • 減少逐項更改樣式,最好一次性更改style,或者將樣式定義為class并一次性更新
        • 避免循環操作dom,創建一個documentFragment或div,在它上面應用所有DOM操作,最后再把它添加到window.document
        • 避免多次讀取offset等屬性。無法避免則將它們緩存到變量
        • 將復雜的元素絕對定位或固定定位,使得它脫離文檔流,否則回流代價會很高

        注意:改變字體大小會引發回流

        再來看一個示例:

        var s = document.body.style;
        
        s.padding = "2px"; // 回流+重繪
        s.border = "1px solid red"; // 再一次 回流+重繪
        s.color = "blue"; // 再一次重繪
        s.backgroundColor = "#ccc"; // 再一次 重繪
        s.fontSize = "14px"; // 再一次 回流+重繪
        // 添加node,再一次 回流+重繪
        document.body.appendChild(document.createTextNode('abc!'));

        簡單層與復合層

        上述中的渲染中止步于繪制,但實際上繪制這一步也沒有這么簡單,它可以結合復合層和簡單層的概念來講。

        這里不展開,進簡單介紹下:

        • 可以認為默認只有一個復合圖層,所有的DOM節點都是在這個復合圖層下的
        • 如果開啟了硬件加速功能,可以將某個節點變成復合圖層
        • 復合圖層之間的繪制互不干擾,由GPU直接控制
        • 而簡單圖層中,就算是absolute等布局,變化時不影響整體的回流,但是由于在同一個圖層中,仍然是會影響繪制的,因此做動畫時性能仍然很低。而復合層是獨立的,所以一般做動畫推薦使用硬件加速

        更多參考:

        普通圖層和復合圖層

        Chrome中的調試

        Chrome的開發者工具中,Performance中可以看到詳細的渲染過程:


        資源外鏈的下載

        上面介紹了html解析,渲染流程。但實際上,在解析html時,會遇到一些資源連接,此時就需要進行單獨處理了

        簡單起見,這里將遇到的靜態資源分為一下幾大類(未列舉所有):

        • CSS樣式資源
        • JS腳本資源
        • img圖片類資源

        遇到外鏈時的處理

        當遇到上述的外鏈時,會單獨開啟一個下載線程去下載資源(http1.1中是每一個資源的下載都要開啟一個http請求,對應一個tcp/ip鏈接)

        遇到CSS樣式資源

        CSS資源的處理有幾個特點:

        • CSS下載時異步,不會阻塞瀏覽器構建DOM樹
        • 但是會阻塞渲染,也就是在構建render時,會等到css下載解析完畢后才進行(這點與瀏覽器優化有關,防止css規則不斷改變,避免了重復的構建)
        • 有例外,media query聲明的CSS是不會阻塞渲染的

        遇到JS腳本資源

        JS腳本資源的處理有幾個特點:

        • 阻塞瀏覽器的解析,也就是說發現一個外鏈腳本時,需等待腳本下載完成并執行后才會繼續解析HTML
        • 瀏覽器的優化,一般現代瀏覽器有優化,在腳本阻塞時,也會繼續下載其它資源(當然有并發上限),但是雖然腳本可以并行下載,解析過程仍然是阻塞的,也就是說必須這個腳本執行完畢后才會接下來的解析,并行下載只是一種優化而已
        • defer與async,普通的腳本是會阻塞瀏覽器解析的,但是可以加上defer或async屬性,這樣腳本就變成異步了,可以等到解析完畢后再執行

        注意,defer和async是有區別的: defer是延遲執行,而async是異步執行。

        簡單的說(不展開):

        • async是異步執行,異步下載完畢后就會執行,不確保執行順序,一定在onload前,但不確定在DOMContentLoaded事件的前或后
        • defer是延遲執行,在瀏覽器看起來的效果像是將腳本放在了body后面一樣(雖然按規范應該是在DOMContentLoaded事件前,但實際上不同瀏覽器的優化效果不一樣,也有可能在它后面)

        遇到img圖片類資源

        遇到圖片等資源時,直接就是異步下載,不會阻塞解析,下載完畢后直接用圖片替換原有src的地方

        loaded和domcontentloaded

        簡單的對比:

        • DOMContentLoaded 事件觸發時,僅當DOM加載完成,不包括樣式表,圖片(譬如如果有async加載的腳本就不一定完成)
        • load 事件觸發時,頁面上所有的DOM,樣式表,腳本,圖片都已經加載完成了

        CSS的可視化格式模型

        這一部分內容很多參考《精通CSS-高級Web標準解決方案》以及參考來源

        前面提到了整體的渲染概念,但實際上文檔樹中的元素是按什么渲染規則渲染的,是可以進一步展開的,此部分內容即: CSS的可視化格式模型

        先了解:

        • CSS中規定每一個元素都有自己的盒子模型(相當于規定了這個元素如何顯示)
        • 然后可視化格式模型則是把這些盒子按照規則擺放到頁面上,也就是如何布局
        • 換句話說,盒子模型規定了怎么在頁面里擺放盒子,盒子的相互作用等等

        說到底: CSS的可視化格式模型就是規定了瀏覽器在頁面中如何處理文檔樹

        關鍵字:

        包含塊(Containing Block)
        控制框(Controlling Box)
        BFC(Block Formatting Context)
        IFC(Inline Formatting Context)
        定位體系
        浮動
        ...

        另外,CSS有三種定位機制:普通流,浮動,絕對定位,如無特別提及,下文中都是針對普通流中的

        包含塊(Containing Block)

        一個元素的box的定位和尺寸,會與某一矩形框有關,這個框就稱之為包含塊。

        元素會為它的子孫元素創建包含塊,但是,并不是說元素的包含塊就是它的父元素,元素的包含塊與它的祖先元素的樣式等有關系

        譬如:

        • 根元素是最頂端的元素,它沒有父節點,它的包含塊就是初始包含塊
        • static和relative的包含塊由它最近的塊級、單元格或者行內塊祖先元素的內容框(content)創建
        • fixed的包含塊是當前可視窗口
        • absolute的包含塊由它最近的position 屬性為absolute、relative或者fixed的祖先元素創建

          • 如果其祖先元素是行內元素,則包含塊取決于其祖先元素的direction特性
          • 如果祖先元素不是行內元素,那么包含塊的區域應該是祖先元素的內邊距邊界

        控制框(Controlling Box)

        塊級元素和塊框以及行內元素和行框的相關概念

        塊框:

        • 塊級元素會生成一個塊框(Block Box),塊框會占據一整行,用來包含子box和生成的內容
        • 塊框同時也是一個塊包含框(Containing Box),里面要么只包含塊框,要么只包含行內框(不能混雜),如果塊框內部有塊級元素也有行內元素,那么行內元素會被匿名塊框包圍

        關于匿名塊框的生成,示例:

        <DIV>
        Some text
        <P>More text
        </DIV>

        div生成了一個塊框,包含了另一個塊框p以及文本內容Some text,此時Some text文本會被強制加到一個匿名的塊框里面,被div生成的塊框包含(其實這個就是IFC中提到的行框,包含這些行內框的這一行匿名塊形成的框,行框和行內框不同)

        換句話說:

        如果一個塊框在其中包含另外一個塊框,那么我們強迫它只能包含塊框,因此其它文本內容生成出來的都是匿名塊框(而不是匿名行內框)

        行內框:

        • 一個行內元素生成一個行內框
        • 行內元素能排在一行,允許左右有其它元素

        關于匿名行內框的生成,示例:

        <P>Some <EM>emphasized</EM> text</P>

        P元素生成一個塊框,其中有幾個行內框(如EM),以及文本Some , text,此時會專門為這些文本生成匿名行內框

        display屬性的影響

        display的幾個屬性也可以影響不同框的生成:

        • block,元素生成一個塊框
        • inline,元素產生一個或多個的行內框
        • inline-block,元素產生一個行內級塊框,行內塊框的內部會被當作塊塊來格式化,而此元素本身會被當作行內級框來格式化(這也是為什么會產生BFC
        • none,不生成框,不再格式化結構中,當然了,另一個visibility: hidden則會產生一個不可見的框

        總結:

        • 如果一個框里,有一個塊級元素,那么這個框里的內容都會被當作塊框來進行格式化,因為只要出現了塊級元素,就會將里面的內容分塊幾塊,每一塊獨占一行(出現行內可以用匿名塊框解決)
        • 如果一個框里,沒有任何塊級元素,那么這個框里的內容會被當成行內框來格式化,因為里面的內容是按照順序成行的排列

        BFC(Block Formatting Context)

        FC(格式上下文)?

        FC即格式上下文,它定義框內部的元素渲染規則,比較抽象,譬如

        FC像是一個大箱子,里面裝有很多元素
        
        箱子可以隔開里面的元素和外面的元素(所以外部并不會影響FC內部的渲染)
        
        內部的規則可以是:如何定位,寬高計算,margin折疊等等

        不同類型的框參與的FC類型不同,譬如塊級框對應BFC,行內框對應IFC

        注意,并不是說所有的框都會產生FC,而是符合特定條件才會產生,只有產生了對應的FC后才會應用對應渲染規則

        BFC規則:

        在塊格式化上下文中
        
        每一個元素左外邊與包含塊的左邊相接觸(對于從右到左的格式化,右外邊接觸右邊)
        
        即使存在浮動也是如此(所以浮動元素正常會直接貼近它的包含塊的左邊,與普通元素重合)
        
        除非這個元素也創建了一個新的BFC

        總結幾點BFC特點:

        1. 內部box在垂直方向,一個接一個的放置
        2. box的垂直方向由margin決定,屬于同一個BFC的兩個box間的margin會重疊
        3. BFC區域不會與float box重疊(可用于排版)
        4. BFC就是頁面上的一個隔離的獨立容器,容器里面的子元素不會影響到外面的元素。反之也如此
        5. 計算BFC的高度時,浮動元素也參與計算(不會浮動坍塌)

        如何觸發BFC?

        1. 根元素
        2. float屬性不為none
        3. positionabsolutefixed
        4. displayinline-block, flex, inline-flex,table,table-cell,table-caption
        5. overflow不為visible

        這里提下,display: table,它本身不產生BFC,但是它會產生匿名框(包含display: table-cell的框),而這個匿名框產生BFC

        更多請自行網上搜索

        IFC(Inline Formatting Context)

        IFC即行內框產生的格式上下文

        IFC規則

        在行內格式化上下文中
        
        框一個接一個地水平排列,起點是包含塊的頂部。
        
        水平方向上的 margin,border 和 padding 在框之間得到保留
        
        框在垂直方向上可以以不同的方式對齊:它們的頂部或底部對齊,或根據其中文字的基線對齊

        行框

        包含那些框的長方形區域,會形成一行,叫做行框

        行框的寬度由它的包含塊和其中的浮動元素決定,高度的確定由行高度計算規則決定

        行框的規則:

        如果幾個行內框在水平方向無法放入一個行框內,它們可以分配在兩個或多個垂直堆疊的行框中(即行內框的分割)
        
        行框在堆疊時沒有垂直方向上的分割且永不重疊
        
        行框的高度總是足夠容納所包含的所有框。不過,它可能高于它包含的最高的框(例如,框對齊會引起基線對齊)
        
        行框的左邊接觸到其包含塊的左邊,右邊接觸到其包含塊的右邊。

        結合補充下IFC規則:

        浮動元素可能會處于包含塊邊緣和行框邊緣之間
        
        盡管在相同的行內格式化上下文中的行框通常擁有相同的寬度(包含塊的寬度),它們可能會因浮動元素縮短了可用寬度,而在寬度上發生變化
        
        同一行內格式化上下文中的行框通常高度不一樣(如,一行包含了一個高的圖形,而其它行只包含文本)
        
        當一行中行內框寬度的總和小于包含它們的行框的寬,它們在水平方向上的對齊,取決于 `text-align` 特性
        
        空的行內框應該被忽略
        
        即不包含文本,保留空白符,margin/padding/border非0的行內元素,
        以及其他常規流中的內容(比如,圖片,inline blocks 和 inline tables),
        并且不是以換行結束的行框,
        必須被當作零高度行框對待

        總結:

        • 行內元素總是會應用IFC渲染規則
        • 行內元素會應用IFC規則渲染,譬如text-align可以用來居中等
        • 塊框內部,對于文本這類的匿名元素,會產生匿名行框包圍,而行框內部就應用IFC渲染規則
        • 行內框內部,對于那些行內元素,一樣應用IFC渲染規則
        • 另外,inline-block,會在元素外層產生IFC(所以這個元素是可以通過text-align水平居中的),當然,它內部則按照BFC規則渲染

        相比BFC規則來說,IFC可能更加抽象(因為沒有那么條理清晰的規則和觸發條件)

        但總的來說,它就是行內元素自身如何顯示以及在框內如何擺放的渲染規則,這樣描述應該更容易理解

        其它

        當然還有有一些其它內容:

        • 譬如常規流,浮動,絕對定位等區別
        • 譬如浮動元素不包含在常規流中
        • 譬如相對定位,絕對定位,Fixed定位等區別
        • 譬如z-index的分層顯示機制等

        這里不一一展開,更多請參考:

        http://bbs.csdn.net/topics/340204423

        JS引擎解析過程

        前面有提到遇到JS腳本時,會等到它的執行,實際上是需要引擎解析的,這里展開描述(介紹主干流程)

        JS的解釋階段

        首先得明確: JS是解釋型語音,所以它無需提前編譯,而是由解釋器實時運行

        引擎對JS的處理過程可以簡述如下:

        1. 讀取代碼,進行詞法分析(Lexical analysis),然后將代碼分解成詞元(token)
        
        2. 對詞元進行語法分析(parsing),然后將代碼整理成語法樹(syntax tree)
        
        3. 使用翻譯器(translator),將代碼轉為字節碼(bytecode)
        
        4. 使用字節碼解釋器(bytecode interpreter),將字節碼轉為機器碼

        最終計算機執行的就是機器碼。

        為了提高運行速度,現代瀏覽器一般采用即時編譯(JIT-Just In Time compiler

        即字節碼只在運行時編譯,用到哪一行就編譯哪一行,并且把編譯結果緩存(inline cache

        這樣整個程序的運行速度能得到顯著提升。

        而且,不同瀏覽器策略可能還不同,有的瀏覽器就省略了字節碼的翻譯步驟,直接轉為機器碼(如chrome的v8)

        總結起來可以認為是: 核心的JIT編譯器將源碼編譯成機器碼運行

        JS的預處理階段

        上述將的是解釋器的整體過程,這里再提下在正式執行JS前,還會有一個預處理階段
        (譬如變量提升,分號補全等)

        預處理階段會做一些事情,確保JS可以正確執行,這里僅提部分:

        分號補全

        JS執行是需要分號的,但為什么以下語句卻可以正常運行呢?

        console.log('a')
        console.log('b')

        原因就是JS解釋器有一個Semicolon Insertion規則,它會按照一定規則,在適當的位置補充分號

        譬如列舉幾條自動加分號的規則:

        • 當有換行符(包括含有換行符的多行注釋),并且下一個token沒法跟前面的語法匹配時,會自動補分號。
        • 當有}時,如果缺少分號,會補分號。
        • 程序源代碼結束時,如果缺少分號,會補分號。

        于是,上述的代碼就變成了

        console.log('a');
        console.log('b');

        所以可以正常運行

        當然了,這里有一個經典的例子:

        function b() {
            return
            {
                a: 'a'
            };
        }

        由于分號補全機制,所以它變成了:

        function b() {
            return;
            {
                a: 'a'
            };
        }

        所以運行后是undefined

        變量提升

        一般包括函數提升和變量提升

        譬如:

        a = 1;
        b();
        function b() {
            console.log('b');
        }
        var a;

        經過變量提升后,就變成:

        function b() {
            console.log('b');
        }
        var a;
        a = 1;
        b();

        這里沒有展開,其實展開也可以牽涉到很多內容的

        譬如可以提下變量聲明,函數聲明,形參,實參的優先級順序,以及es6中let有關的臨時死區等

        JS的執行階段

        此階段的內容中的圖片來源:深入理解JavaScript系列(10):JavaScript核心(晉級高手必讀篇)

        解釋器解釋完語法規則后,就開始執行,然后整個執行流程中大致包含以下概念:

        • 執行上下文,執行堆棧概念(如全局上下文,當前活動上下文)
        • VO(變量對象)和AO(活動對象)
        • 作用域鏈
        • this機制等

        這些概念如果深入講解的話內容過多,因此這里僅提及部分特性

        執行上下文簡單解釋

        • JS有執行上下文
        • 瀏覽器首次載入腳本,它將創建全局執行上下文,并壓入執行棧棧頂(不可被彈出)
        • 然后每進入其它作用域就創建對應的執行上下文并把它壓入執行棧的頂部
        • 一旦對應的上下文執行完畢,就從棧頂彈出,并將上下文控制權交給當前的棧。
        • 這樣依次執行(最終都會回到全局執行上下文)

        譬如,如果程序執行完畢,被彈出執行棧,然后有沒有被引用(沒有形成閉包),那么這個函數中用到的內存就會被垃圾處理器自動回收

        然后執行上下文與VO,作用域鏈,this的關系是:

        每一個執行上下文,都有三個重要屬性:

        • 變量對象(Variable object,VO)
        • 作用域鏈(Scope chain)
        • this

        VO與AO

        VO是執行上下文的屬性(抽象概念),但是只有全局上下文的變量對象允許通過VO的屬性名稱來間接訪問(因為在全局上下文里,全局對象自身就是變量對象)

        AO(activation object),當函數被調用者激活,AO就被創建了

        可以理解為:

        • 在函數上下文中:VO === AO
        • 在全局上下文中:VO === this === global

        總的來說,VO中會存放一些變量信息(如聲明的變量,函數,arguments參數等等)

        作用域鏈

        它是執行上下文中的一個屬性,原理和原型鏈很相似,作用很重要。

        譬如流程簡述:

        在函數上下文中,查找一個變量foo
        
        如果函數的VO中找到了,就直接使用
        
        否則去它的父級作用域鏈中(__parent__)找
        
        如果父級中沒找到,繼續往上找
        
        直到全局上下文中也沒找到就報錯

        this指針

        這也是JS的核心知識之一,由于內容過多,這里就不展開,僅提及部分

        注意:this是執行上下文環境的一個屬性,而不是某個變量對象的屬性

        因此:

        • this是沒有一個類似搜尋變量的過程
        • 當代碼中使用了this,這個 this的值就直接從執行的上下文中獲取了,而不會從作用域鏈中搜尋
        • this的值只取決中進入上下文時的情況

        所以經典的例子:

        var baz = 200;
        var bar = {
            baz: 100,
            foo: function() {
                console.log(this.baz);
            }
        };
        var foo = bar.foo;
        
        // 進入環境:global
        foo(); // 200,嚴格模式中會報錯,Cannot read property 'baz' of undefined
        
        // 進入環境:global bar
        bar.foo(); // 100

        就要明白了上面this的介紹,上述例子很好理解

        更多參考:

        深入理解JavaScript系列(13):This? Yes,this!

        回收機制

        JS有垃圾處理器,所以無需手動回收內存,而是由垃圾處理器自動處理。

        一般來說,垃圾處理器有自己的回收策略。

        譬如對于那些執行完畢的函數,如果沒有外部引用(被引用的話會形成閉包),則會回收。(當然一般會把回收動作切割到不同的時間段執行,防止影響性能)

        常用的兩種垃圾回收規則是:

        • 標記清除
        • 引用計數

        Javascript引擎基礎GC方案是(simple GC):mark and sweep(標記清除),簡單解釋如下:

        1. 遍歷所有可訪問的對象。
        2. 回收已不可訪問的對象。

        譬如:(出自javascript高程)

        當變量進入環境時,例如,在函數中聲明一個變量,就將這個變量標記為“進入環境”。

        從邏輯上講,永遠不能釋放進入環境的變量所占用的內存,因為只要執行流進入相應的環境,就可能會用到它們。

        而當變量離開環境時,則將其標記為“離開環境”。

        垃圾回收器在運行的時候會給存儲在內存中的所有變量都加上標記(當然,可以使用任何標記方式)。

        然后,它會去掉環境中的變量以及被環境中的變量引用的變量的標記(閉包,也就是說在環境中的以及相關引用的變量會被去除標記)。

        而在此之后再被加上標記的變量將被視為準備刪除的變量,原因是環境中的變量已經無法訪問到這些變量了。

        最后,垃圾回收器完成內存清除工作,銷毀那些帶標記的值并回收它們所占用的內存空間。

        關于引用計數,簡單點理解:

        跟蹤記錄每個值被引用的次數,當一個值被引用時,次數+1,減持時-1,下次垃圾回收器會回收次數為0的值的內存(當然了,容易出循環引用的bug)

        GC的缺陷

        和其他語言一樣,javascript的GC策略也無法避免一個問題: GC時,停止響應其他操作

        這是為了安全考慮。

        而Javascript的GC在100ms甚至以上

        對一般的應用還好,但對于JS游戲,動畫對連貫性要求比較高的應用,就麻煩了。

        這就是引擎需要優化的點: 避免GC造成的長時間停止響應。

        GC優化策略

        這里介紹常用到的:分代回收(Generation GC)

        目的是通過區分“臨時”與“持久”對象:

        • 多回收“臨時對象”區(young generation
        • 少回收“持久對象”區(tenured generation
        • 減少每次需遍歷的對象,從而減少每次GC的耗時。

        像node v8引擎就是采用的分代回收(和java一樣,作者是java虛擬機作者。)

        更多可以參考:

        V8 內存淺析

        其它

        可以提到跨域

        譬如發出網絡請求時,會用AJAX,如果接口跨域,就會遇到跨域問題

        可以參考:

        ajax跨域,這應該是最全的解決方案了

        可以提到web安全

        譬如瀏覽器在解析HTML時,有XSSAuditor,可以延伸到web安全相關領域

        可以參考:

        AJAX請求真的不安全么?談談Web安全與AJAX的關系。

        更多

        如可以提到viewport概念,講講物理像素,邏輯像素,CSS像素等概念

        如熟悉Hybrid開發的話可以提及一下Hybrid相關內容以及優化

        ...

        總結

        上述這么多內容,目的是:梳理出自己的知識體系

        本文由于是前端向,所以知識梳理時有重點,很多其它的知識點都簡述或略去了,重點介紹的模塊總結:

        • 瀏覽器的進程/線程模型、JS運行機制(這一塊的詳細介紹鏈接到了另一篇文章)
        • http規范(包括報文結構,頭部,優化,http2.0,https等)
        • http緩存(單獨列出來,因為它很重要)
        • 頁面解析流程(HTML解析,構建DOM,生成CSS規則,構建渲染樹,渲染流程,復合層的合成,外鏈的處理等)
        • JS引擎解析過程(包括解釋階段,預處理階段,執行階段,包括執行上下文、VO、作用域鏈、this、回收機制等)
        • 跨域相關,web安全單獨鏈接到了具體文章,其它如CSS盒模型,viewport等僅是提及概念

        關于本文的價值?

        本文是個人階段性梳理知識體系的成果,然后加以修繕后發布成文章,因此并不確保適用于所有人員

        但是,個人認為本文還是有一定參考價值的

        寫在最后的話

        還是那句話:知識要形成體系

        梳理出知識體系后,有了一個骨架,知識點不易遺忘,而且學習新知識時也會更加迅速,更重要的是容易舉一反三,可以由一個普通的問題,深挖拓展到底層原理

        前端知識是無窮無盡的,本文也僅僅是簡單梳理出一個承載知識體系的骨架而已,更多的內容仍然需要不斷學習,積累

        另外,本文結合從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理這篇文章,更佳噢!

        附錄

        博客

        初次發布2018.03.12于我個人博客上面

        http://www.dailichun.com/2018/03/12/whenyouenteraurl.html

        招聘軟廣

        阿里巴巴釘釘商業化團隊大量hc,高薪股權。機會好,技術成長空間足,業務也有很大的發揮空間!

        還在猶豫什么,來吧?。?!

        社招(P6~P7)

        職責和挑戰

        1. 負責釘釘工作臺。工作臺是幫助企業實現數字化管理和協同的門戶,是擁有億級用戶量的產品。如何保障安全、穩定、性能和體驗是對我們的一大挑戰。
        2. 負責開放能力建設。針對紛繁的業務場景,提供合理的開放方案,既要做到深入用戶場景理解并支撐業務發展,滿足企業千人千面、千行千面的訴求,又要在技術上保障用戶的安全、穩定和體驗。需要既要有技術抽象能力、平臺架構能力,又要有業務的理解和分析能力。
        3. 開放平臺基礎建設。保障鏈路的安全和穩定。同時對如何保障用戶體驗有持續精進的熱情和追求。

        職位要求

        1. 精通HTML5、CSS3、JS(ES5/ES6)等前端開發技術
        2. 掌握主流的JS庫和開發框架,并深入理解其設計原理,例如React,Vue等
        3. 熟悉模塊化、前端編譯和構建工具,例如webpack、babel等
        4. (加分項)了解服務端或native移動應用開發,例如nodejs、Java等
        5. 對技術有強追求,有良好的溝通能力和團隊協同能力,有優秀的分析問題和解決問題的能力。

        前端實習

        面向2021畢業的同學

        1. 本科及以上學歷,計算機相關專業
        2. 熟練掌握HTML5/CSS3/Javascript等web前端技術
        3. 熟悉至少一種常用框架,例如React、vue等
        4. 關注新事物、新技術,有較強的學習能力,有強烈求知欲和進取心
        5. 有半年以上實際項目經驗,大廠加分

        image.png

        image.png

        內推郵箱

        lichun.dlc@alibaba-inc.com

        簡歷發我郵箱,必有回應,符合要求直接走內推?。?!

        一對一服務,有問必答!

        也可加我微信了解更多:a546684355

        參考資料

        查看原文

        贊 580 收藏 1050 評論 42

        撒網要見魚 發布了文章 · 2018-03-08

        2017年終總結、隨想

        轉眼1年已過

        生活總是要繼續,不知不覺18年已經過去了1/6,17年的總結也正在慢慢出爐。

        回顧2017年,可以總結成三個字:很充實

        自從16年開始人生規劃后,生活變得井然有序,
        也并沒有預想中的枯燥,反而是很充實,
        每天睜眼起來可以感知昨日的進步,
        每天睡前可以明確今日的收獲,
        在不斷堅持中進步,
        這種日子很美!

        2017的目標完成情況

        • 前端技術進階(明顯感覺到上升了一個檔次)
        • 堅持寫個人博客 (沒有量化,但是有持續更新)
        • 堅持閱讀(每天堅持閱讀技術書籍,英語讀本)
        • 堅持口琴與吉他(每天堅持口琴,吉他,還入手了電鋼)
        • 日語自學(17年4月左右主動舍棄了,因為精力有限,要做減法)
        • 每天50仰臥起坐+俯臥撐(每天堅持)

        當然了,17年也還有一些額外驚喜,
        如github上首個突破700star的開源項目(上了trending榜),
        如博客社區中寫了幾篇比較受歡迎的文章(segmentfault以及掘金),
        也因此收到了幾家公司的內推邀請

        總的來說,17定下的一些目標基本完成,每天的生活也很充實,沒有虛度

        2018年目標規劃

        1. 技術棧更新,react+node.js熟練
        2. 入門python
        3. 每日習慣依然堅持(口琴,閱讀,鍛煉是底線,其余可以根據時間合理安排)
        4. 每日的技術學習,如基礎知識總結,保持算法敏感,學習新技術等
        5. 堅持寫博客,在社區中幫助更多的人
        6. 身體很重要,培養一個運動習慣,或跑步,或...,具體根據環境而定

        總結下:

        • 需要日常堅持的:3, 4
        • 需要定期堅持的:5, 6
        • 全年范圍的(結合工作與業余):1, 2

        看起來,18年應該又是充實的一年

        一些感想

        論堅持

        如果一定要給我的17年加一個標簽,那就是:堅持

        堅持可以說很不容易也可以說很容易

        說不容易:

        • 因為萬事開頭難,很難開始去做一件事情
        • 因為往往三分鐘熱情,很難持續跟進
        • 因為容易虎頭蛇尾,變得有始無終

        說容易:

        • 因為習慣成自然,當養成習慣后,一天不做都會很不舒服
        • 因為持續激勵,每當感知到自己在不斷進步,就會情不自禁的堅持下去
        • 因為有希望,人生由自己控制。一步一個腳印,朝著目標前進

        當立下計劃,并堅持下去,帶來的最大改變或許就是:
        從此刻起,自己為自己負責,從此不再自怨自艾

        再者,有一句俗話:
        優秀的人不一定都堅持,但堅持的人往往都優秀

        論環境

        環境很重要,因為在一個大環境中,
        大部分人都只能適應環境,
        僅有小部分人能夠保持自己的獨立性,
        至于改變環境的人?非常稀有

        我自己,也經歷了從適應環境,到保持獨立性的改變,
        當然,現在離稀有物種還有點距離,僅能做到保持自己的同時,
        散發點微弱的輻射

        譬如,
        當我堅持口琴時,可以引導著周圍的人一起學習,
        當我堅持鍛煉時,可以引導著周圍的人一起開始,
        當我堅持博客時,可以引導著周圍的人一起嘗試,
        ...

        為什么說環境重要?

        • 當你每天工作時,發現掌握一點點技術就足以滿足需求,那么你是否能堅持自己不斷去鉆研技術?
        • 當你每天工作時,發現周圍的人沒有一個能給予幫助,那么你是否能一個人孤獨的摸索,爬坑?
        • 當你發現周圍的人討論的話題沒有幾個與技術相關,你是否還能保持對技術的敏感性,不斷閱讀,學習新技術?
        • 當你發現周圍的人業余時間都用來打游戲,看直播,刷網頁,你是否還能讓自己日復一日的堅持完成各項目標?

        實際上,大部分人都是做不到的,在一個這樣氛圍的大環境中,
        很多人會逐漸變得懶散,沒有追求,因為那樣會更加輕松

        人都有從眾性,都會傾向于和周圍對比

        • 當你看到周圍的人都沒有學習,在打游戲,看直播時,你也會松一口氣,因為你會覺得:“周圍的人都這樣,那么我也可以”,

        于是你也開始了打游戲,看直播的生活

        • 當你看到周圍的人技術水準都很低時,你也會沾沾自喜,你會覺得:“和他們相比,我已經強很多了”,

        于是你會放松自己對技術的追求,逐漸淪陷

        • 當你周圍的環境都是各種游戲聲,直播聲,打牌聲時,你也會無心學習,只想自己也放松一下

        總的來說

        • 只要沒有突破從眾性,就會很容易受到環境的影響,這樣在不同的環境中,結果可能迥然不同
        • 只有保持了獨立性,知道自己想要什么,才能一直堅持下去,盡可能的少受環境影響,這樣在不同的環境中,結果會更趨于一致

        至于如何突破?這是一個很玄的概念,每個人都會有自己的理解

        譬如我是在堅持了1年口琴后,突然醒悟,發現了堅持的重要性,它能帶來很大的改變,于是之后開始對自己的生活進行規劃,
        從而在心中樹立了自己的目標,盡可能的少受環境影響(當然,影響肯定有),有時候還會輻射些自己的影響
        (如自己的事跡會感染別人)

        總的來說,是與自己的博弈

        論取舍

        有舍必有得,你不可能將所有都占據

        我剛開始時的規劃有一個很大的問題:那就是什么都想學,
        總覺得自己什么都想學,可以什么都去學

        實際上,后來發現,這中觀念有很大的問題,因為:人的精力是有限的,
        你不可能把什么都學會,必須要有取舍,去做最重要的幾件事情

        于是后來就開始做減法了,將一些現階段不是很重要的事情砍掉,
        譬如將定好的日語學習砍掉了(當然了,后續在新的環境中,也有可能會針對性的繼續做減法)

        總的來說:

        • 除了堅持以外,還得學會取舍,狠心將那些不重要的事情砍掉,避免精力被過度分散
        • 如果什么都想做,那么很可能什么都做不好,最后一事無成

        原文地址

        原文在我個人博客上面

        2017年終總結、隨想

        查看原文

        贊 8 收藏 7 評論 3

        撒網要見魚 贊了文章 · 2018-02-12

        深入探究iOS下fixed定位導致的問題

        討論背景

        眾所周知,fixed元素在IOS下的表現是糟糕的,fixed元素在滾動頁面中使用會出現各種奇怪的問題,在微信瀏覽器中使用就更甚(如:頁面滾動,fixed元素與頁面相互分離;頁面滾動,fixed元素消失等)。這些表現過于離奇,顯得沒有邏輯,一時間很難找到對應的解決方案。
        所以筆者決定從一個簡單列表頁出發,把遇到的各種奇怪問題都羅列出來,并探究其出現的原因。以便在開發中規避這些問題。

        假定我們的需求是做一個列表頁,列表頁的頂部放置一些「其他」信息,底部放置一個「創建」按鈕,中間顯示「項目」列表內容。
        設計稿大概是這樣。

        設計稿

        實現方案

        根據需求,我們分別制作了三種解決方案。分別是

        • 利用fixed定位,將「按鈕」放在滾動區「項目列表」外面,解決方案示例1。
        • 利用fixed定位,將「按鈕」放在滾動區「項目列表」里面,解決方案示例2。
        • 利用absolute定位,將「按鈕」放在滾動區「項目列表」里面,并用「項目列表」去填充它所占的內容,解決方案示例3。

        分別在PC和IOS瀏覽器中運行這幾個demo,我們發現,這些demo在PC中的表現都是符合設計需求的。但在IOS瀏覽器中運行,就會各種出現各種的問題,分別對應這幾個現象。

        • 解決方案示例1:從「其他」內容區域開始觸碰屏幕,進行頁面滾動,「按鈕」'脫離'頁面內容區域。

        問題1對應二維碼

        • 解決方案示例2:從「其他」內容區域開始觸碰屏幕,進行頁面滾動,「按鈕」消失了。

        問題2對應二維碼

        • 解決方案示例3:「其他」區域直接消失不見了。

        問題3對應二維碼

        要解釋這幾個現象,我們需要從顏色填充說起。

        滾動填充的顏色

        • 顏色填充示例1。

          • 重點代碼:在這個示例里面,我們不對「項目列表」的高度進行限制,直接讓內容在body中進行滾動。然后將body的背景顏色設置為橘紅色。
          • 操作:進入頁面后直接向上拉動頁面,拉動到不可拖動為止。
          • 現象:我們發現「項目列表」的綠色區域下面,顯示了body的背景顏色橘紅色。
          • 說明:填充的顏色是可以定制的。
          • 疑問:這個顏色填充的區域會不會是body的延伸呢?

        顏色填充1對應二維碼

        • 顏色填充示例2。

          • 重點代碼:去除了Body的背景顏色,改成body的背景圖片并進行平鋪。
          • 操作:同上一個示例。
          • 現象:我們發現「項目列表」的綠色區域下面,填充的依然body的背景顏色,而不是body的背景圖片。
          • 說明:填充的部分并不屬于Body標簽本身。
          • 疑問:那如果我們將body的背景顏色去掉,而在html加上呢?

        顏色填充2對應二維碼

        • 顏色填充示例3。

          • 重點代碼:將body的背景顏色去掉。
          • 操作:同上一個示例。
          • 現象:這次填充的顏色是html的背景顏色。
          • 說明:這再次論證了填充的部分并不是固定某元素的內容,不是某個元素的延伸。而且說明系統找顏色是從滾動區域逐級往上找的,直到找到為止。
          • 疑問:如果body和html的背景顏色都去掉,又會顯示什么顏色呢?

        顏色填充3對應二維碼

        • 顏色填充示例4。

          • 重點代碼:body和html的背景顏色去掉。
          • 操作:同上一個示例。
          • 現象:可以看到填充的是白色。
          • 說明:默認的填充顏色是白色。

        顏色填充4對應二維碼

        • 我們再回到顏色填充示例1。

          • 重點代碼:與示例1相同
          • 操作:這次我們在微信中打開,并改變操作方式,先上拉顯示橘紅色填充內容,再下拉顯示微信的黑邊(即顯示頂部"此頁面由XXX提供"文案)。再重新上拉,到不可拖動為止。
          • 現象:原本下拉填充的橘紅色變成了黑色。而且無論再怎么操作,都不會再重新顯示回橘紅色。
          • 說明:微信內置瀏覽器修改了默認的顏色填充。

        顏色填充5對應二維碼

        小結
        • 滾動填充的顏色是可定制的。
        • 滾動填充的內容并不是標簽的延伸,只會填充純顏色。
        • 滾動填充的顏色是滾動區域逐級往上找background-color確定的。
        • 滾動填充的顏色默認值為白色。
        • 微信會修改滾動填充的顏色值。

        IOS滾動回彈機制

        我們知道IOS是有滾動回彈機制的(即進行滾動時,滾動到最頂部或者最底部顯示的一個回彈動畫。我們上面講的滾動顏色填充就是這個機制的具體實現)前面解決方案示例1中遇到的問題(「按鈕」'脫離'頁面內容區域),就是由于這個機制引起的。
        現在我們先來探究一下,這個滾動回彈機制具體的運行過程是怎么樣的。以下操作均在解決方案示例1下進行。

        重點操作如下過程:

        • 先將示例代碼在IOS內置瀏覽器safari中打開。
        • 用雙指捏起整個頁面(即類似于圖片的縮小操作)。

          • 現象:我們發現,在頁面是可以被縮小的。頁面外部部分是純顏色。
          • 說明:有一個容器包裹著我們的頁面。這個容器通常用于窗口縮放的時候,填充顏色。

        滾動回彈1

        • 雙指重復緩慢地捏起,放松整個頁面。觀察頁面變化。

          • 現象:當剛開始縮小頁面時,外部容器的顏色與滾動填充索引到的顏色(粉紅色)相同。
          • 現象:當逐漸縮小頁面時,外部容器的顏色將從索引到的顏色漸變到白色(這個顏色和我們上面探討到的默認填充顏色相同)。

        滾動回彈2

        • 先將示例代碼在微信內置瀏覽器中打開,重復上面操作。
        • 用雙指捏起整個頁面(即類似于圖片的縮小操作)。

          • 現象:外部容器的顏色變成了黑色,而且容器頂部出現了「此頁面由 XXX 提供」文案。
          • 說明:在微信下,為了顯示「此頁面由 XXX 提供」的提示語,微信自己重寫了這個機制中的顏色,設置為黑色。

        滾動回彈3

        • 雙指重復緩慢地捏起,放松整個頁面。觀察頁面變化。

          • 現象:外部容器的顏色在黑色和粉紅色之間閃動。
          • 現象:越是放松頁面,閃動越頻繁。
          • 說明:微信重設顏色的機制和原生滾動回彈中縮小頁面漸變顏色的機制相沖突。
          • 說明:無論是縮小頁面還是恢復頁面大小,微信都嘗試將容器背景顏色設置為黑色。

        滾動回彈4

        • 打開頁面,先滾動到底部,顯示了粉紅色,再滾動到頂部微信提示文案并顯示黑色,再滾動到底部顯示微信修改后的黑色,再縮小頁面,在滾動到底部。

          • 現象:底部顯示顏色的是粉紅色。
          • 說明:原生漸變顏色終止會覆蓋微信重設顏色的機制。

        滾動回彈5

        • 在上一個操作的前提下,重新滾動到頂部,顯示微信的提示文案,在滾動回底部。(整個過程不進行縮放)

          • 現象:底部顯示的顏色重新被設置回黑色。
          • 說明:只要滾動到頂部,為了顯示微信頂部的提示文案部分,都會觸發微信的顏色修改機制。
          • 說明:由于不進行頁面縮放,原生的漸變機制不會被觸發,原生部分不會進行顏色重置。

        滾動回彈6

        這一系列的操作解釋了這幾個問題:
        • 為什么在微信中,先往上滾再往下滾動頁面,顏色的填充會變成了黑色,而不是body的背景色。因為微信對外部容器的背景色進行了重載。
        • 為什么解決方案示例1中,「按鈕」看起來'脫離'了頁面。

        因為微信對外部容器的背景色設置成了黑色,所以滾動到底部進行回彈的時候,頁面內容和按鈕之間的區域(即顏色填充區域)變成了黑色。而黑色給讓一種"空"的感覺,所以感覺到「按鈕」脫離的頁面內容的錯覺。

        • 在safari中并不會改成黑色,即填充的顏色和Body的背景顏色一致。所以不會有黑色,不會產生微信上?!赴粹o」脫離的頁面內容的錯覺。

        fixed定位基準值問題

        在剛才的示例操作中,不知道大家有沒有發現一個奇怪的問題。
        在頁面縮放過程中,fixed元素與其他元素是在不同的顯示層進行渲染了?

        fixed問題1

        重新執行前面的操作過程。我們發現:

        • fixed元素的定位并不是基于手機屏幕,因為縮放的過程中,「按鈕」隨著縮放進行了上移。
        • fixed元素的定位也不是基于body元素的,因為從回彈機制來說,「按鈕」早已經脫離了body區域(紅色框標記的顏色深粉紅色塊就是body的背景色)。

        fixed問題2

        fixed元素的基準值,其實是介于二者之間的一個顯示窗口(類似于viewPort)。
        這個顯示窗口在不縮放的情況下,等于瀏覽器的窗口大小。
        在縮放的情況下,顯示窗口大概是這樣子。

        fixed問題3

        body內容超出了顯示窗口就形成了回彈部分。
        所以其實如果我們往頁面的左右部分滑動,也是有回彈效果存在的。只是這個手勢操作被IOS寫為頁面「前進」,「后退」這兩個功能罷了。
        如果我們將html的width設置為110%,小心滑動,就能重現左右的回彈效果。左右回彈示例

        左右回彈對應二維碼

        所以在頁面縮放過程中,「fixed元素與其他元素是在不同的顯示層進行渲染」的假像,
        只是由于body相對于顯示窗口同時在橫坐標方向和縱坐標方向發現了位移,
        形成了左右兩部分的回彈顏色填充。
        而fixed元素基于顯示窗口固定,沒有發生位移。而形成的分層的假想。
        這解釋了為什么fixed元素為什么一直在底部,而不是隨著body在回彈機制下滾動。

        IOS下position顯示深度失效

        最后這個問題最簡單,也最離奇,在IOS中,除了設置z-index外,元素只根據元素在代碼中出現的順序決定其顯示的深度。布局格式并不能改變元素的顯示深度。
        分別在PC端和IOS端運行布局示例。

        在PC中,由于「按鈕」fixed定位和「其他」absolute定位脫離文檔流。所以其顯示層級比「項目列表」高,所以覆蓋在「項目列表」外面。

        position深度1

        在IOS中,元素只根據元素在代碼中出現的順序決定其顯示的深度。即:布局格式并不能改變元素的顯示深度。
        所以「項目列表」覆蓋了「其他」和「按鈕」。

        position深度2對應二維碼

        這解釋了最后一個問題(「其他」區域消失不見)。這是由于「項目列表」的padding-top將「其他」區域遮蓋住了。至于為什么要這么設計,后面會講到。

        也解釋了第二個問題(頁面滾動,「按鈕」消失了)。

        • 第二個問題是由于不改變顯示深度,所以「按鈕」仍處于「項目列表」容器內。
        • 而「按鈕」的定位是根據顯示容器,而不是body,所以滾動過程中不會跟著body移動,一定停留在底部。
        • 在滾動時,「按鈕」超出了「項目列表」的顯示區域,「項目列表」設置了overflow,不在其顯示區域的都不會被顯示。所以滾動過程中,「按鈕」逐漸消失。
        • 而多出來的部分是回彈機制填充的內容。所以產生了顏色遮蓋了「按鈕」的錯覺。

        常規解決方案

        講了這么多,那到底有什么方法可以規避上面的問題呢?

        • 既然fixed布局這么多問題,我們改用absolute布局吧。我們改成absolute示例1。但多次滾動后,我們發現了另一個問題,滾動時,「項目列表」的滾動會很容易與外部body下的滾動沖突。特別是當觸碰到非「項目列表」區域的其他內容時(如「其他」或「按鈕」),將觸發的是body的滾動,無法滾動「項目列表」,違背操作意愿。

        對應二維碼

        • 為了減少body滾動的觸發幾率,可以用「項目列表」的padding值對「其他」和「按鈕」區域進行占位。得到absolute示例2。這時,即使從「其他」區域或「按鈕」區域進行觸摸滾動,滾動的依然是「項目列表」中的內容。不會觸發body的滾動回彈。這即是一個好事,也是一個錯誤現象。因為在這個時候,移動「按鈕」和「其他」區域。也會滾動「項目列表」。這和我們的設計是不符的。

        對應二維碼

        而且以上兩個解決方案都還有另一個操作的問題,如果當「項目列表」滾動到最頂部,或者對底部。停止1秒左右(即等待滾動趨勢結束),
        再往相同方向強制滾動(例如,往下滾到最低,靜置1s,再往下滾)。這時,滾動回彈就不會在「項目列表」中進行,而是被放到了body區域上進行。這時在body的滾動趨勢還沒有結束前,無論怎么進行滾動。都會在body中觸發。
        與用戶期待的滾動不符。

        最終解決方案

        上面的一切都是由于頁面最外層的滾動回彈引起的。有沒有方案禁止頁面最外層的滾動回彈呢?很抱歉,筆者沒有找到。
        但是!筆者找到了不顯示滾動回彈顏色填充內容的方法。

        • 那就是fixed大法。直接將整個body設置成position:fixed。這時整個body都基于顯示窗口定位。不會再顯示滾動回彈內容。fixed示例1。然而這僅僅是不顯示顏色填充的內容。滾動回彈其實還是存在的,只是被body擋住了顯示不出來。頁面依然會存在上面的兩個問題。最外部的滾動回彈還是會與「項目列表」區相沖突。

        對應二維碼

        • 怎么解決沖突?去掉「項目列表」的-webkit-overflow-scrolling: touch;樣式,運行代碼。fixed示例2。

        對應二維碼

        • 沖突解決。不過滾動變卡頓了。怎么辦?換個順滑滾動的實現方案唄,例如用IScroll。fixed示例3

        對應二維碼

        查看原文

        贊 70 收藏 70 評論 14

        撒網要見魚 贊了問題 · 2018-02-08

        傳統diff算法的算法復雜度為什么是o(n3)?

        react的diff 算法很厲害的樣子~然后看了很多篇介紹~說是傳統diff算法復雜是o(n3),都是從下面這個論文里的出來了,可是看了還是不清楚???有沒有大神求教。。

        論文算法詳解:http://grfia.dlsi.ua.es/ml/al...

        關注 7 回答 2

        認證與成就

        • 獲得 2308 次點贊
        • 獲得 12 枚徽章 獲得 0 枚金徽章, 獲得 1 枚銀徽章, 獲得 11 枚銅徽章

        擅長技能
        編輯

        開源項目 & 著作
        編輯

        注冊于 2016-10-30
        個人主頁被 10.1k 人瀏覽

        一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕