阅读 10347

我认真起来连面试官都怕(块级作用域,事件代理)

作者 混元霹雳手-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

关注下面的标签,发现更多相似文章
评论