我認真起來連面試官都怕(塊級作用域,事件代理)

混元霹靂手發表於2017-04-23

作者 混元霹靂手-Ziksang

如果你學完這篇文章之後,你回答完面試官之後,拿一張圖告訴面試官

我認真起來連面試官都怕(塊級作用域,事件代理)

在於前端面試,你給面試官講一些官方名詞,我知道react,vue,angular等等,一系列牛B的框架,對於面試來說並沒有卵用,聽多了!!有些有是報著真誠的找工作的態度,有些人只是想面面現在的水平如何,但是我想大家肯定都想在面試官面前秀一把,講一講底層接近原生的東西才叫牛B,我記得我有一個java的同事,面試的時候和我說,面試官問他如何寫一個三角型出來,然後他回答,這樣回答完我再送一個如何寫一個空心三角型,這B裝的打滿分,一分不扣!!要的就是這個爽感,我們看題!!

<body>
    <ul>
        <li>0</li>
        <li>1</li>
        <li>2</li>
    </ul>
    <script>
        var node = document.querySelectorAll('ul li')
        for(var i = 0;i<node.length;i++){
            node[i].addEventListener('click',function(){
                alert('click'+i)
            })
        }
    </script>
</body>複製程式碼

如果你看這道題,你不用腦子去想,用菊花去想肯定,輸出是0,1,2

但是你要知道,為什麼講面試官都是套路呢?
如果你去面試的時候,面試官問你的每一道題,都要大腦過一下,肯定是一些你正常思維想相反的問題

但是吧只要有一些作用域基礎的同學來說,看一眼也就明白什麼回事了,var 沒有塊級作用域導進的最後輸出無論你點那個li,輸出的都是3

正是因為沒有塊級作用用域導致,最後迴圈出來每個事件輸出的都是全域性i,那因為迴圈跳出結束,最後結果就等於3,這個道理很明白,那我們怎麼去解決這個辦法,我們把這三個辦法都玩一下,都是一種漸進行的方法

閉包 ----> es5 forEach -----> es6 let

閉包我總說成包皮

對於閉包這種話題我想網上已經說爛了,就是在函式作用域裡再聲名一個內部作用域,這樣所以執行結果拿到的變數都是不同的,然後就不是拿的全域性變數了。

         var node = document.querySelectorAll('ul li')
        for(var i = 0;i<node.length;i++){
            (function(i){
                node[i].addEventListener('click',function(){
                    alert('click'+i)
                })
            })(i)
        }複製程式碼

我們在繫結事件外層,在迴圈體內層加了一個自執行函式,大家對這個都不陌生在common.js沒有廣泛推出來的時候,無論任何框架,為了防止全域性變數名的汙染,都用這個玩意,此時就行成了一個閉包,每迴圈一次,都進行一次傳參,此時的每個i都是一個自執行函式體內自己的作用域

forEach 運算元組神器

 <body>
    <ul>
        <li>0</li>
        <li>1</li>
        <li>2</li>
    </ul>
    <script>
        var node = document.querySelectorAll('ul li')
        Array.from(node).forEach(function(nodeItem,index){
            nodeItem.addEventListener('click',function(){
                alert('click'+index)
            })
        })
    </script>
</body>複製程式碼

這裡用forEach也行成了一個所謂的閉包,forEach裡的執行函式也行成了一個閉包,每個執行體裡,index都是一區域性作用域,那為什麼用array,from呢,我們也可以用[].slice.call(node)我們類陣列物件轉化成真正的陣列,因為有些低版本的瀏覽器不支援擺了,此時,加油,你已經回答了兩個方法了,再回答一個es6的方法,雖然現在瀏覽器不支援,但是我們一般不會裸用,用bable轉化成es5再用

     var node = document.querySelectorAll('ul li')
    for(let i = 0;i<node.length;i++){
       node[i].addEventListener('click',function(){
          alert('click'+i)
       })
    }複製程式碼

對於2017年以後面試,我相信如果你上面面試的時候你沒有寫會es6我以後都把閉包寫成包皮,現在因為node對es6的支援,大量同學都開始使用es6,此時對這道題,你不用es6回答出這個問題,我想面試官肯定會對你es6這方面直接over,以後我們可能再也不會用var了,因為var會產生太多隱藏問題,無論是變數提升,還是無塊級作用域,也正是var 沒有塊級作用域導致這個面試題的出來。

面試面到這裡我想大家肯想OK,我都回答這麼全了,從閉包講到es5講到es6還要我怎麼樣,大多數百分之80的中高階前端工程師,那你如何展現你個人對js更深刻的理解呢?,就像我朋友說的那句話,我再送你一個空心三角型

~~~~ 你個撲街!!!!!

那我們講講事件委託,事件委託是什麼鬼東西?事件委託的雛形是由事件冒泡來形成的一個通知鏈,那我們看一下什麼是事件冒泡

    var node = document.querySelectorAll('ul li')
    var body = document.querySelectorAll('body')[0]
    body.addEventListener('click',function(){
        alert('body點選事件行')
    })
    for(let i = 0;i<node.length;i++){
       node[i].addEventListener('click',function(){
          alert('click'+i)
       })
    }複製程式碼

文件流就是一個dom樹,當我們點選一個元素的時候,會一直向上冒泡事件,當我們點選Li元素的時候,會向上冒泡,此時,body上的點選事件同時會被執行,那此時就可以衍生出事件代理是什麼個回事了,那事件代理又有什麼好處呢,此時你應該向面示官展示一下你的擴充行為
1.那此時有99個li元素或者更多的li元素,那給每個Li元素都繫結點選事件,那我們啟不是先找到ul再找li再迴圈99次找到對應的事件,此時對效能是一個很大的問題,因為每個函式都是一個物件,每一個物件都有一個佔一個記憶體空間,那我們起不是要開闢99個記憶體
1.一般我們在移動端經常會做一個列表的dropdown,此時我們肯定會往ul裡新增更我的Li元素,那此時會發生什麼?

 <body>
<ul>
    <li>0</li>
    <li>1</li>
    <li>2</li>

</ul>
<button>新增li元素</button>
<script>
    var ul = document.querySelectorAll('ul')[0]
    var node = document.querySelectorAll('ul li')
    var addButton = document.querySelectorAll('button')[0]
    var addIndex = 3

    addButton.addEventListener('click',function(){
        var addli = document.createElement('li')
        addli.innerHTML = `這是新增的元素${addIndex}`
        ul.append(addli)
        addIndex++
    })
    for(let i = 0;i<node.length;i++){
       node[i].addEventListener('click',function(){
          alert('click'+i)
       })
    }
</script>
</body>複製程式碼

當我們向li元素裡新增新的Li節點選,tmd再點選之後新增的元素沒反應,如果我們用jquery,我們肯定要用到juqery事件代理去解決這個辦法,因為對於瀏覽器對js執行的話,讀取js的時候是從上往下讀,因為一開始頁面我們沒有點選操作js,但是會for迴圈只繫結了,ul裡li元素,因為在初始化就已經對js進行了事件繫結,只是沒有執行裡面的function執行函式,只有真正點選的時候才會觸發。

接下來怎麼辦,大家肯定要看不下去了,講事件代理,事件代理呢?前面一堆演示費話,在堅持一下,如果你不把原理給搞透,面試搞隨便給你一個套路,轉一個彎,你就傻B了。

那事件代理就是很簡單的一個道理,代理代理,就是通過事件冒泡把所點選的元素代理 在他的父元素上

<body>
<ul>
    <li>0</li>
    <li>1</li>
    <li>2</li>
</ul>
<script>
    var ul = document.querySelectorAll('ul')[0]
    ul.addEventListener('click',function(){
        alert('你點選的是Li元素')
    })
</script>複製程式碼

此時很簡單的看出我們通過事件冒泡的原理,把事件代理在父級ul上,所以點選每次Li元素都會出發你點選的是Li元素

那問題來了,上面只是一個冒泡事件的假象,只是利用了冒泡原理做出的一個假象,那們要通過事件代理拿到li元素上的一些資訊那我們該怎麼做?

<ul>
    <li>0</li>
    <li>1</li>
    <li>2</li>
</ul>
<script>
    var ul = document.querySelectorAll('ul')[0]
    ul.addEventListener('click',function(e){
        var ev = ev || window.event;
        var target = ev.target || ev.srcElement;
        if(target.nodeName.toLocaleLowerCase() == 'li'){
            alert(target.innerHTML)
        }
    })
</script>複製程式碼

這裡e是一個事件物件,可以簡稱事件源,var target = ev.target || ev.srcElement;只是對ie版本做了一下相容,此時點選li,同時會冒泡觸發到ul上的事件,可以這麼說,li繼承了ul上的事件,那此時就是利用事件冒泡Li同時也擁有了ul的事件,此時我們只要判斷當前節點是不是li標籤,那就出發當前Li標籤的內容

這樣改下就只有點選li會觸發事件了,且每次只執行一次dom操作,如果li數量很多的話,將大大減少dom的操作,優化的效能可想而知!

那問題又來了?

如果我們要對每個button進行不同的操作,我們還可以代理在父節點上不?還是隻操作一次dom,ok沒問題

<body>
<div>
    <button id="add">新增</button>
    <button id="delate">刪除</button>
    <button id="update">更新</button>
</div>
<script>
    var div = document.querySelectorAll('div')[0]
    div.addEventListener('click',function(e){
        var ev = ev || window.event;
        var target = ev.target || ev.srcElement;
        if(target.nodeName.toLocaleLowerCase() == 'button'){
            switch(target.id){
                case 'add':
                    alert('新增');
                    break;
                case 'delate':
                    alert('刪除');
                    break;
                case 'update':
                    alert('更新');
                    break;
            }
        }
    })
</script>
</body>複製程式碼

我們判斷好節點名好,再進行流程控制語句再根據每個dom不同id去判斷不同的操作

那我們再回來前面的問題,當我們新增節點的時候,我們不用事件代理,導致新新增的節點不能執行監聽事件,那如果用事件代理可行?,剛剛的一句話,無所不能!!

<body>
<ul>
    <li>0</li>
    <li>1</li>
    <li>2</li>

</ul>
<button>新增li元素</button>
<script>
    var ul = document.querySelectorAll('ul')[0]
    var addButton = document.querySelectorAll('button')[0]
    var addIndex = 3

    addButton.addEventListener('click',function(){
        var addli = document.createElement('li')
        addli.innerHTML = `這是新增的元素${addIndex}`
        ul.append(addli)
        addIndex++
    })


    ul.addEventListener('click',function(e){
        var ev = ev || window.event;
        var target = ev.target || ev.srcElement;
        if(target.nodeName.toLocaleLowerCase() == 'li'){
            alert(target.innerHTML)
        }
    })
</script>
</body>複製程式碼

媽媽再也不擔心我喝三路奶粉了,一個代理解決一切,同樣的就算你新增還是刪除都不會影響

再請問現在又有一個場景,如果每個li裡面有著其它子節點,比如說ul->li->div-span,那我們點選任何一個都會觸發冒泡事件,那我們如事件代理,我們如何準確的定位到Li呢,解決辦法-----遞迴呼叫

<body>
<ul>
    <li id="1">
        <span>span元素</span>
    </li>
    <li id="2">
        <div>
            <span>div包著一個span元素</span>
        </div>
    </li>
    <li id="3">
        <div>div元素</div>
    </li>

</ul>
<script>
    var ul = document.querySelectorAll('ul')[0]

    ul.addEventListener('click',function(e){
        var ev = ev || window.event;
        var target = ev.target || ev.srcElement;
        while(target.nodeName !== ul){
           if(target.nodeName.toLocaleLowerCase() == 'li'){
              console.log(target.id)
              break
           }
           target = target.parentNode
        }
    })
</script>
</body>複製程式碼

我感覺非常棒沒有毛病 ,雙擊666666,如果我們進行一個遞迴呼叫,如果是代理節點的元素直接跳出,結束遞迴,那如果是Li元素我們就跳出遞迴,如果我們點的是Li的任何子元素,繼續向上節點查詢,直到找到li節點,這很巧妙運用了遞迴操作來進行事件代理解決一些問題

總結

優點:

1減少事件註冊,節省記憶體。比如,在table上代理所有td的click事件。在ul上代理所有li的click事件。
2.簡化了dom節點更新時,相應事件的更新。比如不用在新新增的li上繫結click事件。
當刪除某個li時,不用移解綁上面的click事件。

缺點:

1.事件委託基於冒泡,對於不冒泡的事件不支援。
2.層級過多,冒泡過程中,可能會被某層阻止掉。
3.理論上委託會導致瀏覽器頻繁呼叫處理函式,雖然很可能不需要處理。所以建議就近委託,比如在table上代理td,而不是在document上代理td。
4.把所有事件都用代理就可能會出現事件誤判。比如,在document中代理了所有button的click事件,另外的人在引用改js時,可能不知道,造成單擊button觸發了兩個click事件。

仔細思考!!!從這我發現了,如果不講事件代理這回事,可能大家都認為事件冒泡只是觸發了上級事件,不然,我覺得這說的不完全,如果只是觸發上層事件的話,那我們點選li如何拿到ul所進行的操作方法,應該說,向上冒泡先出發上級事件,然後再繼承上級事件。就是簡簡單單的觸發!!如果我說的有錯的話,大神們可以隨時噴

渣渣前端開發工程師,喜歡鑽研,熱愛分享和講解教學, 微信 zzx1994428 QQ494755899

支援我繼續創作和感到有收穫的話,請向我打賞點吧

我認真起來連面試官都怕(塊級作用域,事件代理)

如果轉載請標註出自@混元霹靂手ziksang

相關文章