【React源码 - Fiber架构之Reconciler】

news2024/11/19 5:24:59

前言

React16架构可以分为三层也是最核心的三个功能分别是:

  • Scheduler(调度器)—调度任务的优先级,高优任务优先进入Reconciler(16新增)
  • Reconciler(协调器)—负责找出变化的组件
  • Renderer(渲染器)— 负责将变化的组件渲染到页面上

本文主要是分析一下Reconciler协调器的理念以及流程。简单就是一句话:Reconciler协调器中主要功能就是使用循环实现可中断递归,并进行Fiber节点的对比,然后打上标记,然后通知Renderer更新

背景

在React中可以通过this.setState、useState、this.forceUpdate、ReactDOM.render等API触发更新,在Reconciler中,mount的组件会调用mountComponent ,update的组件会调用updateComponent 。这两个方法都会递归更新子组件。15版本的时候,是使用递归的方式来遍历Diff对比虚拟DOM的差异然后通知Renderer来进行渲染的,主要是如下功能:

  1. 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM
  2. 将虚拟DOM和上次更新时的虚拟DOM对比,找出本次更新中变化的虚拟DOM
  3. 通知Renderer将变化的虚拟DOM渲染到页面上

由于递归一旦开始就不可中断,如果diff的层级较多的时候,就会出现递归时间超过16ms,导致页面卡顿。以及在ReReconciler和Renderer是交替工作的。如下图:当第一个li在页面上已经变化后,第二个li再进入Reconciler。
在这里插入图片描述由于整个过程都是同步的,所以在用户看来所有DOM是同时更新的
为了解决这些问题,React决定重写这块架构(Fiber架构),用异步可中断的更新来代替同步更新。使用循环的方式来代替递归实现可中断,从代码可以看出,每次循环都会调用shouldYield判断当前是否有剩余时间。

/** @noinline */
function  workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

而且Reconciler与Renderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记(使用二进制),类似这样:

export const Placement = /* 插入      */ 0b0000000000010;                 
export const Update = /*    更新      */ 0b0000000000100;
export const PlacementAndUpdate = /*插入并更新*/ 0b0000000000110;
export const Deletion = /* 删除       */ 0b0000000001000;

整个Scheduler与Reconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer。所以即使Scheduler和Reconciler是异步可中断的,但是用户也不会看到未完全更新数据,因为当处理完了之后,统一通知Renderer更新,而Renderer是同步不可中断更新的。
在这里插入图片描述

接下来解释一下待会儿会提到的一些术语:

  • Reconciler工作的阶段被称为render阶段。因为在该阶段会调用组件的render方法。
  • Renderer工作的阶段被称为commit阶段。就像你完成一个需求的编码后执行git commit提交代码。commit阶段会把render阶段提交的信息渲染在页面上。
  • render与commit阶段统称为work,即React在工作中。相对应的,如果任务正在Scheduler内调度,就不属于work。

双缓存树

都知道React在Fiber架构中使用了双缓存,所以开始之前我们先要了解一下什么是双缓存树。

当我们用canvas绘制动画,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。这种在内存中构建并直接替换的技术叫做双缓存 。

简单来说,双缓存树就是使用两个树来避免更新时的闪烁问题的解决策略,一棵Current Tree(视图中的),一个WorkInProgress Tree(内存中的),这两颗树是可以通过修改alternate指向来互相转化的,当需要更新时,直接用缓存树替换视图树。React使用“双缓存”来完成Fiber树的构建与替换——对应着DOM树的创建与更新。

双缓存Fiber树

在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React应用的根节点通过使current指针在不同Fiber树的rootFiber间切换来完成current Fiber树指向的切换。即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新。

接下来我们以具体例子讲解mount时、update时的构建/替换流程。

Mount时

以下面代码为例:

function  App() {
  const [num, add] = useState(0);
  return (
    <p onClick={() => add(num + 1)}>{num}</p>
  )
}

ReactDOM.render(<App/>, document.getElementById('root'));

mount阶段

首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是所在组件树的根节点。(之所以要区分fiberRootNode与rootFiber,是因为在应用中我们可以多次调用ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber。但是整个应用的根节点只有一个,那就是fiberRootNode。)

ReactDOM.render(<A/>, dom) // rootFiberA
ReactDOM.render(<B/>, dom) // rootFiberB
ReactDOM.render(<C/>, dom) // rootFiberC

fiberRootNode的current会指向当前页面上已渲染内容对应Fiber树,即current Fiber树。
在这里插入图片描述

fiberRootNode.current = rootFiber;

由于是首屏渲染,页面中还没有挂载任何DOM,所以fiberRootNode.current指向的rootFiber没有任何子Fiber节点(即current Fiber树为空)。

render阶段

接下来进入render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树。(下图中右侧为内存中构建的树,左侧为页面显示的树)。在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性,在首屏渲染时只有rootFiber存在对应的current fiber(即rootFiber.alternate)。
在这里插入图片描述
commit阶段

render阶段结束之后,以及有两颗树了,一个Current Fiber Tree在视图显示,一直WorkInProgress Fiber Tree在内存中构建然后,然后在commit阶段修改fiberRootNode的current指针指向workInProgress Fiber树使其变为current Fiber 树,试图更新。
在这里插入图片描述
update阶段

当节点发生更新,就会进入update阶段,再一次进入render阶段,和上面一样根据JSX生成FIber节点,进而构建FIber树(可复用Current FIber Tree的节点)
在这里插入图片描述
workInProgress Fiber 树在render阶段完成构建后进入commit阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为current Fiber 树。
在这里插入图片描述

Reconciler协调器

Reconciler是独立React之外的包,支持其他包引用,包名:react-reconciler,主要功能如下:

  • 输入: 暴露api函数(如: scheduleUpdateOnFiber), 供给其他包(如react包)调用.
  • 注册调度任务: 与调度中心(scheduler包)交互, 注册调度任务task, 等待任务回调.
  • 执行任务回调: 在内存中构造出fiber树, 同时与与渲染器(react-dom)交互, 在- 内存中创建出与fiber对应的DOM节点.
  • 输出: 与Renderer渲染器(react-dom)交互, 渲染DOM节点.

以上功能源码都集中在ReactFiberWorkLoop.js中. 现在将这些功能(从输入到输出)串联起来, 用下图表示:
在这里插入图片描述

  • workInProgress: 代表当前已创建的workInProgress fiber。
  • performUnitOfWork: 创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树。

我们知道Fiber Reconciler是从Stack Reconciler重构而来,通过遍历的方式实现可中断的递归,所以performUnitOfWork的工作可以分为两部分:“递”和“归”。

“递”阶段

首先从rootFiber开始向下深度优先遍历(DFS)。为遍历到的每个Fiber节点调用beginWork方法 。该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。当遍历到叶子节点(即没有子组件的组件)时就会进入下面的“归”阶段。

“归”阶段

在“归”阶段会调用completeWork 处理Fiber节点。当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了。然后进入commit阶段(Renderer渲染器)

以为下面代码为例

function  App() {
  return (
    <div>
      i am
      <span>KaSong</span>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById("root"));

其对应的FIber树为:
在这里插入图片描述
render阶段会依次执行:

1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "i am" Fiber beginWork
5. "i am" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork

之所以没有 “KaSong” Fiber 的 beginWork/completeWork,是因为作为一种性能优化手段,针对只有单一文本子节点的Fiber,React会特殊处理。

这里可以看出,整个递归过程中,主要就是调用beginWork、completeWork函数,下面主要分析这两个函数的作用。

BeginWork

beginWork源码
beginWork函数主要是传入一个current fiber节点来生成子fiber节点的过程,从代码来看,接受三个参数:

  • current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
  • workInProgress:当前组件对应的Fiber节点
  • renderLanes:优先级相关,Scheduler调度器中设置
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...省略函数体
}

从双缓存机制一节我们知道,除rootFiber以外, 组件mount时,由于是首次渲染,是不存在当前组件对应的Fiber节点在上一次更新时的Fiber节点,即mount时current === null。所以我们可以根据current来判断当前是mount还是update。所以可以从这两个方面来分析:

  • update时:如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child。
  • mount时:除fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点
function  beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {

  // update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
  if (current !== null) {
    // ...省略

    // 复用current
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderLanes,
    );
  } else {
    didReceiveUpdate = false;
  }

  // mount时:根据tag不同,创建不同的子Fiber节点
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...省略
    case LazyComponent: 
      // ...省略
    case FunctionComponent: 
      // ...省略
    case ClassComponent: 
      // ...省略
    case HostRoot:
      // ...省略
    case HostComponent:
      // ...省略
    case HostText:
      // ...省略
    // ...省略其他类型
  }
}

update时
我们可以看到,满足如下情况时didReceiveUpdate === false(即可以直接复用前一次更新的子Fiber,不需要新建子Fiber)

  • oldProps === newProps && workInProgress.type === current.type,即props与fiber.type不变
  • !includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够
if (current !== null) {
    const oldProps = current.memoizedProps;   
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false;
      switch (workInProgress.tag) {
        // 省略处理
      }
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderLanes,
      );
    } else {
      didReceiveUpdate = false;
    }
  } else {
    didReceiveUpdate = false;
  }

mount时
当不满足条件时就需要根据tag来新建子Fiber。Tag枚举

// mount时:根据tag不同,创建不同的Fiber节点
switch (workInProgress.tag) {
  case IndeterminateComponent: 
    // ...省略
  case LazyComponent: 
    // ...省略
  case FunctionComponent: 
    // ...省略
  case ClassComponent: 
    // ...省略
  case HostRoot:
    // ...省略
  case HostComponent:
    // ...省略
  case HostText:
    // ...省略
  // ...省略其他类型
}

对于我们常见的组件类型,如(FunctionComponent/ClassComponent/HostComponent),最终会进入reconcileChildren 方法

reconcileChildren
reconcileChildren函数主要做了一下事情:

  • 对于mount的组件,他会创建新的子Fiber节点
  • 对于update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // 对于mount的组件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 对于update的组件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

可以看出,也是通过current来区分是mount还是update,但是不管那个阶段最后都会生成一个子fiber节点并赋值给workInProgress.child

值得一提的是,mountChildFibers与reconcileChildFibers这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers会为生成的Fiber节点带上effectTag属性,而mountChildFibers不会。mount阶段只有rootfiber节点有effectTag属性,调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下

effectTag
我们知道Reconciler主要就是对比构建Fiber树,并标记节点需要进行的操作,就是通过effectTag属性以二进制来保存需要做什么操作

// DOM需要插入到页面中
export const Placement = /*                */ 0b00000000000010;     
// DOM需要更新
export const Update = /*                   */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /*       */ 0b00000000000110;
// DOM需要删除
export const Deletion = /*                 */ 0b00000000001000;

beginWork流程图
在这里插入图片描述

CompleteWork

类似beginWork,completeWork也是针对不同fiber.tag调用不同的处理逻辑

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...省略
      return null;
    }
    case HostRoot: {
      // ...省略
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
  // ...省略

其中需要注意注页面渲染所必须的HostComponent(即原生DOM组件对应的Fiber节点)。它和beginWork一样,我们根据current === null ?判断是mount还是update。同时针对HostComponent,判断update时我们还需要考虑workInProgress.stateNode != null ?(即该Fiber节点是否存在对应的DOM节点)

case HostComponent: {
  popHostContext(workInProgress);
  const rootContainerInstance = getRootHostContainer(); 
  const type = workInProgress.type;

  if (current !== null && workInProgress.stateNode != null) {
    // update的情况
    // ...省略
  } else {
    // mount的情况
    // ...省略
  }
  return null;
}

mount时
mount时的主要逻辑包括三个:

  • 为Fiber节点生成对应的DOM节点
  • 将子孙DOM节点插入刚生成的DOM节点中
  • 与update逻辑中的updateHostComponent类似的处理props的过程
const currentHostContext = getHostContext();
// 为fiber创建对应DOM节点
const instance = createInstance(
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
    workInProgress,
  );
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;

// 与update逻辑中的updateHostComponent类似的处理props的过程
if (
  finalizeInitialChildren(
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
  )
) {
  markUpdate(workInProgress);
}

update时
当节点进行update的时候,fiber节点已经存在对应的Dom节点了,所以主要处理的是props和一些回调

  • onClick、onChange等回调函数的注册
  • 处理style prop
  • 处理DANGEROUSLY_SET_INNER_HTML prop
  • 处理children prop
updateHostComponent = function(                
    current: Fiber,
    workInProgress: Fiber,
    type: Type,
    newProps: Props,
    rootContainerInstance: Container,
  ) {
    const oldProps = current.memoizedProps;
    if (oldProps === newProps) {
      return;
    }
    const instance: Instance = workInProgress.stateNode;
    const currentHostContext = getHostContext();
    const updatePayload = prepareUpdate(
      instance,
      type,
      oldProps,
      newProps,
      rootContainerInstance,
      currentHostContext,
    );
    workInProgress.updateQueue = (updatePayload: any);
    if (updatePayload) {
      markUpdate(workInProgress);
    }
  };

被处理完的props会保存在workInProgress.updateQueue(以key,value形式的数组,奇数下标为key,偶数下标为value),在commit阶段进行更新。

effectList
在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中,最后通过“归”阶段到rootfiber节点,形成以rootfiber为起点的单向链表。
在这里插入图片描述
completeWork流程图
在这里插入图片描述

至此,render阶段全部工作完成。在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。然后在commit阶段,会遍历整个effectList来更新对应的dom节点(fiber.stateNode保存的fiber对应的dom节点)

commitRoot(root);

参考文档

React技术揭秘
图解React

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1367798.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenHarmony基于HDF简单驱动开发实例

背景 OpenHarmony-3.0-LTSqemu_small_system_demoliteos_aqemu 添加配置 device/qemu/arm_virt/liteos_a/hdf_config/device_info/device_info.hcs device_info 新增&#xff1a; sample_host :: host {hostName "sample_host";sample_device :: device {devic…

使用 Docker 和 Diffusers 快速上手 Stable Video Diffusion 图生视频大模型

本篇文章聊聊&#xff0c;如何快速上手 Stable Video Diffusion (SVD) 图生视频大模型。 写在前面 月底计划在机器之心的“AI技术论坛”做关于使用开源模型 “Stable Diffusion 模型” 做有趣视频的实战分享。 因为会议分享时间有限&#xff0c;和之前一样&#xff0c;比较简…

Java学习笔记-day02-在IDEA中使用git忽略提交.idea下的文件

1.在根目录.gitignore文件排除.idea目录 ### IntelliJ IDEA ### .idea2.使用重置Head还原已经add过的文件 创建项目时&#xff0c;可能会有.idea中的文件先add到git后再创建的.gitignore文件&#xff0c;导致文件commit时无法排除&#xff0c;如下所示。 使用重置Head将文件…

【Emgu.CV教程】4.1、无缝融合应用之ColorChange()函数

按照网上专业一些的教程&#xff0c;接下来应该讲讲图像平滑、直方图&#xff0c;但是我前面刚讲完色彩处理&#xff0c;突然又发现了一个无缝融合的应用&#xff0c;也可以改颜色的&#xff0c;索性就先把它讲了&#xff0c;包括四个函数&#xff0c;反正这四个函数在具体工程…

Hyperledger Fabric 自动发现网络信息 discover 工具使用

客户端要往 Fabric 网络中发送请求&#xff0c;首先需要知道网络的相关信息&#xff0c;如网络中成员组织信息、背书节点的地址、链码安装信息等。 在 Fabric v1.2.0 版本之前&#xff0c;这些信息需要调用者手动指定&#xff0c;容易出错&#xff1b;另外&#xff0c;当网络中…

.NET 6中如何使用Redis

1、安装redis Redis在windows平台上不受官方支持&#xff0c;所以想要在window安装Redis就必须去下载windows提供的安装包。安装地址&#xff1a;https://github.com/tporadowski/redis/releases 2、在NueGet安装包 3、在appsettings.json文件里面添加Redis相关配置信息 &quo…

C#之反编译之路(二)

先阅读C#之反编译之路(一)可以增加文章连续性 阅读C#之反编译之路(一) 如何快速定位代码位置 用一个小小的例子举例,用户反馈新能源车牌号无法录入,燃油车牌正常,查看日志报如下错误 拿到关键字车牌号长度错误直接反编译代码 打开dnSpy.exe→加载项目→CtrlF打开搜索框→输入…

CentOS7本地部署分布式开源监控系统Zabbix并结合内网穿透实现远程访问

前言 Zabbix是一个基于WEB界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案。能监视各种网络参数&#xff0c;保证服务器系统的安全运营&#xff1b;并提供灵活的通知机制以让系统管理员快速定位/解决存在的各种问题。 本地zabbix web管理界面限制在只能局域…

spring-boot项目启动类错误: 找不到或无法加载主类 com.**Application

问题&#xff1a;Springboot项目启动报错&#xff1a;错误: 找不到或无法加载主类 com.**Application 解决步骤&#xff1a; 1.File–>Project Structure 2.Modules–>选中你的项目–点击“-”移除 3.重新导入&#xff1a;点击“”号&#xff0c;选择Import Module&…

【Emgu.CV教程】4.2、无缝融合应用之IlluminationChange()函数去除高亮区域

上一篇讲的是ColorChange()函数&#xff0c;今天讲IlluminationChange()函数&#xff0c;它可以去除图片中的高亮区域。试想一下&#xff0c;下面是一张反光背心的夜间照片&#xff0c;反光条颜色特别亮&#xff0c;如果想只把反光的部分变暗一点&#xff0c;其余部分不变&…

文字识别与光学字符识别有什么区别?

随着科技的不断发展&#xff0c;文字识别和光学字符识别技术已经成为我们日常生活和工作中不可或缺的一部分。然而&#xff0c;许多人对于这两者之间的区别并不十分清楚。本文将详细探讨文字识别与光学字符识别之间的差异&#xff0c;以帮助读者更好地理解这两种技术。 文字识…

【JUC】进程和线程

目录 &#x1f4e2;什么是进程?&#x1f3a1;什么是线程?&#x1f680;进程和线程的区别?&#x1f3a2;Java 线程和操作系统的线程有啥区别&#xff1f;&#x1f396;️JDK21的虚拟线程&#x1f3af;虚拟线程和平台线程的对比 &#x1f4e2;什么是进程? 进程是程序的一次执…

Redis 过期删除策略

常见的三种过期删除策略&#xff1a; 定期删除&#xff1b;惰性删除&#xff1b;定时删除&#xff1b; 定期删除策略 每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查&#xff0c;并删除其中的过期key。 定期删除的实现在 expire.c 文件下的 activeExpireCycle …

实现锚点定位功能(React/Vue)

前言 最近接到一个需求&#xff0c;修改某某页面&#xff0c;增加XXX功能&#xff0c;并实现个锚点功能。做产品就是不断优化&#xff0c;增加功能的过程。实现锚点的方式很多&#xff0c; 很多UI库也提供了组件&#xff0c;可以根据自己的需求调整一下组件库也可以实现&#…

HelpLook VS Zendesk:知识库工具的详细对比

在数字化飞速发展的时代&#xff0c;知识已成为企业最宝贵的资产。如何有效地管理和利用这些知识&#xff0c;提高企业的运营效率和创新能力&#xff0c;成为众多企业关注的焦点。而知识库工具成为了企业不可或缺的助手。 HelpLook和Zendesk是市场上两款备受关注的知识库工具&…

机器学习(四) -- 模型评估(4)

系列文章目录 机器学习&#xff08;一&#xff09; -- 概述 机器学习&#xff08;二&#xff09; -- 数据预处理&#xff08;1-3&#xff09; 机器学习&#xff08;三&#xff09; -- 特征工程&#xff08;1-2&#xff09; 机器学习&#xff08;四&#xff09; -- 模型评估…

EF Core 在实际开发中,如何分层?

前言&#xff1a;什么是分层&#xff1f; 分层就是将 EF Core 放在单独的项目中&#xff0c;其它项目如 Asp.net core webapi 项目引用它这样的好处是解耦和项目职责的清晰划分&#xff0c;并且可以重用 EF Core 项目但是也会数据库迁移变得复杂起来 Step by step 步骤 创建一…

java常用应用程序编程接口(API)——String概述及使用案例

前言&#xff1a; 开始学到api的String&#xff0c;整理下心得。打好基础&#xff0c;daydayup! API&#xff1a; API是什么&#xff1f; API&#xff08;Application Programming Interface&#xff09;又名应用程序编程接口。是别人编好的程序的合集。 为什么要使用API&…

计算机基础面试题 |17.精选计算机基础面试题

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

详解Java中的serialVersionUID概念以及作用(附上Demo)

目录 前言1. 概念2. Demo 前言 原本实现Serializable接口的时候一直都没有serialVersionUID属性&#xff0c;直到看到涉及MybatisPlus新项目中都有该属性&#xff0c;于是做了一期学习了解&#xff0c;最后发现该属性类似深度学习训练中的种子seed&#xff0c;类似版本控制&am…