react Diff 算法

一、Fiber 架构

JSX

转换前

js 复制代码
<>  
  <div>  
    <ul>  
      <li>1</li>  
      <li>2</li>  
    </ul>  
    <span>hello</span>  
  </div>  
</>

babel转换后:

js 复制代码
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";  
/*#__PURE__*/_jsx(_Fragment, {  
  children: /*#__PURE__*/_jsxs("div", {  
    children: [/*#__PURE__*/_jsxs("ul", {  
      children: [/*#__PURE__*/_jsx("li", {  
        children: "1"  
      }), /*#__PURE__*/_jsx("li", {  
        children: "2"  
      })]  
    }), /*#__PURE__*/_jsx("span", {  
      children: "hello"  
    })]  
  })  
});

ReactElement

js 复制代码
export interface ReactElementType {  
  $$typeof: symbol | number; // 指明这是一个react element,还有fragment、context等类型  
  type: ElementType; // 对应jsx中标签名  
  key: Key; // 传递的参数key  
  props: Props; // 传递的props  
  ref: Ref; // props中传递的ref  
  __mark: string;  
}

FiberNode

js 复制代码
export class FiberNode {  
  type: any; // 存储函数组件本身、类组件本身、原生 HTML 标签名、React 提供的特殊类型(Fragment、Provider...)  
  tag: WorkTag; // 指明当前是一个什么类型的节点(*FunctionComponent、HostComponent...* )  
  pendingProps: Props; // 存储当前需要更新的props  
  key: Key;  
  stateNode: any; // HostComponent->真实dom节点实例,ClassComponent->this,FunctionComponent->null  
  ref: Ref | null; // ref 用于存储**类组件实例**或**DOM 节点**的引用,在 commit 阶段被赋值,并在 unmount 时清除  
  
  return: FiberNode | null; // 指向父fibernode  
  sibling: FiberNode | null; // 指向兄弟fibernode  
  child: FiberNode | null; // 指向子fibernode  
  index: number; // 当前索引  
  
  memoizedProps : Props | null; // 存放更新完成后的props  
  memoizedState: any; // 链表结构,存放hooks链表(对于useState,存放当前计算的值,并通过next指向下一个hooks,对于 useEffect,memoizedState 存储的是 create 回调、destroy 清理函数和 deps(依赖数组))  
  alternate: FiberNode | null; // 存放双缓存机制中的另一个fibers树对应的fiber  
  flags: Flags; // 存放当前节点的副作用标记(插入、更新、删除等操作)  
  subtreeFlags: Flags; // 存放当前节点的子节点的副作用标记(插入、更新、删除等操作)  
  updateQueue: unknown; // useState、setState触发的更新的Update链表,Update中还存放了优先级信息lane  
  deletions: FiberNode[] | null; // 存放需要在commit阶段被删除的fibernode  
  
  lanes: Lanes; // 当前fiber节点的优先级信息  
  childLanes: Lanes; // 当前fiber的子fiber的优先级信息  
  
  dependencies: FiberDependencies<any> | null; // 存储多个context的链表信息  
  
  constructor(tag: WorkTag, pendingProps: Props, key: Key) {  
   // 实例  
   this.tag = tag;  
   this.key = key || null;  
   // HostComponent <div> div DOM  
   this.stateNode = null;  
   // FunctionComponent () => {}  
   this.type = null;  
  
   // 构成树状结构  
   this.return = null;  
   this.sibling = null;  
   this.child = null;  
   this.index = 0;  
  
   this.ref = null;  
  
   // 作为工作单元  
   this.pendingProps = pendingProps;  
   this.memoizedProps = null;  
   this.memoizedState = null;  
   this.updateQueue = null;  
  
   this.alternate = null;  
   // 副作用  
   this.flags = NoFlags;  
   this.subtreeFlags = NoFlags;  
   this.deletions = null;  
  
   this.lanes = NoLanes;  
   this.childLanes = NoLanes;  
  
   this.dependencies = null;  
  }  
}

2.Diff算法实现

2.1单一节点的diff算法(更新后是单节点) 需要处理的情况:singleElement、singleTextNode...

  1. key相同,type相同 可复用
  1. key相同,type不同 不可复用
  1. key不同,type相同 当前节点不可复用
  1. key不同,type不同 当前节点不可复用
js 复制代码
function reconcileSingleElement(  
  returnFiber: FiberNode,  
  currentFiber: FiberNode | null,  
  element: ReactElementType  
) {  
  const key = element.key;  
  while (currentFiber !== null) {  
   // update  
   if (currentFiber.key === key) {  
    // key相同  
    if (element.$$typeof === REACT_ELEMENT_TYPE) {  
     if (currentFiber.type === element.type) {  
      let props = element.props;  
      if (element.type === REACT_FRAGMENT_TYPE) {  
       props = element.props.children;  
      }  
      // type相同  
      const existing = useFiber(currentFiber, props);  
      existing.return = returnFiber;  
      // 当前节点可复用,标记剩下的节点删除(删除剩下的同级兄弟节点)  
      deleteRemainingChildren(returnFiber, currentFiber.sibling);  
      return existing;  
     }  
  
     // key相同,type不同 删掉所有旧的(包括同级兄弟节点)  
     deleteRemainingChildren(returnFiber, currentFiber);  
     break;  
    } else {  
     if ( __DEV__ ) {  
      console.warn('还未实现的react类型', element);  
      break;  
     }  
    }  
   } else {  
    // key不同,删掉旧的(加入到fiberNode.deletions属性中,在commit阶段被删除)  
    deleteChild(returnFiber, currentFiber); // 此处删除currentFiber  
    currentFiber = currentFiber.sibling; // 继续查看兄弟节点是否可以复用  
   }  
  }  
  // 根据element创建fiber(没有可复用的情况)  
  let fiber;  
  if (element.type === REACT_FRAGMENT_TYPE) {  
   fiber = createFiberFromFragment(element.props.children, key);  
  } else {  
   fiber = createFiberFromElement(element);  
  }  
  fiber.return = returnFiber;  
  return fiber;  
}  
  
  
function reconcileSingleTextNode(  
  returnFiber: FiberNode,  
  currentFiber: FiberNode | null,  
  content: string | number  
) {  
  while (currentFiber !== null) {  
   // update  
   if (currentFiber.tag === HostText) {  
    // 类型没变,可以复用  
    const existing = useFiber(currentFiber, { content });  
    existing.return = returnFiber;  
    deleteRemainingChildren(returnFiber, currentFiber.sibling);  
    return existing;  
   }  
   deleteChild(returnFiber, currentFiber);  
   currentFiber = currentFiber.sibling;  
  }  
  const fiber = new FiberNode(HostText, { content }, null);  
  fiber.return = returnFiber;  
  return fiber;  
}

2.1多节点的diff算法

  1. current 中所有同级fiber 保存在Map

  2. 遍历newChild 数组,对于每个遍历到的element,存在两种情况:

○ 在Map 中存在对应current fiber,且可以复用,从map中移除对应的fiber

○ 在Map 中不存在对应current fiber,或不能复用

判断是插入还是移动

问题:

lastPlacedIndex 的作用

• lastPlacedIndex 表示最后一个被复用的 Fiber 节点在旧子节点列表中的索引。

• 它的作用是记录当前已经处理过的节点中,最后一个不需要移动的节点的位置。

oldIndex 的作用

• oldIndex 是当前新节点对应的旧节点在旧子节点列表中的索引。

• 如果 oldIndex 存在,说明当前新节点可以复用旧节点。

为什么 oldIndex < lastPlacedIndex 才标记为 Placement

• oldIndex >= lastPlacedIndex:

○ 如果 oldIndex 大于或等于 lastPlacedIndex,说明当前节点在旧列表中的位置已经位于最后一个被复用节点的后面

○ 这意味着当前节点不需要移动,因为它已经处于正确的位置(相对于已经处理过的节点)。

○ 因此,更新 lastPlacedIndex 为当前 oldIndex,表示最后一个不需要移动的节点位置更新了。

• oldIndex < lastPlacedIndex:

○ 如果 oldIndex 小于 lastPlacedIndex,说明当前节点在旧列表中的位置位于最后一个被复用节点的前面

○ 这意味着当前节点需要移动,因为它应该位于已经处理过的节点的后面。

○ 因此,标记 newFiber.flags |= Placement,表示该节点需要移动。

  1. 最后Map中剩下的都标记删除(不可复用的fiber会留在map中)
js 复制代码
function reconcileChildrenArray(  
  returnFiber: FiberNode,  
  currentFirstChild: FiberNode | null,  
  newChild: any[]  
) {  
  // 最后一个可复用fiber在current中的index  
  let lastPlacedIndex = 0;  
  // 创建的最后一个fiber  
  let lastNewFiber: FiberNode | null = null;  
  // 创建的第一个fiber  
  let firstNewFiber: FiberNode | null = null;  
  
  // 1.将current保存在map中  
  const existingChildren: ExistingChildren = new *Map*();  
  let current = currentFirstChild;  
  while (current !== null) {  
   const keyToUse = current.key !== null ? current.key : current.index; // 以key或index作为map的key  
   existingChildren.set(keyToUse, current);  
   current = current.sibling;  
  }  
  
  // 2.遍历newChild,寻找是否可复用  
  for (let i = 0; i < newChild.length; i++) {  
   const after = newChild[i];  
   // 从map中寻找是否可以复用  
   const newFiber = updateFromMap(returnFiber, existingChildren, i, after);  
  
   if (newFiber === null) {  
    continue;  
   }  
  
   // 3. 标记移动还是插入  
   newFiber.index = i;  
   newFiber.return = returnFiber;  
  
   if (lastNewFiber === null) {  
    lastNewFiber = newFiber;  
    firstNewFiber = newFiber;  
   } else {  
    lastNewFiber.sibling = newFiber;  
    lastNewFiber = lastNewFiber.sibling;  
   }  
  
   if (!shouldTrackEffects) {  
    continue;  
   }  
  
   const current = newFiber.alternate;  
   if (current !== null) {  
    const oldIndex = current.index;  
    if (oldIndex < lastPlacedIndex) {  
     // 移动  
     newFiber.flags |= Placement;  
     continue;  
    } else {  
     // 不移动  
     lastPlacedIndex = oldIndex;  
    }  
   } else {  
    // mount  
    newFiber.flags |= *Placement*;  
   }  
  }  
  // 4. 将Map中剩下的标记为删除  
  existingChildren.forEach((fiber) => {  
   deleteChild(returnFiber, fiber);  
  });  
  return firstNewFiber;  
}  
  
  
function updateFromMap(  
  returnFiber: FiberNode,  
  existingChildren: ExistingChildren,  
  index: number,  
  element: any  
): FiberNode | null {  
  const keyToUse = getElementKeyToUse(element, index);  
  const before = existingChildren.get(keyToUse); // 从map中找到key对应的FiberNode  
  
  // HostText 之前的fiber和当前fiber都为文本节点,则可以复用  
  if (typeof element === 'string' || typeof element === 'number') {  
   if (before) {  
    if (before.tag === HostText) {  
     existingChildren.delete(keyToUse);  
     return useFiber(before, { content: element + '' });  
    }  
   }  
   return new FiberNode(*HostText*, { content: element + '' }, null);  
  }  
  
  // ReactElement  
  if (typeof element === 'object' && element !== null) {  
   switch (element.$$typeof) {  
    case REACT_ELEMENT_TYPE:  
     if (element.type === REACT_FRAGMENT_TYPE) {  
      return updateFragment(  
       returnFiber,  
       before,  
       element,  
       keyToUse,  
       existingChildren  
      );  
     }  
       
     // 之前也存在ReactElement类型且类型相同,可以复用  
     if (before) {  
      if (before.type === element.type) {  
       existingChildren.delete(keyToUse);  
       return useFiber(before, element.props);  
      }  
     }  
     // 之前不存在,则新建FiberNode  
     return createFiberFromElement(element);  
   }  
  }  
  
  if (Array.isArray(element)) {  
   return updateFragment(  
    returnFiber,  
    before,  
    element,  
    keyToUse,  
    existingChildren  
   );  
  }  
  return null;  
}

3、源码

对应源码地址:github.com/facebook/re...

相关推荐
bug_kada2 小时前
Js 的事件循环(Event Loop)机制以及面试题讲解
前端·javascript
bug_kada2 小时前
深入理解 JavaScript 可选链操作符
前端·javascript
小满xmlc2 小时前
CI/CD 构建部署
前端
_AaronWong2 小时前
视频加载Loading指令:基于Element Plus的优雅封装
前端·electron
KallkaGo2 小时前
threejs复刻原神渲染(三)
前端·webgl·three.js
IT_陈寒4 小时前
Vue3性能优化:掌握这5个Composition API技巧让你的应用快30%
前端·人工智能·后端
excel13 小时前
在 Node.js 中用 C++ 插件模拟 JavaScript 原始值包装对象机制
前端
excel16 小时前
应用程序协议注册的原理与示例
前端·后端
我是天龙_绍18 小时前
浏览器指纹,一个挺实用的知识点
前端