写点什么

前端一面 react 面试题(持续更新中)

作者:beifeng1996
  • 2023-02-27
    浙江
  • 本文字数:8026 字

    阅读完需:约 26 分钟

React 性能优化

  • shouldCompoentUpdate

  • pureComponent 自带 shouldCompoentUpdate 的浅比较优化

  • 结合 Immutable.js 达到最优

为什么 useState 要使用数组而不是对象

useState 的用法:


const [count, setCount] = useState(0)
复制代码


可以看到 useState 返回的是一个数组,那么为什么是返回数组而不是返回对象呢?


这里用到了解构赋值,所以先来看一下 ES6 的解构赋值:

数组的解构赋值
const foo = [1, 2, 3];const [one, two, three] = foo;console.log(one);    // 1console.log(two);    // 2console.log(three);    // 3
复制代码
对象的解构赋值
const user = {  id: 888,  name: "xiaoxin"};const { id, name } = user;console.log(id);    // 888console.log(name);    // "xiaoxin"
复制代码


看完这两个例子,答案应该就出来了:


  • 如果 useState 返回的是数组,那么使用者可以对数组中的元素命名,代码看起来也比较干净

  • 如果 useState 返回的是对象,在解构对象的时候必须要和 useState 内部实现返回的对象同名,想要使用多次的话,必须得设置别名才能使用返回值


下面来看看如果 useState 返回对象的情况:


// 第一次使用const { state, setState } = useState(false);// 第二次使用const { state: counter, setState: setCounter } = useState(0) 
复制代码


这里可以看到,返回对象的使用方式还是挺麻烦的,更何况实际项目中会使用的更频繁。 总结:useState 返回的是 array 而不是 object 的原因就是为了降低使用的复杂度,返回数组的话可以直接根据顺序解构,而返回对象的话要想使用多次就需要定义别名了。

何为 JSX

JSX 是 JavaScript 语法的一种语法扩展,并拥有 JavaScript 的全部功能。JSX 生产 React "元素",你可以将任何的 JavaScript 表达式封装在花括号里,然后将其嵌入到 JSX 中。在编译完成之后,JSX 表达式就变成了常规的 JavaScript 对象,这意味着你可以在 if 语句和 for 循环内部使用 JSX,将它赋值给变量,接受它作为参数,并从函数中返回它。

对 React 和 Vue 的理解,它们的异同

相似之处:


  • 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库

  • 都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板。

  • 都使用了 Virtual DOM(虚拟 DOM)提高重绘性能

  • 都有 props 的概念,允许组件间的数据传递

  • 都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性


不同之处:


1)数据流


Vue 默认支持数据双向绑定,而 React 一直提倡单向数据流


2)虚拟 DOM


Vue2.x 开始引入"Virtual DOM",消除了和 React 在这方面的差异,但是在具体的细节还是有各自的特点。


  • Vue 宣称可以更快地计算出 Virtual DOM 的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。

  • 对于 React 而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate 这个生命周期方法来进行控制,但 Vue 将此视为默认的优化。


3)组件化


React 与 Vue 最大的不同是模板的编写。


  • Vue 鼓励写近似常规 HTML 的模板。写起来很接近标准 HTML 元素,只是多了一些属性。

  • React 推荐你所有的模板通用 JavaScript 的语法扩展——JSX 书写。


具体来讲:React 中 render 函数是支持闭包特性的,所以我们 import 的组件在 render 中可以直接调用。但是在 Vue 中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 完组件之后,还需要在 components 中再声明下。


4)监听数据变化的实现原理不同


  • Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能

  • React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的 vDOM 的重新渲染。这是因为 Vue 使用的是可变数据,而 React 更强调数据的不可变。


5)高阶组件


react 可以通过高阶组件(Higher Order Components-- HOC)来扩展,而 vue 需要通过 mixins 来扩展。


原因高阶组件就是高阶函数,而 React 的组件本身就是纯粹的函数,所以高阶函数对 React 来说易如反掌。相反 Vue.js 使用 HTML 模板创建视图组件,这时模板无法有效的编译,因此 Vue 不采用 HOC 来实现。


6)构建工具


两者都有自己的构建工具


  • React ==> Create React APP

  • Vue ==> vue-cli


7)跨平台


  • React ==> React Native

  • Vue ==> Weex

对虚拟 DOM 的理解?虚拟 DOM 主要做了什么?虚拟 DOM 本身是什么?

从本质上来说,Virtual Dom 是一个 JavaScript 对象,通过对象的方式来表示 DOM 结构。将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次 DOM 修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改 DOM 的重绘重排次数,提高渲染性能。


虚拟 DOM 是对 DOM 的抽象,这个对象是更加轻量级的对 DOM 的描述。它设计的最初目的,就是更好的跨平台,比如 node.js 就没有 DOM,如果想实现 SSR,那么一个方式就是借助虚拟 dom,因为虚拟 dom 本身是 js 对象。 在代码渲染到页面之前,vue 或者 react 会把代码转换成一个对象(虚拟 DOM)。以对象的形式来描述真实 dom 结构,最终渲染到页面。在每次数据发生变化前,虚拟 dom 都会缓存一份,变化之时,现在的虚拟 dom 会与缓存的虚拟 dom 进行比较。在 vue 或者 react 内部封装了 diff 算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。


另外现代前端框架的一个基本要求就是无须手动操作 DOM,一方面是因为手动操作 DOM 无法保证程序性能,多人协作的项目中如果 review 不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动 DOM 操作可以大大提高开发效率。


为什么要用 Virtual DOM:


(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能


下面对比一下修改 DOM 时真实 DOM 操作和 Virtual DOM 的过程,来看一下它们重排重绘的性能消耗∶


  • 真实 DOM∶ 生成 HTML 字符串+ 重建所有的 DOM 元素

  • Virtual DOM∶ 生成 vNode+ DOMDiff+必要的 DOM 更新


Virtual DOM 的更新 DOM 的准备工作耗费更多的时间,也就是 JS 层面,相比于更多的 DOM 操作它的消费是极其便宜的。尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,我依然可以给你提供过得去的性能。 (2)跨平台 Virtual DOM 本质上是 JavaScript 的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp 等。

hooks 和 class 比较的优势?

一、更容易复用代码


二、清爽的代码风格+代码量更少


缺点


状态不同步


不好用的 useEffect,


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

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 中 constructor 和 getInitialState 的区别?

两者都是用来初始化 state 的。前者是 ES6 中的语法,后者是 ES5 中的语法,新版本的 React 中已经废弃了该方法。


getInitialState 是 ES5 中的方法,如果使用 createClass 方法创建一个 Component 组件,可以自动调用它的 getInitialState 方法来获取初始化的 State 对象,


var APP = React.creatClass ({  getInitialState() {    return {         userName: 'hi',        userId: 0     }; }})
复制代码


React 在 ES6 的实现中去掉了 getInitialState 这个 hook 函数,规定 state 在 constructor 中实现,如下:


Class App extends React.Component{    constructor(props){      super(props);      this.state={};    }  }
复制代码

当渲染一个列表时,何为 key?设置 key 的目的是什么

Keys 会有助于 React 识别哪些 items 改变了,被添加了或者被移除了。Keys 应该被赋予数组内的元素以赋予(DOM)元素一个稳定的标识,选择一个 key 的最佳方法是使用一个字符串,该字符串能惟一地标识一个列表项。很多时候你会使用数据中的 IDs 作为 keys,当你没有稳定的 IDs 用于被渲染的 items 时,可以使用项目索引作为渲染项的 key,但这种方式并不推荐,如果 items 可以重新排序,就会导致 re-render 变慢。

何为纯函数(pure function)

一个纯函数是一个不依赖于且不改变其作用域之外的变量状态的函数,这也意味着一个纯函数对于同样的参数总是返回同样的结果。

为何 React 事件要自己绑定 this

在 React 源码中,当具体到某一事件处理函数将要调用时,将调用 invokeGuardedCallback 方法。


function invokeGuardedCallback(name, func, a) {  try {    func(a);  } catch (x) {    if (caughtError === null) {      caughtError = x;    }  }}
复制代码


事件处理函数是直接调用的,并没有指定调用的组件,所以不进行手动绑定的情况下直接获取到的 this 是不准确的,所以我们需要手动将当前组件绑定到 this 上

useEffect 和 useLayoutEffect 的区别

useEffect


基本上 90%的情况下,都应该用这个,这个是在 render 结束后,你的 callback 函数执行,但是不会 block browser painting,算是某种异步的方式吧,但是 class 的 componentDidMount 和 componentDidUpdate 是同步的,在 render 结束后就运行,useEffect 在大部分场景下都比 class 的方式性能更好.


useLayoutEffect


这个是用在处理 DOM 的时候,当你的 useEffect 里面的操作需要处理 DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题, useLayoutEffect 里面的 callback 函数会在**DOM 更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,**阻塞了浏览器的绘制.

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 封装的状态管理工具。

除了在构造函数中绑定 this,还有其它方式吗

你可以使用属性初始值设定项(property initializers)来正确绑定回调,create-react-app 也是默认支持的。在回调中你可以使用箭头函数,但问题是每次组件渲染时都会创建一个新的回调。

父子组件的通信方式?

父组件向子组件通信:父组件通过 props 向子组件传递需要的信息。


// 子组件: Childconst Child = props =>{  return <p>{props.name}</p>}// 父组件 Parentconst Parent = ()=>{    return <Child name="react"></Child>}
复制代码


子组件向父组件通信:: props+回调的方式。


// 子组件: Childconst Child = props =>{  const cb = msg =>{      return ()=>{          props.callback(msg)      }  }  return (      <button onClick={cb("你好!")}>你好</button>  )}// 父组件 Parentclass Parent extends Component {    callback(msg){        console.log(msg)    }    render(){        return <Child callback={this.callback.bind(this)}></Child>        }}
复制代码

react-router 里的 Link 标签和 a 标签的区别

从最终渲染的 DOM 来看,这两者都是链接,都是 标签,区别是∶ <Link>是 react-router 里实现路由跳转的链接,一般配合<Route> 使用,react-router 接管了其默认的链接跳转行为,区别于传统的页面跳转,<Link> 的“跳转”行为只会触发相匹配的<Route>对应的页面内容更新,而不会刷新整个页面。


<Link>做了 3 件事情:


  • 有 onclick 那就执行 onclick

  • click 的时候阻止 a 标签默认事件

  • 根据跳转 href(即是 to),用 history (web 前端路由两种方式之一,history & hash)跳转,此时只是链接变了,并没有刷新页面而<a>标签就是普通的超链接了,用于从当前页面跳转到 href 指向的另一 个页面(非锚点情况)。


a 标签默认事件禁掉之后做了什么才实现了跳转?


let domArr = document.getElementsByTagName('a')[...domArr].forEach(item=>{    item.addEventListener('click',function () {        location.href = this.href    })})
复制代码

Redux 请求中间件如何处理并发

使用 redux-Saga redux-saga 是一个管理 redux 应用异步操作的中间件,用于代替 redux-thunk 的。它通过创建 Sagas 将所有异步操作逻辑存放在一个地方进行集中处理,以此将 react 中的同步操作与异步操作区分开来,以便于后期的管理与维护。 redux-saga 如何处理并发:


  • takeEvery


可以让多个 saga 任务并行被 fork 执行。


import {    fork,    take} from "redux-saga/effects"
const takeEvery = (pattern, saga, ...args) => fork(function*() { while (true) { const action = yield take(pattern) yield fork(saga, ...args.concat(action)) }})
复制代码


  • takeLatest


takeLatest 不允许多个 saga 任务并行地执行。一旦接收到新的发起的 action,它就会取消前面所有 fork 过的任务(如果这些任务还在执行的话)。在处理 AJAX 请求的时候,如果只希望获取最后那个请求的响应, takeLatest 就会非常有用。


import {    cancel,    fork,    take} from "redux-saga/effects"
const takeLatest = (pattern, saga, ...args) => fork(function*() { let lastTask while (true) { const action = yield take(pattern) if (lastTask) { yield cancel(lastTask) // 如果任务已经结束,则 cancel 为空操作 } lastTask = yield fork(saga, ...args.concat(action)) }})
复制代码

setState 是同步异步?为什么?实现原理?

1. setState 是同步执行的


setState 是同步执行的,但是 state 并不一定会同步更新


2. setState 在 React 生命周期和合成事件中批量覆盖执行


在 React 的生命周期钩子和合成事件中,多次执行 setState,会批量执行


具体表现为,多次同步执行的 setState,会进行合并,类似于 Object.assign,相同的 key,后面的会覆盖前面的


当遇到多个 setState 调用时候,会提取单次传递 setState 的对象,把他们合并在一起形成一个新的


单一对象,并用这个单一的对象去做 setState 的事情,就像 Object.assign 的对象合并,后一个


key 值会覆盖前面的 key 值


经过 React 处理的事件是不会同步更新 this.state 的. 通过 addEventListener || setTimeout/setInterval 的方式处理的则会同步更新。


为了合并 setState,我们需要一个队列来保存每次 setState 的数据,然后在一段时间后执行合并操作和更新 state,并清空这个队列,然后渲染组件。

在 React 中怎么使用 async/await?

async/await 是 ES7 标准中的新特性。如果是使用 React 官方的脚手架创建的项目,就可以直接使用。如果是在自己搭建的 webpack 配置的项目中使用,可能会遇到 regeneratorRuntime is not defined 的异常错误。那么我们就需要引入 babel,并在 babel 中配置使用 async/await。可以利用 babel 的 transform-async-to-module-method 插件来转换其成为浏览器支持的语法,虽然没有性能的提升,但对于代码编写体验要更好。

调用 setState 之后发生了什么

在代码中调用 setState 函数之后,React 会将传入的参数与之前的状态进行合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。在 React 得到元素树之后,React 会计算出新的树和老的树之间的差异,然后根据差异对界面进行最小化重新渲染。通过 diff 算法,React 能够精确制导哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。


  • 在 setState 的时候,React 会为当前节点创建一个 updateQueue 的更新列队。

  • 然后会触发 reconciliation 过程,在这个过程中,会使用名为 Fiber 的调度算法,开始生成新的 Fiber 树, Fiber 算法的最大特点是可以做到异步可中断的执行。

  • 然后 React Scheduler 会根据优先级高低,先执行优先级高的节点,具体是执行 doWork 方法。

  • 在 doWork 方法中,React 会执行一遍 updateQueue 中的方法,以获得新的节点。然后对比新旧节点,为老节点打上 更新、插入、替换 等 Tag。

  • 当前节点 doWork 完成后,会执行 performUnitOfWork 方法获得新节点,然后再重复上面的过程。

  • 当所有节点都 doWork 完成后,会触发 commitRoot 方法,React 进入 commit 阶段。

  • 在 commit 阶段中,React 会根据前面为各个节点打的 Tag,一次性更新整个 dom 元素


用户头像

beifeng1996

关注

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

还未添加个人简介

评论

发布
暂无评论
前端一面react面试题(持续更新中)_前端_beifeng1996_InfoQ写作社区