写点什么

前端一面 react 面试题总结

作者:beifeng1996
  • 2023-01-06
    浙江
  • 本文字数:14801 字

    阅读完需:约 49 分钟

redux 与 mobx 的区别?

两者对⽐:


  • redux 将数据保存在单⼀的 store 中,mobx 将数据保存在分散的多个 store 中

  • redux 使⽤plain object 保存数据,需要⼿动处理变化后的操作;mobx 适⽤observable 保存数据,数据变化后⾃动处理响应的操作

  • redux 使⽤不可变状态,这意味着状态是只读的,不能直接去修改它,⽽是应该返回⼀个新的状态,同时使⽤纯函数;mobx 中的状态是可变的,可以直接对其进⾏修改


mobx 相对来说⽐较简单,在其中有很多的抽象,mobx 更多的使⽤⾯向对象的编程思维;redux 会⽐较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助⼀系列的中间件来处理异步和副作⽤


  • mobx 中有更多的抽象和封装,调试会⽐较困难,同时结果也难以预测;⽽redux 提供能够进⾏时间回溯的开发⼯具,同时其纯函数以及更少的抽象,让调试变得更加的容易


场景辨析:


  • 基于以上区别,我们可以简单得分析⼀下两者的不同使⽤场景。

  • mobx 更适合数据不复杂的应⽤:mobx 难以调试,很多状态⽆法回溯,⾯对复杂度⾼的应⽤时,往往⼒不从⼼。

  • redux 适合有回溯需求的应⽤:⽐如⼀个画板应⽤、⼀个表格应⽤,很多时候需要撤销、重做等操作,由于 redux 不可变的特性,天然⽀持这些操作。

  • mobx 适合短平快的项⽬:mobx 上⼿简单,样板代码少,可以很⼤程度上提⾼开发效率。

  • 当然 mobx 和 redux 也并不⼀定是⾮此即彼的关系,你也可以在项⽬中⽤redux 作为全局状态管理,⽤mobx 作为组件局部状态管理器来⽤。

Redux 怎么实现属性传递,介绍下原理

react-redux 数据传输∶ view-->action-->reducer-->store-->view。看下点击事件的数据是如何通过 redux 传到 view 上:


  • view 上的 AddClick 事件通过 mapDispatchToProps 把数据传到 action ---> click:()=>dispatch(ADD)

  • action 的 ADD 传到 reducer 上

  • reducer 传到 store 上 const store = createStore(reducer);

  • store 再通过 mapStateToProps 映射穿到 view 上 text:State.text


代码示例∶


import React from 'react';import ReactDOM from 'react-dom';import { createStore } from 'redux';import { Provider, connect } from 'react-redux';class App extends React.Component{    render(){        let { text, click, clickR } = this.props;        return(            <div>                <div>数据:已有人{text}</div>                <div onClick={click}>加人</div>                <div onClick={clickR}>减人</div>            </div>        )    }}const initialState = {    text:5}const reducer = function(state,action){    switch(action.type){        case 'ADD':            return {text:state.text+1}        case 'REMOVE':            return {text:state.text-1}        default:            return initialState;    }}
let ADD = { type:'ADD'}let Remove = { type:'REMOVE'}
const store = createStore(reducer);
let mapStateToProps = function (state){ return{ text:state.text }}
let mapDispatchToProps = function(dispatch){ return{ click:()=>dispatch(ADD), clickR:()=>dispatch(Remove) }}
const App1 = connect(mapStateToProps,mapDispatchToProps)(App);
ReactDOM.render( <Provider store = {store}> <App1></App1> </Provider>,document.getElementById('root'))
复制代码

React 中发起网络请求应该在哪个生命周期中进行?为什么?

对于异步请求,最好放在 componentDidMount 中去操作,对于同步的状态改变,可以放在 componentWillMount 中,一般用的比较少。


如果认为在 componentWillMount 里发起请求能提早获得结果,这种想法其实是错误的,通常 componentWillMount 比 componentDidMount 早不了多少微秒,网络上任何一点延迟,这一点差异都可忽略不计。


react 的生命周期: constructor() -> componentWillMount() -> render() -> componentDidMount()


上面这些方法的调用是有次序的,由上而下依次调用。


  • constructor 被调用是在组件准备要挂载的最开始,此时组件尚未挂载到网页上。

  • componentWillMount 方法的调用在 constructor 之后,在 render 之前,在这方法里的代码调用 setState 方法不会触发重新 render,所以它一般不会用来作加载数据之用。

  • componentDidMount 方法中的代码,是在组件已经完全挂载到网页上才会调用被执行,所以可以保证数据的加载。此外,在这方法中调用 setState 方法,会触发重新渲染。所以,官方设计这个方法就是用来加载外部数据用的,或处理其他的副作用代码。与组件上的数据无关的加载,也可以在 constructor 里做,但 constructor 是做组件 state 初绐化工作,并不是做加载数据这工作的,constructor 里也不能 setState,还有加载的时间太长或者出错,页面就无法加载出来。所以有副作用的代码都会集中在 componentDidMount 方法里。


总结:


  • 跟服务器端渲染(同构)有关系,如果在 componentWillMount 里面获取数据,fetch data 会执行两次,一次在服务器端一次在客户端。在 componentDidMount 中可以解决这个问题,componentWillMount 同样也会 render 两次。

  • 在 componentWillMount 中 fetch data,数据一定在 render 后才能到达,如果忘记了设置初始状态,用户体验不好。

  • react16.0 以后,componentWillMount 可能会被执行多次。

如何解决 props 层级过深的问题

  • 使用 Context API:提供一种组件之间的状态共享,而不必通过显式组件树逐层传递 props;

  • 使用 Redux 等状态库。

React 事件机制

<div onClick={this.handleClick.bind(this)}>点我</div>
复制代码


React 并不是将 click 事件绑定到了 div 的真实 DOM 上,而是在 document 处监听了所有的事件,当事件发生并且冒泡到 document 处的时候,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。


除此之外,冒泡到 document 上的事件也不是原生的浏览器事件,而是由 react 自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用 event.preventDefault()方法,而不是调用 event.stopProppagation()方法。 JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document 上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。


另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault


实现合成事件的目的如下:


  • 合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力;

  • 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。

如何配置 React-Router 实现路由切换

(1)使用<Route> 组件


路由匹配是通过比较 <Route> 的 path 属性和当前地址的 pathname 来实现的。当一个 <Route> 匹配成功时,它将渲染其内容,当它不匹配时就会渲染 null。没有路径的 <Route> 将始终被匹配。


// when location = { pathname: '/about' }<Route path='/about' component={About}/> // renders <About/><Route path='/contact' component={Contact}/> // renders null<Route component={Always}/> // renders <Always/>
复制代码


(2)结合使用 <Switch> 组件和 <Route> 组件


<Switch> 用于将 <Route> 分组。


<Switch>    <Route exact path="/" component={Home} />    <Route path="/about" component={About} />    <Route path="/contact" component={Contact} /></Switch>
复制代码


<Switch> 不是分组 <Route> 所必须的,但他通常很有用。 一个 <Switch> 会遍历其所有的子 <Route>元素,并仅渲染与当前地址匹配的第一个元素。


(3)使用 <Link>、 <NavLink>、<Redirect> 组件


<Link> 组件来在你的应用程序中创建链接。无论你在何处渲染一个<Link> ,都会在应用程序的 HTML 中渲染锚(<a>)。


<Link to="/">Home</Link>   // <a href='/'>Home</a>
复制代码


是一种特殊类型的 当它的 to 属性与当前地址匹配时,可以将其定义为"活跃的"。


// location = { pathname: '/react' }<NavLink to="/react" activeClassName="hurray">    React</NavLink>// <a href='/react' className='hurray'>React</a>
复制代码


当我们想强制导航时,可以渲染一个<Redirect>,当一个<Redirect>渲染时,它将使用它的 to 属性进行定向。


参考 前端进阶面试题详细解答

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 只能在组件顶层使用,不可在分支语句中使用。、

什么是高阶组件

高阶组件不是组件,是 增强函数,可以输入一个元组件,返回出一个新的增强组件


  • 属性代理 (Props Proxy) 在我看来属性代理就是提取公共的数据和方法到父组件,子组件只负责渲染数据,相当于设计模式里的模板模式,这样组件的重用性就更高了


function proxyHoc(WrappedComponent) {    return class extends React.Component {        render() {            const newProps = {                count: 1            }            return <WrappedComponent {...this.props} {...newProps} />        }    }}
复制代码


  • 反向继承


const MyContainer = (WrappedComponent)=>{    return class extends WrappedComponent {        render(){            return super.render();        }    }}
复制代码

非嵌套关系组件的通信方式?

即没有任何包含关系的组件,包括兄弟组件以及不在同一个父级中的非兄弟组件。


  • 可以使用自定义事件通信(发布订阅模式)

  • 可以通过 redux 等进行全局状态管理

  • 如果是兄弟组件通信,可以找到这两个兄弟节点共同的父节点, 结合父子间通信方式进行通信。

组件通信的方式有哪些

  • ⽗组件向⼦组件通讯: ⽗组件可以向⼦组件通过传 props 的⽅式,向⼦组件进⾏通讯

  • ⼦组件向⽗组件通讯: props+回调的⽅式,⽗组件向⼦组件传递 props 进⾏通讯,此 props 为作⽤域为⽗组件⾃身的函 数,⼦组件调⽤该函数,将⼦组件想要传递的信息,作为参数,传递到⽗组件的作⽤域中

  • 兄弟组件通信: 找到这两个兄弟节点共同的⽗节点,结合上⾯两种⽅式由⽗节点转发信息进⾏通信

  • 跨层级通信: Context 设计⽬的是为了共享那些对于⼀个组件树⽽⾔是“全局”的数据,例如当前认证的⽤户、主题或⾸选语⾔,对于跨越多层的全局数据通过 Context 通信再适合不过

  • 发布订阅模式: 发布者发布事件,订阅者监听事件并做出反应,我们可以通过引⼊event 模块进⾏通信

  • 全局状态管理⼯具: 借助 Redux 或者 Mobx 等全局状态管理⼯具进⾏通信,这种⼯具会维护⼀个全局状态中⼼Store,并根据不同的事件产⽣新的状态

React-Router 的路由有几种模式?

React-Router 支持使用 hash(对应 HashRouter)和 browser(对应 BrowserRouter) 两种路由规则, react-router-dom 提供了 BrowserRouter 和 HashRouter 两个组件来实现应用的 UI 和 URL 同步:


  • BrowserRouter 创建的 URL 格式:xxx.com/path

  • HashRouter 创建的 URL 格式:xxx.com/#/path


(1)BrowserRouter


它使用 HTML5 提供的 history API(pushState、replaceState 和 popstate 事件)来保持 UI 和 URL 的同步。由此可以看出,BrowserRouter 是使用 HTML 5 的 history API 来控制路由跳转的:


<BrowserRouter    basename={string}    forceRefresh={bool}    getUserConfirmation={func}    keyLength={number}/>
复制代码


其中的属性如下:


  • basename 所有路由的基准 URL。basename 的正确格式是前面有一个前导斜杠,但不能有尾部斜杠;


<BrowserRouter basename="/calendar">    <Link to="/today" /></BrowserRouter>
复制代码


等同于


<a href="/calendar/today" />
复制代码


  • forceRefresh 如果为 true,在导航的过程中整个页面将会刷新。一般情况下,只有在不支持 HTML5 history API 的浏览器中使用此功能;

  • getUserConfirmation 用于确认导航的函数,默认使用 window.confirm。例如,当从 /a 导航至 /b 时,会使用默认的 confirm 函数弹出一个提示,用户点击确定后才进行导航,否则不做任何处理;


// 这是默认的确认函数const getConfirmation = (message, callback) => {  const allowTransition = window.confirm(message);  callback(allowTransition);}<BrowserRouter getUserConfirmation={getConfirmation} />
复制代码


需要配合<Prompt> 一起使用。


  • KeyLength 用来设置 Location.Key 的长度。


(2)HashRouter


使用 URL 的 hash 部分(即 window.location.hash)来保持 UI 和 URL 的同步。由此可以看出,HashRouter 是通过 URL 的 hash 属性来控制路由跳转的:


<HashRouter    basename={string}    getUserConfirmation={func}    hashType={string}  />
复制代码


其参数如下


  • basename, getUserConfirmation 和 BrowserRouter 功能一样;

  • hashType window.location.hash 使用的 hash 类型,有如下几种:

  • slash - 后面跟一个斜杠,例如 #/ 和 #/sunshine/lollipops;

  • noslash - 后面没有斜杠,例如 # 和 #sunshine/lollipops;

  • hashbang - Google 风格的 ajax crawlable,例如 #!/ 和 #!/sunshine/lollipops。

react 代理原生事件为什么?

通过冒泡实现,为了统一管理,对更多浏览器有兼容效果


合成事件原理


如果 react 事件绑定在了真实 DOM 节点上,一个节点同事有多个事件时,页面的响应和内存的占用会受到很大的影响。因此 SyntheticEvent 作为中间层出现了。


事件没有在目标对象上绑定,而是在 document 上监听所支持的所有事件,当事件发生并冒泡至 document 时,react 将事件内容封装并叫由真正的处理函数运行。


版权声明:本文为 CSDN 博主「jiuwanli666」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。

React Hooks 在平时开发中需要注意的问题和原因

(1)不要在循环,条件或嵌套函数中调用 Hook,必须始终在 React 函数的顶层使用 Hook


这是因为 React 需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用 Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。


(2)使用 useState 时候,使用 push,pop,splice 等直接更改数组对象的坑


使用 push 直接更改数组无法获取到新值,应该采用析构方式,但是在 class 里面不会有这个问题。代码示例:


function Indicatorfilter() {  let [num,setNums] = useState([0,1,2,3])  const test = () => {    // 这里坑是直接采用push去更新num    // setNums(num)是无法更新num的    // 必须使用num = [...num ,1]    num.push(1)    // num = [...num ,1]    setNums(num)  }return (    <div className='filter'>      <div onClick={test}>测试</div>        <div>          {num.map((item,index) => (              <div key={index}>{item}</div>          ))}      </div>    </div>  )}
class Indicatorfilter extends React.Component<any,any>{ constructor(props:any){ super(props) this.state = { nums:[1,2,3] } this.test = this.test.bind(this) }
test(){ // class采用同样的方式是没有问题的 this.state.nums.push(1) this.setState({ nums: this.state.nums }) }
render(){ let {nums} = this.state return( <div> <div onClick={this.test}>测试</div> <div> {nums.map((item:any,index:number) => ( <div key={index}>{item}</div> ))} </div> </div>
) }}
复制代码


(3)useState 设置状态的时候,只有第一次生效,后期需要更新状态,必须通过 useEffect


TableDeail 是一个公共组件,在调用它的父组件里面,我们通过 set 改变 columns 的值,以为传递给 TableDeail 的 columns 是最新的值,所以 tabColumn 每次也是最新的值,但是实际 tabColumn 是最开始的值,不会随着 columns 的更新而更新:


const TableDeail = ({    columns,}:TableData) => {    const [tabColumn, setTabColumn] = useState(columns) }
// 正确的做法是通过useEffect改变这个值const TableDeail = ({ columns,}:TableData) => { const [tabColumn, setTabColumn] = useState(columns) useEffect(() =>{setTabColumn(columns)},[columns])}

复制代码


(4)善用 useCallback


父组件传递给子组件事件句柄时,如果我们没有任何参数变动可能会选用 useMemo。但是每一次父组件渲染子组件即使没变化也会跟着渲染一次。


(5)不要滥用 useContext


可以使用基于 useContext 封装的状态管理工具。

setState 之后 发生了什么?

  • (1)代码中调用 setState 函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。

  • (2)经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面;

  • (3)在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染;

  • (4)在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。


setState 的调用会引起 React 的更新生命周期的 4 个函数执行。


shouldComponentUpdate


componentWillUpdate


render


componentDidUpdate

React 的生命周期有哪些?

React 通常将组件生命周期分为三个阶段:


  • 装载阶段(Mount),组件第一次在 DOM 树中被渲染的过程;

  • 更新过程(Update),组件状态发生变化,重新更新渲染的过程;

  • 卸载过程(Unmount),组件从 DOM 树中被移除的过程;

1)组件挂载阶段

挂载阶段组件被创建,然后组件实例插入到 DOM 中,完成组件的第一次渲染,该过程只会发生一次,在此阶段会依次调用以下这些方法:


  • constructor

  • getDerivedStateFromProps

  • render

  • componentDidMount

(1)constructor

组件的构造函数,第一个被执行,若没有显式定义它,会有一个默认的构造函数,但是若显式定义了构造函数,我们必须在构造函数中执行 super(props),否则无法在构造函数中拿到 this。


如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数 Constructor


constructor 中通常只做两件事:


  • 初始化组件的 state

  • 给事件处理方法绑定 this


constructor(props) {  super(props);  // 不要在构造函数中调用 setState,可以直接给 state 设置初始值  this.state = { counter: 0 }  this.handleClick = this.handleClick.bind(this)}
复制代码
(2)getDerivedStateFromProps
static getDerivedStateFromProps(props, state)
复制代码


这是个静态方法,所以不能在这个函数里使用 this,有两个参数 propsstate,分别指接收到的新参数和当前组件的 state 对象,这个函数会返回一个对象用来更新当前的 state 对象,如果不需要更新可以返回 null


该函数会在装载时,接收到新的 props 或者调用了 setStateforceUpdate 时被调用。如当接收到新的属性想修改 state ,就可以使用。


// 当 props.counter 变化时,赋值给 state class App extends React.Component {  constructor(props) {    super(props)    this.state = {      counter: 0    }  }  static getDerivedStateFromProps(props, state) {    if (props.counter !== state.counter) {      return {        counter: props.counter      }    }    return null  }
handleClick = () => { this.setState({ counter: this.state.counter + 1 }) } render() { return ( <div> <h1 onClick={this.handleClick}>Hello, world!{this.state.counter}</h1> </div> ) }}
复制代码


现在可以显式传入 counter ,但是这里有个问题,如果想要通过点击实现 state.counter 的增加,但这时会发现值不会发生任何变化,一直保持 props 传进来的值。这是由于在 React 16.4^ 的版本中 setStateforceUpdate 也会触发这个生命周期,所以当组件内部 state 变化后,就会重新走这个方法,同时会把 state 值赋值为 props 的值。因此需要多加一个字段来记录之前的 props 值,这样就会解决上述问题。具体如下:


// 这里只列出需要变化的地方class App extends React.Component {  constructor(props) {    super(props)    this.state = {      // 增加一个 preCounter 来记录之前的 props 传来的值      preCounter: 0,      counter: 0    }  }  static getDerivedStateFromProps(props, state) {    // 跟 state.preCounter 进行比较    if (props.counter !== state.preCounter) {      return {        counter: props.counter,        preCounter: props.counter      }    }    return null  }  handleClick = () => {    this.setState({      counter: this.state.counter + 1    })  }  render() {    return (      <div>        <h1 onClick={this.handleClick}>Hello, world!{this.state.counter}</h1>      </div>    )  }}
复制代码
(3)render

render 是 React 中最核心的方法,一个组件中必须要有这个方法,它会根据状态 state 和属性 props 渲染组件。这个函数只做一件事,就是返回需要渲染的内容,所以不要在这个函数内做其他业务逻辑,通常调用该方法会返回以下类型中一个:


  • React 元素:这里包括原生的 DOM 以及 React 组件;

  • 数组和 Fragment(片段):可以返回多个元素;

  • Portals(插槽):可以将子元素渲染到不同的 DOM 子树种;

  • 字符串和数字:被渲染成 DOM 中的 text 节点;

  • 布尔值或 null:不渲染任何内容。

(4)componentDidMount()

componentDidMount()会在组件挂载后(插入 DOM 树中)立即调。该阶段通常进行以下操作:


  • 执行依赖于 DOM 的操作;

  • 发送网络请求;(官方建议)

  • 添加订阅消息(会在 componentWillUnmount 取消订阅);


如果在 componentDidMount 中调用 setState ,就会触发一次额外的渲染,多调用了一次 render 函数,由于它是在浏览器刷新屏幕前执行的,所以用户对此是没有感知的,但是我应当避免这样使用,这样会带来一定的性能问题,尽量是在 constructor 中初始化 state 对象。


在组件装载之后,将计数数字变为 1:


class App extends React.Component  {  constructor(props) {    super(props)    this.state = {      counter: 0    }  }  componentDidMount () {    this.setState({      counter: 1    })  }  render ()  {    return (      <div className="counter">        counter值: { this.state.counter }      </div>    )  }}
复制代码

2)组件更新阶段

当组件的 props 改变了,或组件内部调用了 setState/forceUpdate,会触发更新重新渲染,这个过程可能会发生多次。这个阶段会依次调用下面这些方法:


  • getDerivedStateFromProps

  • shouldComponentUpdate

  • render

  • getSnapshotBeforeUpdate

  • componentDidUpdate

(1)shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState)
复制代码


在说这个生命周期函数之前,来看两个问题:


  • setState 函数在任何情况下都会导致组件重新渲染吗?例如下面这种情况:


this.setState({number: this.state.number})
复制代码


  • 如果没有调用 setState,props 值也没有变化,是不是组件就不会重新渲染?


第一个问题答案是 ,第二个问题如果是父组件重新渲染时,不管传入的 props 有没有变化,都会引起子组件的重新渲染。


那么有没有什么方法解决在这两个场景下不让组件重新渲染进而提升性能呢?这个时候 shouldComponentUpdate 登场了,这个生命周期函数是用来提升速度的,它是在重新渲染组件开始前触发的,默认返回 true,可以比较 this.propsnextPropsthis.statenextState 值是否变化,来确认返回 true 或者 false。当返回 false 时,组件的更新过程停止,后续的 rendercomponentDidUpdate 也不会被调用。


注意: 添加 shouldComponentUpdate 方法时,不建议使用深度相等检查(如使用 JSON.stringify()),因为深比较效率很低,可能会比重新渲染组件效率还低。而且该方法维护比较困难,建议使用该方法会产生明显的性能提升时使用。

(2)getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState)
复制代码


这个方法在 render 之后,componentDidUpdate 之前调用,有两个参数 prevPropsprevState,表示更新之前的 propsstate,这个函数必须要和 componentDidUpdate 一起使用,并且要有一个返回值,默认是 null,这个返回值作为第三个参数传给 componentDidUpdate

(3)componentDidUpdate

componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。 该阶段通常进行以下操作:


  • 当组件更新后,对 DOM 进行操作;

  • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。


componentDidUpdate(prevProps, prevState, snapshot){}
复制代码


该方法有三个参数:


  • prevProps: 更新前的 props

  • prevState: 更新前的 state

  • snapshot: getSnapshotBeforeUpdate()生命周期的返回值

3)组件卸载阶段

卸载阶段只有一个生命周期函数,componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作:


  • 清除 timer,取消网络请求或清除

  • 取消在 componentDidMount() 中创建的订阅等;


这个生命周期在一个组件被卸载和销毁之前被调用,因此你不应该再这个方法中使用 setState,因为组件一旦被卸载,就不会再装载,也就不会重新渲染。

4)错误处理阶段

componentDidCatch(error, info),此生命周期在后代组件抛出错误后被调用。 它接收两个参数∶


  • error:抛出的错误。

  • info:带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息


React 常见的生命周期如下: React 常见生命周期的过程大致如下:


  • 挂载阶段,首先执行 constructor 构造方法,来创建组件

  • 创建完成之后,就会执行 render 方法,该方法会返回需要渲染的内容

  • 随后,React 会将需要渲染的内容挂载到 DOM 树上

  • 挂载完成之后就会执行 componentDidMount 生命周期函数

  • 如果我们给组件创建一个 props(用于组件通信)、调用 setState(更改 state 中的数据)、调用 forceUpdate(强制更新组件)时,都会重新调用 render 函数

  • render 函数重新执行之后,就会重新进行 DOM 树的挂载

  • 挂载完成之后就会执行 componentDidUpdate 生命周期函数

  • 当移除组件时,就会执行 componentWillUnmount 生命周期函数


React 主要生命周期总结:


  1. getDefaultProps:这个函数会在组件创建之前被调用一次(有且仅有一次),它被用来初始化组件的 Props;

  2. getInitialState:用于初始化组件的 state 值;

  3. componentWillMount:在组件创建后、render 之前,会走到 componentWillMount 阶段。这个阶段我个人一直没用过、非常鸡肋。后来 React 官方已经不推荐大家在 componentWillMount 里做任何事情、到现在 React16 直接废弃了这个生命周期,足见其鸡肋程度了;

  4. render:这是所有生命周期中唯一一个你必须要实现的方法。一般来说需要返回一个 jsx 元素,这时 React 会根据 props 和 state 来把组件渲染到界面上;不过有时,你可能不想渲染任何东西,这种情况下让它返回 null 或者 false 即可;

  5. componentDidMount:会在组件挂载后(插入 DOM 树中后)立即调用,标志着组件挂载完成。一些操作如果依赖获取到 DOM 节点信息,我们就会放在这个阶段来做。此外,这还是 React 官方推荐的发起 ajax 请求的时机。该方法和 componentWillMount 一样,有且仅有一次调用。

constructor 为什么不先渲染?

由 ES6 的继承规则得知,不管子类写不写 constructor,在 new 实例的过程都会给补上 constructor。


所以:constructor 钩子函数并不是不可缺少的,子组件可以在一些情况略去。比如不自己的 state,从 props 中获取的情况

useEffect 与 useLayoutEffect 的区别

(1)共同点


  • 运用效果: useEffect 与 useLayoutEffect 两者都是用于处理副作用,这些副作用包括改变 DOM、设置订阅、操作定时器等。在函数组件内部操作副作用是不被允许的,所以需要使用这两个函数去处理。

  • 使用方式: useEffect 与 useLayoutEffect 两者底层的函数签名是完全一致的,都是调用的 mountEffectImpl 方法,在使用上也没什么差异,基本可以直接替换。


(2)不同点


  • 使用场景: useEffect 在 React 的渲染过程中是被异步调用的,用于绝大多数场景;而 useLayoutEffect 会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 useLayoutEffect 做计算量较大的耗时任务从而造成阻塞。

  • 使用效果: useEffect 是按照顺序执行代码的,改变屏幕像素之后执行(先渲染,后改变 DOM),当改变屏幕内容时可能会产生闪烁;useLayoutEffect 是改变屏幕像素之前就执行了(会推迟页面显示的事件,先改变 DOM 后渲染),不会产生闪烁。useLayoutEffect 总是比 useEffect 先执行。


在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。

React Hook 的使用限制有哪些?

React Hooks 的限制主要有两条:


  • 不要在循环、条件或嵌套函数中调用 Hook;

  • 在 React 的函数组件中调用 Hook。


那为什么会有这样的限制呢?Hooks 的设计初衷是为了改进 React 组件的开发模式。在旧有的开发模式下遇到了三个问题。


  • 组件之间难以复用状态逻辑。过去常见的解决方案是高阶组件、render props 及状态管理框架。

  • 复杂的组件变得难以理解。生命周期函数与业务逻辑耦合太深,导致关联部分难以拆分。

  • 人和机器都很容易混淆类。常见的有 this 的问题,但在 React 团队中还有类难以优化的问题,希望在编译优化层面做出一些改进。


这三个问题在一定程度上阻碍了 React 的后续发展,所以为了解决这三个问题,Hooks 基于函数组件开始设计。然而第三个问题决定了 Hooks 只支持函数组件。


那为什么不要在循环、条件或嵌套函数中调用 Hook 呢?因为 Hooks 的设计是基于数组实现。在调用时按顺序加入数组中,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 的源码里不是数组,是链表。


这些限制会在编码上造成一定程度的心智负担,新手可能会写错,为了避免这样的情况,可以引入 ESLint 的 Hooks 检查插件进行预防。

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

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


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


不同点:


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

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

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

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

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

diff 算法如何比较?

  • 只对同级比较,跨层级的 dom 不会进行复用

  • 不同类型节点生成的 dom 树不同,此时会直接销毁老节点及子孙节点,并新建节点

  • 可以通过 key 来对元素 diff 的过程提供复用的线索

  • 单节点 diff

  • 单点 diff 有如下几种情况:

  • key 和 type 相同表示可以复用节点

  • key 不同直接标记删除节点,然后新建节点

  • key 相同 type 不同,标记删除该节点和兄弟节点,然后新创建节点


用户头像

beifeng1996

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
前端一面react面试题总结_React_beifeng1996_InfoQ写作社区