博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[译] Fiber内幕:深入概述React新的协调算法
阅读量:5948 次
发布时间:2019-06-19

本文共 14818 字,大约阅读时间需要 49 分钟。

原文地址:

如何以及为何从React组件到Fiber节点的一切内容

React使用一个构建用户界面的JavaScript库,它的核心是跟踪组件状态的的变化,然后将更新的状态投影在屏幕上。在React中,我们把这个过程称为协调。我们调用setState方法后,框架会检测state和prop是否发生变化,并重新渲染UI组件。

React文档关于这个机制提供了: React元素的角色,生命周期方法,render方法,以及应用于子组件的diff算法。由render方法返回的不可变的React元素普遍被认为是React的“虚拟DOM”。那个术语早期帮助React解释给人们,但它也造成了一些困惑,也不再在React文档中使用,这篇文章中,我会继续称之为React元素的树。

除了React元素的树,框架总还有用于保留状态的内部实例(组件,DOM节点等)的一棵树。从16版本开始,React推出了内部实例树的新实现,以及管理它的算法(代码上称为Fiber)。想要得知Fiber架构带来的好处,可以参见。

这篇文章花费了我很多时间,而且要是没有 ! ?的个帮助,也不会讲解地如此全面。

这是给你讲解React内部架构系列的第一篇文章。这篇文章中,我想提供算法中重要概念和数据结构的深度概述。一旦我们有足够的背景,我们就可以探索这个算法以及用于遍历和操作fiber树的主要方法。系列中的下一篇文章将示范React如何使用这个算法来初始render以及操作state和props的更新,从那里我们将了解到调度(scheduler)的细节、子协调(child reconciliation)操作以及构建更新链表(effect list)。

这里我将给你讲述相当高级的内容,我保证你阅读后可以理解到并发(Concurrent)React内部工作背后的神奇。如果你想成为React的贡献者的话,这个系列的文章也可以作为你的向导。我一个(就是喜欢死磕源码),所以这里有很多关于React@16.6.0的资源链接。

这确实牵扯很多内容,所以如果你没有马上理解也不必有很大压力,一切都值得花时间。需要注意的是你不必了解这些来使用React,这篇文章是关于React如何内部工作的。

设置一个背景

这里有个我们在整个系列中都会使用到的简单应用。我们有个button,简单的增加数字,然后渲染到屏幕上。

这是实现:

class ClickCounter extends React.Component {    constructor(props) {        super(props);        this.state = {count: 0};        this.handleClick = this.handleClick.bind(this);    }    handleClick() {        this.setState((state) => {            return {count: state.count + 1};        });    }    render() {        return [            ,            {this.state.count}        ]    }}复制代码

你可以在去执行它。正如你看到的,它是一个简单组件,通过render方法返回buttonspan两个子组件。只要你点击button,组件的状态就会在处理器中更新,这继而导致span元素中的text的更新。

React在**协调(reconciliation)**期间有执行很多活动,例如,React在第一次render时执行的操作,以及在我们这个简单的应用中状态更新之后:

  • 更新ClickCounterstate中的count参数
  • 获取和比较ClickCounter的子组件以及他们的props
  • 更新span元素的props

协调期间还执行其他活动,像声明或者更新。所有这些活动在Fiber架构中统一起来被定义为一个“工作(work)”。工作的类型通常取决于React元素(element)的类型,例如,对于一个类组件(class component),React需要创建实例,而对于方法组件(function component)则不需要这样。正如你所知,React中有很多种元素,如类组件、方法组件、host组件(DOM节点)以及Portal等。元素的类型被定义在方法中的第一个参数,这个方法通常用在render方法中来场景一个元素。

在我们探索这些执行的活动以及主要的Fiber算法时,我们先来对React内部使用的数据结构有个认识。

从React元素到Fiber节点

React中每个组件是一个UI表示,我们可以叫它视图(view)或者模板(template),它由render方法返回。这里便是我们ClickCounter的模板:

{this.state.count}复制代码

React元素(Elements)

一旦模板经过JSX编译,最终获得一串React元素。这就是React组件的render方法真实返回的东西,而不是HTML。因为我们没有要求使用JSX,所以ClickCounter组件的render方法也可以写成:

class ClickCounter {    ...    render() {        return [            React.createElement(                'button',                {                    key: '1',                    onClick: this.onClick                },                'Update counter'            ),            React.createElement(                'span',                {                    key: '2'                },                this.state.count            )        ]    }}复制代码

render方法中的React.createElement调用可以创建两个数据结构:

[    {        $$typeof: Symbol(react.element),        type: 'button',        key: "1",        props: {            children: 'Update counter',            onClick: () => { ... }        }    },    {        $$typeof: Symbol(react.element),        type: 'span',        key: "2",        props: {            children: 0        }    }]复制代码

你可以看到React在这些对象上添加当作React元素的来唯一标示,且我们还有typekeyprops来描述这个元素,这些值由你传给了React.createElement方法。这里注意,React是如何把文本内容表达成spanbutton节点的孩子,click处理如何成为button元素props的一部分,这里还有React元素上其他一些字段如ref已经超出了本文的范畴。

React元素ClickCounter没有任何props或者key:

{    $$typeof: Symbol(react.element),    key: null,    props: {},    ref: null,    type: ClickCounter}复制代码

Fiber节点

在**协调(reconciliation)**期间,由render方法返回的每个React元素都将合并到fiber节点的树中,每个React元素都有相对应的fiber节点,不像React元素,fiber不会在每次render时重新创建。这些可变的数据结构带有组件的状态以及DOM。

我们之前讨论的是框架根据React元素的类型来执行不同的活动,在我们简单的应用中,对于类组件ClickCounter,它调用生命周期和render方法,而spanhost组件(DOM节点)则执行DOM变化,所以每个React元素相对应的Fiber节点,这些类型描述了需要完成的工作。

当React元素首次被转化成fiber节点时,React在方法中使用这个元素中的数据创建fiber,在之后发生的更新中,React重用这个fiber节点,且通过相对应的React元素中数据更新必要的属性。

React也可能需要基于key属性在层级中移动节点,或者如果对应的React元素不再由render方法返回时,则删除掉它。

找出方法,你可以看到所有活动的列表,以及React在当前存在fiber节点上执行的对应方法。

因为React为每个React元素都创建了一个fiber,所以只要我们有这些元素的一棵树,那我们就会有fiber节点的一棵树。在我们简单应用案例中,它看起来如下:

所有fiber节点通过一个链表链接起来,这个链表使用了fiber节点中属性:childsiblingreturn。关于如何和为何这种方式,可以查阅我的文章。

当前和正在执行的树(Current and work in progress trees)

在第一次渲染(render)之后,React最后得到了一颗fiber树,它反映了用于渲染UI的应用的状态,这颗树被当作current。当React开始处理更新时,它构建所谓的workInProgress树来反映将来刷新屏幕的状态。

所有工作都在来自workInProgress树的fiber上执行。当React经过当前树时,对于每一个先存在的fiber节点,它都会创建一个替代(alternate)节点,这些节点组成了workInProgress树。这个节点是使用render方法返回的React元素的数据创建的。一旦更新处理完以及所有相关工作完成,React就有一颗替代树来准备刷新屏幕。一旦这颗workInProgress树渲染(render)在屏幕上,它便成了当前树。

React的设计原则之一是连贯性。React总是一次性更新DOM,而不是只显示部分结果。这颗workInProgress树为当做是‘草稿’,它对用户是不可见的,以至于React可以先处理所有组件,然后再刷新他们的改变到屏幕上。

在这个代码中,可以看到很多方法,这些方法持有来着currentworkInProgress树的fiber节点,这是一个这样方法的签名:

function updateHostComponent(current, workInProgress, renderExpirationTime) {...}复制代码

每个fiber节点中的alternate字段持有它的一个副本,这个副本节点表示current树指向workInProgress树的,反之亦然,代码如下:

function createWorkInProgress(current, ...) {  let workInProgress = current.alternate;  if (workInProgress === null) {    workInProgress = createFiber(...);  }  ...  workInProgress.alternate = current;  current.alternate = workInProgress;  ...  return workInProgress;}复制代码

副作用(side-effects)

我们可以认为React中组件是使用state和props的方法,用于计算UI展示。每个其他活动,像DOM变化或者调用生命周期方法,应当认为是一个副作用,或者一个简单的作用。作用(Effects)也在中提及。

你以前可能做过请求数据,订阅,或者在React组件中手动修改DOM,我们把这些操作叫做副作用(或者简说作用),因为它们会影响其他组件,且不能在渲染时完成。

你可以看到很多state和props是如何造成副作用的,

既然应用副作用是一个工作的类型,那一个fiber节点就是除了更新之外还用于跟踪作用的简明机制。每一个fiber节点可以有很多关联的作用,它们被编码到effectTag字段中(effectTag使用位运算的妙处啦)。

所以Fiber中的作用(effects)基本上定义了一个组件实例在其更新操作之后需要完成的,对于host组件(DOM元素),这个工作包括更新、添加和删除元素;对于类组件,React可能需要更新refs,以及调用componentDidMountcomponentDidUpdate生命周期方法。这里当然还有其他一些与fiber类型相对应的作用。

作用列表(Effects list)

React处理更新很快,为了实现这个层次的性能,它采用了个别有趣的技巧,比如,将含有作用的fiber节点用线性列表表示,从而可以快速迭代。迭代线性列表比树要快,且可以不必花时间在没有副作用的节点上。

这个列表的目的是用于标记一些节点,这些节点有DOM更新或者与其关联的副作用。这个列表是finishedWork的子集,且通过nextEffect属性链接起来,而不是currentworkInProgress树中使用的child属性。

对作用列表作了一个类比,就像一颗圣诞树中通过“圣诞灯”来把所有作用节点连接起来。为了虚拟化它,我们设想以下这颗fiber树,其中点亮的节点有一些工作要做,例如,我们更新使得c2插入DOM中、d2c1改变属性,以及d2触发生命周期方法,那作用列表就讲它们连接起来,以至于React之后可以滤过其他节点:

你可以看到含有作用的节点和如何连接起来。当要遍历这些节点时,React使用firstEffect得出列表从哪里开始,那上述的示意图可以用线性列表如下表示:

正如你所见,React执行作用的顺序是从子向上到父的。

Fiber树的根节点(Root of the fiber tree)

每个React应用有一个或多个DOM元素作为容器,在我们的例子中,它是ID是containerdiv元素:

const domContainer = document.querySelector('#container');ReactDOM.render(React.createElement(ClickCounter), domContainer);复制代码

React为这些容器的每个创建一个,你可以通过DOM元素的引用访问它:

const fiberRoot = query('#container')._reactRootContainer._internalRoot复制代码

这个fiber根节点就是React持有fiber树引用的地方,它保存在fiber根节点的current属性上:

const hostRootFiberNode = fiberRoot.current复制代码

fiber树开始于HostRoot的fiber节点的,它由内部创建并将顶层组件作为父节点。这里有一个通过从stateNode属性从HostRootfiber节点返回到FiberRoot的连接:

fiberRoot.current.stateNode === fiberRoot; // true复制代码

你可以通过fiber根节点获取HostFiber节点来探索fiber树,或者你可以像这样从组件实例中获取独立的fiber节点:

compInstance._reactInternalFiber复制代码

Fiber节点结构

我们来看一下由ClickCounter组件创建的fiber节点的数据结构:

{    stateNode: new ClickCounter,    type: ClickCounter,    alternate: null,    key: null,    updateQueue: null,    memoizedState: {
count: 0}, pendingProps: {}, memoizedProps: {}, tag: 1, effectTag: 0, nextEffect: null}复制代码

以及spanDOM元素:

{    stateNode: new HTMLSpanElement,    type: "span",    alternate: null,    key: "2",    updateQueue: null,    memoizedState: null,    pendingProps: {
children: 0}, memoizedProps: {
children: 0}, tag: 5, effectTag: 0, nextEffect: null}复制代码

fiber节点中有许多字段,我已经在前面有描述字段alternateeffectTagnextEffect的目的,现在我看看为何还需要其他字段。

stateNote

持有类组件实例、DOM节点或者其他与这个fiber节点关联的React元素类型的引用,一般来说,我们可以说这个属性用来持有与fiber相关的本地状态。

type

定义与这个fiber关联的方法或者类,对于类组件,它指向类的构造方法;对于DOM元素,它具体为HTML标签;我经常用这个字段来理解fiber节点关联的是什么元素。

tag

定义了,这个用来在协调算法中定义那些工作需要完成。如之前所说,工作的不同取决于React元素类型,方法映射了一个React元素到相对应fiber节点类型。在我们的例子应用中,ClickCounter组件的tag属性值为1,代表了ClassComponent,以及span组件的是5,代表了HostComponent

updateQueue

一个包括状态更新、callbacks以及DOM更新的队列。

memoizedState

fiber中用于创建输出的状态,当处理更新时,它反映了当前已经渲染在屏幕上的状态。

memoizedProps

fiber中在前一次渲染时用于创建输出的props。

pendingProps

由React元素中的新数据而来的已经更新过的props,且需要应用于子组件或者DOM元素。

key

一组子组件的唯一标示,用于React得出列表中哪个改变了、添加了或者删除了。这个与React中在描述的的“列表与key”的功能相关。

你可以从得到fiber节点的整个数据结构。我滤过了一些上面解释过的字段。特别是我跳过了**childsiblingreturn,这些在我中介绍过了。还有一类字段像expirationTimechildExpirationTime以及mode特定用于调度(Scheduler)**。

整体算法

React执行工作主要有两个阶段:rendercommit

在第一个render阶段,React执行由setState或者React.render调度的组件上的更新,且得出在UI上哪些需要被更新,如果它是初始渲染,那React会为每个有render方法返回的元素创建一个新的fiber节点,在后续的更新中,当前存在React元素的fiber会被重用和更新。这个阶段的结果是一颗fiber节点被标记副作用的树。这些作用被描述为在接下来的commit阶段中需要完成的工作,在这个阶段,React取标记作用的fiber树并把它们应用到实例上,遍历作用列表且执行DOM更新以及其他用户可见的变化。

理解在第一个render阶段中执行的工作可以是异步的很重要。React在可用的时间内能处理一个或多个fiber节点,然后停止来保存完成的工作并妥协于一些事件(比如优先级高的UI事件),它之后可以在之前离开的方法在继续执行,然而有时可能会丢弃已完成的工作,并从顶层重来。由于这个阶段的执行的工作不会导致用户可见的变化(如DOM更新),所以这个暂停是可行的。不同的是,接下来的commit阶段总是同步的,因为这个阶段的执行的工作会导致用户可见的变化,这也是为什么React一把完成它们的原因。

调用生命周期方法是React执行的一种工作类型,一些方法执行在render阶段,一些执行在commit阶段,下面是在render阶段中执行的生命周期方法列表:

  • [UNSAFE_]componentWillMount (deprecated)
  • [UNSAFE_]componentWillReceiveProps (deprecated)
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • [UNSAFE_]componentWillUpdate (deprecated)
  • render

正如你所见,一些在render阶段中被遗留的方法从16.3版本开始被标记为UNSAFE,它们在16.x的release版本中被弃用掉了,而它们不带UNSAFE前缀的副本在17.0中将被移除,你可以在阅读更多关于这些改变,以及迁移建议的内容。

你好奇这个的原因吗?

那,我们已经得知**render阶段不会造成副作用(如DOM更新),且React可以对组件异步处理更新(且在的说,甚至可以在多线程中执行)。然后被标记了UNSAFE的声明周期总是被误解或者不易察觉的误用,开发者倾向于把有副作用的逻辑放在这些方法中,这在新的异步渲染策略中可能会导致一些问题。尽管只有他们未标记UNSAFE**前缀的副本被移除掉了,但它们仍然可能在未来的并发模型(Concurrent Mode)中造成问题,当然这个模式你可以不启用。

这里是**commit**阶段执行的生命周期方法列表:

  • getSnapshotBeforeUpdate
  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

因为这些方法在同步的**commit**阶段执行,所有它们可以包含副作用以及触控DOM。

好,我们现在已经有了一定的基础去看看用于遍历树和执行工作的算法。

Render阶段

这个协调算法总是开始于顶层的HostRootfiber节点,这个节点由方法创建,然而React能够跳过已经处理过的fiber节点,直到它找到尚未完成工作的节点,例如,如果你在一个组件树深处调用setState,React将从顶部开始,但是很快就跳过一些节点,找到调用setState方法的组件。

工作循环(work loop)中的主要步骤

所有的fiber节点都会在做处理,这里是这个循环的同步部分的实现:

function workLoop(isYieldy) {  if (!isYieldy) {    while (nextUnitOfWork !== null) {      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);    }  } else {...}}复制代码

在上面代码中,nextUnitOfWork持有一个fiber节点,这个节点来自还有一些工作需要做的workInProgress树,正如React遍历fiber树一样,它使用这个变量来知道是否还有其他未完成工作的fiber节点,当前fiber节点处理之后,这个遍历将要么获取下一个fiber节点的引用,要么为null,在**null**的情况下,React将退出工作循环,并准备提交更新。

这里有4个主要的方法,用于遍历树,以及初始化或者完成工作:

为了示例它们是怎么用的,看看下面遍历fiber树的动画。我以及用demo把这些方法做了个简单是的实现,每个方法都会取fiber节点来处理,正如React沿着树往下走时,你可以看到当前活跃fiber节点的变化,这个视频中你可以清晰地看到算法是如何从一个树枝走到另一个树枝的,它在移动到父节点前,首先得先完成子节点的工作。

注意:垂直直线连接代表兄弟,拐弯连接代表父子,如**b1没有子节点,而b2有一个c1**孩子。

,其中你可以暂停播放,查看当前节点和方法的状态。概念上,你可以把“begin”当作进入组件,把“complete”当作离开组件,你也可以在,正如我解释这些方法所做的事情。

我们从头两个方法**performUnitOfWorkbeginWork**开始:

function performUnitOfWork(workInProgress) {    let next = beginWork(workInProgress);    if (next === null) {        next = completeUnitOfWork(workInProgress);    }    return next;}function beginWork(workInProgress) {    console.log('work performed for ' + workInProgress.name);    return workInProgress.child;}复制代码

performUnitOfWork方法从workInProgress中接受一个fiber节点,调用beginWork方法来开始工作。fiber节点上需要执行的所有活动都将从这个方法开始,对于这个示例的目的,我们只打印一下组件名称,就当是工作已经完成了。beginWork总是返回一个指针,指向循环中要处理的下一个子节点,或者指向null

如果有下一个子节点,它会在**workLoop方法中赋值给nextUnitOfWork变量,然后,如果没有子节点,React知道到达了树枝的末尾,所有就可以完成(complete)当前这个节点。一个节点只要完成了,它就会需要从兄弟和父级节点继续执行工作,这在completeUnitOfWork**方法中进行:

function completeUnitOfWork(workInProgress) {    while (true) {        let returnFiber = workInProgress.return;        let siblingFiber = workInProgress.sibling;        nextUnitOfWork = completeWork(workInProgress);        if (siblingFiber !== null) {            // If there is a sibling, return it            // to perform work for this sibling            return siblingFiber;        } else if (returnFiber !== null) {            // If there's no more work in this returnFiber,            // continue the loop to complete the parent.            workInProgress = returnFiber;            continue;        } else {            // We've reached the root.            return null;        }    }}function completeWork(workInProgress) {    console.log('work completed for ' + workInProgress.name);    return null;}复制代码

你可以从中看到方法大致是一个大的**while循环,React当workInProgress节点没有孩子时就进入这个方法。在完成当前fiber的工作后,它检查是否有还有兄弟,如果有,React退出这个方法,并返回指向兄弟的指针,它将会赋值给nextUnitOfWork**变量,然后React将通过兄弟节点在新树枝上开始执行工作。重要的是明白这种情况中React只有是前面的兄弟节点完成了工作,而它还没有完成父节点的工作,只有所有开始于子节点的树枝上的工作完成了,它才算是为父节点完成了工作,然后原路返回

正如你从实现中所见,**performUnitOfWorkcompleteUnitOfWork方法的目的几乎是迭代,而主要活动发生在beginWorkcompleteWork方法中。在这个系列接下来的文章中,我们将知道,当React进入beginWorkcompleteWork方法中时,ClickCounter组件和span**节点会发生什么。

Commit阶段

这个阶段开始于方法,这里React便会更新DOM,以及调用前前后置突变生命周期方法。

当React进入这个阶段时,它有两颗树和一个作用列表,第一颗树表示了当前渲染在屏幕上的状态,而这里还有在**render阶段构建的一颗替代树,它调用代码中finishedWorkworkInProgress,表示需要在屏幕上反应出来的状态,这颗替代树链接方式类似当前树,通过childsibling**指针链接。

还有作用列表——通过**nextEffect连接起来的finishedWork树的节点子集。记住作用列表是在render**阶段生成,整个渲染(rendering)的要点就是得出哪些节点需要插入、更新、删除,以及哪些组件需要执行它们的生命周期方法,这便是作用列表要告诉我们的,这是会在commit阶段中被迭代的节点集合

为了debugging,当前树可以通过fiber根节点**current属性方法,finishedWork树可以通过当前树上的HostFiber节点的alternate**来访问。

主要运行在commit阶段的方法是,大致如下操作:

  • 标记了**Snapshot作用的节点执行getSnapshotBeforeUpdate**生命周期方法。
  • 标记了**Deletion作用的节点执行componentWillUnmount**生命周期方法。
  • 执行所有DOM的插入、更新和删除。
  • 把**finishedWork**树置为当前树。
  • 标记了**Placement作用的节点执行componentDidMount**生命周期方法。
  • 标记了**Update作用的节点执行componentDidUpdate**生命周期方法。

调用前置突变方法**getSnapshotBeforeUpdate之后,React提交了树中所有副作用。它以两个步骤来做,第一步是执行所有DOM(host)的插入、更新和删除以及ref的卸载,然后React把finishedWork树赋值给FiberRoot,即当workInProgress树为current树,这在commit阶段的第一步和第二步之间执行,便于之前的树在componentWillUnmount是还是当前树,而在componentDidMount/Update**时,完成树(finished work)为当前树。在第二步中,React调用其他所有生命周期方法和ref回调,这些方法在单独步骤中执行,以致整个树中所有的替换、更新和删除已经被调用。

这里运行上述描述方法的大意:

function commitRoot(root, finishedWork) {    commitBeforeMutationLifecycles()    commitAllHostEffects();    root.current = finishedWork;    commitAllLifeCycles();}复制代码

每个子方法都实现了一个循环来迭代作用列表以及检查作用类型,当发现和这个方法目的有关的作用,就应用它。

前置突变(Pre-mutation)生命周期方法

例如这里的一个代码,迭代作用树,并检查一个节点是否是**Snapshot**作用:

function commitBeforeMutationLifecycles() {    while (nextEffect !== null) {        const effectTag = nextEffect.effectTag;        if (effectTag & Snapshot) {            const current = nextEffect.alternate;            commitBeforeMutationLifeCycles(current, nextEffect);        }        nextEffect = nextEffect.nextEffect;    }}复制代码

对于类组件来说,这个作用意味着调用**getSnapshotBeforeUpdate**方法。

DOM更新

是React执行DOM更新的方法,这个方法定义了节点需要完成操作的类型,且执行它:

function commitAllHostEffects() {    switch (primaryEffectTag) {        case Placement: {            commitPlacement(nextEffect);            ...        }        case PlacementAndUpdate: {            commitPlacement(nextEffect);            commitWork(current, nextEffect);            ...        }        case Update: {            commitWork(current, nextEffect);            ...        }        case Deletion: {            commitDeletion(nextEffect);            ...        }    }}复制代码

有趣的是,React在删除操作中,把**commitDeletion方法中调用componentWillUnmount**方法当作其中一部分。

后置突变(Post-mutation)生命周期方法

是React调用所有剩余生命周期方法**componentDidUpdatecomponentDidMount**的方法。

这里我们就讲完了。

转载于:https://juejin.im/post/5cdb5b205188252035420c7f

你可能感兴趣的文章
linux中cacti和nagios整合
查看>>
Parallels Desktop12推出 新增Parallels Toolbox
查看>>
正则表达式验证身份证格式是否正确
查看>>
Firebird(全功能的,免维护的数据库,能够管理多个独立的数据库) V2.1.3 英文特别版...
查看>>
xml格式文件解析
查看>>
ios百度地图-路径规划
查看>>
Python高效编程技巧
查看>>
配置Eclipse使用maven构建项目默认JDK为1.8
查看>>
jsp内置对象以及jsp动作
查看>>
Struts上路_09-数据类型转换
查看>>
Android Eclipse 修改默认查看图片的打开方式
查看>>
CMake与动态链接库(dll, so, dylib)
查看>>
myeclipse(eclipse)乱码处理
查看>>
SpringBoot 过滤器, 拦截器, 监听器 对比及使用场景
查看>>
数据库索引探索
查看>>
struts2使用json需要注意的问题
查看>>
gitlab runner 优化
查看>>
快速添加百度网盘文件到Aria2 猴油脚本
查看>>
mac 无法登录mysql的解决办法
查看>>
Shiro权限判断异常之命名导致的subject.isPermitted 异常
查看>>