浏览器相关面试题(二)

1,262 阅读19分钟

事件系统

浏览器的事件系统是用于处理用户交互和其他浏览器行为的机制。事件系统允许开发者在特定的元素上注册处理函数,以便在事件发生时执行相应的操作。以下是浏览器事件系统的主要组成部分和一些关键概念:

1. 事件流(Event Flow):

事件流描述了从事件被触发到达事件目标的过程。事件流有两种:冒泡阶段(Bubbling Phase)和捕获阶段(Capturing Phase)。

  • 捕获阶段(Capturing Phase): 事件从最外层的祖先元素一直传播到目标元素,逐级捕获。

  • 目标阶段(Target Phase): 事件到达目标元素。

  • 冒泡阶段(Bubbling Phase): 事件从目标元素一直冒泡到最外层的祖先元素。

2. 事件类型(Event Types):

不同的事件类型对应不同的用户行为或浏览器状态变化。常见的事件类型包括:

  • 鼠标事件:

    • clickmousedownmouseupmousemovemouseentermouseleave等。
  • 键盘事件:

    • keydownkeyupkeypress等。
  • 表单事件:

    • submitchangeinputfocusblur等。
  • 窗口事件:

    • loadunloadresizescroll等。

3. 事件对象(Event Object):

每个事件处理函数都接收一个事件对象,该对象包含有关事件的信息,如事件类型、触发元素、鼠标位置等。通过事件对象,开发者可以获取有关事件的详细信息。

element.addEventListener('click', function(event) {
  console.log('Event type:', event.type);
  console.log('Target element:', event.target);
  console.log('Mouse coordinates:', event.clientX, event.clientY);
});

4. 事件监听与处理:

通过 addEventListener 方法可以在元素上注册事件处理函数。该函数接收两个参数:要监听的事件类型和事件处理函数。

var button = document.getElementById('myButton');
button.addEventListener('click', function(event) {
  console.log('Button clicked!');
});

5. 事件委托(Event Delegation):

事件委托是一种利用事件冒泡机制的技术,通过将事件处理函数注册在父元素上,减少事件处理函数的数量,提高性能。

<ul id="myList">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

<script>
var list = document.getElementById('myList');
list.addEventListener('click', function(event) {
  if (event.target.tagName === 'LI') {
    console.log('Item clicked:', event.target.textContent);
  }
});
</script>

上述代码中,只在父元素 <ul> 上注册了一个事件处理函数,通过判断 event.target 的标签名来确定点击的是哪个子元素。

总体来说,浏览器事件系统是构建交互式Web应用的关键组成部分,了解事件流、事件类型、事件对象等概念,有助于更好地理解和处理用户和浏览器的交互。

阻止捕获或者冒泡

event.stopPropagation()  方法阻止捕获和冒泡阶段中当前事件的进一步传播。但是,它不能防止任何默认行为的发生。

阻止捕获阶段:

element.addEventListener('click', function(event) {
  // 阻止事件的捕获阶段传播
  event.stopPropagation();
  
  console.log('Clicked on element without further capturing');
}, true); // 注意第三个参数为 true,表示在捕获阶段处理事件

在上述例子中,stopPropagation() 用于阻止事件在捕获阶段继续传播,因为在 addEventListener 的第三个参数为 true 时表示捕获阶段。

阻止冒泡阶段:

element.addEventListener('click', function(event) {
  // 阻止事件的冒泡阶段传播
  event.stopPropagation();
  
  console.log('Clicked on element without further bubbling');
}, false); // 注意第三个参数为 false(默认值),表示在冒泡阶段处理事件

在上述例子中,stopPropagation() 用于阻止事件在冒泡阶段继续传播,因为在 addEventListener 的第三个参数为 false 或省略时表示冒泡阶段。

在 Internet Explorer(IE)浏览器中,阻止事件继续传播的方法与其他现代浏览器略有不同。IE 使用的是 cancelBubble 属性来实现阻止事件传播,而不是像其他浏览器那样使用 stopPropagation() 方法。

阻止默认行为

w3c的方法是e.preventDefault(),IE则是使用e.returnValue = false;

事件委托

事件委托就是利用事件冒泡处理动态元素事件绑定的方法。

事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

js中事件冒泡我们知道,子元素身上的事件会冒泡到父元素身上。

事件代理就是,本来加在子元素身上的事件,加在了其父级身上。

那就产生了问题:父级那么多子元素,怎么区分事件本应该是哪个子元素的?

答案是:event对象里记录的有“事件源”,它就是发生事件的子元素。

它存在兼容性问题,在老的IE下,事件源是 window.event.srcElement,其他浏览器是 event.target

用事件委托有什么好处呢?

第一个好处是效率高,比如,不用for循环为子元素添加事件了

第二个好处是,js新生成的子元素也不用新为其添加事件了,程序逻辑上比较方便。

为什么要用事件委托

DOM需要事件处理程序,那么我们直接添加,但是如果我们有多个DOM节点需要相同的事件呢,如果循环将会影响性能,这时候就可以使用事件委托。

在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能因为需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少DOM操作的原因;

如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能;

每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差了。

比如100个li,就要占用100个内存空间,如果是1000个,10000个呢,那只能说呵呵了,如果用事件委托,那么我们就可以只对它的父级(如果只有一个父级)这一个对象进行操作,这样我们就需要一个内存空间就够了,是不是省了很多,自然性能就会更好

使用事件委托技术能避免对每个元素添加事件监听器。相反,把事件监听器添加到它们的父元素上。事件监听器会分析从子元素冒泡上来的事件,找到它们是哪个子元素的事件。也就是说,把监听在子元素上的事件监听函数放到它的父元素上,这样新添加的子元素仍然可以执行事件回调函数。

事件委托的三大优势:

  • 减少事件数量
  • 避免内存泄露
  • 预测未来元素。

事件委托的实现

例子一

页面有个ul包含着4个li,鼠标移动到li上,li背景变成红色,移出,背景恢复原色。

不使用事件代理的情况下:

<ul id="ul1">
            <li>111</li>
            <li>222</li>
            <li>333</li>
            <li>444</li>
        </ul>
        
<script type="text/javascript">
    window.onload = function(){
        var oUl = document.getElementById('ul1');
        var aLi = oUl.children;
        console.log(aLi);
        //传统方法,li身上添加事件,需要用for循环,找到每个li
        for (var i=0;i<aLi.length;i++) {
            aLi[i].onmouseover = function() {
                this.style.background = 'red';
            }
            aLi[i].onmouseout = function(){
                this.style.background = '';
            }
        }//for结束
    }
</script>

使用事件代理:

<script type="text/javascript">
 window.onload = function(){
    var oUl = document.getElementById('ul1');      
    oUl.onmouseover = function(ev){
        var ev = ev || window.event;
        var oLi = ev.srcElement || ev.target;
        oLi.style.background = 'red';              
    }
    oUl.onmouseout = function(ev){
        var ev = ev || window.event;
        var oLi = ev.srcElement || ev.target;
        oLi.style.background = '';                
    }
                
}
</script>

但是会发现,鼠标移到了ul身上而不是某个li身上时,获取的事件源是ul,那么整个ul背景将变红,这不是想要的结果,怎么办?

答曰:加个判断。通过事件源的nodeName判断是不是li,是才做出反应,不是不理它。为了防止nodeName在不同浏览器获取的字母大小写不同,加个toLowerCase()

所以,上面的js代码更改如下:

<script type="text/javascript">
window.onload = function(){
    var oUl = document.getElementById('ul1');
    oUl.onmouseover = function(ev){
        var ev = ev || window.event;
        var oLi = ev.srcElement || ev.target;
        if(oLi.nodeName.toLowerCase() == 'li'){
            oLi.style.background = 'red';
        }
    }
                
    oUl.onmouseout = function(ev){
        var ev = ev || window.event;
        var oLi = ev.srcElement || ev.target;
        if(oLi.nodeName.toLowerCase() == 'li'){
            oLi.style.background = '';
        }                
    }
}
</script>

例子二 定我们有一个UL元素,它有几个子元素:

<ul id="parent-list">
       <li id="post-1">Item 1</li>
       <li id="post-2">Item 2</li>
       <li id="post-3">Item 3</li>
       <li id="post-4">Item 4</li>
       <li id="post-5">Item 5</li>
       <li id="post-6">Item 6</li>
   </ul>

我们还假设,当每个子元素被点击时,将会有各自不同的事件发生。你可以给每个独立的li元素添加事件监听器,但有时这些li元素可能会被删除,可能会有新增,监听它们的新增或删除事件将会是一场噩梦,尤其是当你的监听事件的代码放在应用的另一个地方时。但是,如果你将监听器安放到它们的父元素上呢?你如何能知道是那个子元素被点击了?

简单:当子元素的事件冒泡到父ul元素时,你可以检查事件对象的target属性,捕获真正被点击的节点元素的引用。下面是一段很简单的JavaScript代码,演示了事件委托的过程:

// 找到父元素,添加监听器...
    document.getElementById("parent-list").addEventListener("click",function(e) {
        // e.target是被点击的元素!
        // 如果被点击的是li元素
        if(e.target && e.target.nodeName == "LI") {
            // 找到目标,输出ID!
            console.log("List item ",e.target.id.replace("post-",'')," was clicked!");
        }
    });

第一步是给父元素添加事件监听器。当有事件触发监听器时,检查事件的来源,排除非li子元素事件。如果是一个li元素,我们就找到了目标!如果不是一个li元素,事件将被忽略。这个例子非常简单,UL和li是标准的父子搭配。让我们试验一些差异比较大的元素搭配。假设我们有一个父元素div,里面有很多子元素,但我们关心的是里面的一个带有”classA” CSS类的A标记:

// 获得父元素DIV, 添加监听器...
    document.getElementById("myDiv").addEventListener("click",function(e) {
        // e.target是被点击的元素
        if(e.target && e.target.nodeName == "A") {
            // 获得CSS类名
            var classes = e.target.className.split(" ");
                // 搜索匹配!
                if(classes) {
                    // For every CSS class the element has...
                    for(var x = 0; x < classes.length; x++) {
                        // If it has the CSS class we want...
                        if(classes[x] == "classA") {
                            // Bingo!
                            console.log("Anchor element clicked!");
                            // Now do something here....
                        }
                    }
                }
        }
    });

上面这个例子中不仅比较了标签名,而且比较了CSS类名。虽然稍微复杂了一点,但还是很具代表性的。比如,如果某个A标记里有一个span标记,则这个span将会成为target元素。这个时候,我们需要上溯DOM树结构,找到里面是否有一个 A.classA 的元素。

例子三

<div id="box">
        <input type="button" id="add" value="添加" />
        <input type="button" id="remove" value="删除" />
        <input type="button" id="move" value="移动" />
        <input type="button" id="select" value="选择" />
</div>
window.onload = function(){
            var Add = document.getElementById("add");
            var Remove = document.getElementById("remove");
            var Move = document.getElementById("move");
            var Select = document.getElementById("select");
            
            Add.onclick = function(){
                alert('添加');
            };
            Remove.onclick = function(){
                alert('删除');
            };
            Move.onclick = function(){
                alert('移动');
            };
            Select.onclick = function(){
                alert('选择');
            }
            
        }

上边的例子,四个按钮完成四个不同的操作,至少需要四次dom操作,如果运用事件委托能优化吗?

window.onload = function(){
            var oBox = document.getElementById("box");
            oBox.onclick = function (ev) {
                var ev = ev || window.event;
                var target = ev.target || ev.srcElement;
                if(target.nodeName.toLocaleLowerCase() == 'input'){
                    switch(target.id){
                        case 'add' :
                            alert('添加');
                            break;
                        case 'remove' :
                            alert('删除');
                            break;
                        case 'move' :
                            alert('移动');
                            break;
                        case 'select' :
                            alert('选择');
                            break;
                    }
                }
            }
            
        }

事件委托仅仅操作一次dom就完成了,比上边的性能肯定要好一些。

事件委托的优点

事件委托技术可以避免对每个子元素添加监听器,减少操作DOM节点的次数,从而减少浏览器的重排和重绘,提高代码的性能

使用委托事件,只有父元素与DOM存在交互,其他的操作都是在JS虚拟内存中完成的,这样大大提高了性能。

当新添加或者删除子元素节点时,响应的事件可以自动添加或删除,不需要在写程序进行添加或删除。

事件委托三部曲

第一步:给父元素绑定事件 给元素ul添加绑定事件,通过addEventListener为点击事件click添加绑定

第二步:监听子元素的冒泡事件 这里默认是冒泡,点击子元素li会向上冒泡

第三步:找到是哪个子元素的事件 通过匿名回调函数的参数e用来接收事件对象,通过target获取触发事件的目标

另外,event.currentTarget 指向的是监听器直接绑定的那个元素,而 event.target 指向的是我们点击的那个元素。

property 和 attribute

property 和 attribute非常容易混淆,两个单词的中文翻译也都非常相近(property:属性,attribute:特性),但实际上,二者是不同的东西,属于不同的范畴。

  • property 指的是在 JavaScript 中通过 DOM(文档对象模型)访问的元素属性。这些属性通常是 JavaScript 对象的属性,可以直接通过点号或中括号访问。修改这些属性会影响元素的状态,并且这些属性通常是实时更新的。

  • attribute 指的是 HTML 元素上的属性。这些属性是包含在 HTML 标记中的,它们通常在元素创建时设置,而且一般不会实时更新。要获取或修改这些属性,可以使用 getAttributesetAttribute 方法。

  • DOM有其默认的基本属性,而这些属性就是所谓的 property,无论如何,它们都会在初始化的时候在DOM对象上创建。

  • HTML标签上自定义的属性和值会保存在该DOM对象的 attribute属性里面

什么情况下引用计数无法进行有效的回收垃圾,怎么解决

引用计数法在循环引用的情况下不能进行垃圾回收。

解决方式:

  • 手动维护:合适的时机将引用次数减一
  • 采用weakMap ,或者weakSet弱引用

刷新不丢失数据的解决方式

使用DOM Storage。

DOM Storage 分为 sessionStorage 和 localStorage。

localStorage 对象和 sessionStorage 对象使用方法基本相同,它们的区别在于作用的范围不同。

sessionStorage 用来存储与页面相关的数据,它在页面关闭后无法使用。

localStorage 则持久存在,在页面关闭后也可以使用。

防抖和节流,以及应用场景

防抖应用场景:

  • mousemove 鼠标滑动事件
  • input输入停止n秒后触发操作:输入了一个文字又删除这种情况下没必要触发请求
  • 登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖

节流场景:

  • 埋点场景:商品搜索列表、商品橱窗等,用户滑动时 定时 / 定滑动的高度 发送埋点请求
  • 运维系统查看应用运行日志时,每 n 秒刷新一次

跨域的解决方案,项目中如何解决的

解决跨域的方法有很多,下面列举了三种:

  • JSONP

  • CORS:只要后端实现了 CORS,就实现了跨域

  • Proxy:

    通过webpack为我们起一个本地服务器作为请求的代理对象

    通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨域

    amodule.exports = {
        devServer: {
            host: '127.0.0.1',
            port: 8084,
            open: true,// vue项目启动时自动打开浏览器
            proxy: {
                '/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
                    target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址
                    changeOrigin: true, //是否跨域
                    pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'""代替
                        '^/api': "" 
                    }
                }
            }
        }
    }
    
    // 通过axios发送请求中,配置请求的根路径
    axios.defaults.baseURL = '/api'
    
  • 代理服务器nginx配置如下:

    server {
        listen    80;
        # server_name www.josephxia.com;
        location / {
            root  /var/www/html;
            index  index.html index.htm;
            try_files $uri $uri/ /index.html;
        }
        location /api {
            proxy_pass  http://127.0.0.1:3000;
            proxy_redirect   off;
            proxy_set_header  Host       $host;
            proxy_set_header  X-Real-IP     $remote_addr;
            proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
        }
    }
    

SEO

比如在百度中,后台有一个非常庞大的数据库,里面存储了海量关键词,每个关键词对应很多网址,这些网址是百度程序(又称搜索引擎蜘蛛或者网络爬虫)从茫茫互联网上一点一点下载收集来的。这些蜘蛛每天在互联网上爬行,从一个连接到另一个链接,下载其内容,进行分析提炼,找到其关键词。如果蜘蛛认为关键词是对用户有用的而数据库中又没有就会存入到数据库中,反之就丢弃,继续爬行找到最新的、有用的信息保存起来供用户搜索。当用户搜索时就会检索到与关键词相关的网址显示给访客。

一个关键词存在多个网址,那就回存在排名问题。当与关键词最吻合的网址就会排在前边。

SEO 就是传说中的搜索引擎优化,是指为了增加网页在搜索引擎自然搜索结果中的收录数量以及提升排序位置而做的优化行为。

SEO分为白帽SEO和黑帽SEO。白帽SEO,起到了改良和规范网站设计的作用,使网站对搜索引擎和用户更加友好,并且网站也能从搜索引擎中获取合理的流量,这是搜索引擎鼓励和支持的。黑帽SEO,利用和放大搜索引擎政策缺陷来获取更多用户的访问量,这类行为大多是欺骗搜索引擎,一般搜索引擎公司是不支持与鼓励的。

前端SEO

网站结构布局优化:尽量简单,开门见山,提倡扁平化结构。

一般而言,建立的网站结构层次越少,越容易被“蜘蛛”抓取,也就容易被收录。一般中小型网站目录结构超过三级,“蜘蛛”便不愿意往下爬,“万一天黑迷路了怎么办”。并且根据相关调查:访客如果经过跳转3次还没找到需要的信息,很可能离开。因此,三层目录结构也是体验的需要。为此我们需要做到:

  1. 控制首页链接数量网站首页是权重最高的地方,如果首页链接太少,没有“桥”,“蜘蛛”不能继续往下爬到内页,直接影响网站收录数量。但是首页链接也不能太多,一旦太多,没有实质性的链接,很容易影响用户体验,也会降低网站首页的权重,收录效果也不好。

    因此对于中小型企业网站,建议首页链接在100个以内,链接的性质可以包含页面导航、底部导航、锚文字链接等等,注意链接要建立在用户的良好体验和引导用户获取信息的基础之上。

  2. 扁平化的目录层次 尽量让“蜘蛛”只要跳转3次,就能到达网站内的任何一个内页。扁平化的目录结构,比如:“植物”–> “水果” –> “苹果”、“桔子”、“香蕉”,通过3级就能找到香蕉了。

  3. 导航优化 导航应该尽量采用文字方式,也可以搭配图片导航,但是图片代码一定要进行优化,< img >标签必须添加“alt”和“title”属性,告诉搜索引擎导航的定位,做到即使图片未能正常显示时,用户也能看到提示文字。

    其次,在每一个网页上应该加上面包屑导航,好处:从用户体验方面来说,可以让用户了解当前所处的位置以及当前页面在整个网站中的位置,帮助用户很快了解网站组织形式,从而形成更好的位置感,同时提供了返回各个页面的接口,方便用户操作;对“蜘蛛”而言,能够清楚的了解网站结构,同时还增加了大量的内部链接,方便抓取,降低跳出率。

  4. 网站的结构布局–不可忽略的细节 页面头部:logo及主导航,以及用户的信息。

    页面主体:左边正文,包括面包屑导航及正文;右边放热门文章及相关文章,好处:留住访客,让访客多停留,对“蜘蛛”而言,这些文章属于相关链接,增强了页面相关性,也能增强页面的权重。

    页面底部:版权信息和友情链接。

    特别注意:分页导航写法,推荐写法:“首页 1 2 3 4 5 6 7 8 9 下拉框”,这样“蜘蛛”能够根据相应页码直接跳转,下拉框直接选择页面跳转。而下面的写法是不推荐的,“首页 下一页 尾页”,特别是当分页数量特别多时,“蜘蛛”需要经过很多次往下爬,才能抓取,会很累、会容易放弃。

  5. 控制页面的大小,减少http请求,提高网站的加载速度。 一个页面最好不要超过100k,太大,页面加载速度慢。当速度很慢时,用户体验不好,留不住访客,并且一旦超时,“蜘蛛”也会离开。

网页代码优化

1.< title >标题 只强调重点即可,尽量把重要的关键词放在前面,关键词不要重复出现,尽量做到每个页面的< title > 标题中不要设置相同的内容。

2.< meta keywords >标签 关键词,列举出几个页面的重要关键字即可,切记过分堆砌。

3.< meta description >标签 网页描述,需要高度概括网页内容,切记不能太长,过分堆砌关键词,每个页面也要有所不同。

4.< body >中的标签 尽量让代码语义化,在适当的位置使用适当的标签,用正确的标签做正确的事。让阅读源码者和“蜘蛛”都一目了然。比如:h1-h6 是用于标题类的,< nav >标签是用来设置页面主导航的等。

5.< a >标签 页内链接,要加 “title” 属性加以说明,让访客和 “蜘蛛” 知道。而外部链接,链接到其他网站的,则需要加上 el=”nofollow” 属性, 告诉 “蜘蛛” 不要爬,因为一旦“蜘蛛”爬了外部链接之后,就不会再回来了。

6.正文标题要用< h1>标签 “蜘蛛” 认为它最重要,若不喜欢< h1>的默认样式可以通过CSS设置。尽量做到正文标题用< h1>标签,副标题用< h2>标签, 而其它地方不应该随便乱用标题标签。

7.重要内容不要用JS输出,因为“蜘蛛”不认识

8.尽量少使用iframe框架,因为“蜘蛛”一般不会读取其中的内容

9.js代码如果是操作DOM操作,应尽量放在body结束标签之前,html代码之后。