JS核心理论之《React基础概念与虚拟DOM》

1,613 阅读14分钟

React基础

JSX

const element = <h1>Hello, world!</h1>;

JSX,既不是字符串也不是HTML,本质上是一个 JavaScript 的语法扩展,且更接近于JavaScript,是通过React.createElement()创建的一个对象,称为React 元素

React 不强制使用JSX,但将标记与逻辑放在一起形成组件,实现关注点分离。同时,JSX 能够防止XSS注入攻击。

元素渲染

  1. React 元素是不可变对象。一旦被创建,你就无法更改它的子元素或者属性。 更新 UI 唯一的方式是创建一个全新的元素,并将其传入 ReactDOM.render()

  2. React 只更新它需要更新的部分。 React DOM 会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使 DOM 达到预期的状态。

组件&Props

  • 函数组件:接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。
  • class组件:形如
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
  • Props 的只读性: 所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
  • state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。

组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props。 这样的函数被称为“纯函数”,因为该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果。

State&生命周期

setState(updater,[callback])

在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state

为什么要异步?如果setState是同步更新state,而state的更新又会触发组件的重新渲染,那么每次setState都会渲染组件,这对性能是很大的消耗。

  1. 正常React绑定的事件:异步更新
  2. 通过addEventListener绑定的事件:同步更新
  3. 通过setTimeoutt处理点击事件:同步更新

使用 compoentDidUpdatesetState 的回调函数,来保证在更新应用后触发。 批量更新,是基于一个队列和一个变量锁isBatchingUpdates实现。

正确地使用 State的姿势:

  1. 不要直接修改 State

  2. 调用setState不会立即更新

  3. 所有组件使用的是同一套更新机制,当所有组件didmount后,父组件didmount,然后执行更新

  4. 更新时会把每个组件的更新合并,每个组件只会触发一次更新的生命周期。

  5. 钩子函数和合成事件中:

在react的生命周期和合成事件中,react仍然处于他的更新机制中,这时isBranchUpdate为true

按照上述过程,这时无论调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue,将要更新的组件存入dirtyComponent

当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件didmount后会将isBranchUpdate设置为false。这时将执行之前累积的setState

  1. 异步函数和原生事件中

由执行机制看,setState本身并不是异步的,而是如果在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象

在生命周期,根据JS的异步机制,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕,

isBranchUpdate被设置为false,根据上面的流程,这时再调用setState即可立即执行更新,拿到更新结果。

  1. componentDidMount调用setstate

它将会触发一次额外的渲染,但是它将在浏览器刷新屏幕之前发生。这保证了在此情况下即使render()将会调用两次,用户也不会看到中间状态。

componentDidMount本身处于一次更新中,我们又调用了一次setState,就会在未来再进行一次render,造成不必要的性能浪费,大多数情况可以设置初始值来搞定。

  1. componentWillUpdatecomponentDidUpdate 不能调用setState, 会造成死循环,导致程序崩溃。

  2. 推荐:在调用setState时使用函数传递state值,在回调函数中获取最新更新后的state。

生命周期:

  1. 挂载 当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
constructor()
static getDerivedStateFromProps()
render()
componentDidMount()

注意: 下述生命周期方法即将过时,在新代码中应该避免使用它们: UNSAFE_componentWillMount()

  1. 更新 当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()

注意: 下述方法即将过时,在新代码中应该避免使用它们:

UNSAFE_componentWillUpdate() UNSAFE_componentWillReceiveProps()

  1. 卸载 当组件从 DOM 中移除时会调用如下方法:
componentWillUnmount()

事件处理

  1. 在 React 中你不能通过返回false 来阻止默认行为。必须明确调用 preventDefault

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

  1. React事件与原生事件的执行顺序
  • react的所有事件都挂载在document中
  • 当真实dom触发后冒泡到document后才会对react事件进行处理
  • 所以原生的事件会先执行
  • 然后执行react合成事件
  • 最后执行真正在document上挂载的事件
  1. react事件和原生事件可以混用吗?

react事件和原生事件最好不要混用。

原生事件中如果执行了stopPropagation方法,则会导致其他react事件失效。因为所有元素的事件将无法冒泡到document上

this绑定:你必须谨慎对待 JSX 回调函数中的 this,在 JavaScript 中,class 的方法默认不会绑定 this。

方法有三:

  1. 在构造比函数中绑定一下:this.handleClick = this.handleClick.bind(this);
  2. 在类以方法定义事件处理函数时,使用箭头函数: handleClick = () => {console.log('this is:', this);}
  3. 直接在回调函数中使用箭头函数: <button onClick={() => this.handleClick()}>Click me</button>

注意:

[性能优化点]每次渲染 Button 时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。 我们通常建议在构造器中绑定或使用 class fields 语法来避免这类性能问题。

组合vs继承

React 有十分强大的组合模式。我们推荐使用组合而非继承来实现组件间的代码重用。 Props 和 组合为你提供了清晰而安全地定制组件外观和行为的灵活方式。 注意:组件可以接受任意 props,包括基本数据类型,React 元素以及函数。

  1. 使用一个特殊的 {props.children} 来将他们的子组件传递到渲染结果中
  2. 少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用 children,而是自行约定:将所需内容传入 props,并使用相应的 prop,类似于槽 slot 的概念。

Context

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

[代码优化点] Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。 如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案。

一种无需 context 的解决方案是将子组件自身传递下去,因而中间组件无需知道该子组件用到的props

错误边界

部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

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

错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

[代码优化点] 错误边界无法捕获以下场景中产生的错误:

  • 事件处理(了解更多)
  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
  • 服务端渲染
  • 它自身抛出来的错误(并非它的子组件)

Refs转发

Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。这个技巧对高阶组件(也被称为 HOC)特别有用。 Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。

Fragments

React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。

render() {
  return (
    <React.Fragment>
      <ChildA />
      <ChildB />
      <ChildC />
    </React.Fragment>
  );
}

或者使用短语法:<> </>

高阶组件

定义:高阶组件是参数为组件,返回值为新组件的函数。 HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式

const EnhancedComponent = higherOrderComponent(WrappedComponent);

组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。例如:Redux 的 connect

注意:

  1. 不要在 render 方法中使用 HOC。 每次调用 render 函数都会创建一个新的 EnhancedComponent,导致子树每次渲染都会进行卸载,和重新挂载的操作!
  2. 务必复制静态方法。你可以使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法
  3. Refs 不会被传递。

与第三方库协同

我们会添加一个 ref 到这个根 DOM 元素。 在 componentDidMount 中,我们能够获取它的引用这样我们就可以把它传递给 jQuery 插件了。

为了防止 React 在挂载之后去触碰这个 DOM,我们会从 render() 函数返回一个空的 <div />。 这个

元素既没有属性也没有子元素,所以 React 没有理由去更新它,使得 jQuery 插件可以自由的管理这部分的 DOM

class SomePlugin extends React.Component {
  componentDidMount() {
    this.$el = $(this.el);
    this.$el.somePlugin();
  }

  componentWillUnmount() {
    this.$el.somePlugin('destroy');
  }

  render() {
    return <div ref={el => this.el = el} />;
  }
}

性能优化

  1. 部署时使用生产版本,去除一些警告信息
  2. 虚拟化长列表。"虚拟滚动”技术。这项技术会在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建 DOM 节点的数量。react-windowreact-virtualized 是热门的虚拟滚动库。
  3. 避免调停。你可以通过覆盖生命周期方法 shouldComponentUpdate 来进行提速。该方法会在重新渲染前被触发。其默认实现总是返回 true. 如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程。

继承 React.PureComponent 以代替手写 shouldComponentUpdate()。它用当前与之前 props 和 state 的浅比较覆写了 shouldComponentUpdate() 的实现.

shouldComponentUpdate(nextProps, nextState) {
  return true;
}
  1. 不可变数据的力量。不改变原来的对象,使用 ...扩展运算符 或 Object.assign 返回新对象。

Diff算法

  1. 当对比两颗树时,React 首先比较两棵树的根节点。
  • 当根节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树。componentWillUnmount() -> componentWillMount() -> componentDidMount()

  • 当对比两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。然后子节点递归。

  1. 子节点递归

在子元素列表末尾新增元素时,更新开销比较小; 如果只是简单的将新增元素插入到表头,那么更新开销会比较大,不会意识到应该保留后面的,而是会重建每一个子元素 。这种情况会带来性能问题。 通过添加key来解决。

尽量用相同的节点类型和稳定可预测的Key。

Render Prop

render prop 是一个用于告知组件需要渲染什么内容的函数 prop。使用 Props 而非 render。

重要的是要记住,render prop 是因为模式才被称为 render prop ,你不一定要用名为 render 的 prop 来使用这种模式。

将 Render Props 与 React.PureComponent 一起使用时要小心。 如果你在 render 方法里创建函数,那么使用 render prop 会抵消使用 React.PureComponent 带来的优势。 因为浅比较 props 的时候总会得到 false,并且在这种情况下每一个 render 对于 render prop 将会生成一个新的值。

Key的使用方式

react根据key来决定是销毁重新创建组件还是更新组件,原则是:

  • key相同,组件有所变化,react会只更新组件对应变化的属性。
  • key不同,组件会销毁之前的组件,将整个组件重新渲染。

使用index做key存在的问题:

当元素数据源的顺序发生改变时,会重新渲染。而如果使用唯一ID作为key,子组件的值和key均未发生变化,只是顺序发生改变,因此react只是将他们做了移动,并未重新渲染。

虚拟DOM

关于能否提升性能

很多文章说VitrualDom可以提升性能,这一说法实际上是很片面的。

直接操作DOM是非常耗费性能的,这一点毋庸置疑。但是React使用VitrualDom也是无法避免操作DOM的。

如果是首次渲染,VitrualDom不具有任何优势,甚至它要进行更多的计算,消耗更多的内存。

VitrualDom的优势在于React的Diff算法和批处理策略,React在页面更新之前,提前计算好了如何进行更新和渲染DOM。 实际上,这个计算过程我们在直接操作DOM时,也是可以自己判断和实现的,但是一定会耗费非常多的精力和时间,而且往往我们自己做的是不如React好的。 所以,在这个过程中React帮助我们"提升了性能"。

所以,我更倾向于说,VitrualDom帮助我们提高了开发效率,在重复渲染时它帮助我们计算如何更高效的更新,而不是它比DOM操作更快。

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

实现原理

  • React组件的渲染流程
  1. 使用React.createElement或JSX编写React组件,实际上所有的JSX代码最后都会转换成React.createElement(...),Babel帮助我们完成了这个转换的过程。

  2. createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个ReactElement对象(所谓的虚拟DOM)。

  3. ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM

  • 虚拟DOM组成
  • 防止XSS: 借助Symbol.for('react.element')
  • 批处理和事务机制:setState
  • 针对性的性能优化:IE/Edge Fragment
  • 事件机制:自己实现了一套事件机制,其将所有绑定在虚拟DOM上的事件映射到真正的DOM事件,并将所有的事件都代理到document上,自己模拟了事件冒泡和捕获的过程,并且进行统一的事件分发。