【面试记录】第二轮

116 阅读9分钟

一、 com1

tx二轮

0) 全程无技术

大概讲述了一下之前做的难度高的项目

主要沟通离职原因,我这边给的答复是,希望更加专注在技术上面。不希望管理团队,目前带团队沟通成本和开发时间的比例是8:2。

1) 个人规划发展

技术

  1. 函数式方向
  2. 算法方向深入,为源码理解奠定更好的基础。

管理

  1. 去了解认知自己,是否有这方面能力,是否适合自己
  2. 管理涉及东西较多,需要多学习。

对方答复: T9以上需要管理项目,我追问了时间占比。说是不确定。这确实是目前普遍存在的现象。

2) 疑问:

技术人员,达到中高级后,注重技术发展,不喜欢低效率的沟通,带项目带团队会占用大量时间,我希望专注技术有问题吗?

二、 com2

shenggcce

1,只说浏览器渲染过程以及原理

渲染主流程

渲染引擎首先通过网络获得所请求文档的内容,通常以8K分块的方式完成。下面是渲染引擎在取得内容之后的基本流程:

解析html以构建dom树 -> 构建render树 -> 布局render树 -> 绘制render树

1) 浏览器主要组件结构

image.png

2) 渲染阻塞

JS可以操作DOM来修改DOM结构,可以操作CSSOM来修改节点样式,这就导致了浏览器在遇到<script>标签时,DOM构建将暂停,直至脚本完成执行,然后继续构建DOM。如果脚本是外部的,会等待脚本下载完毕,再继续解析文档。现在可以在script标签上增加属性defer或者async。脚本解析会将脚本中改变DOM和CSS的地方分别解析出来,追加到DOM树和CSSOM规则树上。

每次去执行JavaScript脚本都会严重地阻塞DOM树的构建,如果JavaScript脚本还操作了CSSOM,而正好这个CSSOM还没有下载和构建,浏览器甚至会延迟脚本执行和构建DOM,直至完成其CSSOM的下载和构建。所以,script标签的位置很重要。

JS阻塞了构建DOM树,也阻塞了其后的构建CSSOM规则树,整个解析进程必须等待JS的执行完成才能够继续,这就是所谓的JS阻塞页面。

由于CSSOM负责存储渲染信息,浏览器就必须保证在合成渲染树之前,CSSOM是完备的,这种完备是指所有的CSS(内联、内部和外部)都已经下载完,并解析完,只有CSSOM和DOM的解析完全结束,浏览器才会进入下一步的渲染,这就是CSS阻塞渲染。

CSS阻塞渲染意味着,在CSSOM完备前,页面将一直处理白屏状态,这就是为什么样式放在head中,仅仅是为了更快的解析CSS,保证更快的首次渲染。

需要注意的是,即便你没有给页面任何的样式声明,CSSOM依然会生成,默认生成的CSSOM自带浏览器默认样式。

当解析HTML的时候,会把新来的元素插入DOM树里面,同时去查找CSS,然后把对应的样式规则应用到元素上,查找样式表是按照从右到左的顺序去匹配的。

例如:div p {font-size: 16px},会先寻找所有p标签并判断它的父标签是否为div之后才会决定要不要采用这个样式进行渲染)。
所以,我们平时写CSS时,尽量用idclass,千万不要过渡层叠。

3) 回流和重绘(reflow和repaint)

我们都知道HTML默认是流式布局的,但CSS和JS会打破这种布局,改变DOM的外观样式以及大小和位置。因此我们就需要知道两个概念:replaintreflow

4) reflow(回流)

当浏览器发现布局发生了变化,这个时候就需要倒回去重新渲染,这个回退的过程叫reflowreflow会从html这个root frame开始递归往下,依次计算所有的结点几何尺寸和位置,以确认是渲染树的一部分发生变化还是整个渲染树。reflow几乎是无法避免的,因为只要用户进行交互操作,就势必会发生页面的一部分的重新渲染,且通常我们也无法预估浏览器到底会reflow哪一部分的代码,因为他们会相互影响。

5) repaint(重绘)

repaint则是当我们改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸和位置没有发生改变。

需要注意的是,display:none会触发reflow,而visibility: hidden属性则并不算是不可见属性,它的语义是隐藏元素,但元素仍然占据着布局空间,它会被渲染成一个空框。所以visibility:hidden只会触发repaint,因为没有发生位置变化。

另外有些情况下,比如修改了元素的样式,浏览器并不会立刻reflowrepaint一次,而是会把这样的操作积攒一批,然后做一次reflow,这又叫异步reflow或增量异步reflow。但是在有些情况下,比如resize窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行reflow

6) 引起reflow

现代浏览器会对回流做优化,它会等到足够数量的变化发生,再做一次批处理回流。

  • 页面第一次渲染(初始化)
  • DOM树变化(如:增删节点)
  • Render树变化(如:padding改变)
  • 浏览器窗口resize
  • 获取元素的某些属性

浏览器为了获得正确的值也会提前触发回流,这样就使得浏览器的优化失效了,这些属性包括offsetLeft、offsetTop、offsetWidth、offsetHeight、 scrollTop/Left/Width/Height、clientTop/Left/Width/Height、调用了getComputedStyle()

7) 引起repaint

reflow回流必定引起repaint重绘,重绘可以单独触发。
背景色、颜色、字体改变(注意:字体大小发生变化时,会触发回流)

8) 减少reflow、repaint触发次数

  • transform做形变和位移可以减少reflow
  • 避免逐个修改节点样式,尽量一次性修改
  • 使用DocumentFragment将需要多次修改的DOM元素缓存,最后一次性append到真实DOM中渲染
  • 可以将需要多次修改的DOM元素设置display:none,操作完再显示。(因为隐藏元素不在render树内,因此修改隐藏元素不会触发回流重绘)
  • 避免多次读取某些属性
  • 通过绝对位移将复杂的节点元素脱离文档流,形成新的Render Layer,降低回流成本

9) 几条关于优化渲染效率的建议

结合上文有以下几点可以优化渲染效率。

  • 合法地去书写HTML和CSS ,且不要忘了文档编码类型。
  • 样式文件应当在head标签中,而脚本文件在body结束前,这样可以防止阻塞的方式。
  • 简化并优化CSS选择器,尽量将嵌套层减少到最小。
  • DOM 的多个读操作(或多个写操作),应该放在一起。不要两个读操作之间,加入一个写操作。
  • 如果某个样式是通过重排得到的,那么最好缓存结果。避免下一次用到的时候,浏览器又要重排。
  • 不要一条条地改变样式,而要通过改变class,或者csstext属性,一次性地改变样式。
  • 尽量用transform来做形变和位移
  • 尽量使用离线DOM,而不是真实的网页DOM,来改变元素样式。比如,操作Document Fragment对象,完成后再把这个对象加入DOM。再比如,使用cloneNode()方法,在克隆的节点上进行操作,然后再用克隆的节点替换原始节点。
  • 先将元素设为display: none(需要1次重排和重绘),然后对这个节点进行100次操作,最后再恢复显示(需要1次重排和重绘)。这样一来,你就用两次重新渲染,取代了可能高达100次的重新渲染。
  • position属性为absolutefixed的元素,重排的开销会比较小,因为不用考虑它对其他元素的影响。
  • 只在必要的时候,才将元素的display属性为可见,因为不可见的元素不影响重排和重绘。另外,visibility : hidden的元素只对重绘有影响,不影响重排。
  • 使用window.requestAnimationFrame()window.requestIdleCallback()这两个方法调节重新渲染。

2,如何不改变页面本来元素展示情况下,给元素,点击元素时,给元素增加一个背景颜色,一个边框。

3,如何让添加的元素跟随页面的滚动而滚动

答案: 计算位置,存在的问题,如果要展示的太多的话,每次滚动都需要计算所有元素的位置。即使加了节流在一定的情况下也会卡顿。

4,如何统计用户在网页中的事件行为,并上报,如何存储,或者持久化存储,如何跨域上报(image标签),如何处理上报内容长度超过get所能携带的参数的长度。

5,浏览器的图层layer详细说一下

6,浏览器从渲染树到渲染完成发生了什么?就是从渲染树到布局layout发生了什么?

7,递归 循环引用如何处理?

8,递归爆栈如何解决?

9,如何监听页面上所有元素的事件? // 点击事件

window.addEventListener("click", function(e){
    console.log(e.target.tagName);
});

10,如何区分事件发生元素是子元素还是父元素?

1) 通过事件对象获取到event.target,然后对event.target的特征进行判断(比如class)

2) 利用DOM的事件传播机制

在DOM2级事件流中包含事件捕获和事件冒泡两个阶段。触发事件处理程序时,先是进入事件捕获阶段,事件由外层向内层具体元素传播;然后进入事件冒泡阶段,由内层具体元素再向外层进行传播,事件的处理默认是在冒泡阶段。事件捕获是不能被阻止的,否则定位不到具体的元素。

<div class="parent" onclick="handleParent()">
    <div class="children1">
        <div class="children2"></div>
    </div>
</div>

当点击父元素parent时,事件进入捕获阶段: Document → html → body → div.parent → div.children1 → div.children2

然后进入冒泡近段并且执行处理方法:

div.children2(触发事件) → div.children1 (触发事件) → div#parent(触发事件) → body → html → Document

只要获取到 div.children1 给它添加阻止冒泡事件就可以了。

function handleParent(e){
    var e = e || window.event;
    //判断传播路径中是否存在children1
    for(var i=0;i<e.path.length;i++){
        if(e.path[i].className && e.path[i].className.indexOf('children1') > -1){
            e.stopPropagation();
            return false;
        }
    }
    alert('点击的是父元素')
};

总结

  • 事件捕获是不能被阻止的,否则定位不到具体的元素
  • 短板 - 递归爆栈
  • 短板 - 递归循环引用