Vue3 源码解毒 & PK React17

10,657 阅读15分钟

前言

因为最近开始写Vue了,对于我一个react骨灰级玩家来说其实是一个挑战。其实我现在更偏向于写原生JS,因为市场上绝大部分做得好的框架库几乎都脱离不了Vitrual DOM体系。

而我们知道的是,通过Vitrual DOM来更新真实DOM,性能肯定是比不过直接对原生DOM进行操作的性能。如果我能明确知道哪个DOM要发生变化,那直接 document.getElementById(id).xx 多好?

Vitrual DOM的价值从来都不在性能方面。emmm... 今天主题是对Vue的源码进行一个解毒,目的是能够清晰知道Vue到底做了哪些事情,优劣势又分别在哪。

1. 先从Vue 的 diff 算法开始解剖

Vitrual DOM 路线的都逃不过diff算法。 diff算法家家有,那 Vue3diff算法又是长什么样的。

先来看个栗子。

    <ul key="ul1"> 
        <li>渣男<li>
        <li>胖子<li>
        <li>就知道吃<li>
    <ul>

需要转化成:

    <ol key="ul1"> 
        <li>渣男<li>
        <li>胖子<li>
        <li>就知道吃吗?<div>你个渣男!</div><li>
    <ol>

Q: 就把ul变成ol ,key都没变,甚至其子结点都不变。请问 Vue 重新如何渲染?

答: 全部重新渲染一遍。

所以,合理吗? 如果存在即合理,那为什么要这样设计呢? 这里有人要diss我了,这种场景实际开发中太少见了。(被怼得很难过,这个后续再说吧。真的是可以解决这种问题的……😂)

diff的执行策略

  • 同一个虚拟节点,才进行精细化diff比较。
// 先看源码中的一个方法
function isSameVNodeType(n1, n2) { 
// ... 
return n1.type === n2.type && n1.key === n2.key 
}

看方法名你其实就明白了,这是个判断两个VNode 是否是同一个。 看函数返回值你就更加明白,两个VNode要一致就得结点类型一样、key也得一样。

  • 只进行同层比较,不会进行跨层比较 那回到上面的问题,继续看个栗子:
    <ul key="ul1"> 
        <li>渣男<li>
        <li>胖子<li>
        <li>就知道吃吗?<div>你个渣男!</div><li>
    <ul>

Q: 如果 ul 不再变,只是其中一个 li 元素的内容发生了变化。那请问又是咋渲染的?

答:如果li发送变动,只会进行li同层的diff比较,不会进行li子元素div diff 。 我相信使用过Vue的人都知道答案。

patchChildren - 更新子结点

上源码。


      const patchChildren = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized = false) => {
          const c1 = n1 && n1.children;
          const prevShapeFlag = n1 ? n1.shapeFlag : 0;
          const c2 = n2.children;
          const { patchFlag, shapeFlag } = n2;
          // fast path
          if (patchFlag > 0) {
              if (patchFlag & 128 /* KEYED_FRAGMENT */) {
                  // this could be either fully-keyed or mixed (some keyed some not)
                  // presence of patchFlag means children are guaranteed to be arrays
                  /*
                  *1 - patchKeyedChildren
                  */ 
                  patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
                  return;
              }
              else if (patchFlag & 256 /* UNKEYED_FRAGMENT */) {
                  // unkeyed
                  /*
                   * 2 - patchUnkeyedChildren
                   */ 
                  patchUnkeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
                  return;
              }
          }
          // children has 3 possibilities: text, array or no children.
          if (shapeFlag & 8 /* TEXT_CHILDREN */) {
              // text children fast path
              if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {
                  unmountChildren(c1, parentComponent, parentSuspense);
              }
              if (c2 !== c1) {
                  hostSetElementText(container, c2);
              }
          }
          else {
              if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {
                  // prev children was array
                  if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
                      // two arrays, cannot assume anything, do full diff
                      
                      patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
                  }
                  else {
                      // no new children, just unmount old
                      unmountChildren(c1, parentComponent, parentSuspense, true);
                  }
              }
              else {
                  // prev children was text OR null
                  // new children is array OR null
                  if (prevShapeFlag & 8 /* TEXT_CHILDREN */) {
                      hostSetElementText(container, '');
                  }
                  // mount new if array
                  if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
                      mountChildren(c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
                  }
              }
          }
      };

看这段源码你就知道:

  1. 结点有 patchFlag, shapeFlag 两个属性。
  2. patchChildren 入参中 n1 为旧结点,并且prevShapeFlag = n1.shapeFlag
  3. n2 为新结点(旧结点更新后)
  4. patchFlag 为快速通道标志,一旦结点上有这个标志且值 > 0 则直接进行 有key的diff处理。
  5. 非快速通道 则要进行三种判断:文本结点、子结点、没有子结点。 其中遇见array结点则进行递归处理。

我在其中标注了两个地方(源码太多,只展示关键部分)

  • 1 - patchKeyedChildren: 处理有key的节点

  const patchKeyedChildren = (c1/*旧的vnode*/, c2/*新的vnode*/, container, parentAnchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
          let i = 0;/* 记录索引 */
          const l2 = c2.length; /* 新vnode的数量 */
          let e1 = c1.length - 1; // prev ending index : 老vnode 最后一个节点的索引 
          let e2 = l2 - 1; // next ending index : 新节点最后一个节点的索引
          // 1. sync from start
         
          while (i <= e1 && i <= e2) { // ### 1. 头头比较,发现不同就跳出
              const n1 = c1[i];
              const n2 = (c2[i] = optimized
                  ? cloneIfMounted(c2[i])
                  : normalizeVNode(c2[i]));
              if (isSameVNodeType(n1, n2)) {
                  patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
              }
              else {
                  break;
              }
              i++;
          }
          // 2. sync from end
         
          while (i <= e1 && i <= e2) { // ### 2. 尾尾比较,发现不同就跳出
              const n1 = c1[e1];
              const n2 = (c2[e2] = optimized
                  ? cloneIfMounted(c2[e2])
                  : normalizeVNode(c2[e2]));
              if (isSameVNodeType(n1, n2)) {
                  patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
              }
              else {
                  break;
              }
              e1--;
              e2--;
          }
          // 3. common sequence + mount
        
          // 老节点全部patch,还有新节点
          if (i > e1) {  // / 新节点大于老节点
              if (i <= e2) { // // 并且新节点e2指针还没有走完,表示需要新增节点
                  const nextPos = e2 + 1;
                  const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
                  while (i <= e2) {
                      patch(null, (c2[i] = optimized
                          ? cloneIfMounted(c2[i])
                          : normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
                      i++;
                  }
              }
          }
          // 4. common sequence + unmount
         // 新节点全部patch,还有老节点
          else if (i > e2) { // 新节点e2指针全部patch完 
              while (i <= e1) { // 新节点数小于老节点数,需要卸载节点
                  unmount(c1[i], parentComponent, parentSuspense, true);
                  i++;
              }
          }
          
          // 5. unknown sequence : 剩余不确定元素
          // [i ... e1 + 1]: a b [c d e] f g
          // [i ... e2 + 1]: a b [e d c h] f g
          // i = 2, e1 = 4, e2 = 5
          else {
              const s1 = i; // prev starting index
              const s2 = i; // next starting index
              // 5.1 build key:index map for newChildren
              const keyToNewIndexMap = new Map();
              for (i = s2; i <= e2; i++) {
                  const nextChild = (c2[i] = optimized
                      ? cloneIfMounted(c2[i])
                      : normalizeVNode(c2[i]));
                  if (nextChild.key != null) {
                      if (keyToNewIndexMap.has(nextChild.key)) {
                          warn(`Duplicate keys found during update:`, JSON.stringify(nextChild.key), `Make sure keys are unique.`);
                      }
                      keyToNewIndexMap.set(nextChild.key, i);
                  }
              }
              // 5.2 loop through old children left to be patched and try to patch
              // matching nodes & remove nodes that are no longer present
              // code ....
              
              // 5.3 move and mount
              // generate longest stable subsequence only when nodes have moved
              // code ...
             
          }
      };

亲,先看看源码当中那些带数字标号的引文注释,都是源码自带的。 看不懂就再看看中文注释,那是我加的。

好吧,如果看到源码就头疼,那我来总结一下这个方法中的数字 5

5.1 build key,记录新的节点

先看看代码中声明的变量:

const s1 = i  // 第一步遍历到的index
const s2 = i 
const keyToNewIndexMap = new Map()   // 把没有比较过的新的vnode节点,通过map保存
for (i = s2; i <= e2; i++) {
  if (nextChild.key != null) {
    keyToNewIndexMap.set(nextChild.key, i)
  }
}

let j // 新指针j
let patched = 0 
const toBePatched = e2 - s2 + 1 // 没有经过 path 的 新的节点的数量
let moved = false               // 是否需要移动
let maxNewIndexSoFar = 0 

const newIndexToOldIndexMap = new Array(toBePatched)
// 建立一个数组,每个子元素都是0 [ 0, 0, 0, 0, 0, 0 ]
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0;

keyToNewIndexMap变量中,我们得到的结果是:(假设节点 e 的key是 e)。

keyToNewIndexMap = {"e" => 2, "d" => 3, "c" => 4, "h" => 5}

用新指针 j 来记录剩下的新的节点的索引。

newIndexToOldIndexMap 用来存放新节点索引,和旧节点索引。

5.2 匹配节点,删除不存在的节点

for (i = s1; i <= e1; i++) {   /* 开始遍历老节点 */
  const prevChild = c1[i]      // c1是老节点
  if (patched >= toBePatched) {  
    /* 已经patch数量大于等于剩余节点数量,卸载老的节点 */
    unmount(prevChild, parentComponent, parentSuspense, true)
    continue
  }
  let newIndex   // 目标新节点的索引

  /* 如果,老节点的key存在 ,通过key找到对应的新节点的index */
  if (prevChild.key != null) {
    newIndex = keyToNewIndexMap.get(prevChild.key)
  } else {
    /* 
    	如果,老节点的key不存在,遍历剩下的所有新节点
      按我们上面的节点来讲,就是遍历 [e d c h],代码中s2=2  e2=5,
    */
    for (j = s2; j <= e2; j++) {
      if (
        newIndexToOldIndexMap[j - s2] === 0 &&
        isSameVNodeType(prevChild, c2[j])
      ) {
        /* 如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex  */
        newIndex = j
        break
      }
    }
  }

  if (newIndex === undefined) {
    /* 没有找到与老节点对应的新节点,删除当前节点 */
    unmount(prevChild, parentComponent, parentSuspense, true)
  } else {
    /* 把老节点的索引,记录在存放新节点的数组中, */
    newIndexToOldIndexMap[newIndex - s2] = i + 1
    if (newIndex >= maxNewIndexSoFar) {
      maxNewIndexSoFar = newIndex
    } else {
      /* 证明有节点已经移动了   */
      moved = true
    }
    /* 找到新的节点进行patch */
    patch(
      prevChild,
      c2[newIndex],
      container,
      null,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized
    )
    patched++   // 记录已经在新节点中找到了了多少个老节点了
  } 
}

所以你可以理解为主要执行了2步操作:

Step 1:

通过老节点的key,找到新节点的 index,这里有两种情况:

  1. 老节点没有key,遍历剩下的所有新节点,尝试找到索引
  2. 老节点有key,在keyToNewIndexMap中找到索引

Step 2:

  1. 如果第一步依旧没有找到 Index,则表示没有和新节点对应的老节点,删除当前旧节点。
  2. 如果找到了Index,则表示老节点中有对应的节点,赋值新节点索引到newIndex。再把老节点索引,记录到新节点的数组newIndexToOldIndexMap中,这里索引+1,是因为初始值就0,如果直接存放索引,从第一个开始就发生变化那么存入的索引会是0,则会直接被当作没有老节点匹配。

解释判断: newIndex >= maxNewIndexSoFar

因为遍历老数组是从前往后遍历,那么假如说在遍历的时候,就记录该节点在新节点数组中的位置,假如发生倒转,那么就是 maxNewIndexSoFar > newIndex , 就代表说新老节点的某节点已经发生了调换,在 diff 过程中肯定会涉及元素的移动。

// 举个栗子
if 旧节点 = [a, b, c, f];
if 新节点 = [a, f, b, c];

so

循环遍历旧结点:
when Pointer -> b ,newIndex = 2 and maxNewIndexSoFar = 0

when Pointer -> c ,newIndex = 3 and maxNewIndexSoFar = 2

when Pointer -> f ,newIndex = 1 and maxNewIndexSoFar = 3 

result ->  moved = true

// 把流程串起来

旧节点: a b [c d e] f g , c key 存在,d、e 的 key === undefined

新节点: a b [e d c h] f g

得到待处理的节点: [e d c h]

按以上逻辑,先遍历 [c d e]。 

when when Pointer -> c, newIndex = 4 s2 = 2 newIndexToOldIndexMap = [0,0,3,0].执行 patch

when when Pointer -> d, newIndex = undefined ,删除 d
when when Pointer -> e, newIndex = undefined ,删除 e

多么可怕的事实,如果key不存在,直接删除旧结点。 所以得出结论:写Vue代码,一定要注意要有key ?我自己都差点信了😂

提出一个很重要的概念: 最长递增子序列

我会给大家写上中文注释的。😊

// 5.3 move and mount
// generate longest stable subsequence only when nodes have moved

// 移动老节点、创建新节点
const increasingNewIndexSequence = moved
    ? getSequence(newIndexToOldIndexMap)
    : EMPTY_ARR;
// // 用于节点移动判断
j = increasingNewIndexSequence.length - 1;
// looping backwards so that we can use last patched node as anchor
// 向后循环,也就是倒序遍历。 因为插入节点时使用 insertBefore, 即向前插以便我们可以使用最后一个更新的节点作为锚点 
for (i = toBePatched - 1; i >= 0; i--) {
    const nextIndex = s2 + i;
    const nextChild = c2[nextIndex];
    const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
    if (newIndexToOldIndexMap[i] === 0) { // 如果仍然是默认值 0, 证明是一个全新的节点
        // mount new
        patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
    }
    else if (moved) {
        // move if:
        // There is no stable subsequence (e.g. a reverse)
        // OR current node is not among the stable sequence: 当前索引不是最长递增子序列里的值,需要移动
        if (j < 0 || i !== increasingNewIndexSequence[j]) {
            move(nextChild, container, anchor, 2 /* REORDER */);
        }
        else {
        // 是最长递增子序列里的值,则指向下一个
            j--;
        }
    }
}
  • 2 - patchUnkeyedChildren: 处理有没有key的节点 至于没有key的结点咋处理……

辣鸡,非常粗暴,简直无法直视。 自己去看源码。(就是对比新旧结点的length,新的长就直接mount new。 旧的长就先umount old)

小结一下:

  1. 没有key的结点发生变化,直接火葬场吧。
  2. 有key的结点发生变化
    • 头和头比较一下
    • 尾和尾比较一下
    • 头和尾比较一下
    • 找出最长递增子序列,随时移动,随时创建新结点。

2. 时间切片(Time Slicing)

Vue3 抛弃了时间切片,这简直令我……。emmmm, 我还能说什么呢,你不卡谁卡。

关于为什么Vue3不使用时间切片(Time Slicing), 尤雨溪在 Vuejs issue 里面有很详细的回答。 尤雨溪回答的原文地址

好吧。我来翻译一下(我就在想,我不翻译让老铁们直接去看原文会被打吗?)。


在web应用程序中,更新内容丢帧(janky)通常是由大量CPU时间+原始DOM更新的同步操作引起的。时间切片是在CPU工作期间保持应用程序响应的一种尝试,但它只影响CPU工作。但DOM更新的刷新必须仍然是同步的,目的是确保最终DOM状态的一致性。

所以,想象两种丢帧更新的场景:

1.CPU工作时间在16ms以内,但原生DOM的更新操作量很大(例如,mount 大量新的 DOM内容)。无论有没有使用时间切片,该应用程序仍会感觉“僵硬(丢帧)”。

  1. CPU任务非常繁重,需要超过16ms的时间。从理论上讲,时间切片开始发挥作用了。然而,HCI的研究表明,除非它在进行动画,否则对于正常的用户交互,大多数人不会感觉到差异,除非更新时间超过100毫秒。

也就是说,只有当频繁的更新需要超过100毫秒的纯CPU时间时,时间切片才变得实际有用。

也就是说,只有在频繁进行超过100ms的纯CPU任务更新时,时间切片才实际有用。

有趣的地方在于,这样的场景更经常地发生在React中,因为:

  • i. React的虚拟DOM操作( reconciliation 调度算法 )天生就比较慢,因为它使用了大量的Fiber架构

  • ii. React使用JSX来渲染函数相对较于用模板来渲染更加难以优化,模板更易于静态分析。

  • iii. React Hooks将大部分组件树级优化(即防止不必要的子组件的重新渲染)留给了开发人员,开发人员在大多数情况下需要显式地使用useMemo。而且,不管什么时候React接收到了children属性,它几乎总要重新渲染,因为每次的子组件都是一棵新的vdom树。这意味着,一个使用Hook的React应用在默认配置下会过度渲染。更糟糕的是,像useMomo这类优化不能轻易地自动应用,因为:

    1. 它需要正确的deps数组;
    2. 盲目地任意使用它可能会阻塞本该进行的更新,类似与PureComponent

    不幸的是,大多数开发人员都很懒,不会积极地优化他们的应用。所以大多数使用Hook的React应用会做很多不必要的CPU工作。

相比之下,Vue就上面的问题做一下比较:

  1. 本质上更简单,因此虚拟DOM操作更快( no时间切片-> nofiber-> 更低开销);

  2. 通过分析模板进行了大量的AOT优化,减少了虚拟DOM操作的基本开销。Benchmark显示,对于一个典型的DOM代码块来说,动态与静态内容的比例大约是1:4,Vue3的原生执行速度甚至比Svelte更快,在CPU上花费的时间不到React的1/10。

  3. 智能组件树级优化通过响应式跟踪,将插槽编译成函数(避免子元素重复渲染)和自动缓存内联句柄(避免内联函数重复渲染)。除非必要,否则子组件永远不需要重新渲染。这一切不需要开发人员进行任何手动优化。

    这意味着对于同一个更新,React应用可能造成多个组件重新渲染,但在Vue中大部分情况下只会导致一个组件重新渲染。

默认情况下 Vue3应用比React应用花费更少的CPU工作时间, 并且CPU工作时间超过100ms的机会大幅度减少了,除非在一些极端的情况下,DOM可能成为更主要的瓶颈。

现在,时间切片或并发模式带来了另一个问题:因为框架现在安排和协调了所有更新,它在优先级、失效、重新实例化等方面产生了大量额外的复杂性。所有这些逻辑处理都不可能被tree-shaken,这将导致运行时所占CPU内存的大小膨胀。即使包含了Suspense和所有的tree-shaken,Vue 3的运行时仍然只有当前React + React DOM的1/4大小。

注意,这并不是说并发模式作为一个整体是一个坏主意。它确实提供了处理某类问题的有趣的新方法(特别是与协调异步状态转换相关的),但时间切片(作为并发的一个子功能)专门解决了React中比其他框架中更突出的问题,同时也产生了自己的成本。对于Vue 3来说,这种权衡似乎并不值得。


如果你也是个老react玩家,想必你会不服气。 尤雨溪的回复当中看上去好像指出了 react 的一些弊端和短板。恰有一种踩低别人抬高自己的节奏。

尤雨溪指出:

  1. React + React DOM 在运行中所占CPU内存要高于Vue运行时所占内存,比例已经高达 4:1
  2. React Hooks 不好用,即使用好了useMemo 、 memo 也还得保证 deps 的正确性。
  3. React的操作虚拟DOM,其实就是指 React 的调度算法比较慢。而 Vue 通过分析模板进行了大量的 AOT优化,减少了虚拟DOM操作的基本开销。所以Vue的操作虚拟 DOM 要比 React 快。
  4. 并发模式不是坏死,但时间切片就不一定了,至少React 的时间切片作法就不咋地。

作为一个过来人,深知React的一些缺点。 我们换个角度来看待1-4点。

  1. 老实讲,谁跑得快得分时间。 如果React 需要4个小时,Vue需要1个小时,请问你觉得谁快? 但React 跑400ms,Vue跑100ms,请问你觉得谁快?换句话说,针对此问题,真的很有必要吗?前端性能瓶颈如何优化?React好做还是Vue好做?

  2. React Hooks 用起来很好用,但能用好确实不容易。但如果我用好了,这个问题还存在吗?

  3. React 调度算法慢,Vue就相比较下快,那就得分两个方面来

    • React 可以通过 实操写代码来控制快慢,例如每次操作尽可能少的VDOM。 Vue的AOT优化可以让开发人员去做吗?很明显,Vue 不可以。
    • React 真的慢吗? 或者说在操作大量DOM的场景下,Vue 真的优于 React 吗?
  4. 稍微解释一下所谓的 React 时间切片做法。 React 会将Fiber 字任务交给浏览器的空闲时间去完成,这个过程可以随时被中断,中断以后下次还能接着上一次的位置继续执行任务。

    • “时间切片” 在react中的应用远不是为快不快的问题而存在的,而是为了可恢复性。例如用户在做负责的交互行为,或者页面要做复杂动画的时候,如果React加强了自身消耗却保证了交互、动画的流畅性,你觉得值吗?

小结一下

其实,现在市场上关于React 和 Vue 有很多激烈的讨论,都是由于自身的优缺点而产生的。

例如网络上很多人在互相攻击:

“ Vue 只适合小项目,大项目扛不起来”

”React 无数个回调,无数个选择表达式,this绑定…乱!“

“Vue好上手,岗位多”

“大厂基本都用 React,不用 Vue ”

那如果从使用层面上来考虑的话,emmm,列个框吧。

问题VueReact
this混乱源码实现已经处理好了this,不需要你额外处理React Hooks 已经不存在this这个东西了。
上手easynormal
用好normalhard
新手友好极度友好不友好
可扩展性一般
底层实现硬核,能做的都做得挺好硬核,但内容更多
hook细讲细讲

3. Vue3 & React17 比较

Vue 3.0 Beta 版本刚发布的时候,大家吵得很凶。印象深刻的有两点吐槽。

  • 吐槽意大利面代码结构
    • 杂七杂八一堆丢在 setup 里,我还不如直接用 react
    • 代码结构不清晰,语义不明确,这操作无异于把 vue 自身优点都扔了
    • 结构不清晰,担心代码量一上去不好维护
  • 抄袭 React
    • Vue-Composition-Api的主要灵感来源是 React Hooks 的创造力(这也是吐槽最狠的地方)

其实真的用过并且懂 React hooks 的人看到这个都会意识到 Vue Composition API (VCA)hooks 本质上的区别。VCA 在实现上也其实只是把 Vue 本身就有的响应式系统更显式地暴露出来而已。真要说像的话,VCAMobX 还更像一点。

(这里我为Vue洗冤屈了,这说明我还是很可观的。毕竟是研究过Vue源码后的发言)

举一个 Vue CLI UI file explorer 官方吐槽的例子,这个组件是 Vue-CLI 的 gui 中(也就是平常我们命令行里输入 vue ui 出来的那个图形化控制台)的一个复杂的文件浏览器组件,这是 Vue 官方团队的大佬写的,相信是比较有说服力的一个案例了。

自看去github上看,我这就不贴代码了,深夜凌晨1点了都。

然后,看官方给的图你也明白了。

image.png

图左边是原始风格,右边是 hook 风格。

其中一个 hook 风格的方法:

function useCreateFolder(openFolder) {
  // originally data properties
  const showNewFolder = ref(false);
  const newFolderName = ref("");

  // originally computed property
  const newFolderValid = computed(() => isValidMultiName(newFolderName.value));

  // originally a method
  async function createFolder() {
    if (!newFolderValid.value) return;
    const result = await mutate({
      mutation: FOLDER_CREATE,
      variables: {
        name: newFolderName.value,
      },
    });
    openFolder(result.data.folderCreate.path);
    newFolderName.value = "";
    showNewFolder.value = false;
  }

  return {
    showNewFolder,
    newFolderName,
    newFolderValid,
    createFolder,
  };
}

我们来看一下Vue Hook风格下的一段代码:

export default {
  setup() {
    // ...
  },
};

function useCreateFolder(openFolder){
// ...
}
function useCurrentFolderData(networkState) {
  // ...
}

function useFolderNavigation({ networkState, currentFolderData }) {
  // ...
}

function useFavoriteFolder(currentFolderData) {
  // ...
}

function useHiddenFolders() {
  // ...
}

function useCreateFolder(openFolder) {
  // ...
}

再来看看现在的 setup 函数。

export default {
  setup() {
    // Network
    const { networkState } = useNetworkState();

    // Folder
    const { folders, currentFolderData } = useCurrentFolderData(networkState);
    const folderNavigation = useFolderNavigation({ networkState, currentFolderData });
    const { favoriteFolders, toggleFavorite } = useFavoriteFolders(currentFolderData);
    const { showHiddenFolders } = useHiddenFolders();
    const createFolder = useCreateFolder(folderNavigation.openFolder);

    // Current working directory
    resetCwdOnLeave();
    const { updateOnCwdChanged } = useCwdUtils();

    // Utils
    const { slicePath } = usePathUtils();

    return {
      networkState,
      folders,
      currentFolderData,
      folderNavigation,
      favoriteFolders,
      toggleFavorite,
      showHiddenFolders,
      createFolder,
      updateOnCwdChanged,
      slicePath,
    };
  },
};

🐂🍺了,干净不?

对比一下hook原理吧。

还是举个栗子。


<template>
  <div>
    <span>{{count}}</span>
    <button @click="add"> Add By 1 </button>
  </div>
</template>

export default {
    setup() {
        const count = ref(0)

        const add = () => count.value++

        effect(function active(){
            console.log('count changed!', count.value)
        })

        return { count, add }
    }
}

非常简单的一个栗子。

  1. setup只执行一次,
  2. 如果需要在 count 发生变化的时候做某件事,我们只需要引入 effect 函数。
  3. 这个 active 函数只会产生一次,这个函数在读取 count.value 的时候会收集它作为依赖,那么下次 count.value 更新后,自然而然的就能触发 active 函数重新执行了。

总结一下: hook 初始化一次,后用无穷。

再来看个栗子。


export default function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('渣男');
  const add = () => setCount((prev) => prev + 1);

  useEffect(()=>{
      setName(`渣男渣了${count}次`)
  },[count])

  return (
    <div>
      <span>{count}</span>
      <span>{name}</span>
      <button onClick={add}> +1 </button>
    </div>
  );
}

看得出,功能一样,但这是个React 组件。通过引用 <Counter /> 这种方式引入的,我们知道JSX就是js,Babel 实际上会把它编译成 React.createElement(Counter) 这样的函数执行。

也就是说每次渲染,这个函数都会被完整的执行一次。

useState 返回的 count 和 setCount 则会被保存在组件对应的 Fiber 节点上,并且每个 React 函数每次执行 Hook 的顺序必须是相同的。

React Hooks里的钩子函数都是可以被多次调用的,这也是目前我觉得React 对开发者最为友好的一个个创意。我可以充分利用这些钩子函数去最大程度颗粒化我的逻辑,达到高度复用且互不影响。

上述有说到 deps 依赖的弊端。 React Hooks 很多钩子都是需要依赖于状态变量的。 简单点说就是所依赖的状态变量发生了改变,那就可以执行相应的操作。听起来很美好对伐? 但一个搞不好就是闭包陷进…… 你用的好,就牛。用不好你就是辣鸡。

所以如果你是函数式编程风格的死忠粉,React Hooks绝对是你的最爱。

另外,忽然想到网络上一句话: Vue 给你持久,React给你自由。

所以,技术调研的时候,考虑清楚你的场景。其它真没啥,代码总是人写的,Vue再好用也能写成si,React 再难用,写好了也能上天。

凌晨1:26分了,技术文章是写起来就没边了,因为能讲的真的很多很多…… 关于React源码解毒,可以看看过往文章。关于Vue 剩下源码,其实真的不多,相比之下Vue的源码真的少太多了,注释还丰富(比较国人写英文更容易看懂些)。所以,有机会再补上吧。

end