likes
comments
collection
share

React- fiber架构 和 diff算法

作者站长头像
站长
· 阅读数 3

导言

这篇文章主要是为了学习和记录。

讲了react的事件机制 ,diff算法 以及 16.8引入的 fiber架构

react合成事件详情解释

如果dom上绑定了过多的事件处理函数,整个页面响应以及内存可能会有影响。 react为了避免这类DOM事件的滥用,实现了一个中间层。

在document处监听所有支持的事件,当事件发生并冒泡到document上时,交给中间层。

当事件触发时,使用统一的dispatchEvent将指定函数执行

export default class Test extends Component {
    constructor() {
        super(arguments);
        this.onReactClick.bind(this);
    }
    componentDidMount() {
        const parentDom = ReactDOM.findDOMNode(this);
        const childrenDom = parentDom.querySelector(".button");
        childrenDom.addEventListener('click', this.onDomClick, false);
    }
    onDomClick() {  // 事件委托
        console.log('Javascript Dom click');
    }
    onReactClick() {  // react合成事件
        console.log('React click');
    }
    render() {
        return (
            <div>
                <button className="button" onClick={this.onReactClick}>点击</button>
            </div>
        )
    }
}


合成事件和原生事件混合使用

  • 响应顺序

addEventListner

重点是addEventListner的第三个参数

  • 默认是false

事件冒泡和事件捕获

HTML DOM中的事件有两种传播方式,即冒泡和捕获。事件传播是一种在事件发生时定义元素顺序的方法。

  • 冒泡: 即从里到外
  • 捕获:即从外到里

addEventListner的第三个参数设置为true,表示使用捕获捕获

react16.8的 diff算法

虚拟DOM

虚拟DOM的本质是一个对象。由于操作真实DOM的成本比较高,所以推出了虚拟DOM。

在页面结果需要修改时,先修改虚拟DOM的节点,然后再对比 新旧两棵DOM树的差异。

最后把差异patch到真实DOM树上。

另外,也正是 基于虚拟DOM,react实现了自己的一套事件机制,并不少在事件绑定在一个确定的节点上,而是利用事件冒泡的过程,采用事件代理,批量更新的方式,从而也磨皮了各个浏览器之间的事件兼容性问题。

传统的diff算法

在计算一棵树转为另一课树中有哪些变化,传统的diff算法通过循环递归对比节点的方式,依次对比,其时间复杂度是 O(n^3)

举个例子:

function diffTrees(oldTree, newTree) {
  const differences = [];

  function diffNodes(oldNode, newNode, path) {
    if (oldNode === newNode) {
      // 节点完全相同,无需处理
      return;
    }

    if (!oldNode && newNode) {
      // 新增节点
      differences.push({ type: "ADDED", path, node: newNode });
      return;
    }

    if (oldNode && !newNode) {
      // 删除节点
      differences.push({ type: "REMOVED", path, node: oldNode });
      return;
    }

    // 节点内容改变
    if (typeof oldNode === "object" && typeof newNode === "object") {
      // 假设节点是对象,并且我们关心的是它们的属性差异
      const oldKeys = Object.keys(oldNode);
      const newKeys = Object.keys(newNode);

      // 找出属性增加、删除或修改的情况
      for (const key of oldKeys) {
        if (!newNode.hasOwnProperty(key)) {
          // 属性被删除
          differences.push({
            type: "REMOVED_ATTR",
            path: [...path, key],
            oldValue: oldNode[key],
          });
        } else {
          // 递归对比属性值
          diffNodes(oldNode[key], newNode[key], [...path, key]);
        }
      }

      for (const key of newKeys) {
        if (!oldNode.hasOwnProperty(key)) {
          // 属性被添加
          differences.push({
            type: "ADDED_ATTR",
            path: [...path, key],
            newValue: newNode[key],
          });
        }
      }
    } else {
      // 节点内容直接改变(不是对象)
      differences.push({
        type: "CHANGED",
        path,
        oldValue: oldNode,
        newValue: newNode,
      });
    }
  }

  // 开始对比树的根节点
  diffNodes(oldTree, newTree, []);

  return differences;
}

// 示例
const oldTree = {
  name: "Old Root",
  children: [{ name: "Old Child 1" }, { name: "Old Child 2", age: 5 }],
};

const newTree = {
  name: "Root", // 名称变化
  children: [
    { name: "Old Child 1", age: 10 }, // 添加年龄属性
    { name: "Updated Child 2", age: 6 }, // 名称变化
    { name: "New Child 3" }, // 新增节点
  ],
};

const differences = diffTrees(oldTree, newTree);
console.log(differences);

react 的diff算法

O(n^ 3)  转化为O(n)

react 通过三大策略完成了优化

  1. 同级比较

由于dom节点跨层级移动的操作比较少,在对比的过程中,如果发现节点不在了,会完全删除,然后重新创建。

  1. 组件比较

组件比较有两种策略

  • 相同类型组件

  • 不同类型组件

  • 相同类型组件

组件类型没有改变,但属性改变了,react会更新该组件的属性,并调用render方法

  • 不同类型组件

旧的组件会被卸载,新的组件会被挂载

  1. 节点比较

对于同一层级的一子自节点,通过唯一的key进行比较

为什么不推荐使用 index作为 key值呢?

例如:当用 index作为key时

React- fiber架构 和 diff算法

当数组中间插入,或者数组重新排序后,下标对应的组件就改了,那么key也更改,基本所有的节点都必须重新渲染一遍。

使用id,唯一值才是正确的。

react fiber架构

为什么要推出 react fiber呢 ?

得先了解一下一些基本知识

进程与线程

浏览器的一个tab(标签页)是一个进程。其中包含了多个线程。例如

  • GUI渲染线程:主要负责渲染浏览器页面。HTML和 CSS的解析,构建DOM树和 CSSOM树。当页面需要重绘 或者回流时,该线程就会执行。 值得注意的是,GUI渲染线程和 js执行线程是互斥的。

还有一点:

会阻塞GUI渲染线程的是javaScript的同步任务(例如递归,循环,计算密集型任务)

而使用处理异步任务时(如:promise, async/await ),会挂载到异步任务队列中, 并不会阻塞渲染。

  • js执行线程。负责处理js脚本。

react 和 vue的响应式原理

在react中, 组件的状态是不能被修改的。 setState没有修改原来那块内存中的变量,而是去新开辟一块内存。vue则是直接修改保存状态的那块原始内存。

数据修改后,react中,调用setState方法后,会自顶向下重新渲染组件,自顶向下的含义是,该组件以及它的子组件全部需要渲染。

而vue是使用了Object.defineProperty对数据设置的 setter 和 getter进行劫持。 也就是说,vue能知道哪个视图模版中用到了这个数据,然后只更新这个组件即可。

这也就意味了,react的组件渲染是很消耗性能的,因为你要对比当前组件以及子组件的虚拟DOM树。

为什么要推出 react fiber呢?

react组件更新是CPU密集的操作,因为它要做对比新旧两颗虚拟DOM树的操作,找到更新的内容,patch更新到真实DOM上。由于这种操作时通过递归方式实现的,是同步且不可中断的。所以过多节点对比,会直接影响 ui渲染,影响用户体验。

fiber是什么?

react fiber并没有让 diff算法的时间缩短,而是把 diff的过程分成了一小段一小段, 称为 fiber. 由于使用了链表这种结构,可以当有高优先级的任务执行时中断,当浏览器空闲时恢复,底层使用 reqestIdCallback API来判断浏览器是否空闲,最后直到完成diff后,一次性更新到视图上。

替代了原有同步且不可中断的递归diff方式。