深入React源码揭开渲染更新流程的面纱

2023-11-02

转前端一年半了,平时接触最多的框架就是React。在熟悉了其用法之后,避免不了想深入了解其实现原理,网上相关源码分析的文章挺多的,但是总感觉不如自己阅读理解来得深刻。于是话了几个周末去了解了一下常用的流程。也是通过这篇文章将自己的个人理解分享出来。

在具体的源码流程分析之前,根据个人理解,结合网上比较好的文章,先来分析一些概念性的东西。后续再分析具体的流程逻辑。

React 15

架构分层

React 15版本(Fiber以前)整个更新渲染流程分为两个部分:

  • Reconciler(协调器); 负责找出变化的组件
  • Renderer(渲染器); 负责将变化的组件渲染到页面上

Reconciler

React中可以通过setStateforceUpdateReactDOM.render来触发更新。每当有更新发生时,Reconciler会做如下工作:

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

Renderer

在对某个更新节点执行玩Reconciler之后,会通知Renderer根据不同的"宿主环境"进行相应的节点渲染/更新。

React 15的缺陷

React 15diff过程是 递归执行更新 的。由于是递归,一旦开始就"无法中断" 。当层级太深或者diff逻辑(钩子函数里的逻辑)太复杂,导致递归更新的时间过长,Js线程一直卡主,那么用户交互和渲染就会产生卡顿。看个例子: count-demo

<button>        click     <button>
<li>1<li>        ->       <li>2<li>
<li>2<li>        ->       <li>4<li>
<li>3<li>        ->       <li>6<li>

当点击button后,列表从左边的1、2、3变为右边的2、4、6。每个节点的更新过程对用户来说基本是同步,但实际上他们是顺序遍历的。具体步骤如下:

  1. 点击button,触发更新
  2. Reconciler检测到<li1>需要变更为<li2>,则立刻通知Renderer更新DOM。列表变成2、2、3
  3. Reconciler检测到<li2>需要变更为<li4>,通知Renderer更新DOM。列表变成2、4、3
  4. Reconciler检测到<li3>需要变更为<li6>,则立刻通知Renderer更新DOM。列表变成2、4、6

从此可见 ReconcilerRenderer是交替工作 的,当第一个节点在页面上已经变化后,第二个节点再进入Reconciler。由于整个过程都是同步的,所以在用户看来所有节点是同时更新的。如果中断更新,则会在页面上看见更新不完全的新的节点树!

假如当进行到第2步的时候,突然因为其他任务而中断当前任务,导致第3、4步无法进行那么用户就会看到:

<button>        click     <button>
<li>1<li>        ->       <li>2<li>
<li>2<li>        ->       <li>2<li>
<li>3<li>        ->       <li>3<li>

这种情况是React绝对不希望出现的。但是这种应用场景又是十分必须的。想象一下,用户在某个时间点进行了输入事件,此时应该更新input内的内容,但是因为一个不在当前可视区域的列表的更新导致用户的输入更新被滞后,那么给用户的体验就是卡顿的。因此React团队需要寻找一个办法,来解决这个缺陷。

React 16

架构分层

React15架构不能支撑异步更新以至于需要重构,于是React16架构改成分为三层结构:

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

Scheduler

React 15React 16提出的需求是Diff更新应为可中断的,那么此时又出现了两个新的两个问题:中断方式和判断标准;

React团队采用的是 合作式调度,即主动中断和控制器出让判断标准为超时检测。同时还需要一种机制来告知中断的任务在何时恢复/重新执行。 React 借鉴了浏览器的requestIdleCallback接口,当浏览器有剩余时间时通知执行

由于一些原因React放弃使用rIdc,而是自己实现了功能更完备的polyfill,即Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。

Reconciler

React 15Reconciler是递归处理Virtual DOM的。而React16使用了一种新的数据结构:FiberVirtual DOM树由之前的从上往下的树形结构,变化为基于多向链表的"图"。

更新流程从递归变成了可以中断的循环过程。每次循环都会调用shouldYield()判断当前是否有剩余时间。源码地址。

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

前面有分析到React 15中断执行会导致页面更新不完全,原因是因为ReconcilerRenderer是交替工作的,因此在React 16中,ReconcilerRenderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler只是会为变化的Virtual DOM打上代表增/删/更新的标记,而不会发生通知Renderer去渲染。类似这样:

export const Placement = /*             */ 0b0000000000010;
export const Update = /*                */ 0b0000000000100;
export const PlacementAndUpdate = /*    */ 0b0000000000110;
export const Deletion = /*              */ 0b0000000001000;

只有当所有组件都完成Reconciler的工作,才会统一交给Renderer进行渲染更新。

Renderer(Commit)

Renderer根据ReconcilerVirtual DOM打的标记,同步执行对应的渲染操作。

对于我们在上一节使用过的例子,在React 16架构中整个更新流程为:

  1. setState产生一个更新,更新内容为:state.count1变为2
  2. 更新被交给SchedulerScheduler发现没有其他更高优先任务,就将该任务交给Reconciler
  3. Reconciler接到任务,开始遍历Virtual DOM,判断哪些Virtual DOM需要更新,为需要更新的Virtual DOM打上标记
  4. Reconciler遍历完所有Virtual DOM,通知Renderer
  5. Renderer根据Virtual DOM的标记执行对应节点操作

其中步骤2、3、4随时可能由于如下原因被中断:

  • 有其他更高优先任务需要先更新
  • 当前帧没有剩余时间

由于SchedulerReconciler的工作都在内存中进行,不会更新页面上的节点,所以用户不会看见更新不完全的页面。

Diff原则

React的Diff是有一定的 前提假设 的,主要分为三点:

  • DOM跨层级移动的情况少,对 Virtual DOM 树进行分层比较,两棵树只会对同一层次的节点进行比较。
  • 不同类型的组件,树形结构不一样。相同类型的组件树形结构相似
  • 同一层级的一组子节点操作无外乎 更新、移除、新增 ,可以通过 唯一ID 区分节点

无论是JSX格式还是React.createElement创建的React组件最终都会转化为Virtual DOM,最终会根据层级生成相应的Virtual DOM树形结构。React 15 每次更新会成新的Virtual DOM,然后通 递归 的方式对比新旧Virtual DOM的差异,得到对比后的"更新补丁",最后映射到真实的DOM上。React 16 的具体流程后续会分析到

源码分析

React源码非常多,而且16以后的源码一直在调整,目前Github上最新源码都是保留xxx.new.jsxxx.old.js两份代码。react源码 是采用Monorepo结构来进行管理的,不同的功能分在不同的package里,唯一的坏处可能就是方法地址索引起来不是很方便,如果不是对源码比较熟悉的话,某个功能点可能需要通过关键字全局查询然后去一个个排查。开始之前,可以先阅读下官方的这份阅读指南

相关参考视频讲解:进入学习

因为源码实在是太多太复杂了,所有我这里尽可能的最大到小,从面到点的一个个分析。大致的流程如下:

  1. 首先得知道通过JSX或者createElement编码的代码到底会转成啥
  2. 然后分析应用的入口ReactDOM.render
  3. 接着进一步分析setState更新的流程
  4. 最后再具体分析SchedulerReconcilerRenderer的大致流程

触发渲染更新的操作除了ReactDOM.rendersetState外,还有forceUpdate。但是其实是差不多的,最大差异在于forceUpdate不会走shouldComponentUpdate钩子函数。

数据结构

Fiber

开始正式流程分析之前,希望你对Fiber有过一定的了解。如果没有,建议你先看看这则视频。然后,先来熟悉下ReactFiber的大概结构。

export type Fiber = {
   
    // 任务类型信息;
    // 比如ClassComponent、FunctionComponent、ContextProvider
    tag: WorkTag,
    key: null | string,
    // reactElement.type的值,用于reconciliation期间的保留标识。
    elementType: any,
    // fiber关联的function/class
    type: any,
    // any类型!! 一般是指Fiber所对应的真实DOM节点或对应组件的实例
    stateNode: any,
    // 父节点/父组件
    return: Fiber | null,
    // 第一个子节点
    child: Fiber | null,
    // 下一个兄弟节点
    sibling: Fiber | null,
    // 变更状态,比如删除,移动
    effectTag: SideEffectTag,
    // 用于链接新树和旧树;旧->新,新->旧
    alternate: Fiber | null,
    // 开发模式
    mode: TypeOfMode,
    // ...
  };

FiberRoot

每一次通过ReactDom.render渲染的一棵树或者一个应用都会初始化一个对应的FiberRoot对象作为应用的起点。其数据结构如下ReactFiberRoot

type BaseFiberRootProperties = {
   
  // The type of root (legacy, batched, concurrent, etc.)
  tag: RootTag,
  // root节点,ReactDOM.render()的第二个参数
  containerInfo: any,
  // 持久更新会用到。react-dom是整个应用更新,用不到这个
  pendingChildren: any,
  // 当前应用root节点对应的Fiber对象
  current: Fiber,
  // 当前更新对应的过期时间
  finishedExpirationTime: ExpirationTime,
  // 已经完成任务的FiberRoot对象,在commit(提交)阶段只会处理该值对应的任务
  finishedWork: Fiber | null,
  // 树中存在的最旧的未到期时间
  firstPendingTime: ExpirationTime,
  // 挂起任务中的下一个已知到期时间
  nextKnownPendingLevel: ExpirationTime,
  // 树中存在的最新的未到期时间
  lastPingedTime: ExpirationTime,
  // 最新的过期时间
  lastExpiredTime: ExpirationTime,
  // ...
};

Fiber 类型

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // 不确定类型;可能是class或function
export const HostRoot = 3; // 树的根
export const HostPortal = 4; // 一颗子树
export const HostComponent = 5; // 原生节点;根据环境而定,浏览器环境就是div等
export const HostText = 6; // 纯文本节点
export const Fragment = 7;

模式

React 16.13.1版本为止,内置的开发模式有如下几种:

export type TypeOfMode = number;
// 普通模式|Legacy模式,同步渲染,React15-16的生产环境用
export const NoMode = 0b0000;
// 严格模式,用来检测是否存在废弃API(会多次调用渲染阶段生命周期),React16-17开发环境使用
export const StrictMode = 0b0001;
// ConcurrentMode 模式的过渡版本
export const BlockingMode = 0b0010;
// 并发模式,异步渲染,React17的生产环境用
export const ConcurrentMode = 0b0100;
// 性能测试模式,用来检测哪里存在性能问题,React16-17开发环境使用
export const ProfileMode = 0b1000;

本文只分析 ConcurrentMode 模式

JSX与React.createElement

先来看一个最简单的JSX格式编码的组件,这里借助babel进行代码转换,代码看这

// JSX
class App extends React.Component {
   
    render() {
   
        return <div />
    }
}

// babel
var App = /*#__PURE__*/function (_React$Component) {
   
    _inherits(App, _React$Component);

    var _super = _createSuper(App);

    function App() {
   
        _classCallCheck(this, App);

        return _super.apply(this, arguments);
    }

    _createClass(App, [{
   
        key: "render",
        value: function render() {
   
            return /*#__PURE__*/React.createElement("div", null);
        }
    }]);

    return App;
}(React.Component);

关键点在于render方法实际上是调用了React.createElement方法。那么接下来我们只需要分析createElement做了啥即可。我们先看看ReactElement的结构:

let REACT_ELEMENT_TYPE = 0xeac7;
if (typeof Symbol === 'function' && Symbol.for) {
   
    REACT_ELEMENT_TYPE = Symbol.for('react.element');
}

const ReactElement = function (type, key, ref, props) {
   
    const element = {
   
        // 唯一地标识为React Element,防止XSS,JSON里不能存Symbol
        ?typeof: REACT_ELEMENT_TYPE,

        type: type,
        key: key,
        ref: ref,
        props: props,
    }
    return element;
}

很简单的一个数据结构,每个属性的作用都一目了然,就不一一解释了。然后分析React.createElement源码。

防XSS攻击

如果你不清楚XSS攻击,建议先读这篇文章如何防止XSS攻击?。
首先我们编码的组件都会转化为ReactElement的对象。DOM的操作和产生都是有Js脚本产生的。从根本上杜绝了三种XSS攻击(你思品)。

但是React提供了dangerouslySetInnerHTML来作为innerHTML的替代方案。假如某种场景下,接口给了我JSON格式的数据。我需要展示在一个div中。如果被攻击者拦截到了,并将JSON替换为一段ReactElement格式的结构。那么会发生什么呢?

我这里写了一个demo,当去掉?typeof会发现会报错。而Symbol无法JSON化的,因此外部也是无法利用dangerouslySetInnerHTML进行攻击的。具体检测的源码看这里

const hasOwnProperty = Object.prototype.hasOwnProperty;
const RESERVED_PROPS = {
   
    key: true,
    ref: true,
    __self: true,
    __source: true,
};

function createElement(type, config, children) {
   
    let propName;

    // Reserved names are extracted
    const props = {
   };

    let key = null;
    let ref = null;

    if (config !== null) {
   
        if (hasValidRef(config)) {
   
            ref = config.ref;
        }
        if (hasValidKey(config)) {
   
            key = '' + config.key;
        }
    }

    // 过滤React保留的关键字
    for (propName in config) {
   
        if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
   
            props[propName] = config[propName];
        }
    }

    // 遍历children
    const childrenLength = arguments.length - 2;
    if (childrenLength === 1) {
   
        props.children = children;
    } else if (childrenLength > 1) {
   
        const childArray = Array(childrenLength);
        for (let i = 0; i < childrenLength; i++) {
   
            childArray[i] = arguments[i + 2];
        }
        props.children = childArray;
    }

    // 设置默认props
    if (type && type.defaultProps) {
   
        const defaultProps = type.defaultProps;
        for (propName in defaultProps) {
   
            if (props[propName] === undefined) {
   
                props[propName] = defaultProps[propName];
            }
        }
    }

    return ReactElement(type, key, ref, props);
}

注释应该已经够清楚了哈。总结下来就是根据参数来生成一个ReactElement对象,并绑定对应的propskeyref等;

render流程

ReactDOM.render使用参考这里

一般来说,使用React编写应用,ReactDOM.render是我们触发的第一个函数。那么我们先从ReactDOM.render这个入口函数开始分析render的整个流程。

源码中会频繁出现针对hydrate的逻辑判断和处理。这个是跟SSR结合客户端渲染相关,不会做过多分析。源码部分我都会进行省略

ReactDOM.render实际上对ReactDOMLegacy里的render方法的引用,精简后的逻辑如下:

export function render(
    // React.creatElement的产物
    element: React$Element<any>,    container: Container,    callback: ?Function,
) {
   
    return legacyRenderSubtreeIntoContainer(
        null,
        element,
        container,
        false,
        callback,
    );
}

实际上调用的是legacyRenderSubtreeIntoContainer方法,再来看看这个咯

function legacyRenderSubtreeIntoContainer(
    parentComponent: ?React$Component<any, any>, // 一般为null
    children: ReactNodeList,    container: Container,    forceHydrate: boolean,    callback: ?Function,
) {
   

    let root: RootType = (container._reactRootContainer: any);
    let fiberRoot;
    if (!root) {
   
        // [Q]: 初始化容器。清空容器内的节点,并创建FiberRoot
        root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
            container,
            forceHydrate,
        );
        // FiberRoot; 应用的起点
        fiberRoot = root._internalRoot;
        if (typeof callback === 'function') {
   
            const originalCallback = callback;
            callback = function () {
   
                const instance = getPublicRootInstance(fiberRoot);
                originalCallback.call(instance);
            };
        }
        // [Q]: 初始化不能批量处理,即同步更新
        unbatchedUpdates(() => {
   
            updateContainer(children, fiberRoot, parentComponent, callback);
        });
    } else {
   
        // 省略... 跟上面类似,差别是无需初始化容器和可批处理
        // [Q]:咦? unbatchedUpdates 有啥奥秘呢
        updateContainer(children, fiberRoot, parentComponent, callback);
    }
    return getPublicRootInstance(fiberRoot);
}

根据官网的使用文档可知,在这一步会先清空容器里现有的节点,如果有异步回调callback会先保存起来,并绑定对应FiberRoot引用关系,以用于后续传递正确的根节点。注释里我标注了两个[Q]代表两个问题。我们先来仔细分析这两个问题

初始化

从命名上看,legacyCreateRootFromDOMContainer是用来初始化根节点的。
legacyCreateRootFromDOMContainer的返回结果赋值给container._reactRootContainer,而_reactRootContainer从代码上看是作为是否已经初始化的依据,也验证了这一点。不信的话,打开你的React应用,查看下容器元素的_reactRootContainer属性

function legacyCreateRootFromDOMContainer(
  container: Container,  forceHydrate: boolean,
): RootType {
   
  // 省略 hydrate ...
  return createLegacyRoot(container, undefined);
}

export function createLegacyRoot(
  container: Container,  options?: RootOptions,
): RootType {
   
  return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}

function ReactDOMBlockingRoot(
  container: Container,  tag: RootTag,  options: void | RootOptions,
) {
   
  // !!! look here
  this._internalRoot = createRootImpl(container, tag, options);
}

一连串的函数调用,其实就是还回了一个ReactDOMBlockingRoot实例。其中重点在于属性_internalRoot是通过createRootImpl创建的产物。

function createRootImpl(
  container: Container,  tag: RootTag,  options: void | RootOptions,
) {
   
  // 省略 hydrate ...
  const root = createContainer(container, tag, hydrate, hydrationCallbacks);
  // 省略 hydrate ...
  return root;
}

export function createContainer(
  containerInfo: Container,  tag: RootTag,  hydrate: boolean,  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
   
  return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}

export function createFiberRoot(
  containerInfo: any,  tag: RootTag,  hydrate: boolean,  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
   
  // 生成 FiberRoot
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
  if (enableSuspenseCallback) {
   
    root.hydrationCallbacks = hydrationCallbacks;
  }

  // 为Root生成Fiber对象
  const uninitializedFiber = createHostRootFiber(tag);
  // 绑定 FiberRoot 与 Fiber 
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深入React源码揭开渲染更新流程的面纱 的相关文章

随机推荐

  • 网络与信息安全基础知识--网络安全

    说在前面 本系列文章专注于软考备考复习内容梳理 文章内容是对教材中知识点和考点的提炼 备考过程中可以有针对的进行复习 减少阅读量 有的放矢 导航目录 一 网络安全概述 二 网络的信息安全 1 信息的存储安全 2 信息的传输安全 三 防火墙技
  • Mac安装Netcat教程

    Netcat可以用于测试通信连接 Mac安装Netcat方式 打开终端输入 brew install netcat 安装好以后测试 输入 nc 可以看到是这样的 itzhuzhu itzhuzhudeMacBook Pro brew ins
  • 五、【服务器】基本概念-1

    服务器标准 ATCA AdvancedTelecom Computing Architecture 国际标准 ATCA脱胎于在电信 航天 工业控制 医疗器械 智能交通 军事装备等领域应用广泛的新一代主流工业计算技术 CompactPCI标准
  • 网络系统实现技术之IPX与SPX

    IPX SPX Novell NetWare网络 Novell公司为适应网络发展 将主机网络转换为PC网络 开发了Novell NetWare网络系统 该系统中基于客户机 服务器模式 以普通PC机做为客户机 以性能强大的服务器做为服务器 为
  • 什么是算法?

    什么是算法 当人们提到 算法 一词 往往就会把它们当成专属于 人工智能 的范畴 很多专业的计算机人士也是 提起算法就头疼 不知道如何学习算法 慢慢的对算法就会失去兴趣 算法不仅仅是计算机行业特有的 在我们的生活中也处处存在着算法 算法是专注
  • 【积跬步以至千里】Windows无法访问指定设备,路径或文件,您可能没有合适的权限访问

    一 问题描述 今天在使用电脑时突然出现如下状况 然后我打开用户权限发现了原来是权限的问题 我点击编辑 依然不管事 那怎么处理呢 二 解决办法 1 方法一 单独设置 1 在无法打开的文件 文件夹上单击鼠标右键 选择 属性 2 切换到 安全 选
  • __int64、ULONGLONG格式化输出

    Tips 打印日志信息的时候出现的问题 虽然很小 也算提个醒 Code 不考虑溢出的情况 ULONGLONG n1 100 printf d n n1 int64 n2 100 printf d n n2 此处是个坑 printf d d
  • 华为推出手机系统云翻新服务:什么是云翻新?如何使用?

    华为手机系统云翻新是华为推出的一项功能 旨在通过云服务提供系统翻新的服务 它可以帮助用户对手机的系统进行优化和更新 以提高手机的性能和流畅度 具体而言 华为手机系统云翻新功能提供了免费的云空间 用户可以将手机中的系统数据备份到云端 并进行系
  • 一位程序员使用M1 Mac的感受

    作为一个window的java开发者 虽然现在window高配置不卡 但是身边的高级开发者都是使用苹果开发 并且给予高度评价 这里也抱着学习的态度去尝试安利一台MAC作为开发 所以去苹果官网看了一下 但是这次苹果出了一个全新的M1芯片 我在
  • STM32网络通信Web Server中SSI和CGI的应用

    介绍 最近由于项目功能需要 开始研究STM32 WebServer通信以及SSI和CGI应用方法 项目结束后 主要总结浏览器与STM32之间进行通行 STM32作为服务器而浏览器做为客户端进行通行 文件介绍 此部分的代码是根据ST官方的We
  • 免费分享一套 SpringBoot + Vue的排课/选课管理系统,挺漂亮的

    大家好 我是锋哥 看到一个不错的SpringBoot Vue 的排课 选课管理系统 分享下哈 项目介绍 近年来 随着网络学校规模的逐渐增大 人工书写数据已经不能够处理如此庞大的数据 为了更好的适应信息时代的高效性 一个利用计算机来实现学生信
  • Python——requests

    requests是python实现的简单易用的HTTP库 使用起来比urllib简洁很多 因为是第三方库 所以使用前需要cmd安装 pip install requests 安装完成后import一下 正常则说明可以开始使用了 基本用法 r
  • c/c++ 计算字符数组/字符串长度

    1 自定义函数求长度 2 使用strlen 函数 3 使用sizeof 操作符 4 使用length 函数 利用自定义函数的方法 int cont str char s int i 0 while str i 0 return i 利用st
  • 已解决【partially initialized module ‘cv2‘ has no attribute ‘gapi_wip_gst_GStreamerPipeline‘】

    已解决 partially initialized module cv2 has no attribute gapi wip gst GStreamerPipeline 在尝试了几乎所有网上能找到的办法之后 本来已经放弃了 但是过了几天抱着
  • VAN:Visual Attention Network

    Visual Attention Network Submitted on 20 Feb 2022 v1 last revised 11 Jul 2022 this version v5 Computer Vision and Patter
  • 微服务zipkin与turbine同时使用遇到的问题

    最近整合zipkin的时候遇到的问题 如果打开turbine监控时 每个turbine刷新周期内都会有rxjava的调用被zipkin捕获到 由于zipkin中的数据是通过Spring cloud sleuth上传的 查阅https clo
  • 数字化转型下数据库面临的12个挑战

    数字化及数字化转型 是近些年来非常火热的话题 本文将从这一角度切入 谈谈数字化场景下对数据库发展趋势带来的影响 1 数据 数字化 数字化转型 数据 是数字化实现的新引擎 数据是企业开展数字化创新和构建企业数字化基因的核心要素 通过对于服务对
  • React--井字棋小游戏

    安装较新版本的node js 这里使用的是v15 0 1 用以记录React学习笔记 1 搭建本地开发环境 在想要创建项目的文件夹下输入cmd 回车 输入命令npx create react app my app等待项目初始化 等待一段时间
  • 【虾说区块链】4个概念解析区块链

    欢迎收听 虾说区块链 现在区块链这个概念在互联网上相当火热 这里简单做一个普及 不涉及项目推广投资 单纯地对区块链相关基础知识概念作一个说明讲解 本人区块链技术爱好者 结合相关区块链资料总结整理了 虾说区块链 也是自己一个学习笔记 涉及相关
  • 深入React源码揭开渲染更新流程的面纱

    转前端一年半了 平时接触最多的框架就是React 在熟悉了其用法之后 避免不了想深入了解其实现原理 网上相关源码分析的文章挺多的 但是总感觉不如自己阅读理解来得深刻 于是话了几个周末去了解了一下常用的流程 也是通过这篇文章将自己的个人理解分