写点什么

一天梳理完 react 面试题

作者:beifeng1996
  • 2022-11-14
    浙江
  • 本文字数:15831 字

    阅读完需:约 52 分钟

react 和 vue 的区别

相同点:


  1. 数据驱动页面,提供响应式的试图组件

  2. 都有 virtual DOM,组件化的开发,通过 props 参数进行父子之间组件传递数据,都实现了 webComponents 规范

  3. 数据流动单向,都支持服务器的渲染 SSR

  4. 都有支持 native 的方法,react 有 React native, vue 有 wexx


不同点:


  1. 数据绑定:Vue 实现了双向的数据绑定,react 数据流动是单向的

  2. 数据渲染:大规模的数据渲染,react 更快

  3. 使用场景:React 配合 Redux 架构适合大规模多人协作复杂项目,Vue 适合小快的项目

  4. 开发风格:react 推荐做法 jsx + inline style 把 html 和 css 都写在 js 了


vue 是采用 webpack +vue-loader 单文件组件格式,html, js, css 同一个文件

哪些方法会触发 React 重新渲染?重新渲染 render 会做些什么?

(1)哪些方法会触发 react 重新渲染?


  • setState()方法被调用


setState 是 React 中最常用的命令,通常情况下,执行 setState 会触发 render。但是这里有个点值得关注,执行 setState 的时候不一定会重新渲染。当 setState 传入 null 时,并不会触发 render。


class App extends React.Component {  state = {    a: 1  };
render() { console.log("render"); return ( <React.Fragement> <p>{this.state.a}</p> <button onClick={() => { this.setState({ a: 1 }); // 这里并没有改变 a 的值 }} > Click me </button> <button onClick={() => this.setState(null)}>setState null</button> <Child /> </React.Fragement> ); }}
复制代码


  • 父组件重新渲染


只要父组件重新渲染了,即使传入子组件的 props 未发生变化,那么子组件也会重新渲染,进而触发 render


(2)重新渲染 render 会做些什么?


  • 会对新旧 VNode 进行对比,也就是我们所说的 Diff 算法。

  • 对新旧两棵树进行一个深度优先遍历,这样每一个节点都会一个标记,在到深度遍历的时候,每遍历到一和个节点,就把该节点和新的节点树进行对比,如果有差异就放到一个对象里面

  • 遍历差异对象,根据差异的类型,根据对应对规则更新 VNode


React 的处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。在 Virtual DOM 没有出现之前,最简单的方法就是直接调用 innerHTML。Virtual DOM 厉害的地方并不是说它比直接操作 DOM 快,而是说不管数据怎么变,都会尽量以最小的代价去更新 DOM。React 将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。当 DOM 树很大时,遍历两棵树进行各种比对还是相当耗性能的,特别是在顶层 setState 一个微小的修改,默认会去遍历整棵树。尽管 React 使用高度优化的 Diff 算法,但是这个过程仍然会损耗性能.

在 React 中组件的 this.state 和 setState 有什么区别?

this.state 通常是用来初始化 state 的,this.setState 是用来修改 state 值的。如果初始化了 state 之后再使用 this.state,之前的 state 会被覆盖掉,如果使用 this.setState,只会替换掉相应的 state 值。所以,如果想要修改 state 的值,就需要使用 setState,而不能直接修改 state,直接修改 state 之后页面是不会更新的。

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 一样,有且仅有一次调用。


参考:前端react面试题详细解答

什么是受控组件和非受控组件

  • 受状态控制的组件,必须要有 onChange 方法,否则不能使用 受控组件可以赋予默认值(官方推荐使用 受控组件) 实现双向数据绑定


class Input extends Component{    constructor(){        super();        this.state = {val:'100'}    }    handleChange = (e) =>{ //e是事件源        let val = e.target.value;        this.setState({val});    };    render(){        return (<div>            <input type="text" value={this.state.val} onChange={this.handleChange}/>            {this.state.val}        </div>)    }}
复制代码


  • 非受控也就意味着我可以不需要设置它的 state 属性,而通过 ref 来操作真实的 DOM


class Sum extends Component{    constructor(){        super();        this.state =  {result:''}    }    //通过ref设置的属性 可以通过this.refs获取到对应的dom元素    handleChange = () =>{        let result = this.refs.a.value + this.b.value;        this.setState({result});    };    render(){        return (            <div onChange={this.handleChange}>                <input type="number" ref="a"/>                {/*x代表的真实的dom,把元素挂载在了当前实例上*/}                <input type="number" ref={(x)=>{                    this.b = x;                }}/>                {this.state.result}            </div>        )    }}
复制代码

React 的事件和普通的 HTML 事件有什么不同?

区别:


  • 对于事件名称命名方式,原生事件为全小写,react 事件采用小驼峰;

  • 对于事件函数处理语法,原生事件为字符串,react 事件为函数;

  • react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefault()来阻止默认行为。


合成事件是 react 模拟原生 DOM 事件所有能力的一个事件对象,其优点如下:


  • 兼容所有浏览器,更好的跨平台;

  • 将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。

  • 方便 react 统一管理和事务机制。


事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到 document 上合成事件才会执行。

Redux 中异步的请求怎么处理

可以在 componentDidmount 中直接进⾏请求⽆须借助 redux。但是在⼀定规模的项⽬中,上述⽅法很难进⾏异步流的管理,通常情况下我们会借助 redux 的异步中间件进⾏异步处理。redux 异步流中间件其实有很多,当下主流的异步中间件有两种 redux-thunk、redux-saga。


(1)使用 react-thunk 中间件


redux-thunk 优点:


  • 体积⼩: redux-thunk 的实现⽅式很简单,只有不到 20⾏代码

  • 使⽤简单: redux-thunk 没有引⼊像 redux-saga 或者 redux-observable 额外的范式,上⼿简单


redux-thunk 缺陷:


  • 样板代码过多: 与 redux 本身⼀样,通常⼀个请求需要⼤量的代码,⽽且很多都是重复性质的

  • 耦合严重: 异步操作与 redux 的 action 偶合在⼀起,不⽅便管理

  • 功能孱弱: 有⼀些实际开发中常⽤的功能需要⾃⼰进⾏封装


使用步骤:


  • 配置中间件,在 store 的创建中配置


import {createStore, applyMiddleware, compose} from 'redux';import reducer from './reducer';import thunk from 'redux-thunk'
// 设置调试工具const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;// 设置中间件const enhancer = composeEnhancers( applyMiddleware(thunk));
const store = createStore(reducer, enhancer);
export default store;
复制代码


  • 添加一个返回函数的 actionCreator,将异步请求逻辑放在里面


/**  发送get请求,并生成相应action,更新store的函数  @param url {string} 请求地址  @param func {function} 真正需要生成的action对应的actionCreator  @return {function} */// dispatch为自动接收的store.dispatch函数 export const getHttpAction = (url, func) => (dispatch) => {    axios.get(url).then(function(res){        const action = func(res.data)        dispatch(action)    })}
复制代码


  • 生成 action,并发送 action


componentDidMount(){    var action = getHttpAction('/getData', getInitTodoItemAction)    // 发送函数类型的action时,该action的函数体会自动执行    store.dispatch(action)}
复制代码


(2)使用 redux-saga 中间件


redux-saga 优点:


  • 异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中

  • action 摆脱 thunk function: dispatch 的参数依然是⼀个纯粹的 action (FSA),⽽不是充满 “⿊魔法” thunk function

  • 异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理

  • 功能强⼤: redux-saga 提供了⼤量的 Saga 辅助函数和 Effect 创建器供开发者使⽤,开发者⽆须封装或者简单封装即可使⽤

  • 灵活: redux-saga 可以将多个 Saga 可以串⾏/并⾏组合起来,形成⼀个⾮常实⽤的异步 flow

  • 易测试,提供了各种 case 的测试⽅案,包括 mock task,分⽀覆盖等等


redux-saga 缺陷:


  • 额外的学习成本: redux-saga 不仅在使⽤难以理解的 generator function,⽽且有数⼗个 API,学习成本远超 redux-thunk,最重要的是你的额外学习成本是只服务于这个库的,与 redux-observable 不同,redux-observable 虽然也有额外学习成本但是背后是 rxjs 和⼀整套思想

  • 体积庞⼤: 体积略⼤,代码近 2000⾏,min 版 25KB 左右

  • 功能过剩: 实际上并发控制等功能很难⽤到,但是我们依然需要引⼊这些代码

  • ts⽀持不友好: yield⽆法返回 TS 类型


redux-saga 可以捕获 action,然后执行一个函数,那么可以把异步代码放在这个函数中,使用步骤如下:


  • 配置中间件


import {createStore, applyMiddleware, compose} from 'redux';import reducer from './reducer';import createSagaMiddleware from 'redux-saga'import TodoListSaga from './sagas'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;const sagaMiddleware = createSagaMiddleware()
const enhancer = composeEnhancers( applyMiddleware(sagaMiddleware));
const store = createStore(reducer, enhancer);sagaMiddleware.run(TodoListSaga)
export default store;
复制代码


  • 将异步请求放在 sagas.js 中


import {takeEvery, put} from 'redux-saga/effects'import {initTodoList} from './actionCreator'import {GET_INIT_ITEM} from './actionTypes'import axios from 'axios'
function* func(){ try{ // 可以获取异步返回数据 const res = yield axios.get('/getData') const action = initTodoList(res.data) // 将action发送到reducer yield put(action) }catch(e){ console.log('网络请求失败') }}
function* mySaga(){ // 自动捕获GET_INIT_ITEM类型的action,并执行func yield takeEvery(GET_INIT_ITEM, func)}
export default mySaga
复制代码


  • 发送 action


componentDidMount(){  const action = getInitTodoItemAction()  store.dispatch(action)}
复制代码

对 React Hook 的理解,它的实现原理是什么

React-Hooks 是 React 团队在 React 组件开发实践中,逐渐认知到的一个改进点,这背后其实涉及对类组件函数组件两种组件形式的思考和侧重。


(1)类组件: 所谓类组件,就是基于 ES6 Class 这种写法,通过继承 React.Component 得来的 React 组件。以下是一个类组件:


class DemoClass extends React.Component {  state = {    text: ""  };  componentDidMount() {    //...  }  changeText = (newText) => {    this.setState({      text: newText    });  };
render() { return ( <div className="demoClass"> <p>{this.state.text}</p> <button onClick={this.changeText}>修改</button> </div> ); }}

复制代码


可以看出,React 类组件内部预置了相当多的“现成的东西”等着我们去调度/定制,state 和生命周期就是这些“现成东西”中的典型。要想得到这些东西,难度也不大,只需要继承一个 React.Component 即可。


当然,这也是类组件的一个不便,它太繁杂了,对于解决许多问题来说,编写一个类组件实在是一个过于复杂的姿势。复杂的姿势必然带来高昂的理解成本,这也是我们所不想看到的。除此之外,由于开发者编写的逻辑在封装后是和组件粘在一起的,这就使得类组件内部的逻辑难以实现拆分和复用。


(2)函数组件:函数组件就是以函数的形态存在的 React 组件。早期并没有 React-Hooks,函数组件内部无法定义和维护 state,因此它还有一个别名叫“无状态组件”。以下是一个函数组件:


function DemoFunction(props) {  const { text } = props  return (    <div className="demoFunction">      <p>{`函数组件接收的内容:[${text}]`}</p>    </div>  );}
复制代码


相比于类组件,函数组件肉眼可见的特质自然包括轻量、灵活、易于组织和维护、较低的学习成本等。


通过对比,从形态上可以对两种组件做区分,它们之间的区别如下:


  • 类组件需要继承 class,函数组件不需要;

  • 类组件可以访问生命周期方法,函数组件不能;

  • 类组件中可以获取到实例化后的 this,并基于这个 this 做各种各样的事情,而函数组件不可以;

  • 类组件中可以定义并维护 state(状态),而函数组件不可以;


除此之外,还有一些其他的不同。通过上面的区别,我们不能说谁好谁坏,它们各有自己的优势。在 React-Hooks 出现之前,类组件的能力边界明显强于函数组件。


实际上,类组件和函数组件之间,是面向对象和函数式编程这两套不同的设计思想之间的差异。而函数组件更加契合 React 框架的设计理念: React 组件本身的定位就是函数,一个输入数据、输出 UI 的函数。作为开发者,我们编写的是声明式的代码,而 React 框架的主要工作,就是及时地把声明式的代码转换为命令式的 DOM 操作,把数据层面的描述映射到用户可见的 UI 变化中去。这就意味着从原则上来讲,React 的数据应该总是紧紧地和渲染绑定在一起的,而类组件做不到这一点。函数组件就真正地将数据和渲染绑定到了一起。函数组件是一个更加匹配其设计理念、也更有利于逻辑拆分与重用的组件表达形式。


为了能让开发者更好的的去编写函数式组件。于是,React-Hooks 便应运而生。


React-Hooks 是一套能够使函数组件更强大、更灵活的“钩子”。


函数组件比起类组件少了很多东西,比如生命周期、对 state 的管理等。这就给函数组件的使用带来了非常多的局限性,导致我们并不能使用函数这种形式,写出一个真正的全功能的组件。而 React-Hooks 的出现,就是为了帮助函数组件补齐这些(相对于类组件来说)缺失的能力。


如果说函数组件是一台轻巧的快艇,那么 React-Hooks 就是一个内容丰富的零部件箱。“重装战舰”所预置的那些设备,这个箱子里基本全都有,同时它还不强制你全都要,而是允许你自由地选择和使用你需要的那些能力,然后将这些能力以 Hook(钩子)的形式“钩”进你的组件里,从而定制出一个最适合你的“专属战舰”。

React 中什么是受控组件和非控组件?

(1)受控组件 在使用表单来收集用户输入时,例如<input><select><textearea>等元素都要绑定一个 change 事件,当表单的状态发生变化,就会触发 onChange 事件,更新组件的 state。这种组件在 React 中被称为受控组件,在受控组件中,组件渲染出的状态与它的 value 或 checked 属性相对应,react 通过这种方式消除了组件的局部状态,使整个状态可控。react 官方推荐使用受控表单组件。


受控组件更新 state 的流程:


  • 可以通过初始 state 中设置表单的默认值

  • 每当表单的值发生变化时,调用 onChange 事件处理器

  • 事件处理器通过事件对象 e 拿到改变后的状态,并更新组件的 state

  • 一旦通过 setState 方法更新 state,就会触发视图的重新渲染,完成表单组件的更新


受控组件缺陷: 表单元素的值都是由 React 组件进行管理,当有多个输入框,或者多个这种组件时,如果想同时获取到全部的值就必须每个都要编写事件处理函数,这会让代码看着很臃肿,所以为了解决这种情况,出现了非受控组件。


(2)非受控组件 如果一个表单组件没有 value props(单选和复选按钮对应的是 checked props)时,就可以称为非受控组件。在非受控组件中,可以使用一个 ref 来从 DOM 获得表单值。而不是为每个状态更新编写一个事件处理程序。


React 官方的解释:


要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以使用 ref 来从 DOM 节点中获取表单数据。因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。


例如,下面的代码在非受控组件中接收单个属性:


class NameForm extends React.Component {  constructor(props) {    super(props);    this.handleSubmit = this.handleSubmit.bind(this);  }  handleSubmit(event) {    alert('A name was submitted: ' + this.input.value);    event.preventDefault();  }  render() {    return (      <form onSubmit={this.handleSubmit}>        <label>          Name:          <input type="text" ref={(input) => this.input = input} />        </label>        <input type="submit" value="Submit" />      </form>    );  }}
复制代码


总结: 页面中所有输入类的 DOM 如果是现用现取的称为非受控组件,而通过 setState 将输入的值维护到了 state 中,需要时再从 state 中取出,这里的数据就受到了 state 的控制,称为受控组件。

React-Router 怎么设置重定向?

使用<Redirect>组件实现路由的重定向:


<Switch>  <Redirect from='/users/:id' to='/users/profile/:id'/>  <Route path='/users/profile/:id' component={Profile}/></Switch>
复制代码


当请求 /users/:id 被重定向去 '/users/profile/:id'


  • 属性 from: string:需要匹配的将要被重定向路径。

  • 属性 to: string:重定向的 URL 字符串

  • 属性 to: object:重定向的 location 对象

  • 属性 push: bool:若为真,重定向操作将会把新地址加入到访问历史记录里面,并且无法回退到前面的页面。

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.createClass 和 extends Component 的区别有哪些?

React.createClass 和 extends Component 的 bai 区别主要在于:


(1)语法区别


  • createClass 本质上是一个工厂函数,extends 的方式更加接近最新的 ES6 规范的 class 写法。两种方式在语法上的差别主要体现在方法的定义和静态属性的声明上。

  • createClass 方式的方法定义使用逗号,隔开,因为 creatClass 本质上是一个函数,传递给它的是一个 Object;而 class 的方式定义方法时务必谨记不要使用逗号隔开,这是 ES6 class 的语法规范。


(2)propType 和 getDefaultProps


  • React.createClass:通过 proTypes 对象和 getDefaultProps()方法来设置和获取 props.

  • React.Component:通过设置两个属性 propTypes 和 defaultProps


(3)状态的区别


  • React.createClass:通过 getInitialState()方法返回一个包含初始值的对象

  • React.Component:通过 constructor 设置初始状态


(4)this 区别


  • React.createClass:会正确绑定 this

  • React.Component:由于使用了 ES6,这里会有些微不同,属性并不会自动绑定到 React 类的实例上。


(5)Mixins


  • React.createClass:使用 React.createClass 的话,可以在创建组件时添加一个叫做 mixins 的属性,并将可供混合的类的集合以数组的形式赋给 mixins。

  • 如果使用 ES6 的方式来创建组件,那么 React mixins 的特性将不能被使用了。

虚拟 DOM 的引入与直接操作原生 DOM 相比,哪一个效率更高,为什么

虚拟 DOM 相对原生的 DOM 不一定是效率更高,如果只修改一个按钮的文案,那么虚拟 DOM 的操作无论如何都不可能比真实的 DOM 操作更快。在首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,虚拟 DOM 也会比 innerHTML 插入慢。它能保证性能下限,在真实 DOM 操作的时候进行针对性的优化时,还是更快的。所以要根据具体的场景进行探讨。


在整个 DOM 操作的演化过程中,其实主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物。虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。**虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。

为什么列表循环渲染的 key 最好不要用 index

举例说明


变化前数组的值是[1,2,3,4],key就是对应的下标:0,1,2,3变化后数组的值是[4,3,2,1],key对应的下标也是:0,1,2,3
复制代码


  • 那么 diff 算法在变化前的数组找到 key =0 的值是 1,在变化后数组里找到的 key=0 的值是 4

  • 因为子元素不一样就重新删除并更新

  • 但是如果加了唯一的 key,如下


变化前数组的值是[1,2,3,4],key就是对应的下标:id0,id1,id2,id3变化后数组的值是[4,3,2,1],key对应的下标也是:id3,id2,id1,id0
复制代码


  • 那么 diff 算法在变化前的数组找到 key =id0 的值是 1,在变化后数组里找到的 key=id0 的值也是 1

  • 因为子元素相同,就不删除并更新,只做移动操作,这就提升了性能

为什么 React 要用 JSX?

JSX 是一个 JavaScript 的语法扩展,或者说是一个类似于 XML 的 ECMAScript 语法扩展。它本身没有太多的语法定义,也不期望引入更多的标准。


其实 React 本身并不强制使用 JSX。在没有 JSX 的时候,React 实现一个组件依赖于使用 React.createElement 函数。代码如下:


class Hello extends React.Component {  render() {    return React.createElement(        'div',        null,         `Hello ${this.props.toWhat}`      );  }}ReactDOM.render(  React.createElement(Hello, {toWhat: 'World'}, null),  document.getElementById('root'));
复制代码


而 JSX 更像是一种语法糖,通过类似 XML 的描述方式,描写函数对象。在采用 JSX 之后,这段代码会这样写:


class Hello extends React.Component {  render() {    return <div>Hello {this.props.toWhat}</div>;  }}ReactDOM.render(  <Hello toWhat="World" />,  document.getElementById('root'));
复制代码


通过对比,可以清晰地发现,代码变得更为简洁,而且代码结构层次更为清晰。


因为 React 需要将组件转化为虚拟 DOM 树,所以在编写代码时,实际上是在手写一棵结构树。而 XML 在树结构的描述上天生具有可读性强的优势。


但这样可读性强的代码仅仅是给写程序的同学看的,实际上在运行的时候,会使用 Babel 插件将 JSX 语法的代码还原为 React.createElement 的代码。


总结: JSX 是一个 JavaScript 的语法扩展,结构类似 XML。JSX 主要用于声明 React 元素,但 React 中并不强制使用 JSX。即使使用了 JSX,也会在构建过程中,通过 Babel 插件编译为 React.createElement。所以 JSX 更像是 React.createElement 的一种语法糖。


React 团队并不想引入 JavaScript 本身以外的开发体系。而是希望通过合理的关注点分离保持组件开发的纯粹性。

当调用 setState 的时候,发生了什么操作?**

当调用 setState 时, React 做的第一件事是将传递给 setState 的对象合并到组件的当前状态,这将启动一个称为和解( reconciliation)的过程。和解的最终目标是,根据这个新的状态以最有效的方式更新 DOM。为此, React 将构建一个新的 React 虚拟 DOM 树(可以将其视为页面 DOM 元素的对象表示方式)。一旦有了这个 DOM 树,为了弄清 DOM 是如何响应新的状态而改变的, React 会将这个新树与上一个虚拟 DOM 树比较。这样做, React 会知道发生的确切变化,并且通过了解发生的变化后,在绝对必要的情况下进行更新 DOM,即可将因操作 DOM 而占用的空间最小化。

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 数据持久化有什么实践吗?

封装数据持久化组件:


let storage={    // 增加    set(key, value){        localStorage.setItem(key, JSON.stringify(value));    },    // 获取    get(key){        return JSON.parse(localStorage.getItem(key));    },    // 删除    remove(key){        localStorage.removeItem(key);    }};export default Storage;
复制代码


在 React 项目中,通过 redux 存储全局数据时,会有一个问题,如果用户刷新了网页,那么通过 redux 存储的全局数据就会被全部清空,比如登录信息等。这时就会有全局数据持久化存储的需求。首先想到的就是 localStorage,localStorage 是没有时间限制的数据存储,可以通过它来实现数据的持久化存储。


但是在已经使用 redux 来管理和存储全局数据的基础上,再去使用 localStorage 来读写数据,这样不仅是工作量巨大,还容易出错。那么有没有结合 redux 来达到持久数据存储功能的框架呢?当然,它就是 redux-persist。redux-persist 会将 redux 的 store 中的数据缓存到浏览器的 localStorage 中。其使用步骤如下:


(1)首先要安装 redux-persist:


npm i redux-persist
复制代码


(2)对于 reducer 和 action 的处理不变,只需修改 store 的生成代码,修改如下:


import {createStore} from 'redux'import reducers from '../reducers/index'import {persistStore, persistReducer} from 'redux-persist';import storage from 'redux-persist/lib/storage';import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';const persistConfig = {    key: 'root',    storage: storage,    stateReconciler: autoMergeLevel2 // 查看 'Merge Process' 部分的具体情况};const myPersistReducer = persistReducer(persistConfig, reducers)const store = createStore(myPersistReducer)export const persistor = persistStore(store)export default store
复制代码


(3)在 index.js 中,将 PersistGate 标签作为网页内容的父标签:


import React from 'react';import ReactDOM from 'react-dom';import {Provider} from 'react-redux'import store from './redux/store/store'import {persistor} from './redux/store/store'import {PersistGate} from 'redux-persist/lib/integration/react';ReactDOM.render(<Provider store={store}>            <PersistGate loading={null} persistor={persistor}>                {/*网页内容*/}            </PersistGate>        </Provider>, document.getElementById('root'));
复制代码


这就完成了通过 redux-persist 实现 React 持久化本地数据存储的简单应用。

React 如何判断什么时候重新渲染组件?

组件状态的改变可以因为props的改变,或者直接通过setState方法改变。组件获得新的状态,然后 React 决定是否应该重新渲染组件。只要组件的 state 发生变化,React 就会对组件进行重新渲染。这是因为 React 中的shouldComponentUpdate方法默认返回true,这就是导致每次更新都重新渲染的原因。


当 React 将要渲染组件时会执行shouldComponentUpdate方法来看它是否返回true(组件应该更新,也就是重新渲染)。所以需要重写shouldComponentUpdate方法让它根据情况返回true或者false来告诉 React 什么时候重新渲染什么时候跳过重新渲染。

高阶组件存在的问题

  • 静态方法丢失(必须将静态方法做拷贝)

  • refs 属性不能透传(如果你向一个由高阶组件创建的组件的元素添加ref引用,那么ref指向的是最外层容器组件实例的,而不是被包裹的WrappedComponent组件。)

  • 反向继承不能保证完整的子组件树被解析

  • React 组件有两种形式,分别是 class 类型和 function 类型(无状态组件)。


我们知道反向继承的渲染劫持可以控制 WrappedComponent 的渲染过程,也就是说这个过程中我们可以对 elements tree、 state、 props 或 render() 的结果做各种操作。


但是如果渲染 elements tree 中包含了 function 类型的组件的话,这时候就不能操作组件的子组件了。


用户头像

beifeng1996

关注

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

还未添加个人简介

评论

发布
暂无评论
一天梳理完react面试题_React_beifeng1996_InfoQ写作社区