<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>

        大臉貓愛吃魚

        大臉貓愛吃魚 查看完整檔案

        北京編輯東北農業大學  |  食品科學與工程 編輯汽車之家  |  前端開發工程師 編輯填寫個人主網站
        編輯

        人心似無底洞,填不滿也掏不空

        個人動態

        大臉貓愛吃魚 收藏了文章 · 3月11日

        淺談瀏覽器多進程與JS線程

        引言

        一直對瀏覽器的進程、線程的運行一無所知,經過一次的刷刷刷相關的博客之后,對其有了初步的了解,是時候該總結一波了。

        進程、線程之間的關系

        一個進程有一個或多個線程,線程之間共同完成進程分配下來的任務。打個比方:

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

        再完善完善概念:

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

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

        瀏覽器內的進程

        知道了進程與線程之間的關系之后,下面是瀏覽器與進程的關系了。首先,瀏覽器是多進程的,之所以瀏覽器能夠運行,是因為系統給瀏覽器分配了資源,如cpu、內存,簡單的說就是,瀏覽器每打開一個標簽頁,就相當于創建了一個獨立的瀏覽器進程。例如我們查看chrome里面的任務管理器。

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

        除了瀏覽器的標簽頁進程之外,瀏覽器還有一些其他進程來輔助支撐標簽頁的進程,如下:
        ① Browser進程:瀏覽器的主進程(負責協調、主控),只有一個。作用有

        • 負責瀏覽器界面顯示,與用戶交互。如前進,后退等
        • 負責各個頁面的管理,創建和銷毀其他進程
        • 網絡資源的管理,下載等

        ② 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建
        ③ GPU進程:最多一個,用于3D繪制等
        ④ 瀏覽器渲染進程(瀏覽器內核),Renderer進程,內部是多線程的,也就是我們每個標簽頁所擁有的進程,互不影響,負責頁面渲染,腳本執行,事件處理等

        如下圖:

        圖片描述

        瀏覽器內核

        瀏覽器內核,即我們的渲染進程,有名Renderer進程,我們頁面的渲染,js的執行,事件的循環都在這一進程內進行,也就是說,該進程下面擁有著多個線程,靠著這些現成共同完成渲染任務。那么這些線程是什么呢,如下:

        ① 圖形用戶界面GUI渲染線程

        • 負責渲染瀏覽器界面,包括解析HTML、CSS、構建DOM樹、Render樹、布局與繪制等
        • 當界面需要重繪(Repaint)或由于某種操作引發回流(reflow)時,該線程就會執行

        ② JS引擎線程

        • JS內核,也稱JS引擎,負責處理執行javascript腳本
        • 等待任務隊列的任務的到來,然后加以處理,瀏覽器無論什么時候都只有一個JS引擎在運行JS程序

        ③ 事件觸發線程

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

        ④ 定時觸發器線程

        • setIntervalsetTimeout所在線程
        • 定時計時器并不是由JS引擎計時的,因為如果JS引擎是單線程的,如果JS引擎處于堵塞狀態,那會影響到計時的準確
        • 當計時完成被觸發,事件會被添加到事件隊列,等待JS引擎空閑了執行
        • 注意:W3C的HTML標準中規定,setTimeout中低與4ms的時間間隔算為4ms

        ⑤ 異步HTTP請求線程

        • 在XMLHttpRequest在連接后新啟動的一個線程
        • 線程如果檢測到請求的狀態變更,如果設置有回調函數,該線程會把回調函數添加到事件隊列,同理,等待JS引擎空閑了執行

        瀏覽器內核,放圖加強記憶:

        圖片描述

        為什么JS引擎是單線程的

        JavaScript作為一門客戶端的腳本語言,主要的任務是處理用戶的交互,而用戶的交互無非就是響應DOM的增刪改,使用事件隊列的形式,一次事件循環只處理一個事件響應,使得腳本執行相對連續。如果JS引擎被設計為多線程的,那么DOM之間必然會存在資源競爭,那么語言的實現會變得非常臃腫,在客戶端跑起來,資源的消耗和性能將會是不太樂觀的,故設計為單線程的形式,并附加一些其他的線程來實現異步的形式,這樣運行成本相對于使用JS多線程來說降低了很多。

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

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

        因為JS引擎可以修改DOM樹,那么如果JS引擎在執行修改了DOM結構的同時,GUI線程也在渲染頁面,那么這樣就會導致渲染線程獲取的DOM的元素信息可能與JS引擎操作DOM后的結果不一致。為了防止這種現象,GUI線程與JS線程需要設計為互斥關系,當JS引擎執行的時候,GUI線程需要被凍結,但是GUI的渲染會被保存在一個隊列當中,等待JS引擎空閑的時候執行渲染。
        由此也可以推出,如果JS引擎正在進行CPU密集型計算,那么JS引擎將會阻塞,長時間不空閑,導致渲染進程一直不能執行渲染,頁面就會看起來卡頓卡頓的,渲染不連貫,所以,要盡量避免JS執行時間過長。

        JS引擎線程與事件觸發線程、定時觸發器線程、異步HTTP請求線程

        事件觸發線程、定時觸發器線程、異步HTTP請求線程三個線程有一個共同點,那就是使用回調函數的形式,當滿足了特定的條件,這些回調函數會被執行。這些回調函數被瀏覽器內核理解成事件,在瀏覽器內核中擁有一個事件隊列,這三個線程當滿足了內部特定的條件,會將這些回調函數添加到事件隊列中,等待JS引擎空閑執行。例如異步HTTP請求線程,線程如果檢測到請求的狀態變更,如果設置有回調函數,回調函數會被添加事件隊列中,等待JS引擎空閑了執行。
        但是,JS引擎對事件隊列(宏任務)與JS引擎內的任務(微任務)執行存在著先后循序,當每執行完一個事件隊列的時間,JS引擎會檢測內部是否有未執行的任務,如果有,將會優先執行(微任務)。

        WebWorker

        因為JS引擎是單線程的,當JS執行時間過長會頁面阻塞,那么JS就真的對CPU密集型計算無能為力么?

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

        來自MDN的官方解釋

        Web Workers 使得一個Web應用程序可以在與主執行線程分離的后臺線程中運行一個腳本操作。這樣做的好處是可以在一個單獨的線程中執行費時的處理任務,從而允許主(通常是UI)線程運行而不被阻塞/放慢。

        注意點:

        • WebWorker可以想瀏覽器申請一個子線程,該子線程服務于主線程,完全受主線程控制。
        • JS引擎線程與worker線程間通過特定的方式通信(postMessage API,需要通過序列化對象來與線程交互特定的數據)

        所以,如果需要進行一些高耗時的計算時,可以單獨開啟一個WebWorker線程,這樣不管這個WebWorker子線程怎么密集計算、怎么阻塞,都不會影響JS引擎主線程,只需要等計算結束,將結果通過postMessage傳輸給主線程就可以了。

        另外,還有個東西叫 SharedWorker,與WebWorker在概念上所不同。

        • WebWorker 只屬于某一個頁面,不會和其他標簽頁的Renderer進程共享,WebWorker是屬于Renderer進程創建的進程。
        • SharedWorker 是由瀏覽器單獨創建的進程來運行的JS程序,它被所有的Renderer進程所共享,在瀏覽器中,最多只能存在一個SharedWorker進程。

        SharedWorker由進程管理,WebWorker是某一個Renderer進程下的線程。

        瀏覽器的渲染流程

        每個瀏覽器內核的渲染流程不一樣,下面我們主要以webkit為主。
        首先是渲染的前奏:

        1. 瀏覽器輸入url,瀏覽器主進程接管,開了一個下載線程
        2. 然后進行HTTP請求(DNS查詢、IP尋址等等),等待響應,開始下載響應報文。
        3. 將下載完的內容轉交給Renderer進程管理
        4. 開始渲染...

        在說渲染之前,需要理解一些概念:

        • DOM Tree: 瀏覽器將HTML解析成樹形的數據結構。
        • CSS Rule Tree:瀏覽器將CSS解析成樹形的數據結構。
        • Render Tree:DOM樹和CSS規則樹合并后生產Render樹。
        • layout:有了Render Tree,瀏覽器已經能知道網頁中有哪些節點、各個節點的CSS定義以及他們的從屬關系,從而去計算出每個節點在屏幕中的位置。
        • painting: 按照算出來的規則,通過顯卡,把內容畫到屏幕上。
        • reflow(回流):當瀏覽器發現某個部分發生了點變化影響了布局,需要倒回去重新渲染,內行稱這個回退的過程叫 reflow。reflow 會從 <html> 這個 root frame 開始遞歸往下,依次計算所有的結點幾何尺寸和位置。reflow 幾乎是無法避免的?,F在界面上流行的一些效果,比如樹狀目錄的折疊、展開(實質上是元素的顯 示與隱藏)等,都將引起瀏覽器的 reflow。鼠標滑過、點擊……只要這些行為引起了頁面上某些元素的占位面積、定位方式、邊距等屬性的變化,都會引起它內部、周圍甚至整個頁面的重新渲 染。通常我們都無法預估瀏覽器到底會 reflow 哪一部分的代碼,它們都彼此相互影響著。
        • repaint(重繪):改變某個元素的背景色、文字顏色、邊框顏色等等不影響它周圍或內部布局的屬性時,屏幕的一部分要重畫,但是元素的幾何尺寸沒有變。

        注意:display:none的節點不會被加入Render Tree,而visibility: hidden則會,所以display:none會觸發reflow,visibility: hidden會觸發repaint。

        瀏覽器內核拿到響應報文之后,渲染大概分為以下步驟

        1. 解析html生產DOM樹。
        2. 解析CSS規則。
        3. 根據DOM Tree和CSS Tree生成Render Tree。
        4. 根據Render樹進行layout,負責各個元素節點的尺寸、位置計算。
        5. 繪制Render樹(painting),繪制頁面像素信息。
        6. 瀏覽器會將各層的信息發送給GPU,GPU會將各層合成(composite),顯示在屏幕上。

        詳細步驟略去,大概步驟如下,渲染完畢后JS引擎開始執行load事件,繪制流程見下圖。

        圖片描述

        由圖中可以看出,css在加載過程中不會影響到DOM樹的生成,但是會影響到Render樹的生成,進而影響到layout,所以一般來說,style的link標簽需要盡量放在head里面,因為在解析DOM樹的時候是自上而下的,而css樣式又是通過異步加載的,這樣的話,解析DOM樹下的body節點和加載css樣式能盡可能的并行,加快Render樹的生成的速度,當然,如果css是通過js動態添加進來的,會引起頁面的重繪或重新布局。
        從有html標準以來到目前為止(2017年5月),標準一直是規定style元素不應出現在body元素中。

        前面提到了load事件,那么與DOMContentLoaded事件有什么分別。

        • 當 DOMContentLoaded 事件觸發時,僅當DOM加載完成,不包括樣式表,圖片。 (譬如如果有async加載的腳本就不一定完成)
        • 當 onLoad 事件觸發時,頁面上所有的DOM,樣式表,腳本,圖片都已經加載完成了。 (渲染完畢了)

        順序是:DOMContentLoaded -> load

        最后

        寫到這里,總結了也有不少的內容,也對瀏覽器多線程、JS引擎有所了解,后面打算在看看JS的運行機制。前端知識也是無窮無盡,數不清的概念與無數個易忘的知識、各種框架原理,學來學去,還是發現自己知道得太少了。

        查看原文

        大臉貓愛吃魚 收藏了文章 · 3月2日

        解析vue2.0的diff算法

        轉載請注明出處

        本文轉載至我的blog

        目錄

        • 前言

        • virtual dom

        • 分析diff

        • 總結

        前言

        vue2.0加入了virtual dom,有向react靠攏的意思。vue的diff位于patch.js文件中,我的一個小框架aoy也同樣使用此算法,該算法來源于snabbdom,復雜度為O(n)。
        了解diff過程可以讓我們更高效的使用框架。
        本文力求以圖文并茂的方式來講明這個diff的過程。

        virtual dom

        如果不了解virtual dom,要理解diff的過程是比較困難的。虛擬dom對應的是真實dom, 使用document.CreateElementdocument.CreateTextNode創建的就是真實節點。

        我們可以做個試驗。打印出一個空元素的第一層屬性,可以看到標準讓元素實現的東西太多了。如果每次都重新生成新的元素,對性能是巨大的浪費。

        var mydiv = document.createElement('div');
        for(var k in mydiv ){
          console.log(k)
        }

        virtual dom就是解決這個問題的一個思路,到底什么是virtual dom呢?通俗易懂的來說就是用一個簡單的對象去代替復雜的dom對象。
        舉個簡單的例子,我們在body里插入一個class為a的div。

        var mydiv = document.createElement('div');
        mydiv.className = 'a';
        document.body.appendChild(mydiv);
        

        對于這個div我們可以用一個簡單的對象mydivVirtual代表它,它存儲了對應dom的一些重要參數,在改變dom之前,會先比較相應虛擬dom的數據,如果需要改變,才會將改變應用到真實dom上。

        //偽代碼
        var mydivVirtual = { 
          tagName: 'DIV',
          className: 'a'
        };
        var newmydivVirtual = {
           tagName: 'DIV',
           className: 'b'
        }
        if(mydivVirtual.tagName !== newmydivVirtual.tagName || mydivVirtual.className  !== newmydivVirtual.className){
           change(mydiv)
        }
        
        // 會執行相應的修改 mydiv.className = 'b';
        //最后  <div class='b'></div>
        

        讀到這里就會產生一個疑問,為什么不直接修改dom而需要加一層virtual dom呢?

        很多時候手工優化dom確實會比virtual dom效率高,對于比較簡單的dom結構用手工優化沒有問題,但當頁面結構很龐大,結構很復雜時,手工優化會花去大量時間,而且可維護性也不高,不能保證每個人都有手工優化的能力。至此,virtual dom的解決方案應運而生,virtual dom很多時候都不是最優的操作,但它具有普適性,在效率、可維護性之間達平衡。

        virtual dom 另一個重大意義就是提供一個中間層,js去寫ui,ios安卓之類的負責渲染,就像reactNative一樣。

        分析diff

        一篇相當經典的文章React’s diff algorithm中的圖,react的diff其實和vue的diff大同小異。所以這張圖能很好的解釋過程。比較只會在同層級進行, 不會跨層級比較。

        圖片描述

        舉個形象的例子。

        <!-- 之前 -->
        <div>           <!-- 層級1 -->
          <p>            <!-- 層級2 -->
            <b> aoy </b>   <!-- 層級3 -->   
            <span>diff</Span>
          </P> 
        </div>
        
        <!-- 之后 -->
        <div>            <!-- 層級1 -->
          <p>             <!-- 層級2 -->
              <b> aoy </b>        <!-- 層級3 -->
          </p>
          <span>diff</Span>
        </div>

        我們可能期望將<span>直接移動到<p>的后邊,這是最優的操作。但是實際的diff操作是移除<p>里的<span>在創建一個新的<span>插到<p>的后邊。
        因為新加的<span>在層級2,舊的在層級3,屬于不同層級的比較。

        源碼分析

        文中的代碼位于aoy-diff中,已經精簡了很多代碼,留下最核心的部分。

        diff的過程就是調用patch函數,就像打補丁一樣修改真實dom。

        function patch (oldVnode, vnode) {
            if (sameVnode(oldVnode, vnode)) {
                patchVnode(oldVnode, vnode)
            } else {
                const oEl = oldVnode.el
                let parentEle = api.parentNode(oEl)
                createEle(vnode)
                if (parentEle !== null) {
                    api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
                    api.removeChild(parentEle, oldVnode.el)
                    oldVnode = null
                }
            }
            return vnode
        }

        patch函數有兩個參數,vnodeoldVnode,也就是新舊兩個虛擬節點。在這之前,我們先了解完整的vnode都有什么屬性,舉個一個簡單的例子:

        // body下的 <div id="v" class="classA"><div> 對應的 oldVnode 就是
        
        {
          el:  div  //對真實的節點的引用,本例中就是document.querySelector('#id.classA')
          tagName: 'DIV',   //節點的標簽
          sel: 'div#v.classA'  //節點的選擇器
          data: null,       // 一個存儲節點屬性的對象,對應節點的el[prop]屬性,例如onclick , style
          children: [], //存儲子節點的數組,每個子節點也是vnode結構
          text: null,    //如果是文本節點,對應文本節點的textContent,否則為null
        }

        需要注意的是,el屬性引用的是此 virtual dom對應的真實dom,patchvnode參數的el最初是null,因為patch之前它還沒有對應的真實dom。

        來到patch的第一部分,

        if (sameVnode(oldVnode, vnode)) {
            patchVnode(oldVnode, vnode)
        } 

        sameVnode函數就是看這兩個節點是否值得比較,代碼相當簡單:

        function sameVnode(oldVnode, vnode){
            return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel
        }

        兩個vnode的key和sel相同才去比較它們,比如pspan,div.classAdiv.classB都被認為是不同結構而不去比較它們。

        如果值得比較會執行patchVnode(oldVnode, vnode),稍后會詳細講patchVnode函數。

        當節點不值得比較,進入else中

        else {
                const oEl = oldVnode.el
                let parentEle = api.parentNode(oEl)
                createEle(vnode)
                if (parentEle !== null) {
                    api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
                    api.removeChild(parentEle, oldVnode.el)
                    oldVnode = null
                }
            }

        過程如下:

        • 取得oldvnode.el的父節點,parentEle是真實dom

        • createEle(vnode)會為vnode創建它的真實dom,令vnode.el =真實dom

        • parentEle將新的dom插入,移除舊的dom
          當不值得比較時,新節點直接把老節點整個替換了

        最后

        return vnode

        patch最后會返回vnode,vnode和進入patch之前的不同在哪?
        沒錯,就是vnode.el,唯一的改變就是之前vnode.el = null, 而現在它引用的是對應的真實dom。

        var oldVnode = patch (oldVnode, vnode)

        至此完成一個patch過程。

        patchVnode

        兩個節點值得比較時,會調用patchVnode函數

        patchVnode (oldVnode, vnode) {
            const el = vnode.el = oldVnode.el
            let i, oldCh = oldVnode.children, ch = vnode.children
            if (oldVnode === vnode) return
            if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
                api.setTextContent(el, vnode.text)
            }else {
                updateEle(el, vnode, oldVnode)
                if (oldCh && ch && oldCh !== ch) {
                    updateChildren(el, oldCh, ch)
                }else if (ch){
                    createEle(vnode) //create el's children dom
                }else if (oldCh){
                    api.removeChildren(el)
                }
            }
        }

        const el = vnode.el = oldVnode.el 這是很重要的一步,讓vnode.el引用到現在的真實dom,當el修改時,vnode.el會同步變化。

        節點的比較有5種情況

        1. if (oldVnode === vnode),他們的引用一致,可以認為沒有變化。

        2. if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text),文本節點的比較,需要修改,則會調用Node.textContent = vnode.text。

        3. if( oldCh && ch && oldCh !== ch ), 兩個節點都有子節點,而且它們不一樣,這樣我們會調用updateChildren函數比較子節點,這是diff的核心,后邊會講到。

        4. else if (ch),只有新的節點有子節點,調用createEle(vnode),vnode.el已經引用了老的dom節點,createEle函數會在老dom節點上添加子節點。

        5. else if (oldCh),新節點沒有子節點,老節點有子節點,直接刪除老節點。

        updateChildren

        updateChildren (parentElm, oldCh, newCh) {
            let oldStartIdx = 0, newStartIdx = 0
            let oldEndIdx = oldCh.length - 1
            let oldStartVnode = oldCh[0]
            let oldEndVnode = oldCh[oldEndIdx]
            let newEndIdx = newCh.length - 1
            let newStartVnode = newCh[0]
            let newEndVnode = newCh[newEndIdx]
            let oldKeyToIdx
            let idxInOld
            let elmToMove
            let before
            while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
                    if (oldStartVnode == null) {   //對于vnode.key的比較,會把oldVnode = null
                        oldStartVnode = oldCh[++oldStartIdx] 
                    }else if (oldEndVnode == null) {
                        oldEndVnode = oldCh[--oldEndIdx]
                    }else if (newStartVnode == null) {
                        newStartVnode = newCh[++newStartIdx]
                    }else if (newEndVnode == null) {
                        newEndVnode = newCh[--newEndIdx]
                    }else if (sameVnode(oldStartVnode, newStartVnode)) {
                        patchVnode(oldStartVnode, newStartVnode)
                        oldStartVnode = oldCh[++oldStartIdx]
                        newStartVnode = newCh[++newStartIdx]
                    }else if (sameVnode(oldEndVnode, newEndVnode)) {
                        patchVnode(oldEndVnode, newEndVnode)
                        oldEndVnode = oldCh[--oldEndIdx]
                        newEndVnode = newCh[--newEndIdx]
                    }else if (sameVnode(oldStartVnode, newEndVnode)) {
                        patchVnode(oldStartVnode, newEndVnode)
                        api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
                        oldStartVnode = oldCh[++oldStartIdx]
                        newEndVnode = newCh[--newEndIdx]
                    }else if (sameVnode(oldEndVnode, newStartVnode)) {
                        patchVnode(oldEndVnode, newStartVnode)
                        api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
                        oldEndVnode = oldCh[--oldEndIdx]
                        newStartVnode = newCh[++newStartIdx]
                    }else {
                       // 使用key時的比較
                        if (oldKeyToIdx === undefined) {
                            oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
                        }
                        idxInOld = oldKeyToIdx[newStartVnode.key]
                        if (!idxInOld) {
                            api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                            newStartVnode = newCh[++newStartIdx]
                        }
                        else {
                            elmToMove = oldCh[idxInOld]
                            if (elmToMove.sel !== newStartVnode.sel) {
                                api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                            }else {
                                patchVnode(elmToMove, newStartVnode)
                                oldCh[idxInOld] = null
                                api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
                            }
                            newStartVnode = newCh[++newStartIdx]
                        }
                    }
                }
                if (oldStartIdx > oldEndIdx) {
                    before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
                    addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
                }else if (newStartIdx > newEndIdx) {
                    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
                }
        }

        代碼很密集,為了形象的描述這個過程,可以看看這張圖。

        圖片描述

        過程可以概括為:oldChnewCh各有兩個頭尾的變量StartIdxEndIdx,它們的2個變量相互比較,一共有4種比較方式。如果4種比較都沒匹配,如果設置了key,就會用key進行比較,在比較的過程中,變量會往中間靠,一旦StartIdx>EndIdx表明oldChnewCh至少有一個已經遍歷完了,就會結束比較。

        具體的diff分析

        設置key和不設置key的區別:
        不設key,newCh和oldCh只會進行頭尾兩端的相互比較,設key后,除了頭尾兩端的比較外,還會從用key生成的對象oldKeyToIdx中查找匹配的節點,所以為節點設置key可以更高效的利用dom。

        diff的遍歷過程中,只要是對dom進行的操作都調用api.insertBefore,api.insertBefore只是原生insertBefore的簡單封裝。
        比較分為兩種,一種是有vnode.key的,一種是沒有的。但這兩種比較對真實dom的操作是一致的。

        對于與sameVnode(oldStartVnode, newStartVnode)sameVnode(oldEndVnode,newEndVnode)為true的情況,不需要對dom進行移動。

        總結遍歷過程,有3種dom操作:

        1. oldStartVnode,newEndVnode值得比較,說明oldStartVnode.el跑到oldEndVnode.el的后邊了。

        圖中假設startIdx遍歷到1。

        圖片描述

        1. oldEndVnode,newStartVnode值得比較,oldEndVnode.el跑到了oldStartVnode.el的前邊,準確的說應該是oldEndVnode.el需要移動到oldStartVnode.el的前邊”。

        圖片描述

        1. newCh中的節點oldCh里沒有, 將新節點插入到oldStartVnode.el的前邊。

        圖片描述

        在結束時,分為兩種情況:

        1. oldStartIdx > oldEndIdx,可以認為oldCh先遍歷完。當然也有可能newCh此時也正好完成了遍歷,統一都歸為此類。此時newStartIdxnewEndIdx之間的vnode是新增的,調用addVnodes,把他們全部插進before的后邊,before很多時候是為null的。addVnodes調用的是insertBefore操作dom節點,我們看看insertBefore的文檔:parentElement.insertBefore(newElement, referenceElement)
          如果referenceElement為null則newElement將被插入到子節點的末尾。如果newElement已經在DOM樹中,newElement首先會從DOM樹中移除。所以before為null,newElement將被插入到子節點的末尾。

        圖片描述

        1. newStartIdx > newEndIdx,可以認為newCh先遍歷完。此時oldStartIdxoldEndIdx之間的vnode在新的子節點里已經不存在了,調用removeVnodes將它們從dom里刪除。

        圖片描述

        下面舉個例子,畫出diff完整的過程,每一步dom的變化都用不同顏色的線標出。

        1. a,b,c,d,e假設是4個不同的元素,我們沒有設置key時,b沒有復用,而是直接創建新的,刪除舊的。

        圖片描述

        1. 當我們給4個元素加上唯一key時,b得到了的復用。

        圖片描述

        這個例子如果我們使用手工優化,只需要3步就可以達到。

        總結

        • 盡量不要跨層級的修改dom

        • 設置key可以最大化的利用節點

        • 不要盲目相信diff的效率,在必要時可以手工優化

        查看原文

        大臉貓愛吃魚 收藏了文章 · 2020-11-25

        電視機頂盒web開發總結,避免踩坑。

        1.電視機頂盒web開發總結

        針對東方有線機頂盒UUTVOS操作系統中內置的聯彤瀏覽器web開發,總結一些自己在開發中遇到的問題和技巧。瀏覽器是基于Firefox的閹割版,所以開發中有一些莫名其妙的坑。已經嘗試過使用Vue開發機頂盒web項目,體驗較差:首次加載時間長、頁面卡頓。由于項目進度推進,當時沒有嘗試組件懶加載和路由懶加載處理,這樣做或許可以減少首次加載時間。推薦使用 JQuery 進行開發。

        1.1采坑預告

        1.2開發總結

        1.2.1一個WebStrom就夠了?↑

        我們的后臺是現成的,直接把代碼拷貝到服務器上,在機頂盒上就可以隨時預覽到項目。
        • WebStrom 的工具欄中的 Tools>Deployment 可以連接到配置遠程服務器上,每次 CTRL + S 會自動上傳項目文件,好用的不要不要的。墻裂推薦!
        • 通過配置 WebStrom,可以監聽編譯 Sass 文件,CTRL + S 自動編譯就是這么方便。
        • 喜歡 VSCode 的話,未嘗不可,或許 VSCode 里也有這些功能插件,我沒去折騰罷了。
        • 如果在 WS 中使用了 Sass 或者 Less ,每次保存的時候,被編譯后的 CSS 文件是不會自動上傳到服務器上的,需要在 WS 里手動上傳。

        1.2.2用自己喜歡的技術?↑

        • 機頂盒web開發官方文檔推薦用原生 JS 開發,目前來看的話,JQ 用起來方便一些,暫時沒有性能缺陷。
        • Less、Sass 兩個都大愛。變量的威力大大的,就算美工切得是1080p機器的圖,我拿來布局到720p上,利用 Sass 的變量和計算特性,非常容易控制CSS中的屬性值。
        • 做列表渲染的時候用到了 art-template,騰訊出的一個模板引擎,參考它的文檔,還是很容易上手的。官方文檔。

        1.2.3少用JS控制呈現 HTML 元素?↑

        機頂盒瀏覽器的性能非常低,如果還要做視頻播放的話,JS 可發揮的空間相當有限。
        • 一個 Tab 欄下有6個選項,選項里面 HTML 結構基本都是相同的,如果你打算用 JS 復用相同結構的 HTML 代碼的話,趕緊停下,像我圖片上這樣老老實實的 copy 和 paste HTML代碼吧。不然切換 Tab 的時候,隨機的卡頓很惡心。
          clipboard.png
        • 類似 $(id).css({"backgroundImage":"url('...')"})$(id).attr({"src":"./*.jpg"}) 這樣的在 JS 里面控制 UI 顯示層面的操作要避免,盡量直接在 HTML 中完成,最多能接受這個操作: $(id).addClass()。機頂盒瀏覽器就是這么傲嬌。(這是我試出來的,至于JQ操作性能方面的差異本質還是需要研究的。)
        • 機頂盒web中按鈕的尺寸一般都很大,按鈕背景圖這些東西,就不要在 JS 中去操作,如果播放視頻引起了性能高損耗,這個時候web中的UI卡的你一愣一愣的。

        1.2.4“焦點事件”使用一時爽?↑

        • 一定要避免使用"焦點事件"觸發相關操作,焦點事件是高頻率的系統事件,web在機頂盒運行時,焦點事件一般不受開發人員的絕對控制?!笆ソ裹c”事件同樣要避免使用。
        • “焦點事件”與“上下左右按鍵事件”具有一定的耦合性,“焦點事件”使用不當,問題百出。
        • 上下左右按鍵事件,一般都可以替換焦點事件。
        • a:focus {} 這個CSS選擇器可以放心的使用。

        1.2.5萬能的 setTimeout() ?↑

        機頂盒內置的瀏覽器很惡心啊,閹割版的就算了,一些邏輯上的東西跟PC上也不同。
        • 一些操作無論怎么寫都不運行,或者拿不到值(null),特別是在頁面加載、父子頁面跳轉這些場景下。給它加個 setTimeout(function(), ms) 就搞定了,百試百靈,一般人我都不告訴他^_^。
        $(document).keydown(function () {
            if (event.which  === 4097) {
                var distance = $("#list").scrollTop();
                sessionStorage.removeItem("listScrollTopVal");
                sessionStorage.setItem("listScrollTopVal", JSON.stringify(distance));
                // 按下確定鍵后,把獲得焦點的元素的 id 保存到 sessionStorage中,
                // 這個時候就要在外邊加一個延時函數,甚至可以將時間設置成 0ms 也行。
                setTimeout(function () {
                    sessionStorage.removeItem("listFocusItemId");
                    sessionStorage.setItem("listFocusItemId",JSON.stringify(document.activeElement.id));
                }, 100);
            }
        });

        1.2.6絕對定位position:absolte;省時省力?↑

        • 機頂盒的可視區域是固定的,絕對定位是最省時省力的。
        • 擁有絕對定位元素的父元素必須是 position:relative定位,這個是必須的!
        • 多個塊級元素排列在同一行,考慮使用display:inline-block;,優于使用flaot:...浮動布局。

        1.2.7overflow:scroll;不能往上滾動?↑

        電視機的可視區域固定,整個頁面是不滾動的,業務場景中,頁面中的局部需要滾動:列表頁、詳情頁。
        • 在PC上,給需要滾動的元素設置:overflow:scroll; 會出現滾動條,實現滾動。但是在電視機頂盒上,出現了:能往下滾動,不能往上滾動的問題。
        • 解決辦法:給需要滾動的元素包裹一個 <a href="#"></a>。并且必須設置display:block。;
        <div class="content">
            <a href="#/" style="display:block;outline:none;">
              <div class="content-html">需要滾動的內容</div>
            </a>
          </div>

        1.2.8切換視頻播放,加防抖必不可少?↑

        機頂盒瀏覽器的性能本來就很差,在同一個頁面的 Tab 上切換多個視頻播放,按鍵過快的情況下,UI上焦點連續切換過去很多個元素了,視頻的播放地址才挨個往過去切換,這個時候很容易造成卡頓或者瀏覽器假死。
        • 防抖其實就是一個延時函數,可以想象成:刷卡上公交車,只要有人刷卡,司機就不能開車。
        $("#nav--second").keydown(function(event){
            if(event.which === 39) {
                // 這里的EVAN是一個全局的命名空間,EVAN.timer是一個全局變量
                clearTimeout(EVAN.timer);
                EVAN.timer = setTimeout(function () 
                create(EVAN.homePageVideoUrlArr[2]);
                }, EVAN.gap);  // 時間1-2s左右比較合適。
            }
        });
        查看原文

        大臉貓愛吃魚 贊了文章 · 2020-11-25

        電視機頂盒web開發總結,避免踩坑。

        1.電視機頂盒web開發總結

        針對東方有線機頂盒UUTVOS操作系統中內置的聯彤瀏覽器web開發,總結一些自己在開發中遇到的問題和技巧。瀏覽器是基于Firefox的閹割版,所以開發中有一些莫名其妙的坑。已經嘗試過使用Vue開發機頂盒web項目,體驗較差:首次加載時間長、頁面卡頓。由于項目進度推進,當時沒有嘗試組件懶加載和路由懶加載處理,這樣做或許可以減少首次加載時間。推薦使用 JQuery 進行開發。

        1.1采坑預告

        1.2開發總結

        1.2.1一個WebStrom就夠了?↑

        我們的后臺是現成的,直接把代碼拷貝到服務器上,在機頂盒上就可以隨時預覽到項目。
        • WebStrom 的工具欄中的 Tools>Deployment 可以連接到配置遠程服務器上,每次 CTRL + S 會自動上傳項目文件,好用的不要不要的。墻裂推薦!
        • 通過配置 WebStrom,可以監聽編譯 Sass 文件,CTRL + S 自動編譯就是這么方便。
        • 喜歡 VSCode 的話,未嘗不可,或許 VSCode 里也有這些功能插件,我沒去折騰罷了。
        • 如果在 WS 中使用了 Sass 或者 Less ,每次保存的時候,被編譯后的 CSS 文件是不會自動上傳到服務器上的,需要在 WS 里手動上傳。

        1.2.2用自己喜歡的技術?↑

        • 機頂盒web開發官方文檔推薦用原生 JS 開發,目前來看的話,JQ 用起來方便一些,暫時沒有性能缺陷。
        • Less、Sass 兩個都大愛。變量的威力大大的,就算美工切得是1080p機器的圖,我拿來布局到720p上,利用 Sass 的變量和計算特性,非常容易控制CSS中的屬性值。
        • 做列表渲染的時候用到了 art-template,騰訊出的一個模板引擎,參考它的文檔,還是很容易上手的。官方文檔。

        1.2.3少用JS控制呈現 HTML 元素?↑

        機頂盒瀏覽器的性能非常低,如果還要做視頻播放的話,JS 可發揮的空間相當有限。
        • 一個 Tab 欄下有6個選項,選項里面 HTML 結構基本都是相同的,如果你打算用 JS 復用相同結構的 HTML 代碼的話,趕緊停下,像我圖片上這樣老老實實的 copy 和 paste HTML代碼吧。不然切換 Tab 的時候,隨機的卡頓很惡心。
          clipboard.png
        • 類似 $(id).css({"backgroundImage":"url('...')"})$(id).attr({"src":"./*.jpg"}) 這樣的在 JS 里面控制 UI 顯示層面的操作要避免,盡量直接在 HTML 中完成,最多能接受這個操作: $(id).addClass()。機頂盒瀏覽器就是這么傲嬌。(這是我試出來的,至于JQ操作性能方面的差異本質還是需要研究的。)
        • 機頂盒web中按鈕的尺寸一般都很大,按鈕背景圖這些東西,就不要在 JS 中去操作,如果播放視頻引起了性能高損耗,這個時候web中的UI卡的你一愣一愣的。

        1.2.4“焦點事件”使用一時爽?↑

        • 一定要避免使用"焦點事件"觸發相關操作,焦點事件是高頻率的系統事件,web在機頂盒運行時,焦點事件一般不受開發人員的絕對控制?!笆ソ裹c”事件同樣要避免使用。
        • “焦點事件”與“上下左右按鍵事件”具有一定的耦合性,“焦點事件”使用不當,問題百出。
        • 上下左右按鍵事件,一般都可以替換焦點事件。
        • a:focus {} 這個CSS選擇器可以放心的使用。

        1.2.5萬能的 setTimeout() ?↑

        機頂盒內置的瀏覽器很惡心啊,閹割版的就算了,一些邏輯上的東西跟PC上也不同。
        • 一些操作無論怎么寫都不運行,或者拿不到值(null),特別是在頁面加載、父子頁面跳轉這些場景下。給它加個 setTimeout(function(), ms) 就搞定了,百試百靈,一般人我都不告訴他^_^。
        $(document).keydown(function () {
            if (event.which  === 4097) {
                var distance = $("#list").scrollTop();
                sessionStorage.removeItem("listScrollTopVal");
                sessionStorage.setItem("listScrollTopVal", JSON.stringify(distance));
                // 按下確定鍵后,把獲得焦點的元素的 id 保存到 sessionStorage中,
                // 這個時候就要在外邊加一個延時函數,甚至可以將時間設置成 0ms 也行。
                setTimeout(function () {
                    sessionStorage.removeItem("listFocusItemId");
                    sessionStorage.setItem("listFocusItemId",JSON.stringify(document.activeElement.id));
                }, 100);
            }
        });

        1.2.6絕對定位position:absolte;省時省力?↑

        • 機頂盒的可視區域是固定的,絕對定位是最省時省力的。
        • 擁有絕對定位元素的父元素必須是 position:relative定位,這個是必須的!
        • 多個塊級元素排列在同一行,考慮使用display:inline-block;,優于使用flaot:...浮動布局。

        1.2.7overflow:scroll;不能往上滾動?↑

        電視機的可視區域固定,整個頁面是不滾動的,業務場景中,頁面中的局部需要滾動:列表頁、詳情頁。
        • 在PC上,給需要滾動的元素設置:overflow:scroll; 會出現滾動條,實現滾動。但是在電視機頂盒上,出現了:能往下滾動,不能往上滾動的問題。
        • 解決辦法:給需要滾動的元素包裹一個 <a href="#"></a>。并且必須設置display:block。;
        <div class="content">
            <a href="#/" style="display:block;outline:none;">
              <div class="content-html">需要滾動的內容</div>
            </a>
          </div>

        1.2.8切換視頻播放,加防抖必不可少?↑

        機頂盒瀏覽器的性能本來就很差,在同一個頁面的 Tab 上切換多個視頻播放,按鍵過快的情況下,UI上焦點連續切換過去很多個元素了,視頻的播放地址才挨個往過去切換,這個時候很容易造成卡頓或者瀏覽器假死。
        • 防抖其實就是一個延時函數,可以想象成:刷卡上公交車,只要有人刷卡,司機就不能開車。
        $("#nav--second").keydown(function(event){
            if(event.which === 39) {
                // 這里的EVAN是一個全局的命名空間,EVAN.timer是一個全局變量
                clearTimeout(EVAN.timer);
                EVAN.timer = setTimeout(function () 
                create(EVAN.homePageVideoUrlArr[2]);
                }, EVAN.gap);  // 時間1-2s左右比較合適。
            }
        });
        查看原文

        贊 14 收藏 10 評論 8

        大臉貓愛吃魚 收藏了文章 · 2020-07-30

        2019年幾道常見js手寫面試題總結

        最近出去面了幾家試試水,也在整理一些面試題。我已經總結在gitbook/github里了,主要作用就是總結和分享一下自己的心得體會,現在每天還在持續更新中,歡迎大家star,有問題請隨時提issue

        github地址

        gitbook地址

        1. 手寫new操作符

        function newClass(obj, args) {
            let newObj = {};
            newObj.__proto__ = obj.prototype
            obj.call(newObj, args);
            return newObj
        }
        
        function a(text) {
            this.text = text;
        }
        
        let b = newClass(a, 'test');
        console.log(b) // {text: "test"}

        2. 手寫防抖/節流

        // 防抖
        function debounceHandle(fn) {
            let timer = null;
            return function () {
                clearTimeout(timer);
                timer = setTimeout(function () {
                    fn.call(this, arguments);
                }, 300)
            }
        }
        // 節流
        function throttle(fn, delay) {     
            var timer = null;     
            var lastTime = Date.now();     
            return function() {             
                var curTime = Date.now();
                var interval = delay - (curTime - lastTime);  // 計算間隔             
                var context = this;             
                var args = arguments;             
                clearTimeout(timer);              
                if (interval <= 0) {              
                    fn.apply(context, args);                    
                    startTime = Date.now();              
                } else {                    
                    timer = setTimeout(fn, interval);              
                }
            }
        }

        3. 手寫promise

        首先明確三種狀態

        • pending - 進行中
        • fulfilled - 成功
        • rejected - 失敗
        function NewPromise(executor) {
            let _this = this;
            this.state = 'pending';
            this.value = undefined;
            this.reason = undefined;
            this.onFulfilledFunc = [];//保存成功回調
            this.onRejectedFunc = [];//保存失敗回調
        
            executor(resolve, reject);
        
            function resolve(value) {
                if (_this.state === 'pending') {
                    _this.value = value;
                    //依次執行成功回調
                    _this.onFulfilledFunc.forEach(fn => fn(value));
                    _this.state = 'fulfilled';
                }
            }
        
            function reject(reason) {
                if (_this.state === 'pending') {
                    _this.reason = reason;
                    //依次執行失敗回調
                    _this.onRejectedFunc.forEach(fn => fn(reason));
                    _this.state = 'rejected';
                }
            }
        }
        
        NewPromise.prototype.then = function (onFulfilled, onRejected) {
            let self = this;
            if (self.state === 'pending') {
                if (typeof onFulfilled === 'function') {
                    return new NewPromise((resolve, reject) => {
                        self.onFulfilledFunc.push(() => {
                            let x = onFulfilled(self.value);
                            if (x instanceof Promise) {
                                x.then(resolve, reject)
                            } else {
                                resolve(x)
                            }
                        });
                    })
                }
                if (typeof onRejected === 'function') {
                    return new NewPromise((resolve, reject) => {
                        self.onRejectedFunc.push(() => {
                            let x = onRejected(self.value);
                            if (x instanceof Promise) {
                                x.then(resolve, reject)
                            } else {
                                resolve(x)
                            }
                        });
                    })
                }
            }
            if (self.state === 'fulfilled') {
                if (typeof onFulfilled === 'function') {
                    return new NewPromise((resolve, reject) => {
                        let x = onFulfilled(self.value);
                        if (x instanceof Promise) {
                            x.then(resolve, reject)
                        } else {
                            resolve(x)
                        }
                    })
                }
        
            }
            if (self.state === 'rejected') {
                if (typeof onRejected === 'function') {
                    return new NewPromise((resolve, reject) => {
                        let x = onRejected(self.reason);
                        if (x instanceof Promise) {
                            x.then(resolve, reject)
                        } else {
                            resolve(x)
                        }
                    })
                }
            }
        };
        
        let p = new NewPromise((resolve, reject) => {
            console.log(1) // 輸出 1
            resolve(2);
        });
        
        p.then(x => {
            console.log(x); // 輸出 2
            return 3
        }).then(x => {
            console.log(x); // 輸出3
            return 4;
        }).then(x => {
            console.log(x) // 輸出4
            console.log('輸出完畢')
        });

        執行結果

        4. 箭頭函數

        ES6新增箭頭函數,總結起來有如下幾個注意點

        this指向

        let obj = {
            name: 'ronaldo',
            getName: function () {
                return this.name;
            },
            getName2: () => {
                return this.name;
            }
        }
        
        console.log(obj.getName())  // 輸出ronaldo
        console.log(obj.getName2()) // 輸出空,此時this等于window

        無法當構造函數

        var Person = (name, age) => {
            this.name = name;
            this.age = age;
        }
        var p = new Person('messi', 18); // Uncaught TypeError: Person is not a constructor

        arguments參數無法獲取當前傳入的參數

        let func1 = function () {
            console.log(arguments);
        }
        
        let func2 = () => {
            console.log(arguments);
        }
        func1(1, 2, 3);  // Arguments(3)?[1, 2, 3] 參數有1,2,3
        func2(1, 2, 3);  //Arguments() [] 無參數

        但是可以通過剩余參數來獲取箭頭函數傳入參數

        let func1 = function () {
            console.log(arguments);
        }
        
        let func2 = (...args) => {
            console.log(args);
        }
        func1(1, 2, 3);  // Arguments(3)?[1, 2, 3] 參數有1,2,3
        func2(1, 2, 3);  // [1,2,3] 注:純數組,不再是Arguments對象

        5. 解構

        個人理解,解構就是ES6新增對數組和對象實現分離內部元素/屬性對快速操作

        解構還是很好理解的,下面一段代碼理解了就足夠了

        let obj = { d: 'aaaa', e: { f: 'bbbb' }, g: 100 };
        let { d, ...a } = obj;
        console.log(d);
        console.log(a);
        a.e.f = 'cccc';
        console.log(a);
        console.log(obj);

        執行結果

        1. 用...的時候是解構出來的是剩下的所有屬性
        2. 解構是淺拷貝?。。。?!

        6. 手寫EventBus

        function EventBusClass() {
            this.msgQueues = {}
        }
        
        EventBusClass.prototype = {
            // 將消息保存到當前的消息隊列中
            on: function (msgName, func) {
                if (this.msgQueues.hasOwnProperty(msgName)) {
                    if (typeof this.msgQueues[msgName] === 'function') {
                        this.msgQueues[msgName] = [this.msgQueues[msgName], func]
                    } else {
                        this.msgQueues[msgName] = [...this.msgQueues[msgName], func]
                    }
                } else {
                    this.msgQueues[msgName] = func;
                }
            },
            // 消息隊列中僅保存一個消息
            one: function (msgName, func) {
                // 無需檢查msgName是否存在
                this.msgQueues[msgName] = func;
            },
            // 發送消息
            emit: function (msgName, msg) {
                if (!this.msgQueues.hasOwnProperty(msgName)) {
                    return
                }
                if (typeof this.msgQueues[msgName] === 'function') {
                    this.msgQueues[msgName](msg)
                } else {
                    this.msgQueues[msgName].map((fn) => {
                        fn(msg)
                    })
                }
            },
            // 移除消息
            off: function (msgName) {
                if (!this.msgQueues.hasOwnProperty(msgName)) {
                    return
                }
                delete this.msgQueues[msgName]
            }
        }
        
        // 將EventBus放到window對象中
        const EventBus = new EventBusClass()
        EventBus.on('first-event', function (msg) {
            console.log(`訂閱的消息是:${msg}`);
        });
        EventBus.emit('first-event', 123213)
        
        // 輸出結果
        // 訂閱的消息是:123213
        

        7. 手寫LazyMan

        實現一個LazyMan,可以按照以下方式調用:
        LazyMan(“Hank”)輸出:
        Hi! This is Hank!
        ?
        LazyMan(“Hank”).sleep(10).eat(“dinner”)輸出
        Hi! This is Hank!
        //等待10秒..
        Wake up after 10
        Eat dinner~
        ?
        LazyMan(“Hank”).eat(“dinner”).eat(“supper”)輸出
        Hi This is Hank!
        Eat dinner~
        Eat supper~
        ?
        LazyMan(“Hank”).sleepFirst(5).eat(“supper”)輸出
        //等待5秒
        Wake up after 5
        Hi This is Hank!
        Eat supper
        ?
        以此類推。
        
        function _lazyman(name) {
            this.tasks = [];
            var that = this;
            var fn = (function (name) {
                return function () {
                    console.log("Hello I'm " + name);
                    that.next();
                }
            })(name);
        
            this.tasks.push(fn);
        
            setTimeout(function () { that.next() }, 0) // setTimeout延遲0ms也未必是立刻執行哦
        }
        
        _lazyman.prototype = {
            constructor: _lazyman,
        
            //next是實現函數在隊列中順序執行功能的函數
        
            next: function () {
                var fn = this.tasks.shift();
                fn && fn();
            },
        
            sleep: function (time) {
                var that = this;
                var fn = (function (time) {
                    return function () {
                        console.log("sleep......." + time);
                        setTimeout(function () {
                            that.next();
                        }, time)
                    }
                })(time);
                this.tasks.push(fn);
        
                return this; //return this是為了實現鏈式調用
            },
        
            sleepFirst: function (time) {
                var that = this;
                var fn = (function (time) {
                    return function () {
                        console.log("sleep......." + time);
                        setTimeout(function () {
                            that.next();
                        }, time)
                    }
                })(time);
                this.tasks.unshift(fn);
                return this;
            },
        
            eat: function (something) {
                var that = this;
                var fn = (function (something) {
                    return function () {
                        console.log("Eat " + something);
                        that.next();
                    }
                })(something)
                this.tasks.push(fn);
                return this;
            }
        }
        function LazyMan(name) {
            return new _lazyman(name);
        }
        LazyMan("Joe").sleepFirst(3000).eat("breakfast").sleep(1000).eat("dinner");
        // LazyMan('Hank').sleepFirst(5).eat('supper')
        // sleep.......3000
        // Hello I'm Joe
        // Eat breakfast
        // sleep.......1000
        // Eat dinner

        實現思路

        1. LazyMan()不是new出來的,需要在其內部封裝一下return new _lazyman,_lazyman等同于構造函數,這樣我執行一次LazyMan(),就會創建一個對象,是不是有點工廠模式的感覺
        2. 內部用tasks數組存儲所有任務
        3. next()用于執行tasks數組中第一任務,并將其從tasks數組中刪除
        4. sleepFirst()方法,內部將創建的閉包函數,將創建的sleepFirst任務加入tasks數組第一個
        5. eat,sleep,sleepFirst內部是用閉包執行,這樣就能保留傳入的參數,待后續tasks取出任務執行
        6. 重點:_lazyman()中的 setTimeout(function () { that.next() }, 0)最后執行時,tasks里按執行順序存放所有任務,是不是很巧妙,并且每個任務都會執行that.next()

        這個面試題綜合了原型,工廠模式,異步隊列,閉包知識。含金量很高呦

        查看原文

        大臉貓愛吃魚 收藏了文章 · 2020-07-26

        面試中React與Vue的比對

        1.virtual dom

        • 用JS模擬DOM結構,DOM變化的對比,放在JS層做,以提高重繪性能
        • DOM操作昂貴,JS運行效率高,要減少DOM操作
        • 使用:snabbdom的使用
        • 核心API:h函數 h(‘標簽名’,{屬性},[子元素]) 或者h(‘標簽名’,{屬性},’’)
        • patch(container,vnode) 或者 path(vnode,newvnode)

        2.MVVM中jQuery與Vue區別

        視圖與數據的分離,解耦(開放封閉原則)
        以數據驅動視圖,只關心數據,DOM操作被封裝

        3.對MVVM的理解

        • MVC:model view controller

        (數據模型->視圖->控制器->數據模型)

        clipboard.png

        或者 (控制器->數據模型->視圖)

        clipboard.png

        MVVM:model view viewmodel(視圖與數據之間的橋:事件綁定,數據綁定)

        clipboard.png
        clipboard.png

        4.Vue三要素

        • 響應式:修改data屬性后,Vue立刻監聽到
        • 響應式核心:用Object.defineProperty,將data的屬性代理到vm上
        • 模板引擎:

          • 本質:字符串;有邏輯,v-if,v-for等,可以嵌入JS變量,必須用JS才能實現,轉換成html渲染頁面,模板最終轉化成JS函數(render函數:with方法)
        • 渲染

        5.Vue的流程

        • 解析模板成render函數:

          • with的使用,模板中所有信息都被render函數包含
          • 模板中用到的data中的屬性,都變成JS變量,模板中的v-model v-for v-on都變成JS邏輯,render函數返回vnode
        • 響應式開始監聽:

          • Object.defineProperty將data的屬性代理到VM上,使用get監聽是為了防止重復渲染
        • 首次渲染,顯示頁面,且綁定依賴
        • data屬性變化,觸發render

        6.react

        • 組件化:組件封裝,組件復用
        • 組件封裝:封裝視圖、數據、變化邏輯(數據驅動視圖變化)
        • 組件復用:props傳遞,復用
        • JSX本質:JSX是語法糖,需要被解析成JS才能運行,JSX是獨立的標準,可被其他項目使用
        • JSX就是模板,最終要渲染成html
        • 需要vdom的原因:JSX要渲染成html,數據驅動視圖

        7.setState過程

        setState的異步:

        (原因):可能一次執行多次setState,無法規定、限制用戶如何使用setState,沒必要每次setState都重新渲染,考慮性能,即便每次重新渲染,用戶看不到中間渲染的效果,所以只要最后的效果就可

        8.Vue與react

        • 兩者本質的區別:模板和組件化的區別

          • Vue本質是MVVM框架,由MVC發展而來;
          • React是前端組件化框架,由后端組件化發展而來;
        • Vue使用模板
        • React使用JSX
        • React本身就是組件化
        • Vue是在MVVM上擴展的
        • 共同點:

          • 都支持組件化,都是數據驅動視圖
        查看原文

        大臉貓愛吃魚 收藏了文章 · 2019-03-28

        webpack選擇性編譯DefinePlugin(打包自動剔除測試數據)

        背景

        程序在開發的過程中,少不了打印調試用的日志,測試流程時偽造的數據。這些代碼是不能出現在生產環境上的。
        這意味著在程序打包前,需要把相關代碼剔除掉。
        這些事情用人手去做很麻煩,很容易疏漏。而且打包出來測試時遇到了bug,又得重新把測試代碼添加回去。重復整個繁瑣的過程。
        004

        既然人工做這么麻煩而且容易出錯,那能不能用程序幫我們完成這些事情呢?
        當然可以,用選擇性編譯技術就行,本文就介紹在webpack下解決這一問題的方法。
        其實這個方法在webpack官網就有提到,本文只是提供相關示例及做一些延伸。

        選擇性編譯是指根據打包是環境的不同,選擇性地讓特定的語句有效,讓特定的語句無效。
        最簡單的例子,在開發環境中,我們打印日志,但在生產環境中,我們讓所有打印日志的語句無效(讓程序不運行打印的語句,甚至讓打包出來的文件根本就不包含打印日志的語句)。
        選擇性編譯是筆者自己瞎想出來的名詞,不知道用的對不對。
        001

        實踐過程

        首先,先讓我們看一個選擇性打印日志的示例。

        • 新建一個vue項目
        • 打開build/webpack.dev.conf.js文件(項目處于開發環境時使用到的webpack打包配置,運行npm run dev,這個文件就會被執行)
        • 添加如下代碼

          new webpack.DefinePlugin({
              'process.env': config.dev.env,
              IS_DEV: JSON.stringify(true),
          }),

          dev.conf

        • 打開build/webpack.prod.conf.js文件(項目打包生產環境時使用到的webpack打包配置,運行npm run build,這個文件就會被執行)
        • 添加如下代碼

          new webpack.DefinePlugin({
              'process.env': config.dev.env,
              IS_DEV: JSON.stringify(false),
          }),

          prod.conf

        • 打開src/main.js文件(項目入口文件,運行項目時被執行)
        • 添加如下代碼

          if (IS_DEV) {
              console.log('this is dev env');
          } else {
              console.log('this is prod env');
          }

          main

        • 分別在開發環境和生產環境運行程序,查看控制臺。我們發現,在開發環境下,打印了this is dev env。在生產環境下運行打印了this is prod env。
        • 打開打包出來的文件/dist/static/js/app.xxx.js.map,搜索this is prod env。我們發現IS_DEV變成了false。
        • 同理,我們在開發環境下,查看chrome開發者工具,找到相應的app.xxx.js文件。我們發現IS_DEV變成了true。

        app.js.map

        由此我們可以知道

        • 選擇性編譯本質上是字符串的替換,先經過DefinePlugin對代碼中的特定字符串進行替換。再對替換后的代碼進行編譯打包。
        • 需要替換的變量需要分別在webpack.dev.conf.jswebpack.prod.conf.js中指定其轉換后的意義。
        • 為什么在mian.js中使用IS_DEV,程序不會報"IS_DEV is not defined"的錯誤?

        因為瀏覽器在運行代碼時,拿到的文件里面IS_DEV已經被替換成了true或者false,已經不存在IS_DEV這個變量。所以不會報錯。app.js.map

        DefinePlugin的使用說明

        • 除了替換成簡單的布爾值,還可以替換成字符串,數值,數組,對象等。

        如TEST_DATA: JSON.stringify({name:'momo',age:18}),

        • 為什么需要進行JSON.stringify替換?

        008008

        • 另外還可以替換成某個段代碼的值(代碼內容直接使用""包裹即可)。

        如:TWO: "1+1",TOW將被替換成這段代碼的結果,即2。

        ESLint沖突處理

        ESLint是一個用來識別 ECMAScript 并且按照規則給出報告的代碼檢測工具,使用它可以避免低級錯誤和統一代碼的風格。
        配置了eslint的項目在使用選擇性編譯功能時,可能會報出這樣的錯誤。
        http://exlint.org/docs/rules/no-undef 'IS_DEV' is not defined
        正如報錯信息所說的,這是由于eslint檢測代碼時,發現IS_DEV沒有定義(這側面說明了eslint是先于條件編譯執行的。ESLint檢測時,IS_DEV還沒有被替換掉)。解決這個問題有以下三種方法:
        • 簡單粗暴的方法,修改eslint的配置,將error改為warn唄,先打包了再說。具體操作為修改eslintrc.js文件,如果配置文件里面已有第一條語句'eslint':'recommended'(這是引用默認配置)。則往rules里面添加一條規則'no_undef':'warn',以覆蓋掉默認配置。

        如果已經存在'no-undef'的配置,則直接改為'warn'就行。eslint-1

        • 上面的方法影響太大,直接修改了規則。能不能局部修改規則,只在特定語句中忽略掉該規則呢?可以的,使用備注包裹需要忽略的語句就行。eslint-2
        • 但是上面的方法需要在每個用到的地方都多寫兩條語句,略麻。,有沒有更簡單的,一勞永逸的方法呢?有有有~~ 修改eslintrc.js,將IS_DEV配置成一個全局變量,之后eslint就不會認為IS_DEV是未定義的變量了。eslint-3

        選擇性編輯技術的應用場景

        • API接口的環境替換
        • 賬號信息的模擬,數據模擬
        • 日志打印等

        002

        彩蛋

        更優的去除log的方法

        用if邏輯判斷來輸出log可能略顯繁瑣。其實對于控制臺輸出的日志,我們可以通過UglifyJs在打包時來剔除。
        具體操作:

        • 打開build/webpack.prod.conf.js文件,添加如下語句

          compress: {
              warnings: false, // 去除warning警告
              drop_debugger: true, // 發布時去除debugger語句
              drop_console: true // 發布時去除console語句
          },
        • 如果想只去除console.log,同時保留console.error等錯誤提示??梢灾付ㄈコ囟ǖ暮瘮?/p>

          compress: {
              warnings: false, // 去除warning警告
              pure_funcs: ['console.log'], // 配置發布時,不被打包的函數
              // drop_debugger: true, // 發布時去除debugger
              // drop_console: true // 發布時去除console
          }

          注意,添加了這個配置以后,console.log在打包出來的文件就不存在了。所以前面測試用的this is dev env也會消失不見
          console.log

        去除條件編譯中不可達代碼

        • 什么叫不可達代碼?就是無論什么情況下都不會被運行的代碼。

        例如我們在條件編譯打包出來的代碼中

        if (false) {
            console.log('this is dev env');
        } else {
            console.log('this is prod env');
        }
        ```console.log('this is dev env');```就是不可達代碼。
        我們同樣可以使用UglifyJs的功能把這部分無用代碼去除掉。讓條件編譯不留痕跡。
        具體配置如下:
        ```
        compress: {
            warnings: false, // 去除warning警告
            dead_code: true, // 去除不可達代碼
        }, sourceMap: true
        ```
        ![dead_code](http://upyun.luckly-mjw.cn/Assets/selective-compilation/dead_code.png)

        UglifyJs的更多功能

        UglifyJs還用很多強大的功能,如代碼混淆,壓縮,重拍版等。這里附上UglifyJs官方網址。
        英文不好的同學還可以查看對應的中文文檔(其實這才是重點#機智臉)。
        uglifyJS

        完結,撒花花

        003

        查看原文

        大臉貓愛吃魚 贊了文章 · 2019-03-28

        webpack選擇性編譯DefinePlugin(打包自動剔除測試數據)

        背景

        程序在開發的過程中,少不了打印調試用的日志,測試流程時偽造的數據。這些代碼是不能出現在生產環境上的。
        這意味著在程序打包前,需要把相關代碼剔除掉。
        這些事情用人手去做很麻煩,很容易疏漏。而且打包出來測試時遇到了bug,又得重新把測試代碼添加回去。重復整個繁瑣的過程。
        004

        既然人工做這么麻煩而且容易出錯,那能不能用程序幫我們完成這些事情呢?
        當然可以,用選擇性編譯技術就行,本文就介紹在webpack下解決這一問題的方法。
        其實這個方法在webpack官網就有提到,本文只是提供相關示例及做一些延伸。

        選擇性編譯是指根據打包是環境的不同,選擇性地讓特定的語句有效,讓特定的語句無效。
        最簡單的例子,在開發環境中,我們打印日志,但在生產環境中,我們讓所有打印日志的語句無效(讓程序不運行打印的語句,甚至讓打包出來的文件根本就不包含打印日志的語句)。
        選擇性編譯是筆者自己瞎想出來的名詞,不知道用的對不對。
        001

        實踐過程

        首先,先讓我們看一個選擇性打印日志的示例。

        • 新建一個vue項目
        • 打開build/webpack.dev.conf.js文件(項目處于開發環境時使用到的webpack打包配置,運行npm run dev,這個文件就會被執行)
        • 添加如下代碼

          new webpack.DefinePlugin({
              'process.env': config.dev.env,
              IS_DEV: JSON.stringify(true),
          }),

          dev.conf

        • 打開build/webpack.prod.conf.js文件(項目打包生產環境時使用到的webpack打包配置,運行npm run build,這個文件就會被執行)
        • 添加如下代碼

          new webpack.DefinePlugin({
              'process.env': config.dev.env,
              IS_DEV: JSON.stringify(false),
          }),

          prod.conf

        • 打開src/main.js文件(項目入口文件,運行項目時被執行)
        • 添加如下代碼

          if (IS_DEV) {
              console.log('this is dev env');
          } else {
              console.log('this is prod env');
          }

          main

        • 分別在開發環境和生產環境運行程序,查看控制臺。我們發現,在開發環境下,打印了this is dev env。在生產環境下運行打印了this is prod env。
        • 打開打包出來的文件/dist/static/js/app.xxx.js.map,搜索this is prod env。我們發現IS_DEV變成了false。
        • 同理,我們在開發環境下,查看chrome開發者工具,找到相應的app.xxx.js文件。我們發現IS_DEV變成了true。

        app.js.map

        由此我們可以知道

        • 選擇性編譯本質上是字符串的替換,先經過DefinePlugin對代碼中的特定字符串進行替換。再對替換后的代碼進行編譯打包。
        • 需要替換的變量需要分別在webpack.dev.conf.jswebpack.prod.conf.js中指定其轉換后的意義。
        • 為什么在mian.js中使用IS_DEV,程序不會報"IS_DEV is not defined"的錯誤?

        因為瀏覽器在運行代碼時,拿到的文件里面IS_DEV已經被替換成了true或者false,已經不存在IS_DEV這個變量。所以不會報錯。app.js.map

        DefinePlugin的使用說明

        • 除了替換成簡單的布爾值,還可以替換成字符串,數值,數組,對象等。

        如TEST_DATA: JSON.stringify({name:'momo',age:18}),

        • 為什么需要進行JSON.stringify替換?

        008008

        • 另外還可以替換成某個段代碼的值(代碼內容直接使用""包裹即可)。

        如:TWO: "1+1",TOW將被替換成這段代碼的結果,即2。

        ESLint沖突處理

        ESLint是一個用來識別 ECMAScript 并且按照規則給出報告的代碼檢測工具,使用它可以避免低級錯誤和統一代碼的風格。
        配置了eslint的項目在使用選擇性編譯功能時,可能會報出這樣的錯誤。
        http://exlint.org/docs/rules/no-undef 'IS_DEV' is not defined
        正如報錯信息所說的,這是由于eslint檢測代碼時,發現IS_DEV沒有定義(這側面說明了eslint是先于條件編譯執行的。ESLint檢測時,IS_DEV還沒有被替換掉)。解決這個問題有以下三種方法:
        • 簡單粗暴的方法,修改eslint的配置,將error改為warn唄,先打包了再說。具體操作為修改eslintrc.js文件,如果配置文件里面已有第一條語句'eslint':'recommended'(這是引用默認配置)。則往rules里面添加一條規則'no_undef':'warn',以覆蓋掉默認配置。

        如果已經存在'no-undef'的配置,則直接改為'warn'就行。eslint-1

        • 上面的方法影響太大,直接修改了規則。能不能局部修改規則,只在特定語句中忽略掉該規則呢?可以的,使用備注包裹需要忽略的語句就行。eslint-2
        • 但是上面的方法需要在每個用到的地方都多寫兩條語句,略麻。,有沒有更簡單的,一勞永逸的方法呢?有有有~~ 修改eslintrc.js,將IS_DEV配置成一個全局變量,之后eslint就不會認為IS_DEV是未定義的變量了。eslint-3

        選擇性編輯技術的應用場景

        • API接口的環境替換
        • 賬號信息的模擬,數據模擬
        • 日志打印等

        002

        彩蛋

        更優的去除log的方法

        用if邏輯判斷來輸出log可能略顯繁瑣。其實對于控制臺輸出的日志,我們可以通過UglifyJs在打包時來剔除。
        具體操作:

        • 打開build/webpack.prod.conf.js文件,添加如下語句

          compress: {
              warnings: false, // 去除warning警告
              drop_debugger: true, // 發布時去除debugger語句
              drop_console: true // 發布時去除console語句
          },
        • 如果想只去除console.log,同時保留console.error等錯誤提示??梢灾付ㄈコ囟ǖ暮瘮?/p>

          compress: {
              warnings: false, // 去除warning警告
              pure_funcs: ['console.log'], // 配置發布時,不被打包的函數
              // drop_debugger: true, // 發布時去除debugger
              // drop_console: true // 發布時去除console
          }

          注意,添加了這個配置以后,console.log在打包出來的文件就不存在了。所以前面測試用的this is dev env也會消失不見
          console.log

        去除條件編譯中不可達代碼

        • 什么叫不可達代碼?就是無論什么情況下都不會被運行的代碼。

        例如我們在條件編譯打包出來的代碼中

        if (false) {
            console.log('this is dev env');
        } else {
            console.log('this is prod env');
        }
        ```console.log('this is dev env');```就是不可達代碼。
        我們同樣可以使用UglifyJs的功能把這部分無用代碼去除掉。讓條件編譯不留痕跡。
        具體配置如下:
        ```
        compress: {
            warnings: false, // 去除warning警告
            dead_code: true, // 去除不可達代碼
        }, sourceMap: true
        ```
        ![dead_code](http://upyun.luckly-mjw.cn/Assets/selective-compilation/dead_code.png)

        UglifyJs的更多功能

        UglifyJs還用很多強大的功能,如代碼混淆,壓縮,重拍版等。這里附上UglifyJs官方網址。
        英文不好的同學還可以查看對應的中文文檔(其實這才是重點#機智臉)。
        uglifyJS

        完結,撒花花

        003

        查看原文

        贊 38 收藏 30 評論 4

        大臉貓愛吃魚 提出了問題 · 2018-11-30

        webpack 外部擴展 externals引用的問題

        在index.html引入zepto.min.js

        //webpack.config.js
        externals: {
          'zepto': 'Zepto'
        }
        
        //index.js
        import $ from 'zepto'

        這樣引用,就會報錯,Cannot find module 'zepto'
        但是

        //webpack.config.js
        externals: {
          zepto : {
              commonjs: 'Zepto',
              amd: 'Zepto',
              root: '$' // 指向全局變量
           }
        }
        
        //index.js
        window.$

        這樣就沒問題,所以是我哪里寫錯了么

        關注 2 回答 1

        大臉貓愛吃魚 回答了問題 · 2018-11-16

        babel配置:如何減少core-js中各模塊的使用?

        請問后來處理掉了么

        關注 4 回答 2

        認證與成就

        • 獲得 4 次點贊
        • 獲得 14 枚徽章 獲得 0 枚金徽章, 獲得 1 枚銀徽章, 獲得 13 枚銅徽章

        擅長技能
        編輯

        開源項目 & 著作
        編輯

        (??? )
        暫時沒有

        注冊于 2016-06-12
        個人主頁被 768 人瀏覽

        一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕