一个示例检验对 React Diff 的理解

September 10, 2021

前言

React Diff 算法为了将复杂度降低到 O(n),提出了以下几种优化方式:

  • 只对比统一层级的节点
  • 只对同一种类型对比
  • 只对同样的 key 对比(没有设置就都认为 key 是 undefined)

要实现的例子如下,通过点击 “btn” 按钮可以切换显示隐藏 “a” 这个组件。通过这个例子我们可以观察不同实现方式对子组件的影响。根据子组件挂载和卸载打印的日志,你能清楚的知道为什么输出内容不一样你就算学明白 React Diff 机制了。

react diff demo 01

我们先实现 a/b/c 的公共组件,它在挂载和卸载的时候会打印一下 log。

function Comp({ name }) {
  useEffect(() => {
    console.log("mount:", name)
    return () => {
      console.log("remove:", name)
    }
  }, [])
  return <div>{name}</div>
}

观察以下几种实现的差异

方式一

常规实现方式:

const App = () => {
  const [isShow, setShow] = useState(true)
  return (
    <div>
      <button onClick={() => setShow(!isShow)}>btn</button>
      {isShow && <Comp name="a" />}
      <Comp name="b" />
      <Comp name="c" />
    </div>
  )
}

点击两次按钮查看日志:

# 挂载 >>>
mount: a
mount: b
mount: c

# 点击1 >>>
remove: a

# 点击2 >>>
mount: a

查看在线 demo

方式二

此实现方式和上一个 demo 有什么区别呢?

const App = () => {
  const [isShow, setShow] = useState(true)
  if (isShow) {
    return (
      <div>
        <button onClick={() => setShow(!isShow)}>btn</button>
        <Comp name="a" />
        <Comp name="b" />
        <Comp name="c" />
      </div>
    )
  }
  return (
    <div>
      <button onClick={() => setShow(!isShow)}>btn</button>
      <Comp name="b" />
      <Comp name="c" />
    </div>
  )
}

点击两次按钮查看日志:

# 挂载 >>>
mount: a
mount: b
mount: c

# 点击1 >>>
remove: c

# 点击2 >>>
mount: c

查看在线 demo

方式三

不同类型在位置变化的情况下是如何执行?

const Comp1 = (props) => <Comp {...props} />
const Comp2 = (props) => <Comp {...props} />
const Comp3 = (props) => <Comp {...props} />

const App = () => {
  const [isShow, setShow] = useState(true)
  if (isShow) {
    return (
      <div>
        <button onClick={() => setShow(!isShow)}>btn</button>
        <Comp1 key="a" name="a" />
        <Comp2 name="b" />
        <Comp3 name="c" />
      </div>
    )
  }
  return (
    <div>
      <button onClick={() => setShow(!isShow)}>btn</button>
      <Comp2 name="b" />
      <Comp3 name="c" />
    </div>
  )
}

点击两次按钮查看日志:

# 挂载 >>>
mount: a
mount: b
mount: c

# 点击1 >>>
remove: a
remove: b
remove: c
mount: b
mount: c

# 点击2 >>>
remove: b
remove: c
mount: a
mount: b
mount: c

查看在线 demo

方式四

加上 key 之后的变化:

const Comp1 = (props) => <Comp {...props} />
const Comp2 = (props) => <Comp {...props} />
const Comp3 = (props) => <Comp {...props} />

const App = () => {
  const [isShow, setShow] = useState(true)
  if (isShow) {
    return (
      <div>
        <button onClick={() => setShow(!isShow)}>btn</button>
        <Comp1 name="a" />
        <Comp2 key="b" name="b" />
        <Comp3 key="c" name="c" />
      </div>
    )
  }
  return (
    <div>
      <button onClick={() => setShow(!isShow)}>btn</button>
      <Comp2 key="b" name="b" />
      <Comp3 key="c" name="c" />
    </div>
  )
}

点击两次按钮查看日志:

# 挂载 >>>
mount: a
mount: b
mount: c

# 点击1 >>>
remove: a

# 点击2 >>>
mount: a

查看在线 demo