面试题2022

【岗位职责】
1、负责项目前端部分正常迭代开发和维护
2、优化产品质量、性能、用户体验
3、参与新的前端开发技术进行研究和应用推广
【岗位要求】
1、熟悉W3C规范,熟悉HTML、Css/Less、 Es5/6+前端开发技术
2、熟练使用vue,或其它至少1种前端主流框架,并了其原理
3、有成熟的模块化开发思维,熟悉常用前端数据管理的解决方案Redux/Vuex等
4、熟练工程化工具如webpack、Rollup、babel
5、至少熟悉一门非前端的语言,如Node/Java/PHP
6、能够独立进行开发及熟练运用调试,能够充分理解设计需求并落地;
7、对前端技术有持续的热情,专研精神和进取心
8、具有良好的言语表达能力,善于与人沟通合作,有良好的团队意识及高度责任心;
【加分项】
1.有后端开发经验;
2.有开源项目,或者在开源社区活跃者。

一、闭包

什么是闭包

火狐的MDN:闭包是函数和声明该函数的词法环境的组合。
Js高级程序设计:闭包是指有权访问另一个函数作用域中的变量的函数。
百度百科:定义在一个函数内部的函数

闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
简单一句话:闭包即是为了保留某些局部变量的值而存在的。闭包就是能够读取其他函数内部变量的函数,方便程序在上下文里调用。

闭包能做啥

闭包模拟私有成员
编程语言中,比如 Java,是支持将方法、属性声明为私有的,即它们只能被同一个类中的其它方法所调用。而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法有利于限制对代码的访问

var person =  {
    name:'小明'
};
person.name='小红';//随意修改
var pname=person.name;
console.log(pname)
//person里的name,是私有的,只能我person自己内部使用,
//外部环境只能通过我提供的方法去读写,你们再也不能随意的直接person.name来控制我了
var person = (()=> {
    var name='小明';
    var setName=(param)=>{
        name=param
    };
    var getName=()=>{
        return name 
    };
    return{
        setName,
        getName
    }
})();
var pname=person.getName();
console.log(pname)//小明

管理全局命名空间
私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。

写一个闭包

创建闭包的常见方式,就是在一个函数内部创建另一个函数

//有权访问另一个函数作用域内变量的函数都是闭包。这里inc函数访问了demo函数 里面的变量n,所以形成了一个闭包。
function demo(){
    var n = 0;
    return function inc() {
        n++;
        console.log(n);
    }
}
var myInc=demo();//函数inc( )在执行前,被外部函数返回
myInc() //1

二、重排与重绘

浏览器输入URL发生了什么?

  • DNS域名解析出对应的IP
  • 根据IP建立TCP连接
  • 发送HTTP请求
  • 服务器处理请求,并返回响应结果
  • 关闭连接TCP连接,浏览器解析文档

浏览器解析文档?

接上边的,浏览器拿到下载好的文档,如何处理呢?
这是浏览器处理文档的图

  1. 解析HTML,构建DOM树
  2. 解析CSS,生成CSS规则树
  3. 合并DOM树和CSS规则,生成render树
  4. 布局render树,负责各元素节点尺寸、位置计算。这个过程是排版layout
  5. 绘制render树,绘制页面像素信息。这个过程就是绘制,英文叫paint
  6. 浏览器会将各层的信息发送给GPU,GPU会将各层合成,显示在屏幕上

各款渲染引擎的基本工作流程可以抽象为下图: 

实际上在http请求方式不同、有无代理、有无负载均衡等不同场景下访问服务器的细节流程也会有一些差别,但这并不影响我们对整个访问环节的理解,有兴趣可网上自行了解

重排与重绘

都是啥?

当Render tree中的的DOM元素几何属性的变化,比如改变元素的宽高元素的位置,导致浏览器不得不重新计算元素的几何属性,并重新构建渲染树,重新构建页面,这个过程称为“重排”。简单说就是当页面的布局发生变化时,浏览器会回过头来重新渲染。完成重排后,要将重新构建的渲染树渲染到屏幕上,这个过程就是“重绘”。
重排负责元素的几何属性更新,重绘负责元素的样式更新。而且,重排必然带来重绘,但是重绘未必带来重排。比如改变某个元素的背景,这个就不涉及元素的几何属性,所以只发生重绘。

示例图
dom元素的外观属性发生改变--------> 触发页面的重绘
Dom树—————> Render树
dom元素的几何属性发生改变--------> 触发页面的重排,之后重绘


什么会引发重排?

重排发生的根本原理就是元素的几何属性发生了改变,那么我们就从能够改变元素几何属性的角度入手

  • 页面首次初始化渲染(这个无法避免,而且整个页面都将重排,性能开销最大)
  • 元素位置尺寸发生改变
  • 内容改变,文字数量字体 或图片大小等
  • 添加或删除可见的元素
  • 浏览器窗口大小发生改变

什么会引发重绘?

重绘是一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新外观属性重新绘制,使元素呈现新的外观。这些行为由于没有改变元素的几何属性故而不会引起布局变化,也不会重排。也由此推断出下边几种情况可引发重绘

  • visibility、outline、背景色等属性的改变

哪个对浏览器开销严重?

其实两者都会耗费浏览器性能,其开销都是非常昂贵的。若我们不停改变页面的布局,就会造成浏览器大量页面的计算,影响页面性能,出现卡顿。
相比之下,由于重排势必引起重绘,所以重排的开销会重绘更严重,而repaint的速度明显比reflow的速度快。

浏览器的自我优化?

重绘和重排的开销是非常昂贵的,如果我们不停的在改变页面的布局,就会造成浏览器耗费大量的开销在进行页面的计算,这样的话,我们页面在用户使用起来,就会出现明显的卡顿。现在的浏览器其实已经对重排进行了优化,比如如下代码:

var  div = document.querySelector('.div'); 
div.style.width = '200px';
div.style.background = 'red'; 
div.style.height = '300px';

比较久远的浏览器,这段代码会触发页面2次重排,在分别设置宽高的时候,触发2次.
当代的浏览器对此进行了优化,这种思路类似于现在流行的MVVM框架使用的虚拟DOM,对改变的DOM节点进行依赖收集,确认没有改变的节点,就进行一次更新。但是浏览器针对重排的优化虽然思路和虚拟DOM接近,但是还是有本质的区别。大多数浏览器通过队列化修改并批量执行来优化重排过程。也就是说上面那段代码其实在现在的浏览器优化下,只构成一次重排。
但是还是有一些特殊的元素几何属性会造成这种优化失效。比如:

offsetTop, offsetLeft,...
scrollTop, scrollLeft, ...
clientTop, clientLeft, ...
getComputedStyle() (currentStyle in IE)

为什么造成优化失效呢?仔细看这些属性,都是需要实时回馈给用户的几何属性或者是布局属性,当然不能再依靠浏览器的优化,因此浏览器不得不立即执行渲染队列中的“待处理变化”,并随之触发重排返回正确的值。在下边如何优手动化?的第3种方案中给出了解决办法。

如何优手动化?

虽然现在的浏览器其实已经对重排进行了优化,但是有时候我们仍需要手动对其优化。
1、将多次改变样式属性的操作合并成一次操作

const el = document.querySelector("#element");
el.style.borderLeft = " 1px";
el.style.borderRight = "2px";
el.style.padding = "3px";

上述代码会触发浏览器三次重排。然而使用cssText可以合并所有的改变一次处理

const el = document.querySelector('#element');
el.style.cssText = 'border-left: 1px; border-right: 2px;padding: 3px';

沿着这个思路,还有一种减少重排的方法就是切换类名,也只会触发一次重排

// css 
.active {
    border-left: 1px;
    border-right: 2px;
    padding:3px;
}
// javascript
const el = document.querySelector('#element');
el.className = 'active';

2、将DOM离线处理,脱离文档流后对其操作
a、元素设置display:none,元素就不在文档流了。之后的操作将不会触发重排和重排。这种方法种控制元素的显示与隐藏造成两次重排

let ul = document.querySelector('#mylist'); 
ul.style.display = 'none';  
appendNode(ul, data); 
ul.style.display = 'block';

b、通过使用DocumentFragment创建一个dom碎片,在它上面批量操作,之后再添加到文档中,这样只会触发一次重排。

//动态创建20个p标签,先用DocumentFragment 对象来缓存
let pNode;
for(var i=0; i<20; i++){
    pNode = document.createElement('p');
    pNode.innerHTML = i;
};
let fragment = document.createDocumentFragment();
fragment.appendChild(pNode);
//一次性,一起追加到body节点上
document.body.appendChild(fragment);

c、将原始元素复制一份,操作这个复制的节点,然后覆盖原始元素。这种方法也是只有一次重排

let old = document.querySelector('#mylist');
let clone = old.cloneNode(true);
appendNode(clone, data);
old.parentNode.replaceChild(clone, old);

d、position属性设为absolute或fixed。这样此元素也会脱离了文档流,成为一个单独的图层,它的变化不会影响到其他元素。否则会引起父元素以及后续元素频繁的重排

例如 对于复杂动画效果,使用绝对定位让其脱离文档流。
比如你的动画宽高变化,这将使得父元素也变化,设置父父元素变化。导致这些会回流。但是如果将其脱离文档流,就不会影响了

3、其他情况
---读写分离---
浏览器为了减少重排的次数,会将引起重排的代码加入一个渲染队列,到一定的条件再执行重排的代码,如果期间有布局信息的查询操作,为保证你查询结果的实时性,那么这会导致渲染队列强制执行前边的队列。这将导致多一次排版。

function initP() {
  for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  };
}
/*
在每次循环的时候,都读取了box的一个offsetWidth属性值,
然后利用它来更新p标签的width属性。
为了保证读取的及时性,浏览器在每次调用 box.offsetWidth都会刷新渲染队列
所以为了避免刷新队列引起多次重排,代码可以做如下修改
*/
const width = box.offsetWidth;
function initP() {
  for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = width + 'px';
  };
}

---尽量不要使用表格布局---
如果没有定宽,表格每列的宽度由最宽的一列决定。那么很可能在最后一行的宽度超出之前的列宽,引起整体回流,造成table可能需要多次计算,才能确定好其在渲染树中节点的属性,通常要花几倍于同等元素的时间。
---css3GPU硬件加速--
比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场

  • transition替代animation 来做动画。
  • opacity替代visibility
  • filters
  • Will-change

局部重排和全局重排

根据影响范围不同,划分如此。
局部重排:在一个固定宽高的div里面元素的重排,一般不会影响到此元素外的布局
全局重排:元素的重排导致父元素 直至根节点内的元素发生改变,就会导致全局重排

其他说明

重排:谷歌中叫layout,火狐中叫reflow,因此有人也翻译做回流。

参考

oschina
cnblogs

三、VirtualDOM与DomDiff

什么是VDom?

全成VirtualDOM,虚拟Dom。通过js的object对象模拟Dom中的节点,然后再通过特定的Render方法将其渲染成真实的Dom节点。

为什么引入VDom?

DOM“天生就慢”,所以前端各大框架都提供了对DOM操作进行优化的办法,Angular中的是脏值检查,React首先提出了Virtual Dom,Vue2.0也加入了Virtual Dom,与React类似
Jquery横霸天下的时候,频繁操作大量DOM产生巨大的性能损耗。因操作DOM会引起页面的回流或者重绘,带来了很大的性能损耗。如何规避这些问题呢?这时就引入了虚拟dom的概念。
通过虚拟DOM,只需要操作Js对象模拟出来的节点,通过对这些虚拟节点进行修改,后经过diff算法得出一些需要修改的最小单位,再将这些最小单位的视图进行更新。这样做减少了很多不必要的DOM操作,大大提高了性能。

什么是DomDiff?

就是一个拥有牛逼算法的函数,通过个方法,找出新旧两棵VDOM之间的区别,并将这些变更渲染到真实的DOM节点上去

DomDiff的算法分析?

不同框架的Diff实现和策略是不同的。
都知道操作真是的dom开销太大,所以框架们都会选择不同的方式实现
但是目的是相同的,都是减少真是dom的操作,提高页面响应性能
观察下复杂度,传统 diff 算法的复杂度为 O(n^3),而react将其降低到了O(n),vue也是这样

React的diff算法?

首先需要明确,只有在React更新阶段才会有Diff算法的运用;

vdom是react最先提出来的,自然,要先说一下react。
render返回的并不是实际的Dom,只是一些轻量的Js对象,即虚拟DOM
react有一个阶段叫做调和阶段,此阶段就是让虚拟dom转为真实dom。此过程用到了算法,来达到最少的操作真实dom,这个算法,就是react的diff算法。
React采用虚拟DOM技术实现对真实DOM的映射,即React Diff算法的差异查找实质是对两个JavaScript对象的差异查找;

  1. 把vdom tree分层,忽略Web UI中DOM节点跨层级移动;
  2. 拥有相同类型的两个组件产生的DOM结构也是相似的,反之不同
  3. 对于同一层级的一组子节点,通过分配唯一唯一id(key)进行区分

参考

思否

四、new一个对象发生了什么

function Student(name,age) {
    this.name = name;
}
var student = new Student('dsh');

第一步是建立一个新对象
第二步将该对象内置原型对象__proto__ 设置为构造函数prototype那个原型对象
第三步就是将该对象作为this参数调用构造函数,完成成员设置等初始化工作
第四部就是返回此对象
如果我们用手动模拟一个构造内部实现,那么依据上边的原理,代码如下:

function Person(name,age) {
    var that = { //第1步
        __proto__: Person.prototype //第2步
    }
    that.name = name; //第3步
    return that;//第4步
}
var person = new Person('dsh');

四、web缓存

为什么要用web缓存?

  • 减少网络带宽消耗
    从缓存服务器或者本地读取资源,可以更好地节省带宽流量
  • 优化用户体验
    就近‘取材’,使得浏览器能够更快的响应用户内容,加快页面打开速度
  • 降低服务器压力
    降低减少访问源服务器次数,减轻服务器业务处理逻辑及访问数据库等压力

都有哪些技术?

1. 浏览器缓存

1. http缓存(也叫Application Cache)
2. 本地存储(有cookie、storage)
3. 前端数据库(有 indexDB、 WebSQL)
4. manifest离线存储(有Offline Web Application[即应用缓存]与PWA)

打开浏览器 -> 点击F12 -> 点Application。可以看到浏览器的多种缓存机制

其目的是当重复请求某一个文件时不向服务器发送请求,从本地获取

2. 服务器缓存

1. 代理服务器缓存
2. cdn缓存(也叫网关缓存、反向代理缓存)

目的都是让用户的请求走”捷径“,并且都是缓存图片、文件等静态资源

3. 数据库缓存

不做详细说明

需要注意

  • 浏览器页面前进后对,页面直接出现而不刷新,其实是用的是栈的不清空操作来处理的,与浏览器缓存不是一回事
  • 浏览器不同,http缓存表现默认不同。Nginx只返回Last-modified,Apache还多一个ETag
  • 默认http缓存针对的是静态文件类型,如img、css。但是如果后端返回的304指定此次请求走缓存,那么将不会限制类型
  • 缓存都是发生在第二次请求。第一次请求或者强刷不存在缓存
  • http缓存并不是所有都返回304,而是协商缓存才返回

http缓存

一般http缓存都是后端人员设置,前端小伙伴在一开始就很少接触缓存,只知调接口,其实后端小伙伴们给我们做了很多优化处理。
上图中的Frames展示的就是http缓存。http缓存都是从第二次请求开始。
http从v0.9到1.0到v1.1再到最近的2.0,不同时期有不同的操作。

v0.9(不支持缓存)

1991 HTTP 协议诞生,版本是0.9。此协议被设计用于从服务器获取 HTML 文档。整个协议只有1行:GET加文档路径。不支持缓存

---不能使用缓存---
浏览器向服务器请求资源 img.jpg,服务器找到对应资源把内容返回给浏览器。当浏览器再次向服务器请求资源img.jpg时,服务器重新发送完整的数据文件给浏览器。
优点:简单,啥都不用做
缺点:每次请求都查找并返回原始文件,浪费带宽,要知道91年的带宽很贵

v1.0(expires缓存方式)

1996 年发布,一个更加完整,更加接近我们目前对 HTTP 认知的版本。新增了如今耳熟能详的概念:HTTP 头、HTTP 响应状态码、HTTP 方法(增加了 HEAD 和 POST)。通过Response header里新增expires,开始支持cache

---有缓存,更新机制不友好---
浏览器第一次请求img.jpg 时服务器会发送完整的文件,浏览器可以把这个文件存到本地(缓存),下次再需要这个文件时直接从本地获取就行了,这样就能省下带宽了。但是至少要有一个更新机制,不然如果文件内容变了,访问的一直都是本地的老文件。所以expires可设置缓存有效期,这个有效期是个绝对格林威治时间,再一次进行请求的时候,用本地的时间与时间点比较,如果已经过了该时间点,则重新向服务器请求,否则从本地缓存文件中读取。
优点:已经有缓存了,而且可控
缺点:有一个非常大的缺陷,它使用一个固定的时间,要求服务器与客户端的时钟保持严格的同步,并且这一天到来后,服务器还得重新设定新的时间

console.log(url);//url 为每次请求的地址
//node设置Expires
res.writeHead(200, {
    'Content-Type': 'text/html',
    'Expires':new Date('2019-11-10').toUTCString()
});
//前端设置Expires
<meta http-equiv="Expires" content="Thu, 10 Dec 2019 16:00:00"/>


v1.1(Cache-Control缓存方式)

1999年,现在(2020年)chrome、fireFox默认使用的http协议版本诞生。这个协议增加很多的内容,已经相当丰满。其中有几个重要的变化:
原来的每次http请求就发起一次tcp,耗时费力方式已经改为默认持久连接的机制keep-alive。
缓存机制也得到了进一步更新,通过Response header里引入了Cathe-Control属性来设置有效期相对时间(比如多少秒内有效,而不是固定时间)控制缓存来解决了Expires的缺陷。

优点:控制能力更强,以相对时间为判断依据,而且还额外增加了一些其他值。
缺点:如果浏览器再次请求资源超过了max-age,依然会重新发出通向服务器的请求,即便这个文件并没有被修改。反之如果在时间段内被修改了,浏览器也不会发起服务器真请求获取新文件。这是个重大缺陷。
注意:如果在Cache-Control响应头设置了 "max-age" 或者 "s-max-age" 指令,那么Expires头会被忽略。

//node设置Cache-Control
res.writeHead(200, {
    'Content-Type': 'text/html',
    //10s后的刷新 才会发起真正通向服务器的请求,否则是from memory cache
    //除了max-age之外,还有其他值:no-cache、no-store、Public、Private
    'Cache-Control': 'max-age=10' 
});

v1.1升级版(缓存验证Last-Modified和Etag的使用)

如上使用Cache-Control的缺陷,可通过这两个属性来修复。这两个可以只使用一个,不必都用,具体用哪个看实际需求。
它们也不是同时诞生的,last-Modified是老的http标准就有的,ETag是后来的http标准里才有的,算是对modified的优化,解决了一些last-Modified不能解决的问题。
唯一值得重视的是要确保服务器提供这两个属性值给的合理。
验证头Last-Modified:上次修改时间,服务器对比上次修改时间以验证资源是否需要更新,它配合If-modified-Since和If-Unmodified-Since使用

我们请求一个资源,这个资源返回的有Last-Modified。浏览器进行下一次请求的时候,通过If-modified-Since或If-Unmodified-Since把上次传入的Last-Modefied的值带到服务器上,服务器通过读取If-modified-Since的值然后对比资源存在的地方,然后去对比上次修改的时间,如果时间是一样的,代表这个资源还没有重新被修改,服务武器告诉浏览器可以使用缓存里面的内容

http.createServer( (req, res)=> {
    let { url } = req;
    url = url == '/' ? './index.html':`.${url}`;
    fs.readFile(url,(err,data)=>{
        let resStatus = 200;
        let hasModified = req.headers.hasOwnProperty('if-modified-since');
         // 如果还没到过期时间,就返回304,让浏览器调用缓存
        if( hasModified && new Date()<new Date(req.headers['if-modified-since'])){
            resStatus = 304;
        }
        res.writeHead(resStatus, {
            'Content-Type': 'text/html',
            'Cache-Control': 'no-cache',
            'Last-modified': 'Sun, 10 Nov 2019 07:36:00 GMT'
        });
        res.end(data);
    });
}).listen(8888);

验证头Etag:资源内容对应的唯一的数据签名。若资源数据更改,签名也会变,它配合If-Match或If-Non-Match使用

浏览器下一次请求,通过If-Match或If-Non-Match把上一次设置的Etag的值带到服务器上,然后服务器对比浏览器传过来的签名和服务器端资源的签名是否一致,如果相同,就不需要返回一个心的内容。

http.createServer( (req, res)=> {
    let { url } = req;
    url = url == '/' ? './index.html':`.${url}`;
    fs.readFile(url,(err,data)=>{
        let resStatus = 200;
        let imgFileEtag = 'Fpl1rXE-K20cYsLu89YTzUQ3ydDk';//假设这个就是当前请求文件的etag值
        let hasIfNoneMatch = req.headers.hasOwnProperty('if-none-match');
         // 如果文件没有被改变,就返回304,让浏览器调用缓存
        if( hasIfNoneMatch && imgFileEtag == req.headers['if-none-match']){
            resStatus = 304;
        }
        res.writeHead(resStatus, {
            'Content-Type': 'text/html',
            'Cache-Control': 'no-cache',
            'Etag':'Fpl1rXE-K20cYsLu89YTzUQ3ydDk'
        });
        res.end(data);
    });
}).listen(8888);

需要注意的是,以上是我举得两个例子,真实情况要根据某个文件需不需要缓存,如果确定需要,那这个文件有没有被修改再来做出最终决定,而不能像我demo一样,所有的文件都要304缓存。

强制缓存和协商缓存

根据是否向服务器发送真实请求来划分强制缓存和协商缓存。

  • 强缓存:不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network选项中可以看到该请求返回200的状态码;
  • 协商缓存:向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源;
  • 两者的共同点:都是从客户端缓存中读取资源;区别是强缓存不会发请求,协商缓存会发请求。

其他几个重要的缓存相关的‘请求头属性’

vary:这通常是用在与缓存服务器交互的的字段,如果项目用到了cdn等技术,可以用此请求头属性。
表示缓存服务端会以什么基准字段来区分、筛选缓存版本,主要用户通知缓存服务器对于相同的URL有着不同版本的响应,如压缩版和非压缩版。使用vary头有利于内容服务的动态多样性。
如Vary:Accept-Encoding,根据浏览器编码类型来调用指定的缓存版本。
如Vary: User-Agent,根据UA判断启用移动端和桌面端的展示内容

http缓存技术中,我要用谁?

自从http1.1开始,Expires逐渐被Cache-Control取代。如果你需要兼容HTTP1.0的时候需要使用Expires,不然可以考虑直接使用Cache-Control

本节(http缓存)参考

Http 缓存机制
Expires头与Cache-control区别
从 HTTP 0.9 到 QUIC
HTTP0.9、HTTP1.0、HTTP1.1、HTTP2的区别
前端性能优化 — 缓存篇
from memory cache与from disk cache
浏览器缓存之Expires Etag Last-Modified max-age详解

本地存储

这个很简单,查资料就行

前端数据库

考虑到兼容问题,一般不用

manifest离线技术

默认用法由于有缺陷,很少使用。
基于此技术的pwa现在挺火,可以看看

参考

HTTP缓存机制

五、setState相关知识

虚拟Dom

React为了避免复杂且频繁的操作真实Dom带来的低性能影响,为此引入了虚拟Dom的机制。
Virtual DOM本质就是js对象,用JS对象来模拟真实DOM。
Dom和Virtual DOM一一对应。通过Vdom的Diff算法,来最少更新Dom,有效提升了网站性能。

React中调用setState之后发生了什么事情?

第一步:更新状态

  1. 把传递给setState的对象 合并到组件的当前状态
  2. 更新后的newState放入状态队列,并调用内置更新方法
  3. 更新方法判断是将新状态应用到组件上还是放入待更新脏组件列表中
  4. 后者继续重复前边步骤,前者则更新组件们状态,清空队列和列表
  5. 更新状态之前,会进入shouldComponentUpdate,返回true,就落实更新,并按顺序执行组件钩子。否则不更新state,并终止执行

第二步:更新vdom

  1. 最终到render,该钩子返回更新后组件的next vdom对象
  2. 拿到next vdom后,对比pre Vdom。通过diff算法,计算出最少的更新dom的差异队列,并以此更新最终vdom tree

第三步:更新Dom

  1. 拿到差异队列后,最后遍历队列,一次性的更新到真实的dom中,这样将只会引起一次重排。

几个专业术语

setState是批量更新

在React的钩子和合成事件中,多次执行setState,会批量执行然后合并,触发一次调和过程。表现为异步更新。
但是在在原生事件,setTimeout,setInterval,Promise等异步操作中,state会立马更新,并执行调和操作。表现为同步更新。
这一点,现代浏览器已经开始借鉴react,vue这些框架的做法,他们如果会把相邻的批量操作dom动作放入一个栈里,然后等到时机合并为一起操作dom,这样会减少重排次数

调和过程

在React新旧Vdom对比后,通过diff算法,最少更新Vdom和真实Dom。这个过程叫做调和过程。

diff算法

在调和过程中,涉及到到算法,就是diff算法。diff就是比较不同的意思。

setState优化

使用PureComponent做浅比较,或者用shouldComponentUpdate手动优化。
shouldComponentUpdate返回false,会终止组件vdom的计算比较及其后续的真是更新dom,合理处理此处,可避免无用的计算更新。
setState会触发子组件的重新计算vdom(即render执行),但是不一定会重新更新dom。比如以下代码:

this.state = {
      num:0,
};
this.setState({
      num: 0
}).

setState 循环调用风险?

setState的调用会引起React的更新生命周期的4个函数执行。所以这4个钩子不能再调用setState,否则会触发死循环。

  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

React之diff算法详解

传统的diff PK react的diff

传统的浏览器自带的diff通过循环递归对节点进行依次对比,算法复杂度达到 O(n^3) 。React为了提高性能,通过引入Vdom方式,再经过diff算法,将其复杂度已经降低到 O(n);
这个让人津津乐道的算法,基于优秀的三个策略。

diff的三个策略

tree>compenets>element,这是虚拟dom的结构。所以优化diff策略,一般是从这3个层级视角入手。

tree diff

策略1:Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。具体如下:
对树进行分层比较,两棵树只会 对同一层次的节点进行比较,如果发现被删除 或有新增,或者平级移动,则直接创建或删除或移动 来更新最新tree。如果遇到跨级移动,则直接当做删除行为操作,不再对其比较。
这样就只需遍历一次,就能完成整棵DOM树的比较。

component diff

策略2:拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结。具体如下:
如果是同一类型的组件,按照原策略继续比较 Virtual DOM 树即可。(继续比较)
否则将替换整个组件下的所有子节点。(终止比较)
同类型组件,可能内容相同,这样也会继续比较,比较浪费性能,那么对此React 允许用户通过 shouldComponentUpdate()来判断该组件是否需要进行 diff 算法分析,可节省大量计算时间

element diff

策略3:对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。具体如下:
当节点处于同一层级时,diff提供三种节点操作:删除、插入、移动。
新的组件类型不在旧集合里,即全新的节点,需要对新节点执行插入操作
旧组件不在新集合里的或存在但element 不同不能复用。需要执行删除操作
旧集合中有新组件类型,且 element 有相同。则可以进行移动操作,即可复用
如果有多个同类型的组件,react允许添加key作为区分,来进行复用

对比vue 的 diff策略?

两者都是采用虚拟dom策略, 都是用JS对象来模拟真实DOM,然后用虚拟DOM的diff来最小化更新真实DOM。但是两者更新策略却不同。
react 当你 setstate 就会遍历 diff 当前组件所有的子节点子组件, 这种方式开销是很大的, 所以 react 16 采用了 fiber 链表代替之前的树,可以中断的,分片的在浏览器空闲时候执行。
vue 采用代理监听数据,我在某个组件里修改数据,就会明确知道那个组件产生了变化,只用 diff 这个组件就可以了。

diff后的优化

react为每一个组件都提供了一个钩子函数shouldComponentUpdate
当组件经历过diff后,已经计算出某个组件确实需要去更新,然后在更新对应的真是dom的时候,就会走这个函数。这个函数如果返回false,则会忽略这次更新。对应的视图将不会刷新,组件内的状态也不会概念。

总结

react实现了一层虚拟dom,它用来映射浏览器的原生dom树。通过这一层虚拟的dom,可以让react避免直接操作dom,因为直接操作浏览器dom的速度要远低于操作javascript对象。每当组件的属性或者状态发生改变时,react会在内存中构造一个新的虚拟dom与原先老的进行对比,用来判断是否需要更新浏览器的dom树,这样就尽可能的优化了渲染dom的性能损耗。
在此之上,react提供了组件生命周期函数,shouldComponentUpdate,组件在决定重新渲染(虚拟dom比对完毕生成最终的dom后)之前会调用该函数,该函数将是否重新渲染的权限交给了开发者,该函数默认直接返回true,表示默认直接出发dom更新

六、hooks的好处

  1. 摒弃难以理解的基于类的oop,也不用处理this问题了
  2. 无状态组件,不会创建新的实例,避免了不必要的检查和内存分配。
  3. 使用类高阶组件时,会层层嵌套,及其名字问题。使用自定义hooks就不会。
    也有缺点,比如虽然可以返回vdom,但是却不能像类高阶一样对其包装容器

七、react和vue的区别

组件书写方式不同

React推崇all in js的思想,所以有了jsx、Styled-Components
Vue推崇一个文件就是一个组件,结构、表现、行为分离。所以采用了模板

监听数据变化的实现原理不同

Vue 通过 getter/setter 数据数劫持+发布订阅,跟踪每一个组件的依赖关系,能精确知道数据变化,并自动更新
React某组件状态被改变时,当前组件及其子组件都会重新渲染
比较:React的性能优化需要手动去做,而vue的性能优化是自动的,但是vue的响应式机制也有问题,就是当state特别多的时候,Watcher也会很多,会导致卡顿,所以大型应用状态特别多的一般用react,更加可控。

数据可变不同

基于监听变化更新的不同,所以React数据不可变,通过setState统一管理。而vue的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立Watcher来监听,当属性变化的时候,响应式的更新对应的虚拟dom。

数据流不同

vue2.x后,准确来说就只剩组件 <--> DOM的数据流不同了,既双向绑定
vue支持双向绑定,而react不支持。

组件逻辑复用

React有早期mixins到Hoc,renderProps再到hooks(状态逻辑复用)
Vue一直在坚持mixins+指令

相同点

  1. 都支持服务器端渲染
  2. 都有VDom,组件化开发,props参数进行父子数据传递,都实现webCmp规范
  3. 数据驱动视图
  4. 都有支持native的方案,React的React native,Vue的weex
  5. 都有管理状态,React有redux,Vue有自己的Vuex(自适应vue,量身定做)

八、面试题

html-css部分
1.盒子模型的理解?
2.文档流
3.清除浮动?
2.css垂直居中
4.css选择器
6.处理浏览器兼容性
7.自适应和响应式布局
8.跨域的几种方案
js部分
1.js作用域与闭包,以及var、const、let
2.对象的深浅拷贝,及其原因分析
3.对象数组的合并及其副作用 (副作用不清晰,合并对象少用)
4.js原型的理解 (这个有理解不是特别清晰)
5.js事件循环
6.js判断类型
框架部分
vue双向数据流,
vue引用类型数据不刷新的处理办法(不明白问什么 后提醒才知道问的点)
-------评价----------

---html.css---

何为行元素,何为块元素,都有哪些,如何转换?

  1. 块元素(div、p、ul、li、table)
    块元素独占一行
    有宽高和内外边距属性(宽默认为父元素的宽)
    块元素可以包含一切
  2. 行元素(span、select、img)
    在其他元素后边,不换行
    没有宽高,全靠内容撑开,且内外边距也只是部分有效果(具体表现为:内边距有效、外边距只左右有效)
    行元素只能包含行元素
  3. 行块元素(input)
    拥有inline和block的双重特性
  4. 替换元素(iframe、video、img)
    那为什么img是行内元素还可以设置高呢?
    是因为img虽然是行内元素,但也是替换元素,宽高由属性src的内容决定,或外部css决定。
    替换元素是具有固有尺寸,然后再根据加载后的内容进行替换,动态改变为真实大小。
    你可以设置属性或css来进行确定性覆盖。
    因为替换元素加持的原因,让原本无高、宽等的行内元素也有了这些特性

css元素选择器

*选择器 *{xxx}
class选择器.wrapp{xxx}
id选择器#wrapp{xxx}
element选择器a{xxx}
attr选择器伪类选择器a[href='baidu']{xxx}
上下文/派生选择器.wrapp>p{xxx}``.wrapp p{xxx}

关于弹性盒子

弹性盒子是 CSS3 的一种新的布局模式。
弹性盒子由弹性容器(Flex container)和弹性子元素(Flex item)组成。
弹性容器通过设置 display 属性的值为 flex 或 inline-flex将其定义为弹性容器。
弹性容器内包含了一个或多个弹性子元素。
注意: 弹性容器外及弹性子元素内是正常渲染的。弹性盒子只定义了弹性子元素如何在弹性容器内布局。
弹性子元素通常在弹性盒子内,从左到右一行显示。默认情况每个容器只有一行(可以通过flex-wrap来设置需不需要换行)。

  1. 哪个属性是设置弹性盒子的子元素如何分配空间?
    flex 属性用于设置或检索弹性盒模型对象的子元素如何分配空间。
  2. flex是哪个几个属性的简写,分别代表什么意思,如何做到子元素平均分配容器?
    元素属性
  • flex 属性是 flex-grow、flex-shrink 和 flex-basis 属性的简写属性。
  • 首先是   flex-basis  ,basis英文意思是<主要成分>,用于设置或检索弹性盒伸缩基准值,优先级高于width。
    如果不设置默认值是auto,长度将根据内容决定。
  • 其次是   flex-grow,grow英文意思是<扩大,扩展,增加>, 用于设置或检索弹性盒子的扩展比率。当父元素的宽度大于子元素宽度之和时,并且父元素有剩余,子元素就会根据其值进行扩伸,数值越大,扩展的比例越大
  • 最后是   flex-shrink, shrink英文意思是<收缩>,指定了 flex 元素的收缩规则, 当父元素的宽度小于子元素宽度之和时,并且超出了父元素的宽度,子元素就会按照一定的比例进行收缩,数值越大,收缩的比例越大
  1. 元素的排列方向
    容器属性
flex-direction: row|row-reverse|column|column-reverse|initial|inherit;
  1. 元素横向(x轴)纵向(y轴) の 对齐方式
    前两个容器属性,后一个是子元素属性
justify-content: flex-start|flex-end|center|space-between|space-around|initial|inherit;
align-items: stretch|center|flex-start|flex-end|baseline|initial|inherit;
align-self: auto|stretch|center|flex-start|flex-end|baseline|initial|inherit;

其中align-self覆盖容器的 align-items 属性。

  1. 元素排序
    子元素属性
order:*number*|initial|inherit;
  1. 多行容器之垂直对齐方式align-content
    容器属性
    这个属性和align-item类似,有一定的迷惑性,具体百度一下别人的文章
    其主要功能是用于多行垂直居中,因为多行的时候,行之间有一个很大的间隔,用align-content则会清除掉
<style>
        .wrapp{
            border: springgreen solid 1px;
            width: 500px;
            height: 400px;
            display: flex;
            flex-wrap: wrap;
            /* align-items: center; */
            align-content: center;
        }
        .wrapp div{
            height: 100px;
            flex-shrink: 0;
            flex-basis: 200px;
            border: red solid 1px;
        }
    </style>
</head>
    <body>
        <div class="wrapp">
            <div>1</div>
            <div>2</div>
            <div>3</div>
        </div>
    </body>

但如果变成多行容器
使用align-items时效果如下

使用align-content效果如下

关于动画

css编写动画的办法有几种?有什么区别

animation 和 transition 。
transition 是过渡, animation 是动画。
相对于animation,transition从某种层度上讲,动画控制的更粗一些,它唯一能定义动画变化过程效果的便是transition-timing-function属性,而animation提供的keyframe方法,可以让你手动去指定每个阶段的属性。此外animation还封装了循环次数,动画延迟,反向循环等功能,更加自由和强大。

animation

  1. 编写一个div,从左向右移动。
<style>
        @keyframes mymove {
            from { left: 0px; }
            to { left: 200px; }
        }
        div {
            width: 100px;
            height: 100px;
            background: red;
            position: relative;
            animation: mymove 5s infinite;
        }
</style>
<div></div>
  1. 写一个点击按钮可以清除或开始动画,而且有暂停和继续功能的动画
<style>
        @keyframes mymove {
            from { left: 0px; }
            to { left: 200px; }
        }
        div {
            width: 100px;
            height: 100px;
            background: red;
            position: relative;
           
        }
        .active{
            animation: mymove 5s infinite;
        }
    </style>
<body>
    <div></div>
    <button class="paused">暂停/继续</button>
    <button class="replay">开始/重置</button>
</body>
<script>
   
    const div = document.querySelector('div');
    const paused = document.querySelector('.paused');
    const replay = document.querySelector('.replay');
    paused.onclick = function(){
        if(div.style.animationPlayState === 'paused'){
            div.style.animationPlayState = 'running'
        }else{
            div.style.animationPlayState = 'paused'
        }
       
    }
    replay.onclick = function(){
        const activeName = div.getAttribute('class');
        div.style.animationPlayState = ''; // 清除之前点击暂停造成的副作用
        if(activeName === 'active'){
            div.setAttribute('class','')
        }else{
            div.setAttribute('class','active')
        }
    }
</script>
  1. animation属性是由哪些属性简写而成?
    animation 属性是一个简写属性,用于设置六个动画属性:
    | 值 | 描述 |
    | --- | --- |
    | animation-name | 规定需要绑定到选择器的 keyframe 名称。。 |
    | animation-duration | 规定完成动画所花费的时间,以秒或毫秒计。 |
    | animation-timing-function | 规定动画的速度曲线。 |
    | animation-delay | 规定在动画开始之前的延迟。 |
    | animation-iteration-count | 规定动画应该播放的次数。 |
    | animation-direction | 规定是否应该轮流反向播放动画。 |

transform

transition属性设置元素当过渡效果

  1. 写一个hove会动的div
div {  
            width: 100px;  
            height: 100px;  
            background: red;  
            transition: left 2s;  
            position: relative;  
            left: 0;  
        }  
  
        div:hover {  
            left: 200px;  
        }
描述
transition-property 指定CSS属性的name,transition效果
transition-duration transition效果需要指定多少秒或毫秒才能完成
transition-timing-function 指定transition效果的转速曲线
transition-delay 定义transition效果开始的时候

CSS3 transform-style 属性

  1. 元素静态位置转换属性Transform
    提到动态的transition转换,就不得不提静态的transform转换。
    因为名字也很想,所有很多人把它弄混。
    transform是 转换,指的是改变所在元素的外观,它有很多种手段(转换函数)来改变外观,例如 位移、缩放、旋转 等
transform: none|*transform-functions*;
div{
	width:200px;
	height:100px;
    margin-top: 100px;
	background-color:yellow;
	transform:rotate(90deg);
}
</style>
<div>Hello</div>

posted @ 2023-01-12 11:13  丁少华  阅读(64)  评论(0编辑  收藏  举报