写点什么

最近几周 react 面试遇到的题总结

作者:beifeng1996
  • 2022 年 9 月 25 日
    浙江
  • 本文字数:13986 字

    阅读完需:约 46 分钟

Redux 和 Vuex 有什么区别,它们的共同思想

(1)Redux 和 Vuex 区别


  • Vuex 改进了 Redux 中的 Action 和 Reducer 函数,以 mutations 变化函数取代 Reducer,无需 switch,只需在对应的 mutation 函数里改变 state 值即可

  • Vuex 由于 Vue 自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的 State 即可

  • Vuex 数据流的顺序是∶View 调用 store.commit 提交对应的请求到 Store 中对应的 mutation 函数->store 改变(vue 检测到数据变化自动渲染)


通俗点理解就是,vuex 弱化 dispatch,通过 commit 进行 store 状态的一次更变;取消了 action 概念,不必传入特定的 action 形式进行指定变更;弱化 reducer,基于 commit 参数直接对数据进行转变,使得框架更加简易;


(2)共同思想


  • 单—的数据源

  • 变化可以预测


本质上∶ redux 与 vuex 都是对 mvvm 思想的服务,将数据从视图中抽离的一种方案。

React 中的 setState 和 replaceState 的区别是什么?

(1)setState() setState()用于设置状态对象,其语法如下:


setState(object nextState[, function callback])
复制代码


  • nextState,将要设置的新状态,该状态会和当前的 state 合并

  • callback,可选参数,回调函数。该函数会在 setState 设置成功,且组件重新渲染后调用。


合并 nextState 和当前 state,并重新渲染组件。setState 是 React 事件处理函数中和请求回调函数中触发 UI 更新的主要方法。


(2)replaceState() replaceState()方法与 setState()类似,但是方法只会保留 nextState 中状态,原 state 不在 nextState 中的状态都会被删除。其语法如下:


replaceState(object nextState[, function callback])
复制代码


  • nextState,将要设置的新状态,该状态会替换当前的 state。

  • callback,可选参数,回调函数。该函数会在 replaceState 设置成功,且组件重新渲染后调用。


总结: setState 是修改其中的部分状态,相当于 Object.assign,只是覆盖,不会减少原来的状态。而 replaceState 是完全替换原来的状态,相当于赋值,将原来的 state 替换为另一个对象,如果新状态属性减少,那么 state 中就没有这个状态了。


前端react面试题详细解答

React 中怎么检验 props?验证 props 的目的是什么?

React 为我们提供了 PropTypes 以供验证使用。当我们向 Props 传入的数据无效(向 Props 传入的数据类型和验证的数据类型不符)就会在控制台发出警告信息。它可以避免随着应用越来越复杂从而出现的问题。并且,它还可以让程序变得更易读。


import PropTypes from 'prop-types';
class Greeting extends React.Component { render() { return ( <h1>Hello, {this.props.name}</h1> ); }}
Greeting.propTypes = { name: PropTypes.string};
复制代码


当然,如果项目汇中使用了 TypeScript,那么就可以不用 PropTypes 来校验,而使用 TypeScript 定义接口来校验 props。

React 中的高阶组件运用了什么设计模式?

使用了装饰模式,高阶组件的运用:


function withWindowWidth(BaseComponent) {  class DerivedClass extends React.Component {    state = {      windowWidth: window.innerWidth,    }    onResize = () => {      this.setState({        windowWidth: window.innerWidth,      })    }    componentDidMount() {      window.addEventListener('resize', this.onResize)    }    componentWillUnmount() {      window.removeEventListener('resize', this.onResize);    }    render() {      return <BaseComponent {...this.props} {...this.state}/>    }  }  return DerivedClass;}const MyComponent = (props) => {  return <div>Window width is: {props.windowWidth}</div>};export default withWindowWidth(MyComponent);
复制代码


装饰模式的特点是不需要改变 被装饰对象 本身,而只是在外面套一个外壳接口。JavaScript 目前已经有了原生装饰器的提案,其用法如下:


@testable   class MyTestableClass {}
复制代码

React 高阶组件、Render props、hooks 有什么区别,为什么要不断迭代

这三者是目前 react 解决代码复用的主要方式:


  • 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。

  • render props 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术,更具体的说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。

  • 通常,render props 和高阶组件只渲染一个子节点。让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderltem 属性,或是一个可见的容器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。


(1)HOC 官方解释∶


高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。


简言之,HOC 是一种组件的设计模式,HOC 接受一个组件和额外的参数(如果需要),返回一个新的组件。HOC 是纯函数,没有副作用。


// hoc的定义function withSubscription(WrappedComponent, selectData) {  return class extends React.Component {    constructor(props) {      super(props);      this.state = {        data: selectData(DataSource, props)      };    }    // 一些通用的逻辑处理    render() {      // ... 并使用新数据渲染被包装的组件!      return <WrappedComponent data={this.state.data} {...this.props} />;    }  };
// 使用const BlogPostWithSubscription = withSubscription(BlogPost, (DataSource, props) => DataSource.getBlogPost(props.id));
复制代码


HOC 的优缺点∶


  • 优点∶ 逻辑服用、不影响被包裹组件的内部逻辑。

  • 缺点∶ hoc 传递给被包裹组件的 props 容易和被包裹后的组件重名,进而被覆盖


(2)Render props 官方解释∶


"render prop"是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术


具有 render prop 的组件接受一个返回 React 元素的函数,将 render 的渲染逻辑注入到组件内部。在这里,"render"的命名可以是任何其他有效的标识符。


// DataProvider组件内部的渲染逻辑如下class DataProvider extends React.Components {     state = {    name: 'Tom'  }
render() { return ( <div> <p>共享数据组件自己内部的渲染逻辑</p> { this.props.render(this.state) } </div> ); }}
// 调用方式<DataProvider render={data => ( <h1>Hello {data.name}</h1>)}/>
复制代码


由此可以看到,render props 的优缺点也很明显∶


  • 优点:数据共享、代码复用,将组件内的 state 作为 props 传递给调用者,将渲染逻辑交给调用者。

  • 缺点:无法在 return 语句外访问数据、嵌套写法不够优雅


(3)Hooks 官方解释∶


Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。通过自定义 hook,可以复用代码逻辑。


// 自定义一个获取订阅数据的hookfunction useSubscription() {  const data = DataSource.getComments();  return [data];}// function CommentList(props) {  const {data} = props;  const [subData] = useSubscription();    ...}// 使用<CommentList data='hello' />
复制代码


以上可以看出,hook 解决了 hoc 的 prop 覆盖的问题,同时使用的方式解决了 render props 的嵌套地狱的问题。hook 的优点如下∶


  • 使用直观;

  • 解决 hoc 的 prop 重名问题;

  • 解决 render props 因共享数据 而出现嵌套地狱的问题;

  • 能在 return 之外使用数据的问题。


需要注意的是:hook 只能在组件顶层使用,不可在分支语句中使用。、

React 组件的 state 和 props 有什么区别?

(1)props


props 是一个从外部传进组件的参数,主要作为就是从父组件向子组件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新的 props 来重新渲染子组件,否则子组件的 props 以及展现形式不会改变。


(2)state


state 的主要作用是用于组件保存、控制以及修改自己的状态,它只能在 constructor 中初始化,它算是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的 this.setState 来修改,修改 state 属性会导致组件的重新渲染。


(3)区别


  • props 是传递给组件的(类似于函数的形参),而 state 是在组件内被组件自己管理的(类似于在一个函数内声明的变量)。

  • props 是不可修改的,所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

  • state 是在组件中创建的,一般在 constructor 中初始化 state。state 是多变的、可以修改,每次 setState 都异步更新的。

什么是控制组件?

在 HTML 中,表单元素如 <input><textarea><select>通常维护自己的状态,并根据用户输入进行更新。当用户提交表单时,来自上述元素的值将随表单一起发送。而 React 的工作方式则不同。包含表单的组件将跟踪其状态中的输入值,并在每次回调函数(例如onChange)触发时重新渲染组件,因为状态被更新。以这种方式由 React 控制其值的输入表单元素称为受控组件

redux 的三大原则

  • 单一数据源

  • 整个应用的 state 被存储在一个 object tree 中,并且这个 object tree 之存在唯一一个 store 中

  • state 是只读的

  • 唯一改变 state 的方式是触发 action,action 是一个用于描述已经发生时间的对象,这个保证了视图和网络请求都不能直接修改 state,相反他们只能表达想要修改的意图

  • 使用纯函数来执行修改 state 为了描述 action 如何改变 state tree 需要编写 reduce

类组件与函数组件有什么异同?

相同点: 组件是 React 可复用的最小代码片段,它们会返回要在页面中渲染的 React 元素。也正因为组件是 React 的最小编码单位,所以无论是函数组件还是类组件,在使用方式和最终呈现效果上都是完全一致的。


我们甚至可以将一个类组件改写成函数组件,或者把函数组件改写成一个类组件(虽然并不推荐这种重构行为)。从使用者的角度而言,很难从使用体验上区分两者,而且在现代浏览器中,闭包和类的性能只在极端场景下才会有明显的差别。所以,基本可认为两者作为组件是完全一致的。


不同点:


  • 它们在开发时的心智模型上却存在巨大的差异。类组件是基于面向对象编程的,它主打的是继承、生命周期等核心概念;而函数组件内核是函数式编程,主打的是 immutable、没有副作用、引用透明等特点。

  • 之前,在使用场景上,如果存在需要使用生命周期的组件,那么主推类组件;设计模式上,如果需要使用继承,那么主推类组件。但现在由于 React Hooks 的推出,生命周期概念的淡出,函数组件可以完全取代类组件。其次继承并不是组件最佳的设计模式,官方更推崇“组合优于继承”的设计概念,所以类组件在这方面的优势也在淡出。

  • 性能优化上,类组件主要依靠 shouldComponentUpdate 阻断渲染来提升性能,而函数组件依靠 React.memo 缓存渲染结果来提升性能。

  • 从上手程度而言,类组件更容易上手,从未来趋势上看,由于 React Hooks 的推出,函数组件成了社区未来主推的方案。

  • 类组件在未来时间切片与并发模式中,由于生命周期带来的复杂度,并不易于优化。而函数组件本身轻量简单,且在 Hooks 的基础上提供了比原先更细粒度的逻辑组织与复用,更能适应 React 的未来发展。

react-redux 的实现原理?

通过 redux 和 react context 配合使用,并借助高阶函数,实现了 react-redux

在 React 中组件的 props 改变时更新组件的有哪些方法?

在一个组件传入的 props 更新时重新渲染该组件常用的方法是在componentWillReceiveProps中将新的 props 更新到组件的 state 中(这种 state 被成为派生状态(Derived State)),从而实现重新渲染。React 16.3 中还引入了一个新的钩子函数getDerivedStateFromProps来专门实现这一需求。


(1)componentWillReceiveProps(已废弃)


在 react 的 componentWillReceiveProps(nextProps)生命周期中,可以在子组件的 render 函数执行前,通过 this.props 获取旧的属性,通过 nextProps 获取新的 props,对比两次 props 是否相同,从而更新子组件自己的 state。


这样的好处是,可以将数据请求放在这里进行执行,需要传的参数则从 componentWillReceiveProps(nextProps)中获取。而不必将所有的请求都放在父组件中。于是该请求只会在该组件渲染时才会发出,从而减轻请求负担。


(2)getDerivedStateFromProps(16.3 引入)


这个生命周期函数是为了替代componentWillReceiveProps存在的,所以在需要使用componentWillReceiveProps时,就可以考虑使用getDerivedStateFromProps来进行替代。


两者的参数是不相同的,而getDerivedStateFromProps是一个静态函数,也就是这个函数不能通过 this 访问到 class 的属性,也并不推荐直接访问属性。而是应该通过参数提供的 nextProps 以及 prevState 来进行判断,根据新传入的 props 来映射到 state。


需要注意的是,如果 props 传入的内容不需要影响到你的 state,那么就需要返回一个 null,这个返回值是必须的,所以尽量将其写到函数的末尾:


static getDerivedStateFromProps(nextProps, prevState) {    const {type} = nextProps;    // 当传入的type发生变化的时候,更新state    if (type !== prevState.type) {        return {            type,        };    }    // 否则,对于state不进行任何操作    return null;}
复制代码

React.Component 和 React.PureComponent 的区别

PureComponent 表示一个纯组件,可以用来优化 React 程序,减少 render 函数执行的次数,从而提高组件的性能。


在 React 中,当 prop 或者 state 发生变化时,可以通过在 shouldComponentUpdate 生命周期函数中执行 return false 来阻止页面的更新,从而减少不必要的 render 执行。React.PureComponent 会自动执行 shouldComponentUpdate。


不过,pureComponent 中的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候 render 是不会执行的。如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent 一般会用在一些纯展示组件上。


使用 pureComponent 的好处:当组件更新时,如果组件的 props 或者 state 都没有改变,render 函数就不会触发。省去虚拟 DOM 的生成和对比过程,达到提升性能的目的。这是因为 react 自动做了一层浅比较。

react-router4 的核心

  • 路由变成了组件

  • 分散到各个页面,不需要配置 比如<link> <route></route>

React 有哪些优化性能的手段

类组件中的优化手段


  • 使用纯组件 PureComponent 作为基类。

  • 使用 React.memo 高阶函数包装组件。

  • 使用 shouldComponentUpdate 生命周期函数来自定义渲染逻辑。


方法组件中的优化手段


  • 使用 useMemo

  • 使用 useCallBack


其他方式


  • 在列表需要频繁变动时,使用唯一 id 作为 key,而不是数组下标。

  • 必要时通过改变 CSS 样式隐藏显示组件,而不是通过条件判断显示隐藏组件。

  • 使用 Suspense 和 lazy 进行懒加载,例如:


import React, { lazy, Suspense } from "react";
export default class CallingLazyComponents extends React.Component { render() { var ComponentToLazyLoad = null;
if (this.props.name == "Mayank") { ComponentToLazyLoad = lazy(() => import("./mayankComponent")); } else if (this.props.name == "Anshul") { ComponentToLazyLoad = lazy(() => import("./anshulComponent")); }
return ( <div> <h1>This is the Base User: {this.state.name}</h1> <Suspense fallback={<div>Loading...</div>}> <ComponentToLazyLoad /> </Suspense> </div> ) }}
复制代码

在 React 中如何避免不必要的 render?

React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。大多数情况下,React 对 DOM 的渲染效率足以业务日常。但在个别复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,其很重要的一个方向,就是避免不必要的渲染(Render)。这里提下优化的点:


  • shouldComponentUpdate 和 PureComponent


在 React 类组件中,可以利用 shouldComponentUpdate 或者 PureComponent 来减少因父组件更新而触发子组件的 render,从而达到目的。shouldComponentUpdate 来决定是否组件是否重新渲染,如果不希望组件重新渲染,返回 false 即可。


  • 利用高阶组件


在函数组件中,并没有 shouldComponentUpdate 这个生命周期,可以利用高阶组件,封装一个类似 PureComponet 的功能


  • 使用 React.memo


React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo 只能用于函数组件。

对 React 实现原理的理解

简版

  • reactvue 都是基于 vdom 的前端框架,之所以用 vdom 是因为可以精准的对比关心的属性,而且还可以跨平台渲染

  • 但是开发不会直接写 vdom,而是通过 jsx 这种接近 html 语法的 DSL,编译产生 render function,执行后产生 vdom

  • vdom 的渲染就是根据不同的类型来用不同的 dom api 来操作 dom

  • 渲染组件的时候,如果是函数组件,就执行它拿到 vdomclass 组件就创建实例然后调用 render 方法拿到 vdomvue 的那种 option 对象的话,就调用 render 方法拿到 vdom

  • 组件本质上就是对一段 vdom 产生逻辑的封装,函数classoption 对象甚至其他形式都可以

  • reactvue 最大的区别在状态管理方式上,vue 是通过响应式,react 是通过 setStateapi。我觉得这个是最大的区别,因为它导致了后面 react 架构的变更

  • reactsetState 的方式,导致它并不知道哪些组件变了,需要渲染整个 vdom 才行。但是这样计算量又会比较大,会阻塞渲染,导致动画卡顿。所以 react 后来改造成了 fiber 架构,目标是可打断的计算

  • 为了这个目标,不能变对比变更新 dom 了,所以把渲染分为了 rendercommit 两个阶段,render 阶段通过 schedule 调度来进行 reconcile,也就是找到变化的部分,创建 dom,打上增删改的 tag,等全部计算完之后,commit 阶段一次性更新到 dom

  • 打断之后要找到父节点、兄弟节点,所以 vdom 也被改造成了 fiber 的数据结构,有了 parentsibling 的信息

  • 所以 fiber 既指这种链表的数据结构,又指这个 rendercommit 的流程

  • reconcile 阶段每次处理一个 fiber 节点,处理前会判断下 shouldYield,如果有更高优先级的任务,那就先执行别的

  • commit 阶段不用再次遍历 fiber 树,为了优化,react 把有 effectTagfiber 都放到了 effectList 队列中,遍历更新即可

  • dom操作前,会异步调用 useEffect 的回调函数,异步是因为不能阻塞渲染

  • dom 操作之后,会同步调用 useLayoutEffect 的回调函数,并且更新 ref

  • 所以,commit 阶段又分成了 before mutationmutationlayout 这三个小阶段,就对应上面说的那三部分


理解了 vdomjsx组件本质fiberrender(reconcile + schedule) + commit(before mutation、mutation、layout)的渲染流程,就算是对 react 原理有一个比较深的理解


下面展开分析

vdom

为什么 reactvue 都要基于 vdom 呢?直接操作真实 dom 不行么?


考虑下这样的场景:


  • 渲染就是用 dom api 对真实 dom 做增删改,如果已经渲染了一个 dom,后来要更新,那就要遍历它所有的属性,重新设置,比如 idclasNameonclick 等。

  • dom 的属性是很多的:



  • 有很多属性根本用不到,但在更新时却要跟着重新设置一遍。

  • 能不能只对比我们关心的属性呢?

  • 把这些单独摘出来用 JS 对象表示不就行了?

  • 这就是为什么要有 vdom,是它的第一个好处。

  • 而且有了 vdom 之后,就没有和 dom 强绑定了,可以渲染到别的平台,比如 nativecanvas 等等。

  • 这是 vdom 的第二个好处。

  • 我们知道了 vdom 就是用 JS 对象表示最终渲染的 dom 的,比如:


{  type: 'div',  props: {    id: 'aaa',    className: ['bbb', 'ccc'],    onClick: function() {}  },  children: []}
复制代码


然后用渲染器把它渲染出来,但是要让开发去写这样的 vdom 么?那肯定不行,这样太麻烦了,大家熟悉的是 html 那种方式,所以我们要引入编译的手段

dsl 的编译

  • dsldomain specific language,领域特定语言的意思,htmlcss 都是 web 领域的 dsl

  • 直接写 vdom 太麻烦了,所以前端框架都会设计一套 dsl,然后编译成 render function,执行后产生 vdom

  • vuereact 都是这样



这套 dsl 怎么设计呢?前端领域大家熟悉的描述 dom 的方式是 html,最好的方式自然是也设计成那样。所以 vuetemplatereactjsx 就都是这么设计的。vuetemplate compiler 是自己实现的,而 reactjsx 的编译器是 babel 实现的,是两个团队合作的结果。


编译成 render function 后再执行就是我们需要的 vdom。接下来渲染器把它渲染出来就行了。那渲染器怎么渲染 vdom 的呢?

渲染 vdom

渲染 vdom 也就是通过 dom api 增删改 dom。比如一个 div,那就要 document.createElement 创建元素,然后 setAttribute 设置属性,addEventListener 设置事件监听器。如果是文本,那就要 document.createTextNode 来创建。所以说根据 vdom 类型的不同,写个 if else,分别做不同的处理就行了。没错,不管 vue 还是 react,渲染器里这段 if else 是少不了的:


switch (vdom.tag) {  case HostComponent:    // 创建或更新 dom  case HostText:    // 创建或更新 dom  case FunctionComponent:     // 创建或更新 dom  case ClassComponent:     // 创建或更新 dom}
复制代码


react 里是通过 tag 来区分 vdom 类型的,比如 HostComponent 就是元素,HostText 就是文本,FunctionComponentClassComponent 就分别是函数组件和类组件。那么问题来了,组件怎么渲染呢?这就涉及到组件的原理了:

组件

我们的目标是通过 vdom 描述界面,在 react 里会使用 jsx。这样的 jsx 有的时候是基于 state 来动态生成的。如何把 statejsx 关联起来呢?封装成 functionclass 或者 option对象的形式。然后在渲染的时候执行它们拿到 vdom就行了。


这就是组件的实现原理:


switch (vdom.tag) {  case FunctionComponent:        const childVdom = vdom.type(props);
render(childVdom); //... case ClassComponent: const instance = new vdom.type(props); const childVdom = instance.render();
render(childVdom); //...}
复制代码


如果是函数组件,那就传入 props 执行它,拿到 vdom 之后再递归渲染。如果是 class 组件,那就创建它的实例对象,调用 render 方法拿到 vdom,然后递归渲染。所以,大家猜到 vueoption 对象的组件描述方式怎么渲染了么?


{    data: {},    props: {}    render(h) {        return h('div', {}, '');    }}
复制代码


没错,就是执行下 render 方法就行:


const childVdom = option.render();
render(childVdom);
复制代码


大家可能平时会写单文件组件 sfc的形式,那个会有专门的编译器,把 template 编译成 render function,然后挂到 option 对象的render` 方法上



所以组件本质上只是对产生 vdom 的逻辑的封装,函数的形式、option 对象的形式、class 的形式都可以。就像 vue3 也有了函数组件一样,组件的形式并不重要。基于 vdom 的前端框架渲染流程都差不多,vue 和 react 很多方面是一样的。但是管理状态的方式不一样,vue 有响应式,而 react 则是 setStateapi 的方式。真说起来,vue 和 react 最大的区别就是状态管理方式的区别,因为这个区别导致了后面架构演变方向的不同。

状态管理

react 是通过 setStateapi 触发状态更新的,更新以后就重新渲染整个 vdom。而 vue 是通过对状态做代理,get 的时候收集以来,然后修改状态的时候就可以触发对应组件的 render 了。


有的同学可能会问,为什么 react 不直接渲染对应组件呢?


想象一下这个场景:


父组件把它的 setState 函数传递给子组件,子组件调用了它。这时候更新是子组件触发的,但是要渲染的就只有那个组件么?明显不是,还有它的父组件。同理,某个组件更新实际上可能触发任意位置的其他组件更新的。所以必须重新渲染整个 vdom 才行。


vue 为啥可以做到精准的更新变化的组件呢?因为响应式的代理呀,不管是子组件、父组件、还是其他位置的组件,只要用到了对应的状态,那就会被作为依赖收集起来,状态变化的时候就可以触发它们的 render,不管是组件是在哪里的。这就是为什么 react 需要重新渲染整个 vdom,而 vue 不用。这个问题也导致了后来两者架构上逐渐有了差异。

react 架构的演变

  • react15 的时候,和 vue 的渲染流程还是很像的,都是递归渲染 vdom,增删改 dom 就行。但是因为状态管理方式的差异逐渐导致了架构的差异。

  • reactsetState 会渲染整个 vdom,而一个应用的所有 vdom 可能是很庞大的,计算量就可能很大。浏览器里 js 计算时间太长是会阻塞渲染的,会占用每一帧的动画、重绘重排的时间,这样动画就会卡顿。作为一个有追求的前端框架,动画卡顿肯定是不行的。但是因为 setState 的方式只能渲染整个 vdom,所以计算量大是不可避免的。那能不能把计算量拆分一下,每一帧计算一部分,不要阻塞动画的渲染呢?顺着这个思路,react 就改造为了 fiber 架构。

fiber 架构

优化的目标是打断计算,分多次进行,但现在递归的渲染是不能打断的,有两个方面的原因导致的:


  • 渲染的时候直接就操作了 dom 了,这时候打断了,那已经更新到 dom 的那部分怎么办?

  • 现在是直接渲染的 vdom,而 vdom 里只有 children 的信息,如果打断了,怎么找到它的父节点呢?


第一个问题的解决还是容易想到的:


  • 渲染的时候不要直接更新到 dom 了,只找到变化的部分,打个增删改的标记,创建好 dom,等全部计算完了一次性更新到 dom 就好了。

  • 所以 react 把渲染流程分为了两部分: rendercommit

  • render 阶段会找到 vdom 中变化的部分,创建 dom,打上增删改的标记,这个叫做 reconcile,调和。

  • reconcile 是可以打断的,由 schedule 调度。

  • 之后全部计算完了,就一次性更新到 dom,叫做 commit

  • 这样,react 就把之前的和 vue 很像的递归渲染,改造成了 render(reconcile + schdule) + commit 两个阶段的渲染。

  • 从此以后,reactvue 架构上的差异才大了起来。


第二个问题,如何打断以后还能找到父节点、其他兄弟节点呢?


现有的 vdom 是不行的,需要再记录下 parentsilbing 的信息。所以 react 创造了 fiber 的数据结构。



  • 除了 children 信息外,额外多了 siblingreturn,分别记录着兄弟节点、父节点的信息。

  • 这个数据结构也叫做 fiber。(fiber 既是一种数据结构,也代表 render + commit 的渲染流程) react 会先把 vdom 转换成 fiber,再去进行 reconcile,这样就是可打断的了。

  • 为什么这样就可以打断了呢?因为现在不再是递归,而是循环了:


function workLoop() {  while (wip) {    performUnitOfWork();  }
if (!wip && wipRoot) { commitRoot(); }}
复制代码


  • react 里有一个 workLoop 循环,每次循环做一个 fiberreconcile,当前处理的 fiber 会放在 workInProgress 这个全局变量上。

  • 当循环完了,也就是 wip 为空了,那就执行 commit 阶段,把 reconcile 的结果更新到 dom

  • 每个 fiberreconcile 是根据类型来做的不同处理。当处理完了当前 fiber 节点,就把 wip 指向 siblingreturn 来切到下个 fiber 节点。:


function performUnitOfWork() {  const { tag } = wip;
switch (tag) { case HostComponent: updateHostComponent(wip); break;
case FunctionComponent: updateFunctionComponent(wip); break;
case ClassComponent: updateClassComponent(wip); break; case Fragment: updateFragmentComponent(wip); break; case HostText: updateHostTextComponent(wip); break; default: break; }
if (wip.child) { wip = wip.child; return; }
let next = wip;
while (next) { if (next.sibling) { wip = next.sibling; return; } next = next.return; }
wip = null;}
复制代码


函数组件和 class 组件的 reconcile和之前讲的一样,就是调用 render 拿到 vdom,然后继续处理渲染出的 vdom


function updateClassComponent(wip) {  const { type, props } = wip;  const instance = new type(props);  const children = instance.render();
reconcileChildren(wip, children);}
function updateFunctionComponent(wip) { renderWithHooks(wip);
const { type, props } = wip;
const children = type(props); reconcileChildren(wip, children);}
复制代码


  • 循环执行 reconcile,那每次处理之前判断一下是不是有更高优先级的任务,就能实现打断了。

  • 所以我们在每次处理 fiber 节点的 reconcile 之前,都先调用下 shouldYield 方法:


function workLoop() {  while (wip && shouldYield()) {    performUnitOfWork();  }
if (!wip && wipRoot) { commitRoot(); }}
复制代码


  • shouldYiled 方法就是判断待处理的任务队列有没有优先级更高的任务,有的话就先处理那边的 fiber,这边的先暂停一下。

  • 这就是 fiber 架构的 reconcile 可以打断的原理。通过 fiber 的数据结构,加上循环处理前每次判断下是否打断来实现的。

  • 聊完了 render 阶段(reconcile + schedule),接下来就进入 commit 阶段了。

  • 前面说过,为了变为可打断的,reconcile 阶段并不会真正操作 dom,只会创建 dom 然后打个 effectTag 的增删改标记。

  • commit 阶段就根据标记来更新 dom 就可以了。

  • 但是 commit 阶段要再遍历一次 fiber 来查找有 effectTag 的节点,更新 dom么?

  • 这样当然没问题,但没必要。完全可以在 reconcile 的时候把有 effectTag 的节点收集到一个队列里,然后 commit 阶段直接遍历这个队列就行了。

  • 这个队列叫做 effectList

  • react 会在 commit 阶段遍历 effectList,根据 effectTag 来增删改 dom

  • dom 创建前后就是 useEffectuseLayoutEffect 还有一些函数组件的生命周期函数执行的时候。

  • useEffect 被设计成了在 dom 操作前异步调用,useLayoutEffect 是在 dom 操作后同步调用。

  • 为什么这样呢?

  • 因为都要操作 dom 了,这时候如果来了个 effect 同步执行,计算量很大,那不是把 fiber 架构带来的优势有毁了么?

  • 所以 effect 是异步的,不会阻塞渲染。

  • useLayoutEffect,顾名思义是想在这个阶段拿到一些布局信息的,dom 操作完以后就可以了,而且都渲染完了,自然也就可以同步调用了。

  • 实际上 reactcommit 阶段也分成了 3 个小阶段。

  • before mutationmutationlayout

  • mutation 就是遍历 effectList 来更新 dom 的。

  • 它的之前就是 before mutation,会异步调度 useEffect 的回调函数。

  • 它之后就是 layout 阶段了,因为这个阶段已经可以拿到布局信息了,会同步调用 useLayoutEffect 的回调函数。而且这个阶段可以拿到新的 dom 节点,还会更新下 ref

  • 至此,我们对 react 的新架构,rendercommit 两大阶段都干了什么就理清了。

对 componentWillReceiveProps 的理解

该方法当props发生变化时执行,初始化render时不执行,在这个回调函数里面,你可以根据属性的变化,通过调用this.setState()来更新你的组件状态,旧的属性还是可以通过this.props来获取,这里调用更新状态是安全的,并不会触发额外的render调用。


使用好处: 在这个生命周期中,可以在子组件的 render 函数执行前获取新的 props,从而更新子组件自己的 state。 可以将数据请求放在这里进行执行,需要传的参数则从 componentWillReceiveProps(nextProps)中获取。而不必将所有的请求都放在父组件中。于是该请求只会在该组件渲染时才会发出,从而减轻请求负担。


componentWillReceiveProps 在初始化 render 的时候不会执行,它会在 Component 接受到新的状态(Props)时被触发,一般用于父组件状态更新时子组件的重新渲染。

React 中可以在 render 访问 refs 吗?为什么?

<>  <span id="name" ref={this.spanRef}>{this.state.title}</span>  <span>{     this.spanRef.current ? '有值' : '无值'  }</span></>
复制代码


不可以,render 阶段 DOM 还没有生成,无法获取 DOM。DOM 的获取需要在 pre-commit 阶段和 commit 阶段:

在构造函数调用 super 并将 props 作为参数传入的作用是啥?

在调用 super() 方法之前,子类构造函数无法使用this引用,ES6 子类也是如此。将 props 参数传递给 super() 调用的主要原因是在子构造函数中能够通过this.props来获取传入的 props传递 props


class MyComponent extends React.Component {  constructor(props) {    super(props);    console.log(this.props); // { name: 'sudheer',age: 30 }  }}
复制代码


没传递 props


class MyComponent extends React.Component {  constructor(props) {    super();    console.log(this.props); // undefined    // 但是 Props 参数仍然可用    console.log(props); // Prints { name: 'sudheer',age: 30 }  }  render() {    // 构造函数外部不受影响    console.log(this.props); // { name: 'sudheer',age: 30 }  }}
复制代码


上面示例揭示了一点。props 的行为只有在构造函数中是不同的,在构造函数之外也是一样的。

对 React-Fiber 的理解,它解决了什么问题?

React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿


为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。


所以 React 通过 Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:


  • 分批延时对 DOM 进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;

  • 给浏览器一点喘息的机会,它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。


核心思想: Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。

用户头像

beifeng1996

关注

还未添加个人签名 2022.09.01 加入

还未添加个人简介

评论

发布
暂无评论
最近几周react面试遇到的题总结_前端_beifeng1996_InfoQ写作社区