React

193 阅读16分钟

路由实现

  • history.pushState 和 history.replaceState popstate
  • hash hashchange

React key的作用

react key 帮助 React 识别哪些元素改变了,比如被添加或删除。一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。

关于 React Re-Render

1、有四个原因会导致组件 re-render:状态变化、父组件 re-render、Context 变化和 Hooks 变化。 2、如何避免父组件 re-render 导致的 re-render?React.memo!使用 React.memo 后的组件只有 props 变更才会触发 re-render。对于 props 很多且没有很多子组件的组件来说,相比 re-render,检查 props 是否变更带来的消耗可能更大。

虚拟dom的渲染原理

为什么使用 vitrual dom

提高开发效率:只需要告诉 React 你想让视图处于什么状态,React 则通过 VitrualDom 确保 DOM 与该状态相匹配。你不必自己去完成属性操作、事件处理、DOM 更新,React 会替你完成这一切。让我们更关注我们的业务逻辑而非 DOM 操作,这一点即可大大提升我们的开发效率

提升性能:VitrualDom 的优势在于 React 的 Diff 算法和批处理策略,React 在页面更新之前,提前计算好了如何进行更新和渲染 DOM。VitrualDom 帮助我们提高了开发效率,在重复渲染时它帮助我们计算如何更高效的更新,而不是它比DOM操作更快

跨浏览器兼容:React 基于 VitrualDom 自己实现了一套自己的事件机制,自己模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,抹平了各个浏览器的事件兼容性问题

跨平台兼容:VitrualDom 为 React 带来了跨平台渲染的能力。以React Native 为例子。React 根据 VitrualDom 画出相应平台的 ui 层,只不过不同平台画的姿势不同而已

虚拟DOM实现原理

1. JSX 和 createElement

在实现一个React组件时可以选择两种编码方式,第一种是使用JSX编写,第二种是直接使用React.createElement编写。JSX只是为 React.createElement(component, props, ...children) 方法提供的语法糖。所有的JSX 代码最后都会转换成React.createElement(...) ,Babel帮助我们完成了这个转换的过程。

2. 创建虚拟dom

  • createElement()函数
    • 处理 props
      • 将特殊属性 ref、key 从config中取出并赋值
      • 将特殊属性 self、source 从config中取出并赋值
      • 将除特殊属性的其他属性取出并赋值给 props
    • 获取子元素
      • 获取子元素的个数 —— 第二个参数后面的所有参数
      • 若只有一个子元素,赋值给 props.children
      • 若有多个子元素,将子元素填充为一个数组赋值给 props.children
    • 处理默认 props
      • 将组件的静态属性 defaultProps 定义的默认 props 进行赋值
  • 返回 ReactElement 对象(所谓的虚拟DOM)有以下几个属性
    • type:元素的类型,可以是原生html类型(字符串),或者自定义组件(函数或class)
    • key:组件的唯一标识,用于Diff算法,下面会详细介绍
    • ref:用于访问原生dom节点
    • props:传入组件的props
    • owner:当前正在构建的Component所属的Component
    • ?typeof:是一个Symbol类型的变量,这个变量可以防止 XSS
    • self:指定当前位于哪个组件实例(非生产环境)
    • _source:指定调试代码来自的文件(fileName)和代码行数(lineNumber)(非生产环境)

虚拟DOM转换为真实DOM

调用 ReactDOM.render(element, container[, callback]) 将组件进行渲染

1. 初始参数处理

  • 将当前组件使用 TopLevelWrapper 进行包裹
  • 判断根结点下是否已经渲染过元素,如果已经渲染过,判断执行更新或者卸载操作
  • 处理 shouldReuseMarkup 变量,该变量表示是否需要重新标记元素
  • 调用将上面处理好的参数传入 _renderNewRootComponent,渲染完成后调用callback
    • 在 _renderNewRootComponent 中调用 instantiateReactComponent 对我们传入的组件进行分类包装
      • ReactDOMEmptyComponent:空组件
      • ReactDOMTextComponent:文本
      • ReactDOMComponent:原生DOM
      • ReactCompositeComponent:自定义React组件
    • 他们都具备以下三个方法
      • construct:用来接收 ReactElement 进行初始化
      • mountComponent:用来生成 ReactElement 对应的真实 DOM 或 DOMLazyTree
      • unmountComponent:卸载 DOM 节点,解绑事件

2. 批处理、事务调用

  • 在 _renderNewRootComponent 中使用 ReactUpdates.batchedUpdates 调用 batchedMountComponentIntoNode 进行批处理
  • 在 batchedMountComponentIntoNode 中,使用 transaction.perform 调用 mountComponentIntoNode 让其基于事务机制进行调用

3. 生成html

  • 在 mountComponentIntoNode 函数中调用 ReactReconciler.mountComponent 生成原生DOM节点
  • mountComponent 内部实际上是调用了过程1生成的四种对象的 mountComponent 方法

4. 渲染html

  • 在 mountComponentIntoNode 函数中调用将上一步生成的 markup 插入 container 容器
  • 在首次渲染时,_mountImageIntoNode 会清空 container 的子节点后调用 DOMLazyTree.insertTreeBefore

针对性的性能优化

在IE(8-11)和Edge浏览器中,一个一个插入无子孙的节点,效率要远高于插入一整个序列化完整的节点树

  • React 通过 lazyTree,在IE(8-11)和 Edge 中进行单个节点依次渲染节点
  • 在其他浏览器中则首先将整个大的 DOM 结构构建好,然后再整体插入容器
  • 单独渲染节点时,React 还考虑了 fragment 等特殊节点,这些节点则不会一个一个插入渲染

React 事件机制

参考

React 自己实现了一套事件机制,其将所有绑定在虚拟 DOM 上的事件映射到真正的 DOM 事件,并将所有的事件都代理到 document 上,自己模拟了事件冒泡和捕获的过程,并且进行统一的事件分发

React 自己构造了合成事件对象 SyntheticEvent,这是一个跨浏览器原生事件包装器,它抹平了各个浏览器的事件兼容性问题

  • 它具有与浏览器原生事件相同的接口,包括stopPropagation() 和 preventDefault() 等等
  • 在所有浏览器中他们工作方式都相同

setState

setState() 将对组件 state 的更改排入队列批量推迟更新,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。其实setState实际上不是异步,只是代码执行顺序不同,有了异步的感觉

  1. 同步更新

React 引发的事件处理(比如通过onClick引发的事件处理)

React 生命周期函数

  1. 异步更新

绕过React通过 addEventListener 直接添加的事件处理函数

通过 setTimeout || setInterval 产生的异步调用

注意事项

  1. 使用 setState() 改变状态之后,立刻通过this.state拿不到最新的状态

可以使用 componentDidUpdate() 或者 setState(updater, callback) 中的回调函数 callback 保证在应用更新后触发,通常建议使用 componentDidUpdate()

  1. setState合并,在合成事件声明周期钩子中,多次调用会被优化为一次

为了更好的感知性能,React 会在同一周期内会对多个 setState() 进行批处理。通过触发一次组件的更新来引发回流。后调用的 setState() 将覆盖同一周期内先调用 setState() 的值。所以如果是下一个 state 依赖前一个 state 的话,推荐给 setState() 传 function

  1. 当组件已被销毁,调用setState,React会报错

将数据挂载到外部,通过props传入

在组件内部维护一个状态量isUnmounted,componentWillMount中标记为true,在setState前进行判断

setState()被调用之后,源码执行栈(react 参照版本 15.6.0)

  1. React 组件继承自 React.Component,而 setState 是 React.Component 的方法,因此对于组件来讲 setState 属于其原型方法
  2. 调用 enqueueSetState() 将 state 放入 _pendingStateQueue 队列中;调用 enqueueCallback()将 callback 放入 _pendingCallbacks 队列中;
  3. 调用 enqueueUpdate() 如果处于批量更新模式,也就是 isBatchingUpdates 为 true 时,将需要更新的 component 添加到 dirtyComponents 数组中,否则 调用 batchedUpdates() 执行批量更新
  4. 调用 transaction.perform() 发起事务
    • 初始化:事务初始化阶段没有注册方法,故无方法要执行
    • 运行:执行 setSate 时传入的 callback 方法,一般不会传 callback 参数
    • 结束:执行 RESET_BATCHED_UPDATES FLUSH_BATCHED_UPDATES 这两个 wrapper 中的 close 方法
      • RESET_BATCHED_UPDATES 在 close 阶段将 isBatchingUpdates 设置为false
      • FLUSH_BATCHED_UPDATES 在 close 阶段,调用 runBatchedUpdates 方法,循环遍历所有的 dirtyComponents ,调用 updateComponent 刷新组件,并执行它的 pendingCallbacks , 也就是 setState 中设置的 callback

错误边界

组件内的 js 错误会导致 React 的内部状态被破坏,并且在下一次渲染时产生可能无法追踪的错误。部分 UI 的 js 错误不应该导致整个应用崩溃。

错误组件是一种 React 组件,可以捕获并打印发生在其子组件树任何位置的js错误,并且渲染出备用 UI, 而不是那些崩溃的子组件树。

  • 可以捕获的错误: 渲染期间,生命周期方法和整个组件树的构造函数
  • 无法捕获的错误: 事件处理,异步代码(setTimeout回调函数),服务端渲染,它自身抛出来的错误(并非它的子组件)
  • class 组件中定义了一下两个生命周期方法中的任意一个(或两个)
    • static getDerivedStateFromError()渲染备用UI
    • componentDidCatch()打印错误信息

错误边界的工作方式类似于 js 的 catch{},不同的地方在于错误边界只针对React组件, 只有 class 组件才可以成为错误边界组件。

如果一个错误边界无法渲染错误信息,则错误会冒泡到最近的上级错误边界,类似于catch{}的工作机制。

自React 16起,任何未被错误边界捕获的错误将会导致整个React组件树被卸

  • 把错误的UI留在那里比完全移除它更糟糕
  • 同时有可能发现一些已存在应用中但未曾注意到的崩溃

不能处理事件处理方法中的错误

  • 与render方法和生命周期方法不同,事件处理器不会再渲染期间触发如果他们抛出异常,React仍然能够知道需要在屏幕上显示什么
  • 可以使用try{}catch{}进行处理

Suspense

  • Suspense主要是为了解决两个问题:代码分割、数据获取
    • 使用 React.lazy 函数像渲染常规组件一样使用动态引入的组件
    • 配合 React.Suspense 来实现加载时的降级展示
    • fallback 将在加载过程中进行展示
    • 模块加载失败(如网络问题),会触发一个错误,可以通过错误边界来处理
  • 错误边界
    • static getDerivedStateFromError(error) 被设计用于捕获 Render Phase 中的异常,不希望其中产生副作用
    • componentDidCatch(error, info)用于在 Commit Phase 阶段捕获异常
  • 原理
    • 通过 throw 一个 Promise
    • 然后 React.Suspense 通过上文中处理子组件错误的生命周期函数捕获到它
    • 在它没有 resolve 时渲染 fallback
    • resolve 后渲染实际内容
    • 同时该机制内部还做了缓存处理,如果包含缓存数据就不执行 throw,以防止多次重复副作用的执行。

HOC 高阶组件

  • 定义
    • 高阶组件是 增强函数,输入一个元组件,返回一个新的增强组件
    • 主要作用是 代码复用,操作状态和参数
  • 用法
    • 为组件包裹一层默认参数
      function proxyHoc(Comp) {
          return class extends React.Component {
              render() {
                  const newProps = {
                      name: 'tayde',
                      age: 1,
                  }
                  return <Comp {...this.props} {...newProps} />
              }
          }
      }
      
    • 为组件进行包装
      function withMask(Comp) {
          return class extends React.Component {
              render() {
                  return (
                      <>
                          <Comp {...this.props} />
                          <div style={{
                              width: '100%',
                              height: '100%',
                              backgroundColor: 'rgba(0, 0, 0, .6)',
                          }} 
                      </>
                  )
              }
          }
      }
      
    • 通过props将被包裹组件中的state依赖外层,例如转为受控组件
      function withOnChange(Comp) {
          return class extends React.Component {
              constructor(props) {
                  super(props)
                  this.state = {
                      name: '',
                  }
              }
              onChangeName = () => {
                  this.setState({
                      name: 'dongdong',
                  })
              }
              render() {
                  const newProps = {
                      value: this.state.name,
                      onChange: this.onChangeName,
                  }
                  return <Comp {...this.props} {...newProps} />
              }
          }
      }
      const NameInput = props => (<input name="name" {...props} />)
      export default withOnChange(NameInput)
      
    • 根据条件,渲染不同的组件
      function withLoading(Comp) {
          return class extends Comp {
              render() {
                  if(this.props.isLoading) {
                      return <Loading />
                  } else {
                      return super.render()
                  }
              }
          };
      }
      
  • 应用场景
    • 权限控制,通过抽象逻辑,统一对页面进行权限判断,按不同的条件进行页面渲染
    • 性能监控,包裹组件的生命周期,进行统一埋点
  • 使用注意
    • 纯函数: 增强函数应为纯函数,避免侵入修改元组件
    • 避免用法污染: 应透传元组件的无相关参数与事件
    • 命名空间: 为HOC增加特异性的组件名称,便于开发调试和查找问题
    • 引入传递: 如果需要传递元组件的refs引用,使用React.forwardRef
    • 静态方法: 元组件上的静态方法无法被自从传出,会导致业务层无法调用
      • 函数导出
      • 静态方法赋值
    • 重新渲染: 由于增强函数每次调用是返回一个新组件,因此如果在Render中使用增强函数,就会导致每次都重新渲染整个HOC,而且之前的状态也会丢失

React Hook

React中通常使用类定义或者函数定义创建组件。React 16.8 版本推出了 HOOK,通过它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性

  • 优点:
    • 跨组件复用: 改造成本小,不会影响原来的组件层次结构和不会有传说中的嵌套地狱
    • 类定义更为复杂:
      • 时刻需要关注 this 的指向问题
      • class 给组件预编译、代码压缩、热加载等工作带来了很多困难
      • 每个生命周期函数都包含某个业务逻辑的一部分,每个业务逻辑又被分散在每个生命周期函数中
      • 代码复用代价高,高阶组件的使用会使整个组件树变得臃肿
    • 状态与 UI 隔离: 由于 HOOK 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定 HOOK,组件中的状态和 UI 变得更为清晰和隔离
  • 注意
    • 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定
    • 只有 函数定义组件和 hooks 中可以调用 hooks,避免在 类组件或者普通函数 中调用
    • 不能在 useEffect 使用 useState,React 会报错提示
    • 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存
  • 重要钩子
    • useState(initState)
      • 返回一对值:当前状态和用来更新它的函数
      • 类似类组件的 this.setState,但不会自动合并新旧 state
    • useEffect(callback, [source])
      • 给函数组件提供了执行副作用(如数据获取、订阅或手动操作 DOM 等)的能力
      • 它和类组件中的 componentDidMount、componentDidUpdate 及 componentWillUnmount 具有等效的用途,只不过被合并成了一个 API

React16 Fiber

  • Fiber
    • 是对核心算法的一次重新实现
    • 问题:
      • 随着应用变得越来越庞大,整个更新渲染的过程开始变得吃力,大量的组件渲染会导致主进程长时间被占用,导致一些动画或高频操作出现卡顿和掉帧的情况
    • 解决
      • 解决同步阻塞的方法,通常有两种: 异步 与 任务分割。而 React Fiber 便是为了实现任务分割而诞生的。
    • react 的渲染过程不再是一旦开始就不能终止的模式
      • render phase 可以被高优先级打断
      • commit phase 不会被打断
    • 优先级策略:
      • 文本框输入 > 本次调度结束需完成的任务 > 动画过渡 > 交互反馈 > 数据更新 > 不会显示但以防将来会显示的任务
    • 当遇到进程阻塞的问题时,任务分割、异步调用 和 缓存策略 是三个显著的解决思路
  • 废弃掉 componentWill*
    • componentWillMount
    • componentWillReceiveProps
    • componentWillUpdate
  • 添加方法
    • static getDerivedStateFromProps(nextProps, prevState)
    • getSnapshotBeforeUpdate(prevProps, prevState)
    • static getDerivedStateFromError(error)

react 优化

  • 用React的生产版本
  • 使用开发者工具中的分析器对组件进行分析
  • 虚拟化长列表react-window、react-virtualized,会在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建DOM节点的数量
  • 避免重新渲染:
    • 使用 React.memo 创建函数组件
    • 使用pureComponent组件
    • 在组件中实现shouldComponentUpdate方法
  • 使用不可突变数据immutable.js
  • 列表中使用稳定常量作为key,而不是index
  • 事件处理函数this绑定放在constructor中
  • 不滥用props,只传递component需要的props
  • 使用react-loadable异步加载组件
  • 事件节流和防抖

React 预防 XSS攻击

  • 所有的用户输入都需要经过HTML实体编码,React会在运行时动态创建DOM节点然后填入文本内容
  • 在React 应用中确保不会注入任何没显式编写的数据,所有的数据在页面渲染之前都会被转换成字符串,这防止 XSS 进攻

React组件间复用逻辑状态

1. 高阶组件

  • 缺点
    • 组件中属性难以溯源,并且存在属性覆盖的问题
    • 高阶组件改变了当前组件的层级结构,在 React Dev 工具中看到的结构将会变得非常深,这会加大调试的难度。
  • 步骤
    • 对原组件进行一层包裹,并将需要的状态注入其 props
    • 这样在每个需要用到 FriendStatus 的组件外层都使用该高阶组件包裹,就可以在组件内拿到所需的状态
function withFriendStatus(WrappedComponent) {
  return class extends Component {
    state = {
      isOnline: null,
    }
    handleStatusChange = ({ isOnline }) => this.setState({ state });
    componentDidMount() {
      ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
    }
    componentWillUnmount() {
      ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id, this.handleStatusChange);
    }
    render() {
      return <WrappedComponent {...this.props} isOnline={this.state.isOnline} />
    }
  }
}
@withFriendStatus
class FriendListItem extends Component {
  // this.props.isOnline
}

2. Render Props

  • 缺点

    • 改变了原有组件的层级结构,可能会带来 Wrapper Hell 的问题
    • 由于其写法直接包裹了原组件的 render 部分,在使用多层 Render Props 时也会使编码过程中产生 Wrapper Hell,加大了阅读难度
  • 优点

    • 使用了一个接收函数的 prop 解决了逻辑复用,状态将通过该函数参数返回,同时该函数将继续渲染原组件内容
  • 步骤

    • 在使用 Render Props 时,我们只需要将原组件的 render 内容移至逻辑复用组件的 render prop 属性的函数中,此时就可以拿到所需的状态
class FriendStatusProvider extends Component {
  state = {
    isOnline: null,
  }
  handleStatusChange = ({ isOnline }) => this.setState({ state });
  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(this.props.friend.id, this.handleStatusChange);
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id, this.handleStatusChange);
  }
  render() {
    return this.props.children(this.state.isOnline);
  }
}
class FriendListItem extends Component {
  render() {
    return (
      <FriendStatusProvider friend={this.props.friend}>
        {isOnline => (
      	   // 原有组件 render 内容
        )}
      	</FriendStatusProvider>
    );
  }
}

3. 自定义 Hook

  • 优点
    • 自定义 Hook 没有改变原有组件层级结构,避免了 Wrapper Hell
    • 同时轻量、完全独立,易于复用及社区共享
  • 步骤
    • 将要复用的逻辑提取到一个函数中,它被称作自定义 Hook
    • 在使用自定义 Hook 时也和调用函数相同,它和将自定义 Hook 中代码拷贝至调用处的执行结果相同
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });
  return isOnline;
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);
  return (
    // ...
  );
}