React18 之 Suspense
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。
本文作者:佳岚
Suspense
Suspense 组件我们并不陌生,中文名可以理解为暂停or悬停  , 在 React16 中我们通常在路由懒加载中配合 Lazy 组件一起使用 ,当然这也是官方早起版本推荐的唯一用法。
那它暂停了什么? 进行异步网络请求,然后再拿到请求后的数据进行渲染是很常见的需求,但这不可避免的需要先渲染一次没有数据的页面,数据返回后再去重新渲染。so , 我们想要暂停的就是第一次的无数据渲染。
通常我们在没有使用Suspense 时一般采用下面这种写法, 通过一个isLoading状态来显示加载中或数据。这样代码是不会有任何问题,但我们需要手动去维护一个isLoading 状态的值。
const [data, isLoading] = fetchData("/api");
if (isLoading) {
  return <Spinner />;
}
return <MyComponent data={data} />;
当我们使用Suspense 后,使用方法会变为如下, 我们只需将进行异步数据获取的组件进行包裹,并将加载中组件通过fallback传入
return (
  <Suspense fallback={<Spinner />}>
    <MyComponent />
  </Suspense>
);
那 React 是如何知道该显示MyComponent还是Spinner的?
答案就在于MyComponent内部进行fetch远程数据时做了一些手脚。
export const App = () => {
  return (
    <div>
      <Suspense fallback={<Spining />}>
        <MyComponent />
      </Suspense>
    </div>
  );
};
function Spining() {
  return <p>loading...</p>;
}
let data = null;
function MyComponent() {
  if (!data) {
    throw new Promise((resolve) => {
      setTimeout(() => {
        data = 'kunkun';
        resolve(true);
      }, 2000);
    });
  }
  return (
    <p>
      My Component, data is {data}
    </p>
  );
}
Suspense是根据捕获子组件内的异常来实现决定展示哪个组件的。这有点类似于ErrorBoundary ,不过ErrorBoundary是捕获 Error 时就展示回退组件,而Suspense 捕获到的 Error 需要是一个Promise对象(并非必须是 Promise 类型,thenable 的都可以)。
我们知道 Promise 有三个状态,pending、fullfilled、rejected ,当我们进行远程数据获取时,会创建一个Promise,我们需要直接将这个Promise 作为Error进行抛出,由 Suspense 进行捕获,捕获后对该thenable对象的then方法进行回调注册thenable.then(retry) , 而 retry 方法就会开始一个调度任务进行更新,后面会详细讲。

知道了大致原理,这时还需要对我们的fetcher进行一层包裹才能实际运用。
// MyComponent.tsx
const getList = wrapPromise(fetcher('http://api/getList'));
export function MyComponent() {
  const data = getList.read();
  return (
    <ul>
      {data?.map((item) => (
        <li>{item.name}</li>
      ))}
    </ul>
  );
}
function fetcher(url) {
  return new Promise((resove, reject) => {
    setTimeout(() => {
      resove([{ name: 'This is Item1' }, { name: 'This is Item2' }]);
    }, 1000);
  });
}
// Promise包裹函数,用来满足Suspense的要求,在初始化时默认就会throw出去
function wrapPromise(promise) {
  let status = 'pending';
  let response;
  const suspend = promise.then(
    (res) => {
      status = 'success';
      response = res;
    },
    (err) => {
      status = 'error';
      response = err;
    }
  );
  const read = () => {
    switch (status) {
      case 'pending':
        throw suspend;
      default:
        return response;
    }
  };
  return { read };
从上述代码我们可以注意到,通过const data = getList.read() 这种同步的方式我们就能拿到数据了。 注意: 上面这种写法并非一种范式,目前官方也没有给出推荐的写法
为了与Suspense配合,则我们的请求可能会变得很不优雅 ,官方推荐是直接让我们使用第三方框架提供的能力使用Suspense请求数据,如 useSWR 等
下面时useSWR的示例,简明了很多,并且对于Profile组件,数据获取的写法可以看成是同步的了。
import { Suspense } from 'react'
import useSWR from 'swr'
function Profile () {
  const { data } = useSWR('/api/user', fetcher, { suspense: true })
  return <div>hello, {data.name}</div>
}
function App () {
  return (
    <Suspense fallback={<div>loading...</div>}>
      <Profile/>
    </Suspense>
  )
}
Suspense的另一种用法就是与懒加载lazy组件配合使用,在完成加载前展示Loading
<Suspense fallback={<GlobalLoading />}>
   {lazy(() => import('xxx/xxx.tsx'))}
</Suspense>
由此得出,通过lazy返回的组件也应该包裹一层类似如上的 Promise,我们看看 lazy 内部是如何实现的。
其中ctor就是我们传入的() => import('xxx/xxx.tsx'), 执行lazy也只是帮我们封装了层数据结构。ReactLazy.js
export function lazy<T>(
  ctor: () => Thenable<{default: T, ...}>,
): LazyComponent<T, Payload<T>> {
  const payload: Payload<T> = {
    // We use these fields to store the result.
    _status: Uninitialized,
    _result: ctor,
  };
  const lazyType: LazyComponent<T, Payload<T>> = {
    $$typeof: REACT_LAZY_TYPE,
    _payload: payload,
    _init: lazyInitializer,
  };
  return lazyType;
}
React 会在Reconciler过程中去实际执行,在协调的render阶段beginWork中可以看到对lazy单独处理的逻辑。 ReactFiberBeginWork.js
function mountLazyComponent(
  _current,
  workInProgress,
  elementType,
  renderLanes,
) {
  const props = workInProgress.pendingProps;
  const lazyComponent: LazyComponentType<any, any> = elementType;
  const payload = lazyComponent._payload;
  const init = lazyComponent._init;
	// 在此处初始化lazy
  let Component = init(payload);
	// 下略
}
那我们再来看看init干了啥,也就是封装前的lazyInitializer方法,整体跟我们之前实现的 fetch 封装是一样的。
ReactLazy.js
function lazyInitializer<T>(payload: Payload<T>): T {
  if (payload._status === Uninitialized) {
    const ctor = payload._result;
	// 这时候开始进行远程模块的导入
    const thenable = ctor();
    thenable.then(
      moduleObject => {
        if (payload._status === Pending || payload._status === Uninitialized) {
          // Transition to the next state.
          const resolved: ResolvedPayload<T> = (payload: any);
          resolved._status = Resolved;
          resolved._result = moduleObject;
        }
      },
      error => {
        if (payload._status === Pending || payload._status === Uninitialized) {
          // Transition to the next state.
          const rejected: RejectedPayload = (payload: any);
          rejected._status = Rejected;
          rejected._result = error;
        }
      },
    );
  }
  if (payload._status === Resolved) {
    const moduleObject = payload._result;
    }
    return moduleObject.default;
  } else {
    // 第一次执行肯定会先抛出异常
    throw payload._result;
  }
}
Suspense 底层是如何实现的?
其底层细节非常之多,在开始之前,我们先回顾下 React 的大致架构
Scheduler: 用于调度任务,我们每次setState可以看成是往其中塞入一个Task,由Scheduler内部的优先级策略进行判断何时调度运行该Task
Reconciler: 协调器,进行 diff 算法,构建 fiber 树
Renderer: 渲染器,将 fiber 渲染成 dom 节点
Fiber 树的结构, 在 reconciler 阶段,采用深度优先的方式进行遍历,往下递即调用beginWork的过程,往上回溯即调用ComplteWork的过程

我们先直接进入Reconciler 中分析下Suspense的fiber节点是如何被创建的
beginWork
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
	switch (workInProgress.tag) {
		case HostText:
      return updateHostText(current, workInProgress);
    case SuspenseComponent:
      return updateSuspenseComponent(current, workInProgress, renderLanes);
		// 省略其他类型
	}
}
- 在beginWork中会根据**不同的组件类型**执行不同的创建方法, 而Suspense对应的会进入到updateSuspenseComponent
function updateSuspenseComponent(current, workInProgress, renderLanes) {
  const nextProps = workInProgress.pendingProps;
  let showFallback = false;
  // 标识该Suspense是否已经捕获过子组件的异常了
  const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
  if (
    didSuspend
  ) {
    showFallback = true;
    workInProgress.flags &= ~DidCapture;
  } 
  // 第一次组件加载
  if (current === null) {
    const nextPrimaryChildren = nextProps.children;
    const nextFallbackChildren = nextProps.fallback;
    // 第一次默认不展示fallback,因为要先走到children后才会产生异常
    if (showFallback) {
      const fallbackFragment = mountSuspenseFallbackChildren(
        workInProgress,
        nextPrimaryChildren,
        nextFallbackChildren,
        renderLanes,
      );
      const primaryChildFragment: Fiber = (workInProgress.child: any);
      primaryChildFragment.memoizedState = mountSuspenseOffscreenState(
        renderLanes,
      );
      return fallbackFragment;
    }
     else {
      return mountSuspensePrimaryChildren(
        workInProgress,
        nextPrimaryChildren,
        renderLanes,
      );
    }
  } else {
    // 如果是更新,操作差不多,此处略
  }
}
- 第一次updateSuspenseComponent时 ,我们会把mountSuspensePrimaryChildren的结果作为下一个需要创建的fiber, 因为需要先去触发异常。
- 实际上mountSuspensePrimaryChildren会为我们的PrimaryChildren在包上一层OffscreenFiber。
function mountSuspensePrimaryChildren(
  workInProgress,
  primaryChildren,
  renderLanes,
) {
  const mode = workInProgress.mode;
  const primaryChildProps: OffscreenProps = {
    mode: 'visible',
    children: primaryChildren,
  };
  const primaryChildFragment = mountWorkInProgressOffscreenFiber(
    primaryChildProps,
    mode,
    renderLanes,
  );
  primaryChildFragment.return = workInProgress;
  workInProgress.child = primaryChildFragment;
  return primaryChildFragment;
}
什么是OffscreenFiber/Component  ?
通过其需要的 mode 参数值,我们可以大胆的猜测,应该是一个能控制是否显示子组件的组件,如果hidden,则会通过 CSS 样式隐藏子元素。

在这之后的 Fiber 树结构

当我们向下执行到MyComponent 时,由于抛出了错误,当前的reconciler阶段会被暂停
让我们再回到 Reconciler 阶段的起始点可以看到有Catch语句。renderRootConcurrent
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
 // 省略..
  do {
    try {
      workLoopConcurrent();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
 // 省略..
}
performConcurrentWorkOnRoot(root, didTimeout) {
	// 省略..
	let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);
  // 省略..
}
我们再看看错误处理函数handleError中做了些什么  handleError
function handleError(root, thrownValue): void {
	// 这时的workInProgress指向MyComponent
  let erroredWork = workInProgress;
  try {
    throwException(
      root,
      erroredWork.return,
      erroredWork,
      thrownValue,
      workInProgressRootRenderLanes,
    );
    completeUnitOfWork(erroredWork);
}
function throwException(root: FiberRoot, returnFiber: Fiber, sourceFiber: Fiber, value: mixed, rootRenderLanes: Lanes)
{
  // 给MyComponent打上未完成标识
  sourceFiber.flags |= Incomplete;
  if (
    value !== null &&
    typeof value === 'object' &&
    typeof value.then === 'function'
  ) {
    // wakeable就是我们抛出的Promise
    const wakeable: Wakeable = (value: any);
    // 向上找到第一个Suspense边界
    const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);
    if (suspenseBoundary !== null) {
      // 打上标识
      suspenseBoundary.flags &= ~ForceClientRender;
      suspenseBoundary.flags |= ShouldCapture;
      // 注册监听器
			attachRetryListener(suspenseBoundary, root, wakeable, rootRenderLanes);
			return;
  }
}
主要做了三件事
- 给抛出错误的组件打上Incomplete标识
- 如果捕获的错误是 thenable 类型,则认定为是 Suspense 的子组件,向上找到最接近的一个Suspense边界,并打上ShouldCapture标识
- 执行attachRetryListener对 Promise 错误监听,当状态改变后开启一个调度任务重新渲染 Suspense
在错误处理的事情做完后,就不应该再往下递了,开始调用completeUnitOfWork往上归, 这时由于我们给 MyComponent  组件打上了Incomplete 标识,这个标识表示由于异常等原因渲染被搁置,那我们是不是就要开始往上找能够处理这个异常的组件?
我们再看看completeUnitOfWork 干了啥
function completeUnitOfWork(unitOfWork: Fiber): void {
 // 大致逻辑
  let completedWork = unitOfWork;
  if ((completedWork.flags & Incomplete) !== NoFlags) {
      const next = unwindWork(current, completedWork, subtreeRenderLanes);
			if (next) {
					workInProgress = next;
					return
			}
			// 给父节点打上Incomplete标记
			if (returnFiber !== null) {
		      returnFiber.flags |= Incomplete;
		      returnFiber.subtreeFlags = NoFlags;
		      returnFiber.deletions = null;
			}
	}
}
可以看到最终打上Incomplete 标识的组件都会进入unwindWork流程 , 并一直将祖先节点打上Incomplete 标识,直到unwindWork 中找到一个能处理异常的边界组件,也就ClassComponent, SuspenseComponent , 会去掉ShouldCapture标识,加上DidCapture标识
这时,对于Suspense来说需要的DidCapture已经拿到了,下面就是重新从Suspense 开始走一遍beginWork流程
再次回到 Suspense 组件, 这时由于有了DidCapture 标识,则展示fallback
对于fallback组件的fiber节点是通过mountSuspenseFallbackChildren 生成的
function mountSuspenseFallbackChildren(
  workInProgress,
  primaryChildren,
  fallbackChildren,
  renderLanes,
) {
  const primaryChildProps: OffscreenProps = {
    mode: 'hidden',
    children: primaryChildren,
  };
  let primaryChildFragment = mountWorkInProgressOffscreenFiber(
      primaryChildProps,
      mode,
      NoLanes,
    );
  let fallbackChildFragment = createFiberFromFragment(
      fallbackChildren,
      mode,
      renderLanes,
      null,
    );
  primaryChildFragment.return = workInProgress;
  fallbackChildFragment.return = workInProgress;
  primaryChildFragment.sibling = fallbackChildFragment;
  workInProgress.child = primaryChildFragment;
  return fallbackChildFragment;
}
它主要做了三件事
- 将PrimaryChild即Offscreen组件通过css隐藏
- 将fallback组件又包了层Fragment返回
- 将fallbackChild作为sibling链接至PrimaryChild

到这时渲染 fallback 的 fiber 树已经基本构建完了,之后进入commit阶段从根节点rootFiber开始深度遍历该fiber树 进行 render。
等待一段时间后,primary组件数据返回,我们之前在handleError中添加的监听器attachRetryListener 被触发,开始新的一轮任务调度。注:源码中调度回调实际在 Commit 阶段才添加的。
这时由于Suspense 节点已经存在,则走的是updateSuspensePrimaryChildren 中的逻辑,与之前首次加载时 monutSuspensePrimaryChildren不同的是多了删除的操作, 在 commit 阶段时则会删除fallback 组件, 展示primary组件。updateSuspensePrimaryChildren
if (currentFallbackChildFragment !== null) {
    // Delete the fallback child fragment
    const deletions = workInProgress.deletions;
    if (deletions === null) {
      workInProgress.deletions = [currentFallbackChildFragment];
      workInProgress.flags |= ChildDeletion;
    } else {
      deletions.push(currentFallbackChildFragment);
    }
  }
至此,Suspense 的一生我们粗略的过完了,在源码中对 Suspense 的处理非常多,涉及到优先级相关的本篇都略过。
Suspense 中使用了Offscreen组件来渲染子组件,这个组件的特性是能根据传入 mode 来控制子组件样式的显隐,这有一个好处,就是能保存组件的状态,有些许类似于 Vue 的keep-alive 。其次,它拥有着最低的调度优先级,比空闲时优先级还要低,这也意味着当 mode 切换时,它会被任何其他调度任务插队打断掉。

useTransition
useTransition 可以让我们在不阻塞 UI 渲染的情况下更新状态。useTransition 和 startTransition 允许将某些更新标记为低优先级更新。默认情况下,其他更新被视为紧急更新。React 将允许更紧急的更新(例如更新文本输入)来中断不太紧急的更新(例如展示搜索结果列表)。
其核心原理其实就是将startTransition 内调用的状态变更方法都标识为低优先级的lane (lane优先级参考)去更新。
const [isPending, startTransition] = useTransition()
startTransition(() => {
	setData(xxx)
})
一个输入框的例子
function Demo() {
  const [value, setValue] = useState();
  const [isPending, startTransition] = useTransition();
  return (
    <div>
      <h1>useTramsotopm Demo</h1>
      <input
        onChange={(e) => {
          startTransition(() => {
            setValue(e.target.value);
          });
        }}
      />
      <hr />
      {isPending ? <p>加载中。。</p> : <List value={value} />}
    </div>
  );
}
function List({ value }) {
  const items = new Array(5000).fill(1).map((_, index) => {
    return (
      <li>
        <ListItem index={index} value={value} />
      </li>
    );
  });
  return <ul>{items}</ul>;
}
function ListItem({ index, value }) {
  return (
    <div>
      <span>index: </span>
      <span>{index}</span>
      <span>value: </span>
      <span>{value}</span>
    </div>
  );
}
当我每次进行输入时,会触发 List 进行大量更新,但由于我使用了startTransition  对List的更新进行延后 ,所以Input输入框不会出现明显卡顿现象
演示地址https://stackblitz.com/edit/stackblitz-starters-kmkcjs?file=src%2Ftransition%2FList.tsx

由于更新被滞后了,所以我们怎么知道当前有没有被更新呢?
这时候第一个返回参数isPending 就是用来告诉我们当前是否还在等待中。
但我们可以看到,input组件目前是非受控组件 ,如果改为受控组件 ,即使使用了startTransition 一样会出现卡顿,因为 input 响应输入事件进行状态更新应该是要同步的。
所以这时候下面介绍的useDeferredValue 作用就来了。
useDeferredValue
useDeferredValue 可让您推迟更新部分 UI, 它与useTransition 做的事差不多,不过useTransition 是在状态更新层,推迟状态更新来实现非阻塞,而useDeferredValue 则是在状态已经更新后,先使用状态更新前的值进行渲染,来延迟因状态变化而导致的组件重新渲染。
它的基本用法
function Page() {
  const [value, setValue] = useState('');
  const deferredValue = useDeferredValue(setValue);
}
我们再用useDeferredValue 去实现上面输入框的例子
function Demo() {
  const [value, setValue] = useState('');
  const deferredValue = useDeferredValue(value);
  return (
    <div>
      <h1>useDeferedValue Demo</h1>
      <input
        value={value}
        onChange={(e) => {
          setValue(e.target.value)
        }}
      />
      <hr />
      <List value={deferredValue} />
    </div>
  );
}
我们将input作为受控组件 ,对于会因输入框值而造成大量渲染的List,我们使用deferredValue 。
其变化过程如下
- 当输入变化时,deferredValue首先会是变化前的旧值进行重新渲染,由于值没有变,所以 List 没有重新渲染,也就没有出现阻塞情况,这时,input 的值能够实时响应到页面上。
- 在这次旧值渲染完成后,deferredValue 变更为新的值,React 会在后台开始对新值进行重新渲染,List组件开始 rerender,且此次 rerender 会被标识为低优先级渲染,能够被中断
- 如果此时又有输入框输入,则中断此次后台的重新渲染,重新走1,2的流程
我们可以打印下deferredValue  的值看下
初始情况输入框为1,打印了两次1

输入2时,再次打印了两次1,随后打印了两次2

参考
最后
欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star
- 大数据分布式任务调度系统——Taier
- 轻量级的 Web IDE UI 框架——Molecule
- 针对大数据领域的 SQL Parser 项目——dt-sql-parser
- 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
- 一个速度更快、配置更灵活、使用更简单的模块打包器——ko
- 一个针对 antd 的组件测试工具库——ant-design-testing
React18 之 Suspense的更多相关文章
- react18 来了,我 get 到...
		大家好! 本文主要是关于即将发布的 react 18 的新特性.那么 react18 带来了什么呢? 详情可以关注 github React 18 工作组仓库 1. automatic batchin ... 
- 构建基于React18的电子表格程序
		背景 2022年3月29日,React正式发布18.0.0.本次升级内容包括开箱即用的改进,如自动批处理.新的API(如startTransition)和支持Suspense 的流式服务器端渲染.关于 ... 
- React Suspense 尝鲜,处理前后端IO异步操作
		简单介绍一下Suspense Suspense主要用来解决网络IO问题,它早在2018年的React 16.6.0版本中就已发布.它的相关用法有些已经比较成熟,有的相对不太稳定,甚至经历了重命名.删除 ... 
- 深度理解 React Suspense(附源码解析)
		本文介绍与 Suspense 在三种情景下使用方法,并结合源码进行相应解析.欢迎关注个人博客. Code Spliting 在 16.6 版本之前,code-spliting 通常是由第三方库来完成的 ... 
- [React] Asynchronously Load webpack Bundles through Code-splitting and React Suspense
		One approach to building high performance applications with webpack is to take advantage of code-spl ... 
- React源码 Suspense 和 ReactLazy
		React 16.6 提供的一个新的开放一部分功能的 Suspense 代码 import React, { Suspense, lazy } from 'react' const LazyComp ... 
- [React] Write a generic React Suspense Resource factory
		Using Suspense within our component isn't exactly ergonomic. Let's put all that logic into a reusabl ... 
- [React] Handle React Suspense Errors with an Error Boundary
		Error Boundaries are the way you handle errors with React, and Suspense embraces this completely. Le ... 
- [React] Fetch Data with React Suspense
		Let's get started with the simplest version of data fetching with React Suspense. It may feel a litt ... 
- React.lazy和React.Suspense异步加载组件
		在React16.6中引入了React.lazy和React.Suspense,这两个组件,可以用来实现异步加载组件. 例如: const johanComponent = React.lazy(() ... 
随机推荐
- [转帖]关系模型到 Key-Value 模型的映射
			https://cn.pingcap.com/blog/tidb-internal-2 在这我们将关系模型简单理解为 Table 和 SQL 语句,那么问题变为如何在 KV 结构上保存 Table 以 ... 
- [转帖]一份快速实用的 tcpdump 命令参考手册
			http://team.jiunile.com/blog/2019/06/tcpdump.html tcpdump 简介 对于 tcpdump 的使用,大部分管理员会分成两类.有一类管理员,他们熟知 ... 
- [转帖]AMD第四代宵龙 9174F 亮眼
			https://www.amd.com/zh-hans/processors/epyc-9004-series#%E8%A7%84%E6%A0%BC 型号规格 型号 CPU 核心数量 线程数量 最 ... 
- [转帖]Springboot配置https访问
			https://www.cnblogs.com/feifuzeng/p/14709372.html 介绍 该篇博文主要介绍如何配置Springboot使其打包部署的服务必须通过HTTPS协议才可访问, ... 
- 【解决了一个小问题】alert manager要怎么样才能触发告警到企业微信上?
			作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 07-15:花了几个小时仍然是没走通这个流程,把中间结果记 ... 
- github clone或访问慢
			做技术的我们经常会访问github.com,有时出现github访问非常慢或者git clone速度很慢,git push也很慢 原因很简单:github被高高的墙屏蔽了. 所以解决方案就是手动把 c ... 
- 每日一道面试题:Java中序列化与反序列化
			写在开头 哈喽大家好,在高铁上码字的感觉是真不爽啊,小桌板又拥挤,旁边的小朋友也比较的吵闹,影响思绪,但这丝毫不影响咱学习的劲头!哈哈哈,在这喧哗的车厢中,思考着这样的一个问题,Java中的对象是如何 ... 
- DevelopTool
			目录 01-PostMan常用玩法详解 
- 人工智能自然语言处理:N-gram和TF-IDF模型详解
			人工智能自然语言处理:N-gram和TF-IDF模型详解 1.N-gram 模型 N-Gram 是一种基于统计语言模型的算法.它的基本思想是将文本里面的内容按照字节进行大小为 N 的滑动窗口操作,形成 ... 
- PaddleNLP基于ERNIR3.0文本分类:WOS数据集为例(层次分类)
			相关项目链接: Paddlenlp之UIE模型实战实体抽取任务[打车数据.快递单] Paddlenlp之UIE分类模型[以情感倾向分析新闻分类为例]含智能标注方案) 应用实践:分类模型大集成者[Pad ... 
