【前端面试】从小作坊走到大厂,回头看这条路真的好长

5,393 阅读37分钟

引言

作者16年毕业,从寂寂无名的小作坊工作,到金融中厂,目前就职于字节。

作者在20年、21年的时候参加了20家以上的上海杭州地区大厂中厂面试。

接下来一段时间我会陆陆续续的将这20多家的面试真题按照类别进行分享。

链接

【前端面试】面试官:我看看你的React Hooks掌握的怎么样?

【前端面试】披荆斩棘,25道高频React面试题

关于面试

如果你是来自中小厂想去大厂工作的同学,那我确实有很多内容可以跟你分享。

准备时间

我每次面试都会提前准备一个月,并且以面代练两个月吧,就我的情况来讲,想找一份比我上一次工作钱更多的单位那真的是一大把,所以当时去面试的公司也比较多。

我第一次面试的时候也是比较菜的,很多的基础问题因为没有准备回答的都不是很好,甚至在面试一家大厂的时候,面试官直接对我说:回去好好准备一个月再出来吧,别糟蹋了你的简历和工作经历。很多公司是会有面试记录的,如果你第一次表现的太差了,第二年可不一定有机会再面试了。我去年投字节的时候就碰到了这个问题,后来是找猎头才推进去的,所以同学们要珍惜每一次的面试机会。

表达和表述

网络让这个时代更加的透明,诸如面试刷题的文章也多如牛毛,有些同学急于求成,背题、只看重点题,这个方法是没错的,但是你也将你的面试变成了一场赌博,赌面试官问你这些高频的题目。我们面试当中也会碰到很多这样的同学,常规题回答的贼溜,难一点点就磕磕巴巴,这个一看就是临时突击的同学。所以对于每个知识点你都要有自己的话术,总结一份自己的回答,并且关注这个回答的内容有没有你不熟悉可能会被面试官深入问的字眼。

简历

我个人推荐简历的内容要相对精简一点。 有些同学会的不多,把自己会的一股脑全写上去了,甚至会的哪些js基础知识点都写上去了。u1s1,真没必要。会什么框架就写什么框架好了,你会多少让面试官在面试的时候了解你。

关于项目经历,我个人推荐从以下几个方面入手

  • 使用场景
  • 你做了哪些工作
  • 难点在哪里
  • 取得了什么样收益

广告时间

程序员和NBA球员一样,黄金时间就这么几年,如果你是一个有进取心的少年,想摆脱如今的工作困境,来找我,我可以跟你分享我是怎么一步步走过来。

作者这两年的面试经历几乎覆盖了上海地区和杭州地区的全部大厂和部分中厂,也可以给你提供帮助:

  • 找工作建议
  • 如何准备面试
  • 如何学习
  • 薪资评估
  • 未来规划和成长

因为作者也在公司做一面面试官,众所周知一面是最难的,可以帮你做

  • 大厂的模拟面试,
  • 前端知识点的探底,帮你差缺补漏。
  • 最粗暴的,你可以简历发我,可以帮你把关。

二维码:

p9-juejin.byteimg.com/tos-cn-i-k3…

只需要两杯咖啡钱~ 欢迎在点击链接扫描添加我的企业微信了解。祝大家在2022期间面试顺利~

面试分享 React基础(不包含hooks)

我想了想最能帮助大家的模块应该还是React模块,所以我先分享一下React非Hooks部分的手稿吧,我会每天花点时间陆陆续续修订一下每个面试题,补充一些自己关于这个问题的感悟。

如果你发现我的解答有什么问题,欢迎评论和私信我。

react生命周期(16.3版本前后对比)

初始化阶段

  •     constructor 构造函数
  •     getDefaultProps props默认值 
  •     getInitialState state默认值

挂载阶段

  •     componentWillMount 组件初始化渲染前调用
  •     render 组件渲染
  •     componentDidMount组件挂载到 DOM后调用

更新阶段

  •     componentWillReceiveProps 组件将要接收新 props前调用

  •     shouldComponentUpdate 组件是否需要更新

  •     componentWillUpdate 组件更新前调用

  •     render 组件渲染

  •     componentDidUpdate 组件更新后调用 卸载阶段

  • componentWillUnmount


初始化阶段

  •     constructor 构造函数
  •     getDefaultProps props默认值
  •     getInitialState state默认值

挂载阶段

  •     static getDerivedStateFromProps(props,state)
  •     render
  •     componentDidMount

getDerivedStateFromProps:组件每次被 rerender的时候,包括在组件构建之后(虚拟 dom之后,实际 dom挂载之前),每次获取新的 props或 state之后;

每次接收新的props之后都会返回一个对象作为新的 state,返回null则说明不需要更新 state;配合 componentDidUpdate,可以覆盖 componentWillReceiveProps的所有用法

更新阶段

  •     static getDerivedStateFromProps(props,state)
  •     shouldComponentUpdate
  •     render
  •     getSnapshotBeforeUpdate(prevProps,prevState)
  •     componentDidUpdate

getSnapshotBeforeUpdate:触发时间: update发生的时候,在 render之后,在组件 dom渲染之前;返回一个值,作为 componentDidUpdate的第三个参数;配合 componentDidUpdate, 可以覆盖 componentWillUpdate的所有用法

卸载阶段

  •     componentWillUnmount 错误处理
  •     componentDidCatch React16新的生命周期弃用了 componentWillMount、componentWillReceivePorps,componentWillUpdate新增了 getDerivedStateFromProps、getSnapshotBeforeUpdate来代替弃用的三个钩子函数。

React16并没有删除这三个钩子函数,但是不能和新增的钩子函数混用, React17将会删除这三个钩子函数,新增了对错误的处理( componentDidCatch)

如果你是三年以下开发,这道题可能用来热场,听听看你对16.3之后的几个API的熟悉程度互相之间如何替代?为什么要删除钩子函数?错误处理的生命周期怎么用?引申下如果继续使用这几个API会出现什么样的问题?甚至可以转到Hooks挑几个让你用hooks怎么去实现相同的生命周期效果?

副作用的理解

  • 首先解释纯函数(Pure function):给一个 function 相同的参数,永远会返回相同的值,并且没有副作用;这个概念拿到 React 中,就是给一个 Pure component 相同的 props, 永远渲染出相同的视图,并且没有其他的副作用;纯组件的好处是,容易监测数据变化、容易测试、提高渲染性能等;
  • 副作用(Side Effect)是指一个 function 做了和本身运算返回值无关的事,比如:修改了全局变量、修改了传入的参数、甚至是 console.log(),所以 ajax 操作,修改 dom 都是算作副作用的;

Virtual DOM的理解

VirtualDOM的定义

本质上是JS对象,这个对象就是更加轻量级的对DOM的描述。

React为解决操作DOM的痛点提出了一个新的思想,即始终整体刷新页面,当发生前后状态变化时,React会自动更新UI,但是缺点就是很慢。因此,没有改变的DOM节点让它保持原样不懂,仅仅创建并替换变更过的DOM节点实现了节点的复用,因此问题转化为如何对比两个DOM节点的差异。因为DOM是树形解构,完成的树形结构diff算法复杂度为O(n^3)。

React 做了三种优化来降低复杂度:

1.如果父节点不同,放弃对子节点的比较,直接删除旧节点然后添加新的节点重新渲染;

2.如果子节点有变化,VirtualDOM不会计算变化,二次重新渲染;

3通过设定唯一的key值来比较节点;

真实的DOM有非常多的属性,大部分属性对于DIff是没有任何用处的,所以如果用更轻量级的JS对象来代替复杂的DOM节点,就可以避免大量对DOM的查询操作。

VirtualDOM的作用

1.在牺牲部分性能的前提下,增加了可维护性。

2.可以使框架跨平台

3.组件的高度抽象化

VirtualDOM的缺点

1.首次渲染大量的DOM,由于多个一层虚拟DOM的计算,会稍微慢一点

2.需要在内存中维护一份DOM的副本

3.如果虚拟 DOM 大量更改,这是合适的。但是单一的,频繁的更新的话,虚拟 DOM 将会花费更多的时间处理计算的工作。所以,如果你有一个 DOM 节点相对较少页面,用虚拟 DOM,它实际上有可能会更慢。

setState的机制

setState是同步的还是异步的?

在 React的生命周期和合成事件中, React仍然处于他的更新机制中,这时无论调用多少次 setState,都会不会立即执行更新,而是将要更新的值存入 _pendingStateQueue,将要更新的组件存入 dirtyComponent。

当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件 didmount后会将批处理标志设置为 false。这时将取出 dirtyComponent中的组件以及 _pendingStateQueue中的 state进行更新。这样就可以确保组件不会被重新渲染多次。

componentDidMount() {
  this.setState({
        index: this.state.index + 1
  })
  console.log('state', this.state.index);
}

所以,如上面的代码,当我们在执行 setState后立即去获取 state,这时是获取不到更新后的 state的,因为处于 React的批处理机制中, state被暂存起来,待批处理机制完成之后,统一进行更新。

所以。setState本身并不是异步的,而是 React的批处理机制给人一种异步的假象。

异步代码和原生事件中

componentDidMount() {
    setTimeout(() => {
      console.log('调用setState');
      this.setState({
              index: this.state.index + 1
      })
            console.log('state', this.state.index);
      }, 0);
}

如上面的代码,当我们在异步代码中调用 setState时,根据 JavaScript的异步机制,会将异步代码先暂存,等所有同步代码执行完毕后在执行,这时 React的批处理机制已经走完,处理标志设被设置为 false,这时再调用 setState即可立即执行更新,拿到更新后的结果。

在原生事件中调用 setState并不会出发 React的批处理机制,所以立即能拿到最新结果

最佳实践

setState的第二个参数接收一个函数,该函数会在 React的批处理机制完成之后调用,所以你想在调用 setState后立即获取更新后的值,请在该回调函数中获取

·  setState接收一个新的状态

·  该接收到的新状态不会被立即执行么,而是存入到pendingStates(等待队列)中

setState的执行原理

可以分为两类:

1、批量更新类:即react内部的执行函数,执行setState的执行逻辑,都是批量更新处理,其中包括:react内部事件(合成事件)和生命周期;

2、非批量更新类:即上面两种情况以外的情况,经常见到的:原生事件、setTimeout、fetch等等;

再讲解之前,先说明两个概念:

1、事务:可以理解为,一个正常的函数外层又被包裹了一层。这层包裹处理,包括一个或多个的函数执行前的处理函数(initialize函数)、一个和多个函数执行后的处理函数(close函数);React很多的逻辑处理,都使用了事务的概念;

2、合成事件和原生事件的关系和区别:

区别:原生事件就是addEventListener写法的事件!而合成事件,就是直接书写react中的onClick、onChange等;

关系:合成事件可以理解为react对原生事件的包裹封装;原生事件相当于上面事务概念中的正常的函数,而经过包装处理形成的事务,就是react中的合成事件。

原生事件中,setState会直接触发render更新,所以栗子在原生事件中的执行顺序是,先render然后执行callback,setState事务执行完毕,然后执行打印。打印拿到的就是setState更新之后的状态,以此类推,所以出现了上面原生事件的打印顺序,这就很明了了。

而合成事件则不然,它直接发起事务1,在函数执行之前开始批量更新状态(isBatchedUpdates为true,默认值是false!),开启之后,执行合成事件中的setState,此时处于批量更新状态,这时setState不会触发render更新,而是做了两件事情:收集state和callback。

默认批量更新是处于关闭的状态,那么会直接执行batchedUpdates(此函数就是更新渲染函数)。这里就是批量更新状态是否开启的分叉口:当开启批量更新时,则是把状态push到数组(dirtyComponents)中。

收集完状态以后,执行事务的close函数,它里面做了些什么呢?一个是关闭批量更新状态,一个是正式发起对收集的状态的处理,这里又开启了一个新事务:即事务2。事务2,经过复杂的处理,处理更新了收集的state,也就是dirtyComponents。处理完以后,执行事务2的close函数,它重置了整个更新的状态,也是在这里处理执行事务1中收集的callback;

最后,总结一下setState:

1、setState的执行,分为两大类:一类是生命周期和合成函数;一类是非前面的两种情况;

2、两种类型下,setState都是同步执行,只是在批量更新类中,state和callback被收集起来延迟处理了,可以理解为数据的异步执行;而非批量更新类中的setState直接触发更新渲染。

3、callback与state同时收集,处理是在render之后,统一处理的。

合成事件机制和原理

由于fiber机制的特点,生成一个fiber节点时,它对应的dom节点有可能还未挂载,onClick这样的事件处理函数作为fiber节点的prop,也就不能直接被绑定到真实的DOM节点上。

为此,React提供了一种“顶层注册,事件收集,统一触发”的事件机制。

所谓“顶层注册”,其实是在root元素上绑定一个统一的事件处理函数。“事件收集”指的是事件触发时(实际上是root上的事件处理函数被执行),构造合成事件对象,按照冒泡或捕获的路径去组件中收集真正的事件处理函数。“统一触发”发生在收集过程之后,对所收集的事件逐一执行,并共享同一个合成事件对象。这里有一个重点是绑定到root上的事件监听并非我们写在组件中的事件处理函数,注意这个区别,下文会提到。

以上是React事件机制的简述,这套机制规避了无法将事件直接绑定到DOM节点上的问题,并且能够很好地利用fiber树的层级关系来生成事件执行路径,进而模拟事件捕获和冒泡,另外还带来两个非常重要的特性:

  • 对事件进行归类,可以在事件产生的任务上包含不同的优先级
  • 提供合成事件对象,抹平浏览器的兼容性差异

本文会对事件机制进行详细讲解,贯穿一个事件从注册到被执行的生命周期。

总结

  1. 事件处理函数不是绑定到组件的元素上的,而是绑定到root上,这和fiber树的结构特点有关,即事件处理函数只能作为fiber的prop。
  2. 绑定到root上的事件监听不是我们在组件里写的事件处理函数,而是一个持有事件优先级,并能传递事件执行阶段标志的监听器。

合成事件对象

在组件中的事件处理函数中拿到的事件对象并不是原生的事件对象,而是经过React合成的SyntheticEvent对象。它解决了不同浏览器之间的兼容性差异。抽象成统一的事件对象,解除开发者的心智负担。

Fiber

大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是吧;运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用setState更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI。整个过程是一气呵成,不能被打断的。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。

解决主线程长时间被 JS 运算占用这一问题的基本思路,是将运算切割为多个步骤,分批完成。也就是说在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后,再继续之前未完成的任务。

window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这些延迟触发但关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。

React 框架内部的运作可以分为 3 层:

Virtual DOM 层,描述页面长什么样。

Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。

Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。

这次改动最大的当属 Reconciler 层了,React 团队也给它起了个新的名字,叫Fiber Reconciler。

Fiber Reconciler 在执行过程中,会分为 2 个阶段。

阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。

阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。

阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。

Fiber Reconciler 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树。这棵树是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。

Fiber 树在首次渲染的时候会一次过生成。在后续需要 Diff 的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程。

如果过程中有优先级更高的任务需要进行,则 Fiber Reconciler 会丢弃正在生成的树,在空闲的时候再重新执行一遍。

在构造 Fiber 树的过程中,Fiber Reconciler 会将需要更新的节点信息保存在Effect List当中,在阶段二执行的时候,会批量更新相应的节点。

Fiber 大概是怎么实现的

链表、一次Fiber循环所有节点访问两次、requestIdleCallback

Fiber深度理解

需求

  1. 每一个状态的改变不是都需要马上反应在UI界面上

  2. 不同的状态改变应该有不同的优先级,如动画,输入等应该有更高的优先级

  3. 对于大量状态的改变复杂操作应该进行更好的调度,避免UI页面卡顿

virtual Dom数据结构改变

在react16以前,虚拟树的存在形式、渲染方式都是树状的。什么是树状的呢?就是从根节点开始,递归嵌套地渲染根节点的子节点。

如图,先渲染第一层的节点,再渲染第二层的第一个节点、第二层第一个节点的子节点……

重要的是,这个过程是一个不可打断的过程,只能等渲染完,才能去做别的事

react fiber实现之后,是用一个单链表的形式去渲染的

如图

渲染的顺序是

a1->b1->b2->c1->d1->d2->b3->c2

顺序并没有改变,但是构成结点之间相互联系的结构改变了。

原来的结构,只是单纯的父节点->子节点的关系

现在,变成了单纯的链式关系

从图中可以看出,a1跟b2并没有直接的关系(除了return,后面会说return),a1只是“通知”b1要进行渲染,由b1“通知”b2进行渲染。

b2及其子节点渲染完之后,再“通知”a1:“我渲染完了”

等b3及其子节点也渲染完之后,a1会变成“渲染完成”的状态,就完成了整棵树的渲染

Fiber结构

fiber树的整体结构是一个双向循环链表,这种结构能够更加快速的找到相对应的节点。

在Reconcile过程中为了能够知道之前节点的信息,需要将新的fiber节点与老fiber节点进行关联。

Fiber中会同时存在两种fiber tree,每次Reconcile的过程就是新fiber tree构建的过程,当commit之后新的fiber tree就变成了current fiber tree,如此循环往复。

Fiber Effect

在Reconcile的过程中,需要给节点设置状态,与旧节点相比需要达到的状态。每个fiber节点构建完成后(设置自己的effectTag状态),如果有effect则将自己以及其子孙元素放入父节点的effects中,这样层层构建,最终新的fiber tree的effects中存储的就是所有要处理的fiber node。然后进入到commit阶段,将所有的fiber node进行到dom的转换,进行UI页面的刷新。

Fiber 调度

Fiber既然是一个虚拟栈,那么就需要进行调度。我们可以利用该函数在浏览器空闲的时候来执行我们的代码,这样可以达到不阻塞页面渲染的目的。

Fiber 优先级

为了更好的用户体验,需要让优先级更高的任务优先执行,如动画,输入等。Fiber中分为五种优先级,每种优先级对应一个过期时间。

时间分片,通俗地说,就是将任务分成几种类型,具体为:

1.立即需要执行的任务

2.用户无操作期间需要执行的任务

3.正常任务

4.低优先级任务

5.浏览器空闲时才执行的任务

按照优先级执行,如果插入了新的任务,那么也按照优先级重新排序

这个模块是用两个es6的新API实现的,分别是window.requestAnimationFrame和window.requestIdleCallback。具体用法可以查MDN

每次循环,如果有过期的任务,那么无论如何要把过期的任务执行完毕,然后如果有剩余时间则按照到过期时间小的优先执行,以此类推。

Jsx的本质是什么

JSX是语法糖,无法直接被浏览器解析,需要转换为js 通过babel创建tagnode,createelement

// 自定义组件jsx代码

// 自定义组件jsx代码
return (
    <div>
        <Input addTitle={this.addTitle.bind(this)}/>
        <List data={this.state.list}/>
    </div>
);

// 解析结果
return React.creatElement(‘div’, null,
    React.createElement(Input, {addTitle: this.addTitle.bind(this)}),
    React.createElement(List, {data: this.state.list})
)

可以看到React.createElement这里传的第一个参数不是字符串形式的了,而是一个函数类型,其实是构造函数。

自定义组件的解析:

  • ‘div’ 直接渲染即可,vdom可以做到
  • Input和List,是自定义组件(class),vdom默认不认识
  • 因此Input和List定义的时候必须声明 render函数
  • 根据props初始化实例,然后执行实例的render函数
  • render函数返回的还是vnode对象 最后替换node 通过上面三个例子可以看到是使用了React.createElement()方法进行的jsx转换,这也是为什么要import React的原因。 熟悉vdom的人应该可以发现React.createElement这里的用法和传参都很像vdom里的h函数,那两者之间的关系是什么呢?
  • 我们理解React.createElement就是h函数,返回的都是vnode ** 那vdom里的patch函数又是何时调用的呢:**
  • 初次渲染 - ReactDOM.render(, container) :会触发patch(container, vnode)
  • re-render - setState:会触发patch(vnode, newVnode)
  • 为何需要vdom: JSX需要渲染成html, 数据驱动视图
  • React.createElement和h,都生成vnode
  • 何时patch: ReactDOM.render(…)和setState
  • 自定义组件的解析:初始化实例,然后执行render

如何做数据管理的 state是如何运用的(store 的设计)

范式化

多数情况下我们的应用是要配合 Redux 或者 MobX 使用的。拿 Redux 举例,Redux store 的组织是一门大学问,Redux 官方推荐将 store 组织得扁平化和范式化,所谓扁平化,就是整个 store 的嵌套关系不要太深,实体之下不再挂载实体,扁平化带来的好处是

当某些数据需要在不同的地方出现时,就会存在必然重复。例如,可能存在很多 state 部分都要存储同一份“用户评论列表”,这样需要花费很多心思去保障多处“用户评论列表”数据状态一致,否则就会造成页面数据不同步的 Bug;

嵌套深层的数据结构,会直接造成你 reducers 编写复杂。比如,你想更新一个很深层次的数据片段,很容易代码就变得丑陋。

造成负面的性能影响。 即便你使用了类似 immutable.js 这样的不可变数据类库,最大限度的想保障深层数据带来的性能压力,那你是否知道 immutable.js 采用的 “Persistent data structure” 思路,更新节点会造成同一条链儿上的祖先节点的更新。更恐怖的是,也许这些都会关联到众多 React 组件的 re-render;

范式化是指尽量去除数据的冗余,因为这样会给维护数据的一致性带来困难,就像官方推荐 state 记录尽可能少的数据,不应该存放计算得到的数据和 props 的副本,而是将他们直接在 render 中使用,这也是避免了维护数据一致性的困难,并且避免了相同数据满天飞不知道源头数据是哪个的尴尬。

state vs store

首先要明确,不要将所有的状态全部放在 store 中,其实再延伸一下可以延伸出 render(){} 中的变量,也就是 store vs state vs render,store 中应该存放异步获取的数据或者多个组件需要访问的数据等等,redux 官方文档中也有写什么数据应该放入 store 中。

  • 应用中的其他部分需要用到这部分数据吗?
  • 是否需要根据这部分原始数据创建衍生数据?
  • 这部分相同的数据是否用于驱动多个组件?
  • 你是否需要能够将数据恢复到某个特定的时间点(比如:在时间旅行调试的时候)?
  • 是否需要缓存数据?(比如:直接使用已经存在的数据,而不是重新请求)
    而 store 中不应该保存 UI 的状态(除非符合上面的某一点,比如回退时页面的滚动位置)。UI 的状态应该被限定在 UI 的 state 中,随着组件的卸载而销毁。而 state 也应该用最少的数据表示尽可能多的信息。在 render 函数中,根据 state 去衍生其他的信息而不是将这样冗余的信息都存在 state 中。store 和 state 都应该尽可能的做到熵最小,具体的可以看 redux store取代react state合理吗?。而 render 中的变量应该尽可以去承担一个衍生数据的责任,这个过程是无副作用的,可以减少在 state 中产生冗余数据的情况。

React的常见优化

1.重新渲染上/多组件优化:

存在维度的划分:层级;class和函数组件;节点多寡

节点少:class : scu-->purecomponent

节点少:函数:memo

节点多:immutable+scu/memo

那怎么讲:对于react的性能优化还是很有必要,不能因为父组件渲染而子组件没有变化也得跟着渲染从而产生不必要的花销。解决原理是通过props和state的浅对比来判断子组件是否渲染,具体操作是在class组件下通过生命周期的scu来判断,也可以通过继承purecomponent来减少每次重复写法;在函数组件下通过memo来判断;在大量节点的情况下就考虑用immutable配合了。

优化原理:通过对于props和state的浅对比来判断是否重新渲染

浅对比:对象的对比他们的内存地址,只要内存地址一致,就不重新渲染,反之,对象的内存地址不一致,就渲染

immutable:每次操作都会产生一个新的对象出来,由于它会复用之前数据的数据结构,所以产生新的数据也很快,ImmutableJS提供了不可变的数据,即要让数据改变只能通过创建新数据的方式,而不能直接修改,这很大程度的降低了前后两个数据比较时的复杂度。

2.传参优化: 切记将props/state以展开形式传递给子组件,除非子组件需要用到父组件的所有props/state。

3.Key:对于数组形式的数据,遍历时React会要求你为每一个数据值添加Key,而Key必须时独一无二的,在选取Key值时尽量不要用索引号,因为如果当数据的添加方式不是顺序添加,而是以其他方式(逆序,随机等),会导致每一次添加数据,每一个数据值的索引号都不一样,这就导致了Key的变化,而当Key变化时,React就会认为这与之前的数据值不相同,会多次执行渲染,会造成大量的性能浪费。所以只在万不得已时,才将数据的索引号当做Key。

4.使用React.Lazy和React.Suspense做分片打包,实现组件的按需加载,大大提高页面速度

5.调整CSS而不是强制组件加载和卸载:尽量的减少组件的创建和销毁,这样对于性能还是有一定的损耗的,我们可以将组件隐藏掉,比如加hidden属性,控制css的display、opacity、visibility等等隐式隐藏的方法。

请描述diff算法,key值是如何做比较的

  • MOVE_EXISTING -- 存在相同的节点则复用以前的 DOM 节点,做移动操作。
  • INSERT_MARKUP -- 新的节点不在旧集合里则插入新的节点。
  • REMOVE_NODE -- 新集合里在旧集合中对应的 node 不同,不能直接复用和更新,需要执行删除操作,或者旧集合中的节点不在新集合里的。
  • 遍历 newChildrens,基于 key 判断 newChild 是否在 oldChildrens 存在相同的节点. 1. 如果存在相同节点(prevChild === nextChild) 1. 判断原先节点的变化顺序(不考虑头部新插入的节点) 1. 节点的挂载顺序变大(从前往后),移动节点 2. 节点的挂载顺序变小(从后往前或不变),不做操作 2. 节点的 _mountIndex 变为新集合中的 index 2. 如果不存在相同节点, 1. 之前存在相同 2. 在上一个新集合中的节点后插入新节点
  • 遍历 oldChildrens,移除在新集合中不存在的节点

React的渲染机制

React 在内部维护了一套虚拟 DOM(VDOM),在内部维护着一颗 VDOM 树,这颗 VDOM 树映射到浏览器真实的 DOM 树,React 通过更新 VDOM 树来对真实 DOM 更新,VDOM 是 plain object 所以很明显操作 VDOM 的开销要比操作真实 DOM 快得多,再加上 React 内部的 reconciler(调节器,这个模块用于发起顶层组件或者子组件的挂载渲染重绘),React 会在 reconsilation (重新编译)之后最小化的进行 VDOM 的更新,再 patch (修复)到真实 DOM 上最终完成用户看得到的更新。

React的错误处理机制(比如一个组件报错了 ,我希望那个组件白屏 而不是阻塞)

JS的错误处理机制 除了try catch Promise.reject

Promise除了catch 还能怎么捕获报错

如果一个类似json.ParseInt报错 如果没有try catch怎么处理

错误边界介绍

部分 UI 中的 JavaScript 错误不应该破坏整个应用程序。 为了解决 React 用户的这个问题,React 16引入了一个 “错误边界(Error Boundaries)” 的新概念。

错误边界是 React 组件,它可以在子组件树的任何位置捕获 JavaScript 错误,记录这些错误,并显示一个备用 UI ,而不是使整个组件树崩溃。 错误边界(Error Boundaries) 在渲染,生命周期方法以及整个组件树下的构造函数中捕获错误。

使用方法

如果一个类组件定义了生命周期方法中的任何一个(或两个)static getDerivedStateFromError() 或 componentDidCatch(),那么它就成了一个错误边界。 使用static getDerivedStateFromError()在抛出错误后渲染回退UI。 使用 componentDidCatch() 来记录错误信息。

捕获范围

组件内异常,也就是异常边界组件能够捕获的异常,主要包括:

  1. 渲染过程中异常;
  2. 生命周期方法中的异常;
  3. 子组件树中各组件的constructor构造函数中异常。 不能捕获的异常,主要是异步及服务端触发异常:
  4. 事件处理器中的异常; 处理方法: 使用try/catch代码进行捕获
  5. 异步任务异常,如setTiemout,ajax请求异常等; 处理方法:使用全局事件window.addEventListener捕获
  6. 服务端渲染异常;
  7. 异常边界组件自身内的异常;
    处理方法:将边界组件和业务组件分离,各司其职,不能在边界组件中处理逻辑代码,也不能在业务组件中使用didcatch
    错误边界尽可以捕获其子组件的错误,无法捕获其自身的错误;如果一个错误边界无法渲染错误信息,则错误会向上冒泡至最接近的错误边界。这也类似于 JavaScript 中 catch {} 的工作机制
    如何放置错误边界
    错误边界的粒度完全取决于你的应用。你可以将其包装在最顶层的路由组件并为用户展示一个 “发生异常(Something went wrong)“的错误信息,就像服务端框架通常处理崩溃一样。你也可以将单独的插件包装在错误边界内部以保护应用不受该组件崩溃的影响。
    借鉴Facebook的message项目,他们应用错误边界的方式是将大的模块应用错误边界包裹,这样当一个主要模块因为意外的错误崩溃后,其它组件仍然能够正常交互
    错误边界实战
    首先我定义了一个高阶组件
import React from 'react'
const ErrorBoundary = errorInfo => WrapComponent => {
return class ErrorBoundary extends React.Component{
constructor(props) {
super(props);
this.state = { hasError: false };
}
// 这个静态方法和componentDidCatch方法定义一个即可
static getDerivedStateFromError(error) {
// 当发生错误时,设置hasError为true,然后展示自己的错误提示组件
return { hasError: true };
}
componentDidCatch(error, info) {
// 这里可以将报错信息上报给自己的服务
// logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
return {errorInfo}
;
}
return ;
}
}
}
export default ErrorBoundary

接下来可以使用边界组件包裹业务组件,这里列举我认为react项目中可以处理的错误方式,例如事件处理器的错误,异步错误,promise错误,渲染错误等

import React from 'react'
import ErrorBoundary from '../../utils/ErrorBoundary'
@ErrorBoundary('i am not ok')
export default class Error extends React.Component{
constructor() {
super()
}
componentWillMount() {
window.addEventListener('error', event => {
console.log(event)
}, true)
window.addEventListener('unhandledrejection', event => {
console.log(event)
})
}
// 这个异步错误 ErrorBoundary组件不会捕获到 但是在入口写的全局window.onerror事件捕获到了
componentDidMount() {
setTimeout(() => {
// console.log(b)
}, 100)
}
// 事件处理器中的错误 onerror也可以捕获到
// 这里如果想要hold住错误 需要使用try catch
handleEventError = () => {
console.log(error)
}
// promise 如果reject 但是没有写catch语句的话 会报错
// 但是onerror和try-catch和ErrorBoundary组件都无法捕获
// 需要写一个全局unhandledrejection 事件捕获
handlePromiseError = () => {
const promise = new Promise((resolve, reject) => {
reject()
})
promise.then()
}
render() {
return 
}
}

受控组件和非受控组件

受控以及非受控组件的边界划分取决于当前组件对于子组件值的变更是否拥有控制权。如若有则该子组件是当前组件的受控组件; 如若没有则该子组件是当前组件的非受控组件。

受控组件: 在HTML的表单元素中,它们通常自己维护一套state,并随着用户的输入自己进行UI上的更新,这种行为是不被我们程序所管控的。而如果将React里的state属性和表单元素的值建立依赖关系,再通过onChange事件与setState()结合更新state属性,就能达到控制用户输入过程中表单发生的操作。被React以这种方式控制取值的表单输入元素就叫做受控组件。

非受控组件:要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 DOM 节点中获取表单数据。

React Patch

React构建虚拟标签,执行组件的生命周期,更新state,计算diff等,这一系列的操作都是在virtualDOM中执行的,此时浏览器并未显示出更新的数据。React Patch实现了最后这关键的一步,将tree diff算法计算出来的差异队列更新到真实的DOM节点上,最终让浏览器能够渲染出更新的数据。

Patch主要是通过遍历差异队列实现的,遍历差异队列时,通过更新类型进行相应的插入、移动和移除等操作。

React并不是计算出一个差异就执行一次patch,而是计算出全部的差异并放入差异队列后,再一次性的去执行Patch方法完成真实的DOM更新。

React Reconciliation一致性比较

当你使用 React ,在任何一个单点时刻你可以认为 render() 函数的作用是创建 React 元素树。在下一个 state 或props 更新时,render() 函数将会返回一个不同的 React 元素树。接下来 React 将会找出如何高效地更新 UI 来匹配最近时刻的 React 元素树。

目前存在大量通用的方法能够以最少的操作步骤将一个树转化成另外一棵树。然而,这个算法是复杂度为O(n3),其中n 为树中元素的个数。

如果你在 React 中展示 1000 个元素,那么每次更新都需要10亿次的比较,这样的代价过于昂贵。然而,React 基于以下两个假设实现了时间复杂度为 O(n) 的算法:

  1. 不同类型的两个元素将会产生不同的树。
  2. 开发人员可以使用一个 key prop 来指示在不同的渲染中那个那些元素可以保持稳定。

处理过程:

当执行 setState() 或首次 render() 时,进入工作循环,循环体中处理的单元为 Fiber Node, 即是拆分任务的最小单位,从根节点开始,自顶向下逐节点构造 workInProgress tree(构建中的新 Fiber Tree)。

Fiber 之前架构卡顿的原因

React 中调用 render() 和 setState() 方法进行渲染和更新时,主要包含两个阶段:

调度阶段(Reconciler): Fiber 之前的 reconciler(被称为 Stack reconciler)是自顶向下的递归算法,遍历新数据生成新的Virtual DOM,通过 Diff 算法,找出需要更新的元素,放到更新队列中去。

渲染阶段(Renderer): 根据所在的渲染环境,遍历更新队列,调用渲染宿主环境的 API, 将对应元素更新渲染。在浏览器中,就是更新对应的DOM元素,除浏览器外,渲染环境还可以是 Native、WebGL 等等。

Fiber 之前的调度策略 Stack Reconciler,这个策略像函数调用栈一样,递归遍历所有的 Virtual DOM 节点,进行 Diff,一旦开始无法中断,要等整棵 Virtual DOM 树计算完成之后,才将任务出栈释放主线程。而浏览器中的渲染引擎是单线程的,除了网络操作,几乎所有的操作都在这个单线程中执行,此时如果主线程上用户交互、动画等周期性任务无法立即得到处理,影响体验。

React 事件绑定原理理解

react中的事件都是合成事件,不是把每一个dom的事件绑定在dom上,而是把事件统一绑定到document中,触发时通过事件冒泡到document进行触发合成事件,因为是合成事件,所以我们无法去使用e.stopPropagation去阻止,而是使用e.preventDefault去阻止。

1.事件注册:组件更新或者装载时,在给dom增加合成事件时,需要将增加的target传入到document进行判断,给document注册原生事件回调为dispatchEvent(统一的事件分发机制)。

2.事件存储:EventPluginHub负责管理React合成事件的callback,它将callback存储到listennerBank中,另外还存储了负责合成事件的Plugin,Event存储到listennerbank中,每一个元素在listennerBank中会有唯一的key。

3.事件触发执行:点击时冒泡到docunment中,触发注册原生事件的回调dispatchEvent,获取到触发这个事件的最深层元素,事件执行利用react的批处理机制。

案例

<div onClick={this.parentClick} ref={ref => this.parent = ref}>
      <div onClick={this.childClick} ref={ref => this.child = ref}>
          test
      </div>
</div>
点击test后
1.首先获取到this.child
2.遍历此元素的所有父元素,依次对每一级元素进行处理
3.构成合成事件
4.将每一级的合成事件存储在eventQueen事件队列中
5.遍历,是否组织冒泡,是则停止,否则继续
6.释放已经完成的事件

4.合成事件:循环所有类型的eventPlugin,对应每个事件类型,生成不同的事件池,如果是空,则生成新的,有则用之前的,根据唯一key获取到指定的回调函数,再返回带有参数的回调函数。

5.总流程:组件装载/更新 -- 新增/删除事件 -- eventplugin添加到ListennerBank中监听事件 -- 触发事件 -- 生成合成事件 -- 通过唯一key获取到指定函数 -- 执行指定回调函数 -- 执行完毕后释放

React Router懒加载

React利用 React.lazy 与 import() 实现了渲染时的动态加载 ,并利用 Suspense 来处理异步加载资源时页面应该如何显示的问题。

import()原理

function import(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
    script.type = "module";
    script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;
 
    script.onload = () => {
      resolve(window[tempGlobal]);
      delete window[tempGlobal];
      script.remove();
    };
 
    script.onerror = () => {
      reject(new Error("Failed to load module script with URL " + url));
      delete window[tempGlobal];
      script.remove();
    };
 
    document.documentElement.appendChild(script);
  });

React.lazy 原理

对于最初 React.lazy() 所返回的 LazyComponent 对象,其 _status 默认是 -1,所以 首次渲染 时,会进入 readLazyComponentType 函数中的 default 的逻辑,这里才会真正异步执行 import(url) 操作,由于并未等待,随后会检查模块是否 Resolved,如果已经Resolved了(已经加载完毕)则直接返回 moduleObject.default (动态加载的模块的默认导出),否则将通过 throw 将 thenable 抛出到上层。

React.Suspense原理

React 捕获到异常之后,会判断异常是不是一个 thenable,如果是则会找到 SuspenseComponent ,如果 thenable 处于 pending 状态,则会将其 children 都渲染成 fallback 的值,一旦 thenable 被 resolve 则 SuspenseComponent 的子组件会重新渲染一次。


企业微信

p9-juejin.byteimg.com/tos-cn-i-k3…