React18系列reconciler从0实现过程详解

目录
  • 引言
  • React-Dom包
    • createRoot
    • hostConfig.ts
  • React-reconciler包
    • createContainer() 函数
    • render() 调用
    • 创建更新createUpdate
    • 将更新推进队列enqueueUpdate
    • 开始调用scheduleUpdateOnFiber
    • wookLoop
    • beginWork开始
    • updateHostRoot
    • reconcileChildren
    • reconcileSingleElement
    • 例子
    • completeWork开始
    • completeUnitOfWork
    • completeWork
    • appendAllChildren
    • 回到completeUnitOfWork
    • 回到renderRoot

引言

本系列是讲述从0开始实现一个react18的基本版本。由于React源码通过Mono-repo 管理仓库,我们也是用pnpm提供的workspaces来管理我们的代码仓库,打包我们使用rollup进行打包。

仓库地址

我们这一次主要写有关调和(reconciler)和ReactDom,React将调和单独的抽出一个包,暴露出入口,通过不同的宿主环境去调用不同的api。

React-Dom包

这个包主要是提供浏览器环境的一些dom操作。主要是提供2个文件hostConfig.ts 以及root.ts。 想想我们在React18中,是通过如下方式调用的。所以我们需要提供一个方法createRoot方法,返回要给包含render函数的对象。

import ReactDOM from 'react-dom/client';
ReactDOM.createRoot(root).render(<App />)

createRoot

主要功能是2个,第一个是创建根fiberNode节点, 第二个创建更新(初始化主要是用于渲染),开始调度。

//createRoot.ts 文件
import {
  createContainer,
  updateContainer,
} from "../../react-reconciler/src/filerReconciler";
export function createRoot(container: Container) {
  const root = createContainer(container);
  return {
    render(element: ReactElementType) {
      updateContainer(element, root);
    },
  };
}

createRoot.js主要是调用的react-reconcilercreateContainer方法和updateContainer方法。我们之后看看这2个方法主要的作用

hostConfig.ts

主要是创建各种dom,已经dom的插入操作

export const createInstance = (type: string, props: any): Instance => {
  // TODO 处理props
  const element = document.createElement(type);
  return element;
};
export const appendInitialChild = (
  parent: Instance | Container,
  child: Instance
) => {
  parent.appendChild(child);
};
export const createTextInstance = (content: string) => {
  return document.createTextNode(content);
};
export const appendChildToContainer = appendInitialChild;

React-reconciler包

createContainer() 函数

从上面我们可以知道,首先调用的createContainerupdateContainer,我们把它写到filerReconciler.tscreateContainer接受传入的dom元素。

/**
 * ReactDOM.createRoot()中调用
 * 1. 创建fiberRootNode 和 hostRootFiber。并建立联系
 * @param {Container} container
 */
export function createContainer(container: Container) {
  const hostRootFiber = new FiberNode(HostRoot, {}, null);
  const fiberRootNode = new FiberRootNode(container, hostRootFiber);
  hostRootFiber.updateQueue = createUpdateQueue();
  return fiberRootNode;
}

可以看到我们在这里主要就是2个事情

调用了2个方法去创建2个不同的fiberNode,一个是hostRootFiber,一个是fiberRootNode

创建一个更新队列,并将其赋值给hostRootFiber

/**
 * 顶部节点
 */
export class FiberRootNode {
  container: Container; // 不同环境的不同的节点 在浏览器环境 就是 root节点
  current: FiberNode;
  finishedWork: FiberNode | null; // 递归完成后的hostRootFiber
  constructor(container: Container, hostRootFiber: FiberNode) {
    this.container = container;
    this.current = hostRootFiber;
    hostRootFiber.stateNode = this;
    this.finishedWork = null;
  }
}
export class FiberNode {
  constructor(tag: WorkTag, pendingProps: Props, key: Key) {
    this.tag = tag;
    this.pendingProps = pendingProps;
    this.key = key;
    this.stateNode = null; // dom引用
    this.type = null; // 组件本身  FunctionComponent () => {}
    // 树状结构
    this.return = null; // 指向父fiberNode
    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; // 双缓存树指向(workInProgress 和 current切换)
    this.flags = NoFlags; // 副作用标识
    this.subtreeFlags = NoFlags; // 子树中的副作用
  }
}

接下来,我们看看createUpdateQueue里面的执行逻辑。执行了一个函数,返回了一个对象。所以现在hostRootFiberupdateQueue指向了这个指针

/**
 * 初始化updateQueue
 * @returns {UpdateQueue<Action>}
 */
export const createUpdateQueue = <State>() => {
  return {
    shared: {
      pending: null,
    },
  } as UpdateQueue<State>;
};

我们从上面createRoot执行完后,返回了一个render函数,我们接下来看看render后的执行过程,是怎么渲染到页面的。

render() 调用

createRoot执行后,创建了一个rootFiberNode, 并返回了render调用,主要是执行了updateContainer用于去渲染初始化的工作。

updateContainer接受2个参数,第一个参数是传入的ReactElement(), 第二个参数是fiberRootNode

主要是做3件事情:

  • 创建一个更新事件
  • 把更新事件推进队列中
  • 调用调度,开始更新
/**
 * ReactDOM.createRoot().render 中调用更新
 * 1. 创建update, 并将其推到enqueueUpdate中
 */
export function updateContainer(
  element: ReactElementType | null,
  root: FiberRootNode
) {
  const hostRootFiber = root.current;
  const update = createUpdate<ReactElementType | null>(element);
  enqueueUpdate(
    hostRootFiber.updateQueue as UpdateQueue<ReactElementType | null>,
    update
  );
  // 插入更新后,进入调度
  scheduleUpdateOnFiber(hostRootFiber);
  return element;
}

创建更新createUpdate

实际上就是创建一个对象,由于初始化的时候传入的是ReactElementType(), 所以返回的是App对应的ReactElement对象

/**
 * 创建更新
 * @param {Action<State>} action
 * @returns {Update<State>}
 */
export const createUpdate = (action) => {
  return {
    action,
  };
};

将更新推进队列enqueueUpdate

接受2个参数,第一个参数是我们创建一个更新队列的引用,第二个是新增的队列

/**
 * 更新update
 * @param {UpdateQueue<Action>} updateQueue
 * @param {Update<Action>} update
 */
export const enqueueUpdate = <State>(
  updateQueue: UpdateQueue<State>,
  update: Update<State>
) => {
  updateQueue.shared.pending = update;
};

执行到这一步骤,我们得到了更新队列,其实是一个ReactElement组件 及我们调用render传入的jsx对象。

开始调用scheduleUpdateOnFiber

接受FiberNode开始执行我们的渲染工作, 一开始渲染传入的是hostFiberNode 之后其他更新传递的是对应的fiberNode

export function scheduleUpdateOnFiber(fiber: FiberNode) {
  // todo 调度功能
  let root = markUpdateFromFiberToRoot(fiber);
  renderRoot(root);
}

wookLoop

执行完上面的操作后,接下来进入的调和阶段。开始我们要明白一个关键词:

workInProgress: 表示当前正在调和的fiber节点,之后简称wip

beginWork: 主要是根据当前fiberNode创建下一级fiberNode,在update时标记placement(新增、移动)ChildDeletion(删除)

completeWork: 在mount时构建Dom Tree, 初始化属性,在Update时标记Update(属性更新),最终执行flags冒泡

flags冒泡我们下一节讲。

从上面我们可以看到调用了scheduleUpdateOnFiber方法,开始从根部渲染页面。scheduleUpdateOnFiber主要是执行了2个方法:

markUpdateFromFiberToRoot: 由于我们更新的节点可能不是hostfiberNode, 这个方法就是不管传入的是那个节点,返回我们的根节点rootFiberNode

// 从当前触发更新的fiber向上遍历到根节点fiber
function markUpdateFromFiberToRoot(fiber: FiberNode) {
  let node = fiber;
  let parent = node.return;
  while (parent !== null) {
    node = parent;
    parent = node.return;
  }
  if (node.tag === HostRoot) {
    return node.stateNode;
  }
  return null;
}

renderRoot: 这里是我们wookLoop的入口,也是调和完成后,将生成的fiberNode树,赋值给finishedWork,并挂在根节点上,进入commit的入口。

function renderRoot(root: FiberRootNode) {
  // 初始化,将workInProgress 指向第一个fiberNode
  prepareFreshStack(root);
  do {
    try {
      workLoop();
      break;
    } catch (e) {
      if (__DEV__) {
        console.warn("workLoop发生错误", e);
      }
      workInProgress = null;
    }
  } while (true);
  const finishedWork = root.current.alternate;
  root.finishedWork = finishedWork;
  // wip fiberNode树  树中的flags执行对应的操作
  commitRoot(root);
}

prepareFreshStack函数: 用于初始化当前节点的wip, 并创建alternate 的双缓存的建立。 由于我们开始的时候传入的hostFiberNode, 经过createWorkInProgress后,创建了一个新的fiberNode 并通过alternate相互指向。并赋值给wip

let workInProgress: FiberNode | null = null;
function prepareFreshStack(root: FiberRootNode) {
  workInProgress = createWorkInProgress(root.current, {});
}
export const createWorkInProgress = (
  current: FiberNode,
  pendingProps: Props
): FiberNode => {
  let wip = current.alternate;
  if (wip === null) {
    //mount
    wip = new FiberNode(current.tag, pendingProps, current.key);
    wip.stateNode = current.stateNode;
    wip.alternate = current;
    current.alternate = wip;
  } else {
    //update
    wip.pendingProps = pendingProps;
    // 清掉副作用(上一次更新遗留下来的)
    wip.flags = NoFlags;
    wip.subtreeFlags = NoFlags;
  }
  wip.type = current.type;
  wip.updateQueue = current.updateQueue;
  wip.child = current.child;
  wip.memoizedProps = current.memoizedProps;
  wip.memoizedState = current.memoizedState;
  return wip;
};

接下来我们来分析一下workLoop中到底是如何生成fiberNode树的。它本身函数执行很简单。就是不停的根据wip进行单个fiberNode的处理。 此时wip指向的hostRootFiber。开始执行performUnitOfWork进行递归操作,其中递:beginWork,归:completeWork。React通过DFS,首先找到对应的叶子节点。

function workLoop() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}
function performUnitOfWork(fiber: FiberNode): void {
  const next = beginWork(fiber); // next 是fiber的子fiber 或者 是null
  // 工作完成,需要将pendingProps 复制给 已经渲染的props
  fiber.memoizedProps = fiber.pendingProps;
  if (next === null) {
    // 没有子fiber
    completeUnitOfWork(fiber);
  } else {
    workInProgress = next;
  }
}

beginWork开始

主要是向下进行遍历,创建不同的fiberNode。由于我们传入的是HostRoot,所以会走到updateHostRoot分支

/**
 * 递归中的递阶段
 * 比较 然后返回子fiberNode 或者null
 */
export const beginWork = (wip: FiberNode) => {
  switch (wip.tag) {
    case HostRoot:
      return updateHostRoot(wip);
    case HostComponent:
      return updateHostComponent(wip);
    case HostText:
      // 文本节点没有子节点,所以没有流程
      return null;
    default:
      if (__DEV__) {
        console.warn("beginWork未实现的类型");
      }
      break;
  }
  return null;
};

updateHostRoot

这个方法主要是2个部分:

  • 根据我们之前创建的更新队列获取到最新的值
  • 创建子fiber
/**
	processUpdateQueue: 是根据不同的类型(函数和其他)生成memoizedState
*/
function updateHostRoot(wip: FiberNode) {
  const baseState = wip.memoizedState;
  const updateQueue = wip.updateQueue as UpdateQueue<ElementType>;
  // 这里获取之前的更新队列
  const pending = updateQueue.shared.pending;
  updateQueue.shared.pending = null;
  const { memoizedState } = processUpdateQueue(baseState, pending); // 最新状态
  wip.memoizedState = memoizedState; // 其实就是传入的element
  const nextChildren = wip.memoizedState; // 就是我们传入的ReactElement 对象
  reconcileChildren(wip, nextChildren);
  return wip.child;
}

reconcileChildren

调和子节点, 根据是否生成过,分别调用不同的方法。通过上面我们知道传入的hostFiber, 此时是存在alternate属性的,所以会走到reconcilerChildFibers分支。

根据当前传入的returnFiberhostFiberNode以及currentFiber为null,newChild为ReactElementType。我们可以判断接下来会走到reconcileSingleElement的执行。其中placeSingleChild是打标记使用的,我们暂时先不研究。

/**
	wip: 当前正在执行的父fiberNode
	children: 即将要生成的子fiberNode
*/
function reconcileChildren(wip: FiberNode, children?: ReactElementType) {
  const current = wip.alternate;
  if (current !== null) {
    // update
    wip.child = reconcilerChildFibers(wip, current?.child, children);
  } else {
    // mount
    wip.child = mountChildFibers(wip, null, children);
  }
}
function reconcilerChildFibers(
    returnFiber: FiberNode,
    currentFiber: FiberNode | null,
    newChild?: ReactElementType | string | number
  ) {
    // 判断当前fiber的类型
    if (typeof newChild === "object" && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(returnFiber, currentFiber, newChild)
          );
        default:
          if (__DEV__) {
            console.warn("未实现的reconcile类型", newChild);
          }
          break;
      }
    }
    // Todo 多节点的情况 ul > li * 3
    // HostText
    if (typeof newChild === "string" || typeof newChild === "number") {
      return placeSingleChild(
        reconcileSingleTextNode(returnFiber, currentFiber, newChild)
      );
    }
    if (__DEV__) {
      console.warn("未实现的reconcile类型", newChild);
    }
    return null;
  };
}

reconcileSingleElement

从名字我们可以看出是通过ReactElement 创建单一的fiberNode。通过reconcileSingleElement我们就可以得出了一个新的子节点,然后通过return指向父fiber。此时的fiberNode树如下图。

  /**
   * 根据reactElement对象创建fiber并返回
   */
  function reconcileSingleElement(
    returnFiber: FiberNode,
    _currentFiber: FiberNode | null,
    element: ReactElementType
  ) {
    const fiber = createFiberFromElement(element);
    fiber.return = returnFiber;
    return fiber;
  }
export function createFiberFromElement(element: ReactElementType): FiberNode {
  const { type, key, props } = element;
  let fiberTag: WorkTag = FunctionComponent;
  if (typeof type === "string") {
    // <div/>  type : 'div'
    fiberTag = HostComponent;
  } else if (typeof type !== "function" && __DEV__) {
    console.log("未定义的type类型", element);
  }
  const fiber = new FiberNode(fiberTag, props, key);
  fiber.type = type;
  return fiber;
}

调用完后,此时回到了reconcileChildren函数的这一句代码执行,指定wip的child指向。此时函数执行完毕。

// 省略无关代码
function reconcileChildren(wip: FiberNode, children?: ReactElementType) {
    wip.child = reconcilerChildFibers(wip, current?.child, children);
}

执行完后返回updateHostRoot函数调用reconcileChildren的地方。然后返回wip的child。

  function updateHostRoot(wip) {
      const baseState = wip.memoizedState;
      reconcileChildren(wip, nextChildren);
      return wip.child;
  }

执行完updateHostRoot函数后,返回调用它的beginWork中。beginWork也同样返回了当前wip的child节点。

export const beginWork = (wip: FiberNode) => {
  switch (wip.tag) {
    case HostRoot:
      return updateHostRoot(wip);
  }
}

执行完后,我们最后又回到了最开始调用beginWork的地方。进行接下来的操作,主要是将已经渲染过的属性赋值。然后将wip赋值给下一个刚刚生成的子节点。以便于开始下一次的递归中调用。

  function performUnitOfWork(fiber) {
      const next = beginWork(fiber); // next 是fiber的子fiber 或者 是null
      // 工作完成,需要将pendingProps 复制给 已经渲染的props
      fiber.memoizedProps = fiber.pendingProps;
      if (next === null) {
          // 没有子fiber
          completeUnitOfWork(fiber);
      }
      else {
          workInProgress = next;
      }
  }

由于workInProgress不等于null, 说明还有子节点。继续进行workLoop调用。又开始了新的一轮。直到我们到达了叶子节点。

  function workLoop() {
      while (workInProgress !== null) {
          performUnitOfWork(workInProgress);
      }
  }

例子

例如,如下例子,当遍历到hcc文本节点后,由于我们节点是没有调和流程的。所以执行到beginWork后,返回了一个null。正式结束了递归调用中的“递" 过程。此时的fiberNode树如下图所示。

const jsx = <div><span>hcc</span></div>
const root = document.querySelector('#root')
ReactDOM.createRoot(root).render(jsx)

completeWork开始

从上面的beginWork操作后,此时我们wip在文本节点hcc的节点位置.

completeUnitOfWork

接下来执行performUnitOfWork中的completeUnitOfWork的逻辑部分,我们看看completeUnitOfWork的逻辑部分。 我们传入的最底部的叶子节点。首先会对当前节点进行completeWork的方法调用。

function completeUnitOfWork(fiber) {
  let node = fiber;
  do {
      completeWork(node);
      const sibling = node.sibling;
      if (sibling !== null) {
          workInProgress = sibling;
          return;
      }
      node = node.return;
      workInProgress = node;
  } while (node !== null);
}

completeWork

首次我们会接受到一个最底部的子fiberNode,由于是第一次mount,所以当前的fiber下不会存在alternate属性的,所以会走到构建Dom的流程。

/**
 * 递归中的归
 */
export const completeWork = (wip: FiberNode) => {
  const newProps = wip.pendingProps;
  const current = wip.alternate;
  switch (wip.tag) {
    case HostComponent:
      if (current !== null && wip.stateNode) {
        //update
      } else {
        // 1. 构建DOM
        const instance = createInstance(wip.type, newProps);
        // 2. 将DOM插入到DOM树中
        appendAllChildren(instance, wip);
        wip.stateNode = instance;
      }
      bubbleProperties(wip);
      return null;
    case HostText:
      if (current !== null && wip.stateNode) {
        //update
      } else {
        // 1. 构建DOM
        const instance = createTextInstance(newProps.content);
        // 2. 将DOM插入到DOM树中
        wip.stateNode = instance;
      }
      bubbleProperties(wip);
      return null;
    case HostRoot:
      bubbleProperties(wip);
      return null;
    default:
      if (__DEV__) {
        console.warn("未实现的completeWork");
      }
      break;
  }
};
// 根据逻辑判断,走到下面的逻辑判断,传入了文本
// 1. 构建DOM
const instance = createTextInstance(newProps.content);
// 2. 将DOM插入到DOM树中
wip.stateNode = instance;

经过completeWork后,我们给当前的wip添加了stateNode属性,用于指向生成的Dom节点。 执行完completeWork后,继续返回到completeUnitOfWork中,查找sibling节点,目前我们demo中没有,所以会向上找到当前节点的return指向。继续执行completeWork工作,此时的结构变成了如下图:

由于我们wip目前是HostComponent, 所以走到了如下的completeWork的逻辑。这里 根据type创建不同的Dom元素,和之前一样,绑定到对应的stateNode属性上。我们可以看到除了这2个,还执行了一个函数appendAllChildren。我们去看看这个函数的作用是什么

// 1. 构建DOM
const instance = createInstance(wip.type);
// 2. 将DOM插入到DOM树中
appendAllChildren(instance, wip);
wip.stateNode = instance;

appendAllChildren

接受2个参数,第一个是刚刚通过wip的type生成的对应的dom, 另外一个是wip本身。 它的作用就是把我们上一步产生的dom节点,插入到刚刚产生的父dom节点上,形成一个局部的小dom树。

它本身存在一个复杂的遍历过程,因为fiberNode的层级和DOM元素的层级可能不是一一对应的。

/**
 * 在parent的节点下,插入wip
 * @param {FiberNode} parent
 * @param {FiberNode} wip
 */
function appendAllChildren(parent: Container, wip: FiberNode) {
  let node = wip.child;
  while (node !== null) {
    if (node?.tag === HostComponent || node?.tag === HostText) {
      appendInitialChild(parent, node?.stateNode);
    } else if (node.child !== null) {
      node.child.return = node;
      // 继续向下查找
      node = node.child;
      continue;
    }
    if (node === wip) {
      return;
    }
    while (node.sibling === null) {
      if (node.return === null || node.return === wip) {
        return;
      }
      // 向上找
      node = node?.return;
    }
    node.sibling.return = node.return;
    node = node.sibling;
  }
}

我们用这个图来说明一下流程

  • 当前的”归“到了div对应的fiberNode。我们获取到node是第一个子元素的span, 执行appendInitialChild方法,把对应的stateNode的dom节点插入parent中。
  • 接下来执行由于node.sibling不为空,所以会将node 复制给第二个span。然后继续执行appendInitialChild。以此执行到第三个span节点。
  • 第三个span节点对应的sibling为空,所以开始向上查找到node.return === wip结束函数调用。
  • 此时三个span产生的dom,都已经插入到parent(div dom)中。

回到completeUnitOfWork

经过上述操作后,我们继续回到completeUnitOfWork的调用,继续向上归并。到上述例子的div节点。直到我们遍历到hostFiberNode, 它是没有return属性的,所以返回null,结束了completeUnitOfWork的执行。回到了最开始的workLoop。此时的workInProgress等于null, 结束循环。

  function workLoop() {
      while (workInProgress !== null) {
          performUnitOfWork(workInProgress);
      }
  }

回到renderRoot

执行完workLoop, 就回到了renderRoot的部分。此时我们已经得到了完整的fiberNode树,以及相应的dom元素。此时对应的结果如下图:

那么生成的fiberNode树是如何渲染的界面上的,我们下一章的commit章节介绍,如何打标签和渲染,更多关于React18系列reconciler实现的资料请关注我们其它相关文章!

(0)

相关推荐

  • React commit源码分析详解

    目录 总览 commitBeforeMutationEffects commitMutationEffects 插入 dom 节点 获取父节点及插入位置 判断当前节点是否为单节点 在对应位置插入节点 更新 dom 节点 更新 HostComponent 更新 HostText 删除 dom 节点 unmountHostComponents commitNestedUnmounts commitUnmount commitLayoutEffects 执行生命周期 处理回调 总结 总览 commit

  • React18从0实现dispatch update流程

    目录 引言 hooks原理 useState初始化(mount) mountState mountWorkInProgressHook 处理hook数据 生成dispatch 更新的总结 useState触发更新(dispatch) updateState hook数据从哪里来 updateWorkInProgressHook hook在条件语句中报错 useState计算 双缓存树 总结 引言 本系列是讲述从0开始实现一个react18的基本版本.由于React源码通过Mono-repo 管理

  • react-native只保留3x图原理解析

    目录 引言 1. 输出构建产物 2. RN如何决定加载哪张scale图片? 3. repo中是否可以只保留3x图? 3.1 资源上传 3.2 资源下载 4. 结论 引言 我们的react-native项目中,一张图片一般会存在1x, 1.5x, 2x, 3x几种尺寸(1.5x是android特有的),以便在不同屏幕尺寸的手机加载对应尺寸的图片. 1. 输出构建产物 如果我们在代码中引入了一张图片,例如 // index.js import bg from './bg.png'; . ├── in

  • React 状态管理工具优劣势示例分析

    目录 什么是状态管理? React 状态管理方案 方案介绍 方案对比 Source 相关建议 什么是状态管理? “状态”是描述应用程序当前行为的任何数据.这可能包括诸如“从服务器获取的对象列表”.“当前选择的项目”.“当前登录用户的名称”和“此模式是否打开?”等值. 众所周知,我们在研发一个复杂应用的过程中,一套好的状态管理方案是必不可少的,既能提升研发效率,又能降低研发维护成本,那么状态管理方案那么多,它们有什么不同,我们又该如何选择适合当前应用的方案呢? 本期将主要就 react 的常用状态

  • react native图片解析流程详解

    目录 正文 1. 构建输出的产物 2. js bundle分析 3. 图片source拼接 3.1 如果bundle放在服务器(本地开发) 3.2 bundle内置在app中(app下载bundle和assets后执行) 4. Image style的witdh和height没有声明会发生什么? 正文 我们知道,在react-native中加载一张图片需要使用Image组件,其有两种使用方式 import bg from './bg.png'; // 1. 加载本地图片资源 <Image sou

  • react hooks d3实现企查查股权穿透图结构图效果详解

    目录 前言 最终效果: 版本信息: 股权穿透图基础功能: 股权结构图基础功能: 股权穿透图代码 股权结构图代码 总结: 前言 umi+antd-admin 框架中使用hooks结合d3完成类似股权穿透图和股权结构图(web) 最终效果: 股权穿透图 股权结构图 版本信息: "d3": "4.13.0", "antd": "3.24.2", "umi": "^2.7.7", 股权穿透图基础

  • react echarts tree树图搜索展开功能示例详解

    目录 前言 最终效果 版本信息 核心功能: 关键思路: 附上代码 数据data.js 功能: TreeUtils 总结: 前言 umi+antd-admin 框架中使用类组件+antd结合echarts完成树图数据展示和搜索展开功能 最终效果 版本信息 "antd": "3.24.2", "umi": "^2.7.7", "echarts": "^4.4.0", "echart

  • React18系列reconciler从0实现过程详解

    目录 引言 React-Dom包 createRoot hostConfig.ts React-reconciler包 createContainer() 函数 render() 调用 创建更新createUpdate 将更新推进队列enqueueUpdate 开始调用scheduleUpdateOnFiber wookLoop beginWork开始 updateHostRoot reconcileChildren reconcileSingleElement 例子 completeWork开

  • vue3使用Vite打包组件库从0搭建过程详解

    目录 手动搭建一个用于测试组件库组件 Vue3 项目 初始化 ts 搭建一个基于 vite 的 vue3 项目 安装插件 配置 vite.config.ts 新建入口 html 文件 app.vue 入口 main.ts 配置脚本启动项目 手动搭建一个用于测试组件库组件 Vue3 项目 本篇文章将在项目中引入 typescript,以及手动搭建一个用于测试组件库组件 Vue3 项目 因为我们是使用 Vite+Ts 开发的是 Vue3 组件库,所以我们需要安装 typescript.vue3,同时

  • JS组件系列之JS组件封装过程详解

    前言: 之前分享了那么多bootstrap组件的使用经验,这篇文章打算研究下JS组件的扩展和封装,我们来感受下JQuery为我们提供$.Extend的神奇,看看我们怎么自定义自己的组件,比如我们想扩展一个$("#id").MyJsControl({})做我们自己的组件,我们该如何去做呢,别急,我们慢慢来看看过程. 一.扩展已经存在的组件 1.需求背景 很多时候,我们使用jquery.ajax的方式向后台发送请求,型如 $.ajax({ type: "post", u

  • vue-cli3.0 脚手架搭建项目的过程详解

    1.安装vue-cli 3.0 npm install -g @vue/cli # or yarn global add @vue/cli 安装成功后查看版本:vue -V(大写的V) 2.命令变化 vue create --help 用法:create [options] <app-name> 创建一个由 `vue-cli-service` 提供支持的新项目 选项: -p, --preset <presetName>       忽略提示符并使用已保存的或远程的预设选项   -d

  • springboot jpa分库分表项目实现过程详解

    这篇文章主要介绍了springboot jpa分库分表项目实现过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 分库分表场景 关系型数据库本身比较容易成为系统瓶颈,单机存储容量.连接数.处理能力都有限.当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从库.优化索引,做很多操作时性能仍下降严重.此时就要考虑对其进行切分了,切分的目的就在于减少数据库的负担,缩短查询时间. 分库分表用于应对当前互联网常见的两个场景--大数

  • Java 用Prometheus搭建实时监控系统过程详解

    上帝之火 本系列讲述的是开源实时监控告警解决方案Prometheus,这个单词很牛逼.每次我都能联想到带来上帝之火的希腊之神,普罗米修斯.而这个开源的logo也是火,个人挺喜欢这个logo的设计. 本系列着重介绍Prometheus以及如何用它和其周边的生态来搭建一套属于自己的实时监控告警平台. 本系列受众对象为初次接触Prometheus的用户,大神勿喷,偏重于操作和实战,但是重要的概念也会精炼出提及下.系列主要分为以下几块 Prometheus各个概念介绍和搭建,如何抓取数据(本次分享内容)

  • redis debug环境搭建过程详解(使用clion)

    目录 概要 环境搭建的大体思路 windows下安装linux工具链 什么是make和cmake 安装c语言开发的ide 具体安装步骤 cygwin安装 clion安装及插件安装 克隆redis 代码 如何调试 如何调试redis-server 概要 最近写了spring系列,这个系列还在进行中,然后有些同学开始叫我大神,然后以为我各方面都比较厉害,当然了,我是有自知之明的,大佬大神什么的,当作一个称呼就好,如果真的以为自己就是大神,那可能就走偏了. 其实我不少方面都比较薄弱,比如redis.m

  • C语言从编译到运行过程详解

    目录 C语言从编译到运行 一.前言 二.C程序编译过程 三.阶段过程 1.预处理阶段 2.编译阶段 3.汇编阶段 4.链接阶段 C语言从编译到运行 一.前言 最近在看CSAPP(深入理解计算机系统)然后以前也学过C语言,但是从来没有深究写好的C代码是怎么编译再到执行的. 所以现在自己学习,然后记录下来. 以最常用的hello world!程序为例 程序名: main.c #include <stdio.h> int main() { printf("Hello world!\n&qu

  • C#多线程系列之async和await用法详解

    目录 async和await async await 从以往知识推导 创建异步任务 创建异步任务并返回Task 异步改同步 说说awaitTask 说说 asyncTask<TResult> 同步异步? Task封装异步任务 关于跳到await变异步 为什么出现一层层的await async和await async 微软文档:使用 async 修饰符可将方法.lambda 表达式或匿名方法指定为异步. 使用 async 修饰的方法,称为异步方法. 例如: 为了命名规范,使用 async 修饰的

  • React18 中的 Suspense API使用实例详解

    目录 什么是新的 ReactJS Suspense API,什么时候应该使用它? 什么是Suspense API? 什么是 transition API? 最后 什么是新的 ReactJS Suspense API,什么时候应该使用它? 何时使用:当组件开始变大并且您在同一页面上有许多组件时,您可能希望开始优化下载到客户端浏览器的方式和时间. 为此,React 为您提供了lazyAPI,它允许您将组件标记为lazy,这意味着被lazy包裹的组件,将会在第一次真正使用时被加载,而不是页面初始化的时

随机推荐