React18高级面试题

621 阅读5分钟

React 一共有 5 种主流的通信方式:

  1. props 和 callback 方式
  2. ref 方式。
  3. React-redux 或 React-mobx 状态管理方式。
  4. context 上下文方式。
  5. event bus 事件总线。
  • 需要手动绑定和解绑。
  • 对于小型项目还好,但是对于中大型项目,这种方式的组件通信,会造成牵一发动全身的影响,而且后期难以维护,组件之间的状态也是未知的。
  • 一定程度上违背了 React 数据流向原则。

1. 理解jsx

jsx可控性

 /* 第一步 : 扁平化 children  */
    const flatChildren = React.Children.toArray(children);
    console.log(flatChildren);
    /* 第二步 : 除去文本节点 */
    const newChildren = [];
    React.Children.forEach(flatChildren, (item) => {
      if (React.isValidElement(item)) newChildren.push(item);
    });
    /* 第三步,插入新的节点 */
    const lastChildren = React.createElement(
      `div`,
      { className: "last" },
      `say goodbye`
    );
    newChildren.push(lastChildren);

    /* 第四步:修改容器节点 */
    const newReactElement = React.cloneElement(
      reactElement,
      {},
      ...newChildren
    );
    return newReactElement;

Babel 解析 JSX 流程

const fs = require('fs')
const babel = require("@babel/core")

     /* 第一步:模拟读取文件内容。 */
fs.readFile('./element.js',(e,data)=>{ 
    const code = data.toString('utf-8')
    /* 第二步:转换 jsx 文件 */
    const result = babel.transformSync(code, {
        plugins: ["@babel/plugin-transform-react-jsx"],
    });
    /* 第三步:模拟重新写入内容。 */
    fs.writeFile('./element.js',result.code,function(){})
})

2. 为什么会有state同步现象

如:

reduce = () => {
  setTimeout(() => {
    console.log('reduce setState前的count', this.state.count)
    this.setState({
      count: this.state.count - 1
    });
    console.log('reduce setState后的count', this.state.count)
  },0);
}
打印:2,1

因为setTimeout 中的 setState脱离了React事件系统,在 React 事件执行之前通过 isBatchingEventUpdates=true 打开开关,开启事件批量更新,当该事件结束,再通过 isBatchingEventUpdates = false; 关闭开关。所以批量更新规则被打破

enqueueSetState(){
  /* 每一次调用`setState`,react 都会创建一个 update 里面保存了 */
  const update = createUpdate(expirationTime, suspenseConfig);
  /* callback 可以理解为 setState 回调函数,第二个参数 */
  callback && (update.callback = callback) 
  /* enqueueUpdate 把当前的update 传入当前fiber,待更新队列中 */
  enqueueUpdate(fiber, update); 
  /* 开始调度更新 */
  scheduleUpdateOnFiber(fiber, expirationTime);
}

/* 在`legacy`模式下,所有的事件都将经过此函数同一处理 */
function dispatchEventForLegacyPluginEventSystem(){
  // handleTopLevel 事件处理函数
  batchedEventUpdates(handleTopLevel, bookKeeping);
}

function batchedEventUpdates(fn,a){
  /* 开启批量更新  */
 isBatchingEventUpdates = true;
try {
  /* 这里执行了的事件处理函数, 比如在一次点击事件中触发setState,那么它将在这个函数内执行 */
  return batchedEventUpdatesImpl(fn, a, b);
} finally {
  /* try 里面 return 不会影响 finally 执行  */
  /* 完成一次事件,批量更新  */
  isBatchingEventUpdates = false;
}
  • unstable_batchedUpdates
  • flushSync

3. 函数组件和类组件本质的区别是什么呢?

对于类组件来说,底层只需要实例化一次,实例中保存了组件的 state 等状态。对于每一次更新只需要调用 render 方法以及对应的生命周期就可以了。但是在函数组件中,每一次更新都是一次新的函数执行,一次函数组件的更新,里面的变量会重新声明。

为了能让函数组件可以保存一些状态,执行一些副作用钩子,React Hooks 应运而生,它可以帮助记录 React 中组件的状态,处理一些额外的副作用。

  • 类组件this指向问题
class Index extends React.Component {
  constructor(...arg) {
    super(...arg); /* 执行 react 底层 Component 函数 */
  }
  state = {}; /* state */
  static number = 1; /* 内置静态属性 */
  handleClick = () => console.log(111); /* 方法: 箭头函数方法直接绑定在this实例上 */
  componentDidMount() {
    /* 生命周期 */
    console.log(Index.number, Index.number1); // 打印 1 , 2
  }
  render() {
    /* 渲染函数 */
    return (
      <div style={{ marginTop: '50px' }} onClick={this.handerClick}>
        hello,React!
      </div>
    );
  }
}
Index.number1 = 2; /* 外置静态属性 */
Index.prototype.handleClick = () => console.log(222); /* 方法: 绑定在 Index 原型链的 方法*/

答:结果是 111 。

因为在 class 类内部,箭头函数是直接绑定在实例对象上的,而第二个 handleClick 是绑定在 prototype 原型链上的,它们的优先级是:实例对象上方法属性 > 原型链对象上方法属性。

  • 函数组件prototype
function Index(){
  console.log(Index.number) // 打印 1 
  const [ message , setMessage  ] = useState('hello,world') /* hooks  */
  return <div onClick={() => setMessage('let us learn React!')  } > { message } </div> /* 返回值 作为渲染ui */
}
Index.number = 1 /* 绑定静态属性 */

注意:不要尝试给函数组件 prototype 绑定属性或方法,即使绑定了也没有任何作用,因为通过上面源码中 React 对函数组件的调用,是采用直接执行函数的方式,而不是通过new的方式。

4. 生命周期

类组件:

  1. 组件挂载时: constructor(初始化state、绑定函数this、函数防抖) -> static getDerivedStateFromProps(根据prop、state返回新的state,返回null时state发生变化) --> render -> componentDidMount
  2. 组件更新时:static getDerivedStateFromProps -> shouldComponentUpdate(nextProps, nextState) => boolean(性能优化点,返回false不更新) -> render -> getSnapshotBeforeUpdate(prevProps, prevState)(在dom更新前执行,返回值将作为第三个参数传递到componetDidUpdate)-> componentDidUpdate
  3. 组件卸载时:componetWillUnMount

函数组件:

一句话概括如何选择 useEffect 和 useLayoutEffect :修改 DOM ,改变布局就用 useLayoutEffect ,其他情况就用 useEffect。因为useEffect是异步的,执行是在浏览器绘制视图之后,接下来又改 DOM ,就可能会导致浏览器再次回流和重绘。而且由于两次绘制,视图上可能会造成闪现突兀的效果。

  • useInsertionEffect 主要是解决 CSS-in-JS 在渲染中注入样式的性能问题。

应用:

class ScrollView extends React.Component {
  /* -----自定义事件---- */
  /* 控制滚动条滚动 */
  handerScroll = (e) => {
    const { scroll } = this.props;
    scroll && scroll(e);
    this.handerScrolltolower();
  };
  /* 判断滚动条是否到底部 */
  handerScrolltolower() {
    const { scrolltolower } = this.props;
    const { scrollHeight, scrollTop, offsetHeight } = this.node;
    if (scrollHeight === scrollTop + offsetHeight) {
      /* 到达容器底部位置 */
      scrolltolower && scrolltolower();
    }
  }
  node = null;

  /* ---——---生命周期------- */
  constructor(props) {
    super(props);
    this.state = {
      /* 初始化 Data */ list: [],
    };
    this.handerScrolltolower = debounce(
      this.handerScrolltolower,
      200
    ); /* 防抖处理 */
  }
  /* 接收props, 合并到state */
  static getDerivedStateFromProps(newProps) {
    const { data } = newProps;
    console.log(data, "asdsadsadsad");
    return {
      list: data.list || [],
    };
  }
  /* 性能优化,只有列表数据变化,渲染列表 */
  shouldComponentUpdate(newProps, newState) {
    return newState.list !== this.state.list;
  }
  /* 获取更新前容器高度 */
  getSnapshotBeforeUpdate() {
    return this.node.scrollHeight;
  }
  /* 获取更新后容器高度 */
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log("scrollView容器高度变化:", this.node.scrollHeight - snapshot);
  }
  /* 绑定事件监听器 - 监听scorll事件 */
  componentDidMount() {
    this.node.addEventListener("scroll", () => {
      console.log("-------滚动条滚动------");
    });
  }
  /* 解绑事件监听器 */
  componentWillUnmount() {
    this.node.removeEventListener("scroll", this.handerScroll);
  }
  render() {
    const { list } = this.state;
    const { component } = this.props;
    return (
      <div className="list_box" ref={(node) => (this.node = node)}>
        <div>
          {list.map((item) =>
            React.createElement(component, { item, key: item.id })
          )}
        </div>
      </div>
    );
  }
}

ref

  • 获取 ref的三种模式
  • forwardRef 转发 Ref