• wordpress CMS主题:ssmay主题 wordpress CMS主题:ssmay主题
  • 首页 > 前端开发 > React专题:抽象UI

    React专题:抽象UI

    作者: 分类:前端开发 点击: 240 次
    wordpress CMS主题:ssmay主题

      扫描下面的二维码,“关注”我的百家号。

      本文是『horseshoe·React专题』系列文章之一,后续会有更多专题推出

      来我的 GitHub repo 阅读完整的专题文章

      来我的 个人博客 获得无与伦比的阅读体验

      所有的UI都是树形结构,大节点嵌套小节点。于是React开发人员开始想,既然它们有这些共性,能不能把它们抽象出来呢?

      于是就有了所谓的Virtual DOM,但我更愿意称它为抽象UI。抽象比虚拟更加触及精髓,因为DOM是一个只存在于网页中的概念。

      对象

      一个节点就是一个对象,对象之间的嵌套关系对应于DOM节点之间的嵌套关系。

      这个对象要描述哪些关键信息呢?

      • 节点类型。
      • 节点属性。注意,子节点也是属性之一。
      • 服务于React自身的信息。

      下面是一个节点的对象表示法。

      {
          $$typeof: Symbol(react.element),
          key: null,
          props: {
              children: Object || Array,
          },
          ref: null,
          type: 'div',
          _owner: {
              alternate: {},
              child: {},
              effectTag: 5,
              expirationTime: 0,
              firstEffect: null,
              index: 0,
              key: null,
              lastEffect: null,
              memoizedProps: null,
              memoizedState: null,
              mode: 0,
              nextEffect: null,
              pendingProps: null,
          },
      }
      

      diff

      有了抽象UI,那么组件挂载的过程究竟是怎样的呢?

      首先呢,初次挂载的时候肯定是把抽象UI编译成DOM标签,然后插入到container当中。

      没错,React再通天,最终也是要使用innerHTML来挂载DOM 的。

      const setInnerHTML = createMicrosoftUnsafeLocalFunction(function(node, html) {
          node.innerHTML = html;
      });
      

      然后呢,每当有state或props更新,再将抽象UI编译一次,插入到container当中。

      当然不是这样的啦!

      React怎么可能跟我们写的过家家代码一样呢。

      初始挂载的时候,React确实是会使用innerHTML接口,但是一旦挂载完毕,后续的更新都是打补丁,也就是精准的局部更新。

      打补丁就涉及到两个问题:

      • 如何知道在哪里打补丁?
      • 打补丁的频率。

      这就要说到抽象UI的diff算法。

      当React决定来一波更新的时候,它会生成一套新的抽象UI树。注意,这时候React手里有两套抽象UI树了,通过对比这两棵树,React就能知道哪里产生了变化。

      按照常识,假如某棵树的某个父节点由div换成了ul,但是子节点没有任何变化,那我们只需要修改该父节点的类型即可。因为只有这里变了,这才是打补丁的正确方式。

      但是我们要考虑另一个问题:比较的过程所花的时间。

      实现上述常识操作的算法复杂度是O(n^3),因为要递归比较。也就是说如果抽象UI树非常庞大,那以JavaScript的尿性是吃不消的,页面会出现卡顿。

      所以React不得不做一些妥协。只比较同级的节点可以让算法复杂度降低到O(n),相对来讲,是一个比较好的折中方案。那么上面的例子,一旦父节点由div换成了ul,那么React就会简单粗暴的替换掉所有子节点。

      每次setState都会触发diff算法吗?

      当然不会。因为不是真正的UI,所以React可以控制它的更新频率,这样也可以提升性能。

      React生命周期大致可以分为三个阶段:挂载,更新,卸载。在挂载阶段内和一次更新阶段内,diff算法只会运行一次,这就是打补丁的频率。

      这也是人们说setState是异步的原因。

      两种变化类型

      现在我们知道,React只会对同一层级的节点进行比较。

      那么就会产生三种结果:没有变化,节点类型改变(或者同时节点属性),节点类型不变节点属性改变。

      没有变化是最好的,早点收工。

      节点类型改变,比如说div变成了ul,那么不再检查属性,直接替换。同时所有的子节点也要替换。当然如果新的抽象UI树没有对应的节点,那就意味着删除了。

      节点类型不变节点属性改变,那么DOM树结构不变,只是对该节点的属性做一些增删改操作。

      列表

      上面说到,React会依次对同一层级的节点进行比较,如果类型不同则重新挂载节点,如果类型相同属性不同则更新节点。

      我们来看看列表,如果我给一个列表unshift一项,可以想象,React在比较列表的每一项时,结果都是类型相同属性不同,于是需要更新列表的每一项。但其实我们只是往列表插入一项而已,原生写法只需要insertBefore就好了。

      算法不能太复杂,有没有什么其他的办法呢?

      如果给列表的每一项加一个唯一标识符,React在比较的时候就能知道某一项不是被去掉了,而是换了位置。也就能学会插入操作了。

      所以开发者在使用map方法渲染列表时,都会要求给列表每一项加一个唯一的key属性,它就是让React变得更聪明的脑白金。

      import React from 'react';
      
      const App = ({ list }) => {
          return (
              <div>
                  {list.map(v => <div key={v}>{v}</div>)}
              </div>
          );
      }
      
      export default App;
      

      有人问了:我不用map方法渲染,手动写一个列表,React打算怎么处理?

      不处理!

      你想,map方法渲染的列表是数据映射出来的,开发者很容易操作数组。但是在JSX中手动写一个列表,首先不推荐大家这样做(吃力不讨好),其次大家很少会在React中操作DOM,所以你也不能把这个列表玩出花来。最重要的,React没办法识别这是一个列表。

      还有一点,React不推荐使用列表的索引来充当key属性值,因为对列表进行插入或者删除操作时,索引也会相应的改变,它是不稳定的,key存在的意义也就没有了。

      但是有些时候我们实在找不到唯一的稳定的某个值,那就只能用索引来搪塞了。

      那意思React是说:孩子,不为难你了,早点下班吧。

      Fiber

      一般的显示器刷新频率都是60Hz。这是什么意思呢?意味着屏幕画面每秒刷新60次,或者大约每16.7ms屏幕画面刷新一次。这是人眼比较舒服的频率。

      对网页而言,亦是如此。

      这意味着如果JavaScript计算任务连续占用浏览器主线程超过16.7ms,网页就没办法做到60Hz的刷新率,结果就是我们常说的页面卡顿甚至白屏。

      一般来说,只要不陷入死循环或者太深的调用栈,JavaScript可以从容的执行,甚至大概率还有剩余时间,行到水穷处,坐看云起时。

      偏偏哪,React优化性能的机制是依靠大量的JavaScript计算。如果是一个大型的项目,组件可能会有上百层的嵌套,这个调用栈之深可想而知。

      所以为了更加平滑的体验,React开发组闭关两年,祭出了Fiber这个大杀器。

      可以把Fiber理解为虚拟调用栈。计算机术语中除了进程(Process)、线程(Thread),还有纤程(Fiber),React就是取其更精密的并发控制之意。

      React这个开源项目大体上可以分为两部分,一部分是构建抽象UI,并提供变化检测,叫Reconciler。另一部分是将抽象UI渲染到具体的平台上,叫Renderer。

      Reconciler的主要工作是计算,Renderer的主要工作是排版和绘制。

      首先,要将Reconciler拆分成更小的事务,再将这些事务从用户体验的角度划分优先级,从而可以实现更细微的调度,而不是像之前一口气跑完。至于排版和绘制,那本来就是平滑体验的关键,去打断它没有任何意义。

      调度工具

      接下来就轮到大佬上场了。

      requestAnimationFrameAPI:刀耕火种时期的前端想要写动画,得用setTimeout或者setInterval来触发一些定时动作。首先,如果定时器执行频率比屏幕刷新率高,有一些动作可能会被忽略,浏览器直接渲染下一个动作去了。这就是定时器与屏幕刷新步调不一致导致的丢帧。还有,定时器是异步任务,如果主线程被占用它就只能眼巴巴的等着。而requestAnimationFrame从名字就能看出来,它是为动画而生的,杀手锏是浏览器会保证它的执行频率和屏幕刷新率一致(如果有空余时间的话),就是那种被大佬罩着的人。优先级比较高。

      requestIdleCallbackAPI:这个接口更有意思,它会告诉开发者,在16.7ms之内,浏览器把活干完还剩下多少时间,然后把剩下时间的控制权交给开发者。当然如果浏览器自己都不够用,你就喝汤吧。所以它不保证一定会执行,行就行,不行就拉倒。

      React就是通过这两个API来实现事务的调度的。

      Reconciler从一个风风火火的意气少年变成了一个小心翼翼的中年男人,每执行一小段事务就探出头来看看,浏览器当前有没有高优先级的事务在排队啊?如果有赶紧让开啊,等浏览器处理完咱们再偷偷的进村啊。

      对生命周期的影响

      我们说生命周期分为挂载、更新和卸载,生命周期从另一个角度又可以分为render前和render后。

      之所以这样划分是因为React本身有Reconciler和Renderer两部分,现在的问题在哪呢?

      因为Reconciler现在是可以打断的,也就是说render前的生命周期钩子有可能被执行多次,而且它的表现是没办法预测的,因为是否被打断要依据当时的情况。

      这也就是componentWillMount、componentWillReceiveProps和componentWillUpdate生命周期钩子要被逐步替换掉的原因。

      React专题一览

      什么是UI

      JSX

      可变状态

      不可变属性

      生命周期

      组件

      事件

      操作DOM

      抽象UI



      欢迎“关注”我的百家号。

      头条二维码
      加入我的QQ群
      头条二维码
      关注我的百家号

    文章作者:sunny
    本文地址:http://wanlimm.com/77202006218491.html
    版权所有 © 转载时必须以链接形式注明作者和原始出处!

    上一篇:
    下一篇:
    wordpress CMS主题:ssmay主题

    或许你会感兴趣的文章:

    发表评论

    电子邮件地址不会被公开。 必填项已用*标注

    This site uses Akismet to reduce spam. Learn how your comment data is processed.