前端面试之React相关(未完待续、可能续更、也可能断更)

125 阅读8分钟

React篇

React是什么

React是一个前端UI框架,通过组件化的方式解决视图层开发复用的问题,本质是一个组件化框架。

通过数据来驱动视图层进行更改,组件化的存在可以使项目尽可能的达到低耦合、高内聚

而通过学习React之后,在web和移动端都可以进行开发

怎么去设计一个组件

考虑情况——什么时候去拆分组件

分两种:

  • 多个不同页面或同一页面下的相同逻辑,我们一般都会拆出来做公共组件
  • 单个页面,业务逻辑复杂,拆分组件已达成单组件单业务逻辑的目的,提高开发效率以及提高代码质量

如何去做

单一职责

最基本的就是单一组件单一职责,也就是上面提到的所谓相同逻辑。

像第二种情况,一般不用考虑太多东西,只要完成A组件处理A逻辑,B组件处理B逻辑。组件接收数据,处理完成后,把数据暴露给父组件(如果需要)。

而第一种情况下,你需要考虑:既然A页面和B页面都在使用同一个逻辑,那以后在需求迭代的过程中,是否还会存在C页面D页面也需要使用它。

复用性&可维护性

这种情况下,你就需要思考:你定义的这个组件,最终结果能否让新的页面拿来即用。

不然最后在需求迭代的过程中,你编写C页面的时候,为了复用旧组件,结果导致你对旧组件的修改为了适配C页面时,还不得不跑去A页面和B页面的代码进行修改。

那岂不是更浪费时间,甚至增加了代码风险。

或者说你之前写的组件已经很不错了,不会涉及到对原有逻辑页面的修改,仅需对组件内部做一点适配。那么此时你也需要思考:这种组件内部增加逻辑分支的事情,是否达到了一个组件不该承受的重量。

举个例子吧,以前我做过一个航空公司的电商官网:

比如一张机票是否允许退票、要验证同一订单的所有乘客状态、航班状态、是否携带儿童/婴儿。退儿童/婴儿机票时,还得判断是否负责航司对于乘机人数量及类型的规定(一个大人最多带三小孩)巴拉巴拉一大堆。

当时我还是个应届生boy,我当时去维护这段代码我的头都要爆炸了。没有任何注释、判断条件复杂、分支极其之多

所以当组件内部逻辑分支过多时,就要思考是否要给组件减重了,就算真的没啥办法去减重,也务必把注释写好。

再举个细一点的例子,也是日常开发中经常遇到的场景:

A页面和B页面共用组件C。但A页面并不需要组件C的数据作为回调,但B页面需要。而这个组件是A页面编写的时候抽离出来的,当时还没有B页面(但产品已经明确告知,这个地方可能以后会有其他地方也要用)。

那么这时候对组件C的设计,就必然要考虑一个getData的方法,去获取内部数据。

所谓复用性大概就是这种概念。

PS:如果是TS项目的话,可以通过类型规范来保障入参的可控性

吐槽一下

其实设计组件这件事,本身是一个比较吃经验和思路的事情。这个问题简单回答的话,其实就是几个词低耦合,单⼀职责,可复⽤性,可维护性。可是落实到代码中呢,初学者根本就是一头雾水。

我其实挺讨厌这种假大空的问题,但是有时候必须要应付这些东西。

应届生在进入工作的时候,编写代码尽可能的去思考一下,如何让自己的代码能够变得更加的美观、更加的合理高效。

React的虚拟dom和diffing算法

虚拟dom

因为⾸先说说为什么要使⽤Virturl DOM,因为操作真实DOM的耗费的性能代价太⾼,所以react内部使⽤js实现了⼀套dom结构,在每次操作在和真实dom之前,使⽤实现好的diff算法,对虚拟dom进⾏⽐较。这样对比下来,就会知道我们需要删除/新增/修改哪些节点,最后再去更新真实的dom结构。

diff算法

首先大概描述一下react定义的虚拟dom结构,他会有自己的dom类型、props、子节点。

  1. 首先对比节点类型是否相同;如果不相同,则删除该节点再插入新节点。
  2. 如果相同,那么开始对比前后的props是否有更新,有则记录更新;再去递归子节点结构,回到第1步。
  3. 当dom结构完全递归结束,代表diff结束

细节补充版:

  • 把树形结构按层级分解,只进行同层级比较
  • 给列表结构的每个元素添加唯一的key属性,方便比较
  • React只会进行同组件的比较,比如A组件在某次点击后被替换成了B组件,那么diff不会对组件内部在进行比较,哪怕这俩组件内部结构一致。

React Fiber

扩展提问方式

  • setState调用之后发生了什么?
  • 触发多次setState,会触发几次render?

React Fiber干了什么

众所周知,JS进程与GUI进程是互斥的。因为diff算法是同步的,而React操作真实Dom渲染的appendChildApi也是同步的。如果这时候存在大量节点等待渲染,就会导致JS线程占用时间过长,导致页面卡顿。

而React Fiber就是为了解决这个问题的。它会把时间进行分割处理,当某个碎片时间到了之后,会去任务列表寻找是否有新的、优先级更高的任务需要处理。

具体过程主要在两个步骤上:

  1. diff阶段 其实也就是diff算法作用的过程,而这个阶段是可以被打断的,也就是上面说到的时间切片化处理优先级更高的事件。
  2. 渲染阶段 把之前收集到的更新渲染到真实的dom上。

而在diff阶段时,有个需要注意的点是:

有个误区是总认为一次setState就会触发一次render(对应React Fiber一次渲染)。但真实情况是,React会收集触发时间较为集中的一堆setState,通过diff后。只通过一次render去渲染真实Dom。

这么干的好处是节省性能。举个例子:

五险一金计算器,你输入了工资之后,点击计算按钮,会得出若干个数字(state分开储存)。你肉眼见到的效果就是数字一个接一个的出现(这里是比较极端的),而使用了React Fiber之后,就应该是同时刷新出来。

例子不太合适,但意思应该都懂了,不懂我也当你懂了。

State和Props的区别

  • State是组件内部的状态,多数时候用于对用户事件行为的存储结果。
  • Props是父组件传递给子组件的配置,子组件通过对Props的获取来完成父组件交给他们的任务。

组件可以修改自身的state,但不可直接修改props。但可以通过props传递来的回调函数来修改父组件的对应数据,我不能解决问题,但我可以把提出问题的人解决掉

函数式组件与类组件的区别

React 函数式组件和类组件的区别,不是只有state和性能! - 掘金 (juejin.cn)

个人见解:

  • 在语义上,类组件的生命周期方法可读性更高;

  • 在使用上来说,增加了hooks的函数式组件,更加的方便一点。

React组件的性能优化

React.memo

用法:React.memo(Component, Func)

  • 第一个参数是需要性能优化的组件
  • 第二个参数是判断函数是否需要更新的函数。默认是对函数的props进行浅比较。

这是组件层级上的优化

useMemo && useCallback

// 例子2
function Child({title, onChangeTitle}){
  console.log('子组件渲染')
  return(
      <>
        <div>{title.value}</div>
        <button onClick={() => onChangeTitle('我是新的title')}>Button</button>
      </>
  )
}
const NewChild = React.memo(Child);

function Parent(){
    const [count, setCount] = useState(1);
    const [title, setTitle] = useState('我是子组件');
    const onChangeTitle = (text) => {
      dosomething()
      setTitle(text)
    }
    const memoTitle = { value: title }
    // 优化后
    // const onChangeTitle = useCallback((text) => {
    //   dosomething()
    //   setTitle(text) 
    // },[]) 
    // const memoTitle = useMemo(() => ({value: title}), [title])
    return(
        <div>
            <NewChild title={memoTitle} onChangeTitle={onChangeTitle}>
            <button onClick={() => setCount(count+1)}>add</button>
            <div>count:{count}</div>
        </div>
    )
}

这两个都是组件内部对变量和函数进行性能优化的

因为组件每次render的时候,不做特殊设定的时候,都会重新定义新的变量和函数。

而这两个hook就是用来只在期望的时候重新定义。

React设计模式

「React 进阶」 学好这些 React 设计模式,能让你的 React 项目飞起来🛫️ - 掘金 (juejin.cn)