什么是js数据流_为什么我更喜欢 Mobx,而不是 Redux immutable

关于我
儿葱,25岁,工作 3 年半,在百度工作 3 年,现在美团任职,对于 Node.js Web React 方向比较熟悉。Github: imcuttle 邮箱: imcuttle@163.com


下面进入正题,我将从一个实际的问题场景出发,层层讲诉 React 中 Mobx 和 Redux 的原理和对比。

在业务开发中,我们经常会需要开发一些复杂组件,其中不仅包括交互的复杂,也包括涉及到的数据结构复杂,如书写一个无限层级的树组件,其中的复杂度包括但不限于:

  • 数据嵌套递归逻辑
  • 各个节点更新渲染的逻辑处理
  • 节点渲染性能的考量

以一个我碰到的实际业务场景为例:需要开发一个将 markdown文本渲染成 html 元素的组件。

设计考虑

大部分比较快速的实现方式比较简单,将 markdown 转换成 html 文本,然后使用 dangerouslySetInnerHTML 渲染 html 文本。

这种方式比较快捷,但是没有模块化、不方便注入交互逻辑。如需支持允许外部控制渲染各个 markdown 语法类型的结构体,这种方式不能够满足。

const MarkdownView = ({markdown}) => <div className="markdown-body" dangerouslySetInnerHTML={{__html: marked(markdown)}} />

所以这时候就应该设计成结构化渲染 markdown,如使用 remark 将 markdown 解析成为 Markdown AST (Abstract Syntax Tree 抽象语法树)

如以下 markdown 文本:

## Header
> Alpha bravo charlie.

将会解析成如下 MD AST

[
  {
    type: 'heading',
    depth: 1,
    children: [{type: 'text', value: 'Header'}]
  },
  {
    type: 'blockquote',
    children: [{
      type: 'paragraph',
      children: [{type: 'text', value: 'Alpha bravo charlie.'}]
    }]
  }
]

然后,我们只需要通过渲染 MD AST 即可

import React from "react";

const renderers = {
  heading: (props) => {
    const { depth } = props.node;
    const tagName = `h${depth}`;
    return React.createElement(tagName, {}, props.children);
  },
  paragraph: (props) => <p>{props.children}</p>,
  blockquote: (props) => <blockquote>{props.children}</blockquote>,
  text: (props) => <span>{props.node.value}</span>
};

const renderNode = (node, props) => {
  const Comp = renderers[node.type];
  return (
    <Comp {...props} node={node}>
      {(node.children || []).map((childNode, index) =>
        renderNode(childNode, { parent: node, key: index })
      )}
    </Comp>
  );
};

const MarkdownView = ({ mdast }) => {
  return (
    <div className="markdown-body">
      {!!mdast && mdast.map((node, i) => renderNode(node, { key: i }))}
    </div>
  );
};

export default MarkdownView;

以上一段剪短的代码就是简单的实现,但是这段代码只是功能正确,在遇到大体量的 MDAST,同时涉及到 MDAST 更新渲染的时候,该 MarkdownView 性能较低。

优化组件的更新渲染性能,我们从以下 2 个点展开:

  1. 理清 React 的单向数据流和组件更新模式
  2. 优化更新性能
    1. immutable 操作
    2. 细化更新颗粒度
    3. 善于用 shouldComponentUpdate
    4. mobx?
      mutable 操作

React 单向数据流和组件更新模式

以 MarkdownView 为例,MarkdownView 组件接收 mdast 数据,随后通过 props 传递给各 renderer 组件,这种从父组件传递数据到子组件的模式是单向数据流

  <MarkdownView>  # 将 props 分发下去
              
    <heading>   <text>

由于这种模式是从上往下的数据传递,如涉及到子组件的更新,一般需要通过父组件数据改变而触发,也就是外部的 Props 修改来驱动更新;

如下代码,实现了点击 text 文本后,更新渲染为 input,用于更新 text.value:

const Text = (props) => {
  const [editing, setEditing] = React.useState(false);
  const [value, setValue] = React.useState(props.node.value);
  React.useEffect(() => {
    setValue(props.node.value);
  }, [props.node.value]);

  if (editing) {
    return (
      <input
        autoFocus
        onBlur={() => {
          setEditing(false);
          // 注意这里
          props.node.value = value;
        }}
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
    );
  }

  return <span onClick={() => setEditing(true)}>{props.node.value}</span>;
};

具体代码见如下中的 mutable 模式:

https://codesandbox.io/s/markdown-view-9b7yq?file=/src/App.js​codesandbox.io

效果是,blur 之后修改的 value 是可以渲染出来的,但是如果我们把代码修改成如下:

onBlur={() => {
  setEditing(false);
  // 注意这里
  requestAnimationFrame(() => {
    props.node.value = value;
  })
}}

结果 value 不能被同步渲染出来,这是因为

props.node.value = value;

这种 mutable 操作,不能够触发组件的更新,而第一个例子之所以能够起效果,是因为 onBlur 中 setEditing(false) 触发更新是异步的,React 会进行批更新优化,这个话题后续有空可以细谈。

什么 mutable 操作呢?如下:

const ref = { name: 'John' }
// mutable 操作,执行后,ref 引用未发生改变
ref.name = 'Tom'
// immutable 操作,执行后,ref 引用发生改变
const newRef = { ...ref, name: 'Tom' }

也就是说在修改完 props.node.value 后,才触发 render 更新,所以可以生效;而 requestAnimationFrame 导致 props.node.value 修改发生在 render 之后;

基于 React 单向数据流原理,那么正确的 React 代码书写应该是如何的呢?


正确的基于 React 单向数据流的代码

根据 React 单向数据流原理,应该修改最外层的 mdast,然后从上向下的触发子组件渲染,如下父子组件渲染关系:

     A
   /   
  B     C
 /      
D   E     F

MDAST 修改后,A 组件渲染驱动 B / C,B 驱动 D / E,C 驱动 F;但是这样也会带来一个问题:每一次 MDAST 修改,A 组件触发渲染,都会导致所有子组件进行渲染更新,对于比较庞大的组件树来说,效率低。

所以 React 引入了 shouldComponentUpdate 生命周期,下文用 scu 代替

在 MDAST 修改后,A 组件触发 scu,判断 A 组件可以 render 之后,B / C 组件触发 scu,依次进行下去。

所以为了提升效率,一般都会浅比较:新旧的 props 和 state, 来判断是否需要进行更新。因此一般会使用 immutable.js 或 immer 来对数据进行更新,但是最小程度的修改引用;

如下图,演示的是 immutable 库修改数据所做的事情:

2462b0ac569190116d8571df7fe5d36a.gif

可以看到对于一个深层嵌套的数据,修改黄色节点,会导致其祖先节点引用的修改,但其他节点引用都不会修改。

这样对应在组件中,每次修改节点数据,导致根组件数据改变,进而触发单向数据流渲染,对于已经书写妥当 scu 的组件,其相关的数据只要没有发生改变,是不会触发渲染更新的,减少不必要的渲染更新,提高性能,具体可以参考下例中的 immutable 模式

markdown-view - CodeSandbox​codesandbox.io
b7d8c87b766f4eb1566208044e3c3b3d.png

对于单向数据流渲染更新,需要注意:

  1. 一个 react 组件才有自己的特定生命周期。如果对于MarkdownView 组件中各个节点数据的渲染统一都是通过一个个的方法体渲染,是不会提高更新颗粒度的,所以需要尽量多的拆分组件。
  2. 错误的 scu 书写会导致渲染更新错误

那么基于单向数据流的更新渲染,有没有它的问题存在呢?

答案是有的,随着数据量的增加,嵌套深度增加,每一次的底层组件数据修改,都会回溯到根组件的更新,每一次更新渲染也需要不断的 scu 和从父到子的 render ,即使是在 react diff 、react fiber 的优化下,还是会有对应的瓶颈出现。那么有没有一种不需要回溯渲染的方式呢?

答案依然是有的。

终极杀招 Mobx

使用 mobx 可以避免父到子的回溯渲染,如下例子中的 mutable mobx 模式

markdown-view - CodeSandbox​codesandbox.io
7dc02b35151639f9add325dbab291e16.png

mobx react 的更新渲染机制于官方的单向数据触发的渲染不同,mobx 会将 mdast 变成一个被观察的数据,在每一次 mutable 修改中,都会被观察到数据的改动。

一旦在发现同步渲染所访问到的数据发生改动,就会主动的触发组件的更新,如 forceUpdate,这样一来 mutable 的操作修改,只会影响到使用了该数据的组件,这些组件进行主动的更新,而不会回溯进行父到子的更新渲染,大大减少了不必要的 suc 和 render

总结

综上,需要书写一个高性能的复杂组件,需要考虑的点颇多,最终有两种方式可供大家选择:

  1. 基于 immutable 库和单向数据流 和 适量的组件拆分、scu 的书写来实现被动的更新。如 slate.js 的实现
  2. 基于 mobx 和适量的 observer 组件单元 来实现组件自驱的更新

后面贴一个其他同学做的测试数据,可以看到 mobx 性能更佳

redux、immutablejs和mobx性能对比(三) - 渴望做梦 - 博客园​www.cnblogs.com
6632a14ee3cdaffc0005f3447144becd.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值