最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue3 源码解毒 & PK React17

    正文概述 掘金(是渣男吧)   2021-07-29   544

    前言

    因为最近开始写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点了都。

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

    Vue3 源码解毒 & PK React17

    图左边是原始风格,右边是 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


    下载网 » Vue3 源码解毒 & PK React17

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元