前端面试锦集之 react 万字长文面试总结

一、react 面试题

  1. redux 中间件的原理的理解,答案如下所示:
  • redux 是一个应用数据流框架,主要是解决了组件间状态共享的问题,原理是集中式管理,主要有三个核心方法,actionstorereducer
  • 工作流程是 view 调用 storedispatch 接收 action 传入 storereducer 进行 state 操作,view 通过 store 提供的 getState 获取最新的数据
  • 新增 state,对状态的管理更加明确,通过 redux,流程更加规范了,减少手动编码量,提高了编码效率,同时缺点时当数据更新时有时候组件不需要,但是也要重新绘制,有些影响效率。一般情况下,我们在构建多交互,多数据流的复杂项目应用时才会使用它们
  • 常用的一些 redux 中间件,如下所示:
    • redux-thunk:处理异步操作
    • redux-saga:处理异步操作
    • redux-promise:处理异步操作,actionCreator 的返回值是promise
  • 中间件的执行原理和koa中间件的执行原理类似,但是不是洋葱型的,而是半个洋葱,因为redux是单向执行的。当你应用了中间件,在触发一个action操作的时候,action操作就会经过先经过中间件,最终再形成dispatch(action)。一个触发一个action动作的时候,代码的执行逻辑。thunk是允许dispatch一个函数,而不是一个对象
  • thunk中间件的内部,代码如下所示:
function createThunkMiddleware(extraArgument) {
     

  return ({
      dispatch, getState }) => next => action => {
     
    // 如果是函数,就执行函数
    if (typeof action === 'function') {
     
        return action(dispatch, getState, extraArgument);
    }
    // 如果不是,执行下一个中间件
    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

  • 中间件的内部逻辑,代码如下所示:
const store = createStore(reducer, preloadedState, enchancer);

// 如果没有中间件,正常触发一个action;
// 如果有中间件的时候 creatStore内部的执行逻辑是这样的
// enchancer 就是你应用的中间件,调用applyMiddleware得到的组合中间件

function applyMiddleware(...middlewares) {
     
  return createStore => (...args) => {
     
  
    // 该创建的store还是要创建的,只传入了两个参数,没有中间件,得到的是正常的store
    const store = createStore(...args)
    let dispatch = () => {
     
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }
    // 把getState、dispatch传给中间件
    const middlewareAPI = {
     
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 下面方法返回来了一个函数数组,中间件被剥离到
    // next => {}
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 再执行下面的,中间件就被剥离到
    // action => {}
    dispatch = compose(...chain)(store.dispatch)

    return {
     
      ...store,
      dispatch
    }
  }
}

// 下面得到的结果是 
// 假设中间件为 a b
// a(b(store.dispatch))
return enchancer(createStore)(reducer, preloadedState)// 结合上面thunk的源码{
      dispatch, getState }) => next => action => {
     
    if (typeof action === 'function') {
     
        return action(dispatch, getState);
    }
    return next(action);
  };

  • 经过上面的操作后,经过中间件包装后的store的样子,假设中间件为 a b c,每一个中间件配发了一个原始storedispatch,中间件函数嵌套执行,如下所示
    • 没有中间件,代码如下
      store = {
               
          dispatch,
          ... ...,
          subscribe,
          getState
      }
      
      
    • 经过中间包装后,代码如下
      store = {
               
          dispatch: a(b((store.dispatch))),
          ... ...,
          subscribe,
          getState
      }
      
      
  • redux-thunk就是一个封装函数,允许store.dispatch一个函数,compsoe内部,如下所示:
// compose本身并不改变函数的执行,将函数组合后又返回了一个函数
    function compose(...funcs) {
     
      if (funcs.length === 0) {
     
        return arg => arg
      }
      if (funcs.length === 1) {
     
        return funcs[0]
      }
        return funcs.reduce((a, b) => (...args) => a(b(...args)))
    }

  1. 你会把数据统一放到 redux 中管理,还是共享数据放在 redux 中管理,答案如下所示:
  • 不是react native,所有数据都要放在redux中管理。如果只是把共用的数据放在redux中,一个组件中会既有state、props和redux存储数据,那么当页面出现问题,要从state、props和redux三个方面检查,开发程序是很快的,但是最费时间的是程序后期的可维护性和代码的可调节性。如果数据都放在redux中管理,项目出错以后,就只用检查redux,定位错位很快。不要想着state中的数据只会供一个组件使用,在项目越来越大的时候,说不准别的组件会需要使用,redux中可以存储5GB的数据。所以,能用redux的时候一定要用redux,对于后期的维护来说很方便

  • immutable,当你把reduximmutable这个库结合使用的时候,你整个项目的性能会达到最优,而且非常非常简单。没有数据臃肿的顾虑,你不存在redux中,你也需要存储在state或者props中。

  1. componentWillReceiveProps 的调用时机,答案如下所示:
  • 父组件中改变了props传值时触发的函数
  • props改变的时候才会调用,父组件第一次往子组件传值的时候,不会调用
  1. react 性能优化的最佳实践,答案如下所示:
  • PureComponent,自带shouldcomponentupdate,是一个浅比较,代码如下所示:
class Test extends React.PureComponents {
     
    constructor(props) {
     
        super(props)
    }
    
    render() {
     
        return <div>hello</div>
    }
}
  • 通过与immutable.js库的结合,完美的解决react的性能问题
  • 优化的核心是减少不必要的渲染
  • 增加shouldComponentUpdate钩子对新旧propsstate进行比较,如果值相同则阻止更新,避免不必要的渲染,或者使用PureReactComponent替代Component,其内部已经封装了shouldComponentUpdate的浅比较逻辑
  • 对于列表或其他结构相同的节点,为其中的每一项增加唯一key属性,以方便Reactdiff算法中对该节点的复用,减少节点的创建和删除操作
  • render函数中减少类似onClick={() => {doSomething()}}的写法,每次调用render函数时均会创建一个新的函数,即使内容没有发生任何变化,也会导致节点没必要的重渲染,建议将函数保存在组件的成员对象中,这样只会创建一次
  • webpack-bundle-analyzer分析当前页面的依赖包,是否存在不合理性
  1. 虚拟dom的理解,虚拟dom会提升代码性能的原因,答案如下所示:
  • 虚拟dom就是真实dom的一个js对象
  • 以前需要两个页面的差异,需要去比对真实dom的比对,真实的dom节点会有事件,属性,还会有各种各样的方法。所以两个真实dom的比对会非常耗性能。于是把dom对象变成js对象,js对象就没有dom对象上乱七八糟的特性了,js对象就比较快。
  1. webpack中,借助loader完成的JSX代码的转化,还是babel,答案如下所示:
  • babel - preset-react 所去完成的转化
  1. 调用setState后,发生的过程,答案如下所示:
  • 调用setState函数之后,react会将传入的参数对象与组件当前的状态合并,然后触发调和过程(Reconciliation),以高效方式根据新的状态构建React元素树并且着手重新渲染整个UI界面
  • React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染,按需渲染,不是全部渲染
  • 调用函数,通过一个函数返回一个对象,代码如下所示:
this.setState({
     
    age: this.state.age + 1
}) // 如果是连续点击一个按钮调用这个setState,会出现数值不是一个一个加上去的,而是会出现一次几个的变化,因为react会把多个setState整合为一个,最后在改变state。

this.setState((prevState) => ({
     
    age: ++ prevState.age
})) // 不管你怎么疯狂的点击按钮,都会一个一个往上加。
  1. setState是异步的,这个点你在什么时候遇到过坑,答案如下所示:
  • 使用setState的时候返回传一个函数,同上所示
  • 对于setState 是异步还是同步主要看是谁在调用它,大部分情况下是异步的,小部分情况是同步的
  • 异步的情况,如下所示:
    • React 代理的合成事件中调用,如 onClick、onChange 事件,这些事件都是 React 为了代理原生事件而封装出来的一整套事件机制,我们称之为合成事件
    • 在钩子函数(生命周期)中调用
    • setState 的异步不是真的异步,setState 本身的执行过程是同步的,是 React 将多个 setState 进行合并和批量更新,导致其看起来像是异步的
  • 同步的情况,如下所示:
    • 在原生事件中调用,代码如下:

             class App extends React.Component{
               
            ...componentDidMount(){
               
                document.querySelector('#A').addEventListener('click',()=>{
               
                    this.setState({
               
                        // 这里的 setState 是同步的
                    })
                })
              }
            }
      
    • 在setTimeout()中调用

  1. refs的作用是什么,你在什么业务场景下使用过refs,答案如下所示:
  • refs的作用是操作dom,访问 DOM 元素或者某个组件实例
  • 在业务场景下使用过refs,展示一个图片,获取图片的宽高,这也是因为react不能直接操作dom
  • 放大镜,获取图片宽高等等
  • 当页面滚动,监听页面滚动的事件,代码如下所示:
class Test extends Component {
     
    // 需求:当页面滚动,监听页面滚动的事件
    constructor(props) {
     
        super(props);
        this.state = {
     
            top: 0
        }
        this.handleWindowScroll = this.handleWindowScroll.bind(this)
    }
    
    handleWindowScroll() {
     
        this.setState(() => ({
     
            top: document.body.scrollTop
        }))
    }
    
    componentDidMount() {
     
        window.addEventListener('scroll', this.handleWindowScroll)
    }
    
    componentWillUnmount() {
     
        window.removeEventListener('scroll', this.handleWindowScroll)
    }
    
    render() {
     
        return <div>{
     this.state.top}</div>
    }
}
  1. ref是一个函数,好处是什么,答案如下所示:
  • 方便react在销毁或者重新渲染组件的时候去有效的去清空ref里面的东西,防止内存泄漏,以后ref不要用字符串的形式了,要用函数式的写法
  • 代码如下所示:
class Test extends Component {
     
  componentDidMount() {
     
    this.elem
  }

  render() {
     
    return <div ref={
     (div) => {
      this.elem = div }}></div> // ref使用的时候最好使用函数
  }
}
  1. 高阶组件的理解,它的本质是什么,答案如下所示:
  • react里面不要去使用继承,为什么,设计模式中有这样一句话“组合优于继承”,react这种组件式的编程,是一种组合类型的设计模式,一定是优于继承的,可维护性是比继承高的多,react中所有问题都是可以利用组件拼合的方法解决的。
  • 高阶组件实际上就是一个函数,接收参数,返回参数。对一个组件进行包装,然后再返回一个组件。为什么对一个组件进行包装呢,因为组件有可能很多地方要用,这个组件的大部分东西在别的地方都可以直接用,只有少数的地方有区别,我们就可以把共用的地方写到高阶组件里面去,而通过往高阶组件里面传递参数,来去动态这个组件在使用时的差异。
  • 高阶组件地狱,新版本的hook解决了这个问题,代码如下所示:
<A>
  <B>
    <C>
      <D />
    </C>
  </B>
</A>
  • 高阶组件其实就是一个函数而已,只不过参数是一个组件而已,返回了一个新的组件。复用组件的业务逻辑 react-redux connect 其实就是一个高阶组件。HOC 是纯函数,没有副作用。纯函数是输入确定,输出就一定确定
  1. 受控组件和非受控组件的区别,答案如下所示:
  • HTML中,类似 ,