前端面试:react相关(一小部分)

189 阅读13分钟

什么是函数式编程
一种编程规范,react和redux用的就是函数式编程的思想
常见的编程规范:

  • 命令式编程:关心的是解决问题的步骤,一步步的告诉浏览器应该做什么
  • 面向对象编程:封装、继承、多态
  • 事件驱动编程:事件的订阅和触发
  • 函数式编程: 一种编程规范,将要做的行为封装成一个函数
    函数是第一等公民,和其他数据类型一样可以用于赋值、传参、其他函数的返回值
    函数式编程的五大特性:
    1:纯函数:相同的输入肯定会有相同输出,完全独立且无副作用的函数
    无副作用是指:函数内部的操作不会对外部产生影响
// 是纯函数
    function add(x,y){
        return x + y
    }
    // 输出不确定,不是纯函数
    function random(x){
        return Math.random() * x
    }
    // 有副作用,不是纯函数
    function setColor(el,color){
        el.style.color = color ;
    }
    // 输出不确定、有副作用,不是纯函数
    var count = 0;
    function addCount(x){
        count+=x;
        return count;
    }

2:不可变性:数据一旦被创建则不能被修改,如果需要修改则必须复制出一个副本
目的:保证数据的稳定性。防止依赖的数据被不知的修改,导致异常

    1. 不同于const,const定义的数据它的属性仍然会被修改
    1. 类似于Object.freeze,数据冻结。但是freeze的数据深层属性有可能会被修改
    1. immutable.js JS中的数据不可变库
  • 3:高阶函数:将函数作为参数或返回值,也可能同时具备
    例如:Array.map、reduce、filter
  • 4:组合 将功能划分为小型可重复的纯函数,将这些小型可复用的纯函数组合成大的函数。
  • compose:接收的参数从后往前执行

JSX的原理:
由于react的核心思想是虚拟DOM,而虚拟DOM的原理是用JS对象模拟真实的DOM,比较两个虚拟DOM的差异,将两个虚拟DOM的差异应用到真是的DOM上去。
DOM元素的结构其实只有三个,标签、属性、子元素。如果直接用JS对象来描述所有HTML的话,代码很长且结构不是很清晰
ReactJS对原生JS的语法做了一下拓展。让JS语言支持这种在JS中写HTML代码的方式,这就是JSX

JSX是不能直接被浏览器解析的,所以需要babel这类的工具进行转译,将JSX转化成React.createElement的方法来调用,基于createElement将参数处理为JSX对象。再用render把JSX对象按照动态创建DOM的形式渲染到页面上

const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

PS:render接受三个参数
render(JSX,虚拟DOM最后被插入的DOM,callback<虚拟DOM插入页面后触发的回调函数,此时已经是真实的DOM>)

虚拟DOM的好处:
虚拟DOM速度快、diff算法很厉害、直接操作DOM消耗性能大

首次渲染
虚拟DOM的首次渲染过程很长,JSX解析,生成虚拟DOM树,通过各种计算最终调用DOM绘制视图, 很明显如果直接使用HTML可以直接创建元素,会更快,白屏时间也会更短。多了一层计算消耗和内存消耗,无疑是一种性能损耗

速度很快
假设我们现在的需求是改一个标题文案,虚拟DOM我们需要调用setState去更新。
而React并不知道我们state会改变的是什么,它需要逐层diff确定变动后再去更新。
所以相对而言我们直接去修改DOM的内容,省去diff会更直接。

所以到底为什么需要使用虚拟DOM呢?
我们的项目中,需求肯定不仅仅只是修改一个标题这么简单。所以当我们需要大量且分散的去更新页面中的元素时,虚拟DOM的优势就展示出来了。

  • 首先我们可以避免去大量的DOM操作,DOM操作其实就是一个在页面元素寻找DOM的过程,那这个过程也是需要从根元素去找到这个DOM,然后再去操作。这个过程代码不够健壮,而且如果是复杂的逻辑,很显然通过DOM操作来实现就会变得复杂
  • 其次虚拟DOM可以使我们的渲染和逻辑进行分开管理。且React的虚拟DOM有组件模式策略这个概念。将整个DOM树进行碎片化。并且可以通过shouldComponentUpdate去给不需要更新的组件添加不需要更新的标识,主动去打断更新流。还可以避免不必要的更新
  • 虚拟DOM是JS对象,可以完成跨平台
  • 虚拟DOM是对DOM的统一集中管理,更加的稳定、高效

diff算法
diff算法是什么?

  • diff算法是虚拟DOM的核心。当通过setState去修改数据时,会在内存中新建一棵完整的虚拟DOM树,通过比较新旧两棵树的差异,来针对性的更新。diff也就是一种可以比较出两个对象差异的算法。
  • diff除了虚拟DOM树的对比还可以用到其他的场景,比如我们经常使用的版本控制

diff算法的优化策略

同层对比策略
只做同层级的比对,忽略夸层级的元素移动
传统的diff算法需要两层循环,每两个节点之间进行对比。制定了同层级比对的策略之后,节点只需要跟同层级的节点进行对比
如果真的出现了跨层级元素的移动,会直接将旧元素删除,在新的位置上创建。

唯一标识策略
虽然有了同层比对策略已经提升了时间性能。但是如果是相同类型的两个元素对比时,仅仅通过type是无法判断位置的准确移动的。所以此时我们需要给每个元素添加一个唯一的标识,用于识别和区分节点。key的作用就是使diff算法更准确,避免性能损耗

组件模式策略
虚拟DOM的更新依赖于树遍历,找到需要更新的目标节点。假设我们只需要更新某个子节点,但是必须要从根节点开始逐层diff,对比两个完整的虚拟DOM树。这就很耗性能了。
但是如果将子节点拆分成一个独立的模块,便可以只调用这个独立模块的diff。这便是组件模式,将整个虚拟DOM树碎片化
jsx.png 如果我们需要同时更新A/D节点,可以给B1这个节点添加一个不需要更新的标识,避免不必要的更新造成性能损耗。这就是shouldComponentUpdate做的事情

diff的实现

  • 当新旧节点不同时,直接创建或删除
  • 当新旧节点相同时:
    • 首先:比对属性和事件
    • 其次:递归比对子级列表
    • 新旧节点都有子级列表,则进入子级列表对比
    • 新节点有,旧节点没有,则创建子级
    • 新节点没有,旧节点有,则删除

子级列表的对比时diff的难点,它的对比流程是:

  • 首先两端的四个节点开始两两对比
  • 如果均不匹配,开始key的对比
  • 如果key匹配的上,则移动并更新节点
  • 如果key匹配不上,则在对应的位置上新增节点
  • 全部对比完成之后,将剩余的节点删除或者新增

针对diff对比的一些优化方案

  1. props传递
    props的值是引用类型时,避免直接传递,而是使用引用传递。因为在对比的时候引用类型的数据即使对象属性完全一致也不相等的,因此diff的时候还是会逐层循环这个对象。造成性能浪费。当使用引用传递时,数据的修改不会修改源对象,应该遵循数据不可变原则,创建新的对象。这样就可以降低diff的性能损耗

  2. key的使用 通过使用key给每个节点添加一个唯一的标识。当列表需要同层移动时,能避免元素重新渲染,提高diff的性能 不同的key会使节点被判定为非同类节点。

  3. 组件化 组件化是将虚拟DOM树碎片化的一种手段,能够实现局部更新,这个过程可以减少触发diff的节点数量。 组件的创建比diff对比更为复杂,避免过度组件化,比如生命周期的执行、组件对比等。对于复用率低的静态元素直接使用元素节点更合适 对于不需要生命周期的组件可以写成无状态组件(取决于state)

React Fiber
问一:为什么会有React Fiber?
因为React的更新、加载组件的过程是同步的,这个过程中会做很多事情,比如调用生命周期函数、对比两个虚拟DOM、将虚拟DOM应用到真实DOM上,完成更新。这个过程是完全同步的,也就意味着,在这个过程中,浏览器的主线程是完全被React占用的。组件少的时候不会有什么问题,但是如果项目很庞大,组件很多的时候会出现问题。假设加载一个组件需要一毫秒,项目中200个组件就需要200ms,如果这个时候用户在页面输入了内容,浏览器是无暇去处理的。这就会造成页面的卡顿,在浏览器处理完React组件的更新和加载之后,页面会咔咔咔出现所有用户所输入的内容,很影响用户体验

React Fiber做的事就是将整个同步过程拆分,进行片段化的处理。虽然整体完成时间还是很长,但是每个片段完成之后会给其他任务一个执行的机会,片段执行完成后,如果有优先级更高的任务需要执行,那就会先执行优先级高的操作,如果没有再继续执行。维护每个片段中的数据结构这就是Fiber

问二:React Fiber出现会对原有的使用造成什么影响?
首先需要明确的是,Fiber的更新过程分为两个阶段:render阶段和commit阶段,第一阶段是去查找哪些个DOM需要更新,这个过程是允许被打断的。在一个任务执行片段完成后,会回去看看有没有优先级更高的任务需要执行。如果被优先级更高的任务打断执行,就需要再找时机执行,所以render这个阶段的生命周期会出现多次被调用的情况。而在commit阶段,会直接把DOM更新完,不会被打断。

render之前的生命周期函数:

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • commit阶段的生命周期函数
  • componentDidMount
  • componentDidUpdate
  • componentDidUnmount 新增的两个生命周期函数
  • getDerivedStateFromProps(nextState,prevState)
    是个纯函数,他的输出完全取决于输入,最终根据nextState和prevState计算出预期的状态改变,返回值会被送给setState
  • getSnapshotBeforeUpdate()render之后执行,会返回一个snapshot,作为DidUpdate的第三个参数

JSX的优点

  • JSX 执行更快,因为它在编译为 JavaScript 代码后进行了优化。
  • 它是类型安全的,在编译过程中就能发现错误。
  • 使用 JSX 编写模板更加简单快速。

React中的setState
setState作为React中管理state的重要方法,它有三个特征

  • setState不会立即修改React组件中的state
  • 在React中更新state中的数据,需要通过setState({key:value})来实现,通过this.state.key = xxx只能改变state中的数据的值,不会更新到页面上。
  • setState会引发一次组件的更新过程,来重绘视图
  • setState的调用会引发四个生命周期(shouldComponentUpdate、componentWillUpdate、render、componentDidUpdate),直到调用至render时,setState的数据才会被更新,如果shouldComponentUpdate返回的是false,则不会调用render函数。this.state也不会更新,直到下次render被触发时更新
  • 多次setState调用会产生合并的效果

React的四种组件模式

  • 类组件(容器组件)
    组件里会处理逻辑,比如事件点击,ajax请求,生命周期函数
  • 函数组件(UI组件,无状态组件)
    组件中的数据完全由外部的props来控制,只有render函数,没有生命周期
  • 高阶组件
    • 是一种函数,参数接受一个组件,返回一个新的组件
    • 拥有自己状态的函数,可以解决组件复用麻烦、class中this混乱、生命周期的逻辑复杂等问题
    • 提供了很多hook来解决不同的事情。比如useState、useEffect
  • 渲染属性
    使用一个值为函数的prop来传递需要动态渲染的组件

Redux的作用及原理
问一:为什么会有redux?
React是一个单向数据流,只能由父级去更新组件,没有数据向上回流的能力。如果项目数据很多,模块之间的交互很复杂,仅仅通过父子组件之前的状态流动是不能很好的达到这个效果,因此需要一个机制统一管理state。将state放到组件的最顶部,能够灵活地将state各取所需的分发给项目中的各个组件。这个机制就是Redux

Redux的简介:

  • Redux是react对所有数据进行统一管理的一个机制
  • Redux将项目中所有的数据存放到store中
  • 组件改变state的唯一方式就是通过调用store的dispatch()方法,派发一个action,这个action被对应的reducer进行处理,返回newState
  • 组件派发(dispatch)行为(action)给store,不是直接去通知组件
  • 组件通过订阅store的state来刷新自己的视图

Redux的核心API

  • createStore:创建一个store
  • store.dispatch:向store派发Action的内容
  • store.getStore:获取store中的数据
  • store.subscribe:store数据变动后自动自动更新

问二:Redux的原理和工作流是什么呢?

组件派发(dispatch)行为(action)给store,store将state和action分发给你reducer,reducer中进行数据的修改(setState),将nerState返回,各个组件通过订阅store中的state来更新自己的视图

React-Redux的工作流

  • React-redux的作用就是将store直接集成到React应用最顶层的props中,只要各个组件访问顶层的props即可

  • React-redux将组件分为容器组件和UI组件

    • 容器组件:组件里面会处理逻辑,比如点击事件,接口请求,改变state,除了render外还有其他的函数,如生命周期。类组件

    • UI组件:只负责显示和交互,内部不会处理逻辑,状态全由外部的props控制,只有render函数,没有生命周期。一般可以写成无状态组件(只是一个函数)

React-redux的两个核心 1、Provider
因为React-redux做的事情就是将store放到应用的最顶层,而Provider就是最顶层的那个组件,将store作为参数传入

<Provider store = {store}>
    		<App />
<Provider>

这个组件的作用就是让所有的组件都能访问到Redux中的数据

2、connect

  • connect(mapStateToProps, mapDispatchToProps)(MyComponent)
  • mapStateToProps:将state中的数据映射到props中去,让数据通过this.props.key就能访问的到,完成渲染
  • mapDispatchToProps:把各种dispatch也变成props,通过this.props.fn即可访问的到。

Redux-Thunk/Redux-sage的用处,作用于哪个部分

在action之后,在reducer处理数据之前,很多时候需要进行一些异步操作,比如ajax发送并拿到数据之后再进入reducer
作用在action和store中间