React源码解析

React核心源码解读 - 学习笔记 《 1. 配置 React 源码本地调试环境 使用 create-react-app 脚手架创建项目 npx create-react-app react-test 弹射 create-react-app 脚手架内部配置 npm run eject 克隆 react 官方源码 (在项目的根目录下进行克隆) git clone --branch v16.13.1 --depth=1 https://github.com/facebook/react.git src/react 链接本地源码 / 阅读详情

前言

  • 适合有一定 React 项目经验阅读,默认对 React 的常用 api 较为熟悉
  • 研究 React 源码是结合网上的一些分析文章+自己看代码理解
  • 最开始看是因为项目中遇到性能问题,网上没有相关资料,所以想找到具体影响的点
  • 以下的代码解析以 15.4.1 版本为基础,且去除了开发环境的warning,为了区分,保留的注释都为英文,新增的注释为中文,尽量保持原注释
  • 文中有部分自己的演绎、理解、猜测,如有误烦请指出

基础概念

ReactElement

  • 数据类,只包含 props refs key 等
  • 由 React.creatElement(ReactElement.js) 创建,React.createClass 中 render 中返回的实际也是个ReactElement

ReactComponent

  • 控制类,包含组件状态,操作方法等
  • 包括字符组件、原生 DOM 组件、自定义组件(和空组件)
  • 在挂载组件(mountComponent)的时候,会调用到 instantiateReactComponent
    方法,利用工厂模式,通过不同的输入返回不同的 component

代码(instantiateReactComponent.js):

function instantiateReactComponent(node, shouldHaveDebugID) {
var instance;
if (node === null || node === false) {
instance = ReactEmptyComponent.create(instantiateReactComponent);
} else if (typeof node === 'object') {
var element = node;
// Special case string values
if (typeof element.type === 'string') {
  instance = ReactHostComponent.createInternalComponent(element);
} else if (isInternalComponentType(element.type)) {
  // This is temporarily available for custom components that are not string
  // representation, we can drop this code path.
} else {
  instance = new ReactCompositeComponentWrapper(element);
}
} else if (typeof node === 'string' || typeof node === 'number') {
instance = ReactHostComponent.createInstanceForText(node);
} else {
}
// These two fields are used by the DOM and ART diffing algorithms
// respectively. Instead of using expandos on components, we should be
// storing the state needed by the diffing algorithms elsewhere.
instance._mountIndex = 0;
instance._mountImage = null;
return instance;
}
  • ReactDOMTextComponent 只关心文本,ReactDOMComponent 会稍微简单一些,ReactCompositeComponent 需要关心的最多,包括得到原生 DOM 的渲染内容

ReactClass

这个比较特殊,对比 ES5 写法: var MyComponent = React.createClass({}),ES6写法:class MyComponent extends React.Component,为什么用createClass却得到了Component呢?通过源码来看,这两个 api 的实现几乎是一样的,也可以看到,ES6 的写法简洁的多,不用那些getInitialState等特定 api,React 在之后的版本也会抛弃createClass这个 api。并且,在此 api 中,React 进行了autobind。
ReactClass.js:

var ReactClass = {
  createClass: function (spec) {
    // ensure that Constructor.name !== 'Constructor'
    var Constructor = identity(function (props, context, updater) {
      // Wire up auto-binding
      if (this.__reactAutoBindPairs.length) {
        bindAutoBindMethods(this);
      }
      this.props = props;
      this.context = context;
      this.refs = emptyObject;
      this.updater = updater || ReactNoopUpdateQueue;
      this.state = null;
      // ReactClasses doesn't have constructors. Instead, they use the
      // getInitialState and componentWillMount methods for initialization.
      var initialState = this.getInitialState ? this.getInitialState() : null;
      this.state = initialState;
    });
    Constructor.prototype = new ReactClassComponent();
    Constructor.prototype.constructor = Constructor;
    Constructor.prototype.__reactAutoBindPairs = [];
    injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor));
    mixSpecIntoComponent(Constructor, spec);
    // Initialize the defaultProps property after all mixins have been merged.
    if (Constructor.getDefaultProps) {
      Constructor.defaultProps = Constructor.getDefaultProps();
    }
    // Reduce time spent doing lookups by setting these on the prototype.
    for (var methodName in ReactClassInterface) {
      if (!Constructor.prototype[methodName]) {
        Constructor.prototype[methodName] = null;
      }
    }
    return Constructor;
  }
}
var ReactClassComponent = function () {};
_assign(ReactClassComponent.prototype, ReactComponent.prototype, ReactClassMixin);

ReactComponent.js:

function ReactComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}
ReactComponent.prototype.isReactComponent = {};
ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};
ReactComponent.prototype.forceUpdate = function (callback) {
  this.updater.enqueueForceUpdate(this);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'forceUpdate');
  }
};

对象池

  • 开辟空间是需要一定代价的
  • 如果引用释放而进入 gc,gc 会比较消耗性能和时间,如果内存抖动(大量的对象被创建又在短时间内马上被释放)而频繁 gc 则会影响用户体验
  • 既然创建和销毁对象是很耗时的,所以要尽可能减少创建和销毁对象的次数
  • 使用时候申请(getPooled)和释放(release)成对出现,使用一个对象后一定要释放还给池子(释放时候要对内部变量置空方便下次使用)

代码(PooledClass.js):

// 只展示部分
var oneArgumentPooler = function (copyFieldsFrom) {
  var Klass = this;
  if (Klass.instancePool.length) {
    var instance = Klass.instancePool.pop();
    Klass.call(instance, copyFieldsFrom);
    return instance;
  } else {
    return new Klass(copyFieldsFrom);
  }
};
var standardReleaser = function (instance) {
  var Klass = this;
  if (Klass.instancePool.length < Klass.poolSize) {
    Klass.instancePool.push(instance);
  }
};
var DEFAULT_POOL_SIZE = 10;
var DEFAULT_POOLER = oneArgumentPooler;
var addPoolingTo = function (CopyConstructor, pooler) {
  // Casting as any so that flow ignores the actual implementation and trusts
  // it to match the type we declared
  var NewKlass = CopyConstructor;
  NewKlass.instancePool = [];
  NewKlass.getPooled = pooler || DEFAULT_POOLER;
  if (!NewKlass.poolSize) {
    NewKlass.poolSize = DEFAULT_POOL_SIZE;
  }
  NewKlass.release = standardReleaser;
  return NewKlass;
};
var PooledClass = {
  addPoolingTo: addPoolingTo,
  oneArgumentPooler: oneArgumentPooler,
  twoArgumentPooler: twoArgumentPooler,
  threeArgumentPooler: threeArgumentPooler,
  fourArgumentPooler: fourArgumentPooler,
  fiveArgumentPooler: fiveArgumentPooler
};
module.exports = PooledClass;

使用例子(ReactUpdate.js):

var transaction = ReactUpdatesFlushTransaction.getPooled();
destructor: function () {
    this.dirtyComponentsLength = null;
    CallbackQueue.release(this.callbackQueue);
    this.callbackQueue = null;
    ReactUpdates.ReactReconcileTransaction.release(this.reconcileTransaction);
    this.reconcileTransaction = null;
  }
ReactUpdatesFlushTransaction.release(transaction);
  • 可以看到,如果短时间内生成了大量的对象占满了池子,后续的对象是不能复用只能新建的
  • 对比连接池、线程池:完成任务后并不销毁,而是可以复用去执行其他任务

事务机制

  • React 通过事务机制来完成一些特定操作,比如 merge state,update component

示意图(Transaction.js):

这里写图片描述

代码(Transaction.js):

var TransactionImpl = {
  perform: function (method, scope, a, b, c, d, e, f) {
    var errorThrown;
    var ret;
    try {
      this._isInTransaction = true;
      // Catching errors makes debugging more difficult, so we start with
      // errorThrown set to true before setting it to false after calling
      // close -- if it's still set to true in the finally block, it means
      // one of these calls threw.
      errorThrown = true;
      this.initializeAll(0);
      ret = method.call(scope, a, b, c, d, e, f);
      errorThrown = false;
    } finally {
      try {
        if (errorThrown) {
          // If `method` throws, prefer to show that stack trace over any thrown
          // by invoking `closeAll`.
          try {
            this.closeAll(0);
          } catch (err) {}
        } else {
          // Since `method` didn't throw, we don't want to silence the exception
          // here.
          this.closeAll(0);
        }
      } finally {
        this._isInTransaction = false;
      }
    }
    return ret;
  },
  // 执行所有 wrapper 中的 initialize 方法
  initializeAll: function (startIndex) {
  },
  // 执行所有 wrapper 中的 close 方法
  closeAll: function (startIndex) {
  }
};
module.exports = TransactionImpl;

可以看到和后端的事务是有差异的(有点类似AOP),虽然都叫transaction,并没有commit,而是自动执行,初始方法没有提供rollback,有二次封装提供的(ReactReconcileTransaction.js)
下文会提到事务机制的具体使用场景

事件分发

  • 框图(ReactBrowserEventEmitter.js)
    这里写图片描述

  • 组件上声明的事件最终绑定到了 document 上,而不是 React 组件对应的 DOM 节点,这样简化了 DOM 原生事件,减少了内存开销

  • 以队列的方式,从触发事件的组件向父组件回溯,调用相应 callback,也就是 React 自身实现了一套事件冒泡机制,虽然 React对合成事件封装了stopPropagation,但是并不能阻止自己手动绑定的原生事件的冒泡,所以项目中要避免手动绑定原生事件

  • 使用对象池来管理合成事件对象的创建和销毁,好处在上文中有描述

  • ReactEventListener:负责事件注册和事件分发

  • ReactEventEmitter:负责事件执行

  • EventPluginHub:负责事件的存储,具体存储在listenerBank

  • Plugin: 根据不同的事件类型,构造不同的合成事件,可以连接原生事件和组件

  • 当事件触发时,会调用ReactEventListener.dispatchEvent,进行分发:找到具体的 ReactComponent,然后向上遍历父组件,实现冒泡

  • 代码较多,就不具体分析了,这种统一收集然后分发的思路,可以用在具体项目中

生命周期

整体流程:
这里写图片描述

  • 主要讲述mount和update,里面也有很多相类似的操作
  • componentWillMount,render,componentDidMount 都是在 mountComponent 中被调用

分析 ReactCompositeComponent.js 中的mountComponent,发现输出是return {?string} Rendered markup to be inserted into the DOM.

mountComponent: function (transaction, hostParent, hostContainerInfo, context) {
    var _this = this;
    this._context = context;
    this._mountOrder = nextMountID++;
    this._hostParent = hostParent;
    this._hostContainerInfo = hostContainerInfo;
    var publicProps = this._currentElement.props;
    var publicContext = this._processContext(context);
    var Component = this._currentElement.type;
    var updateQueue = transaction.getUpdateQueue();
    // Initialize the public class
    var doConstruct = shouldConstruct(Component);
    // 最终会调用 new Component()
    var inst = this._constructComponent(doConstruct, publicProps, publicContext, updateQueue);
    var renderedElement;
    // Support functional components
    if (!doConstruct && (inst == null || inst.render == null)) {
      renderedElement = inst;
      inst = new StatelessComponent(Component);
      this._compositeType = CompositeTypes.StatelessFunctional;
    } else {
      // 大家经常在用户端用到的 PureComponent,会对 state 进行浅比较然后决定是否执行 render
      if (isPureComponent(Component)) {
        this._compositeType = CompositeTypes.PureClass;
      } else {
        this._compositeType = CompositeTypes.ImpureClass;
      }
    }
    // These should be set up in the constructor, but as a convenience for
    // simpler class abstractions, we set them up after the fact.
    inst.props = publicProps;
    inst.context = publicContext;
    inst.refs = emptyObject;
    inst.updater = updateQueue;
    this._instance = inst;
    // Store a reference from the instance back to the internal representation
    // 以 element 为 key,存在了 Map 中,之后会用到
    ReactInstanceMap.set(inst, this);
    var initialState = inst.state;
    if (initialState === undefined) {
      inst.state = initialState = null;
    }
    this._pendingStateQueue = null;
    this._pendingReplaceState = false;
    this._pendingForceUpdate = false;
    var markup;
    if (inst.unstable_handleError) {
      markup = this.performInitialMountWithErrorHandling(renderedElement, hostParent, hostContainerInfo, transaction, context);
    } else {
      markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context);
    }
    if (inst.componentDidMount) {
       transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
    }
    return markup;
  }
function shouldConstruct(Component) {
  return !!(Component.prototype && Component.prototype.isReactComponent);
}
可以看到,mountComponet 先做实例对象的初始化(props, state 等),然后调用performInitialMount挂载(performInitialMountWithErrorHandling最终也会调用performInitialMount,只是多了错误处理),然后调用componentDidMount
transaction.getReactMountReady()会得到CallbackQueue,所以只是加入到队列中,后续执行
我们来看performInitialMount(依然在 ReactCompositeComponent.js 中)
performInitialMount: function (renderedElement, hostParent, hostContainerInfo, transaction, context) {
    var inst = this._instance;
    var debugID = 0;
    if (inst.componentWillMount) {
      inst.componentWillMount();
      // When mounting, calls to `setState` by `componentWillMount` will set
      // `this._pendingStateQueue` without triggering a re-render.
      if (this._pendingStateQueue) {
        inst.state = this._processPendingState(inst.props, inst.context);
      }
    }
    // If not a stateless component, we now render
    // 返回 ReactElement,这也就是上文说的 render 返回 ReactElement
    if (renderedElement === undefined) {
      renderedElement = this._renderValidatedComponent();
    }
    var nodeType = ReactNodeTypes.getType(renderedElement);
    this._renderedNodeType = nodeType;
    var child = this._instantiateReactComponent(renderedElement, nodeType !== ReactNodeTypes.EMPTY);
    this._renderedComponent = child;
    var markup = ReactReconciler.mountComponent(child, transaction, hostParent, hostContainerInfo, this._processChildContext(context), debugID);
    return markup;
  }
  • performInitialMount 中先调用componentWillMount,这个过程中 merge state,然后调用_renderValidatedComponent(最终会调用inst.render() )返回 ReactElement,然后调用_instantiateReactComponent 由 ReactElement 创建 ReactComponent,最后进行递归渲染。

  • 挂载之后,可以通过setState来更新(机制较为复杂,后文会单独分析),此过程通过调用updateComponent来完成更新。我们来看updateComponent(依然在 ReactCompositeComponent.js 中)

updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {
    var inst = this._instance;
    var willReceive = false;
    var nextContext;
    // context 相关,React 建议少用 context
    // Determine if the context has changed or not
    if (this._context === nextUnmaskedContext) {
      nextContext = inst.context;
    } else {
      nextContext = this._processContext(nextUnmaskedContext);
      willReceive = true;
    }
    var prevProps = prevParentElement.props;
    var nextProps = nextParentElement.props;
    // Not a simple state update but a props update
    if (prevParentElement !== nextParentElement) {
      willReceive = true;
    }
    // An update here will schedule an update but immediately set
    // _pendingStateQueue which will ensure that any state updates gets
    // immediately reconciled instead of waiting for the next batch.
    if (willReceive && inst.componentWillReceiveProps) {
      inst.componentWillReceiveProps(nextProps, nextContext);
    }
    var nextState = this._processPendingState(nextProps, nextContext);
    var shouldUpdate = true;
    if (!this._pendingForceUpdate) {
      if (inst.shouldComponentUpdate) {
        shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
      } else {
        if (this._compositeType === CompositeTypes.PureClass) {
          // 这里,就是上文提到的,PureComponent 里的浅比较
          shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState);
        }
      }
    }
    this._updateBatchNumber = null;
    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      // Will set `this.props`, `this.state` and `this.context`.
      this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext);
    } else {
      // If it's determined that a component should not update, we still want
      // to set props and state but we shortcut the rest of the update.
      this._currentElement = nextParentElement;
      this._context = nextUnmaskedContext;
      inst.props = nextProps;
      inst.state = nextState;
      inst.context = nextContext;
    }
  }
  • updateComponent中,先调用componentWillReceiveProps,然后 merge state,然后调用shouldComponentUpdate判断是否需要更新,可以看到,如果组件内部没有自定义,且用的是 PureComponent,会对 state 进行浅比较,设置shouldUpdate,最终调用_performComponentUpdate来进行更新。而在_performComponentUpdate中,会先调用componentWillUpdate,然后调用updateRenderedComponent进行更新,最后调用componentDidUpdate(过程较简单,就不列代码了)。下面看一下updateRenderedComponent的更新机制(依然在 ReactCompositeComponent.js 中)
_updateRenderedComponent: function (transaction, context) {
    var prevComponentInstance = this._renderedComponent;
    var prevRenderedElement = prevComponentInstance._currentElement;
    var nextRenderedElement = this._renderValidatedComponent();
    var debugID = 0;
    if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
      ReactReconciler.receiveComponent(prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context));
    } else {
      var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
      ReactReconciler.unmountComponent(prevComponentInstance, false);
      var nodeType = ReactNodeTypes.getType(nextRenderedElement);
      this._renderedNodeType = nodeType;
      var child = this._instantiateReactComponent(nextRenderedElement, nodeType !== ReactNodeTypes.EMPTY);
      this._renderedComponent = child;
      var nextMarkup = ReactReconciler.mountComponent(child, transaction, this._hostParent, this._hostContainerInfo, this._processChildContext(context), debugID);
      this._replaceNodeWithMarkup(oldHostNode, nextMarkup, prevComponentInstance);
    }
  },

可以看到,如果需要更新,则调用ReactReconciler.receiveComponent,会递归更新子组件,否则直接卸载然后挂载。所以,重点是在shouldUpdateReactComponent的判断,React 为了简化 diff,所以有一个假设:在组件层级、type、key 不变的时候,才进行比较更新,否则先 unmount 然后重新 mount。来看shouldUpdateReactComponent(shouldUpdateReactComponent.js) :

function shouldUpdateReactComponent(prevElement, nextElement) {
  var prevEmpty = prevElement === null || prevElement === false;
  var nextEmpty = nextElement === null || nextElement === false;
  if (prevEmpty || nextEmpty) {
    return prevEmpty === nextEmpty;
  }
  var prevType = typeof prevElement;
  var nextType = typeof nextElement;
  // 如果前后两次都为文本元素,则更新
  if (prevType === 'string' || prevType === 'number') {
    return nextType === 'string' || nextType === 'number';
  } else {
    // 如果为 ReactDomComponent 或 ReactCompositeComponent,则需要层级 type 和 key 相同,才进行 update(层级在递归中保证相同)
    return nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key;
  }
}

接下来是重头戏:setState,上文中已经提到了此 api 为:

ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

可以看到这里只是简单的调用enqueueSetState放入队列中,而我们知道,不可能这么简单的。来看enqueueSetState(ReactUpdateQueue.js中),this.updater会在 mount 时候赋值为updateQueue

enqueueSetState: function (publicInstance, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
    if (!internalInstance) {
      return;
    }
    // 获取队列,如果为空则创建
    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    // 将待 merge 的 state 放入队列
    queue.push(partialState);
    // 将待更新的组件放入队列
    enqueueUpdate(internalInstance);
  },
  function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
      // 上文提到的以 element 为 key 存入 map,这里可以取到 component
    var internalInstance = ReactInstanceMap.get(publicInstance);
    if (!internalInstance) {
      return null;
    }
    return internalInstance;
  }

再来看enqueueUpdate(ReactUpdates.js):

function enqueueUpdate(component) {
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}
  • 可以看到,如果不处于isBatchingUpdates时,则调用batchingStrategy.batchedUpdates,如果处于的话,则将 component 放入 dirtyComponents 中等待以后处理。这样保证了避免重复 render,因为mountComponent和updateComponent 执行的开始,会将isBatchingUpdates设置为true,之后以事务的方式处理,包括最后时候将isBatchingUpdates置为false。
  • 大家一定对 batchingStrategy 和 dirtyComponents的定义,batchingStrategy由ReactUpdates.injection 注入,而dirtyComponents 是定义在ReactUpdates.js 中,也就是说二者都为全局的
  • 综上,在特定生命周期getInitialState,componentWillMount,render,componentWillUpdate中调用setState,并不会引起updateComponent(componentDidMount、componentDidUpdate 中会)。来看batchedUpdates(ReactDefaultBatchingStrategy.js):
batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      // 注意这里,上一个代码块中可以看到,当 isBatchingUpdates 为 false 时,callback 为 enqueueUpdate 自身
      // 所以即以事务的方式处理
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  }
  var transaction = new ReactDefaultBatchingStrategyTransaction();
  • 可以看到,当以事务的方式调用进入enqueueUpdate时,isBatchingUpdates已经为true,所以执行dirtyComponents.push(component);。
  • 注意到callbakc其实就是自身enqueueUpdate,当isBatchingUpdates为false时,也用transaction.perform调用enqueueUpdate,使得结果一样
  • 详细介绍事务 transaction 的应用,上文中提到过,事务可以利用wrapper封装,开始和结束时会调用所有 wrapper 的相应方法,来看这两个wrapper: RESET_BATCHED_UPDATES FLUSH_BATCHED_UPDATES(ReactDefaultBatchingStrategy.js):
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
// flushBatchedUpdates 在 ReactUpdates.js 中
var flushBatchedUpdates = function () {
  // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
  // asapEnqueued 为提前执行回调,暂不分析
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }
    if (asapEnqueued) {
    }
  }
};
  • 但是,仔细看上面的过程,把组件放入 dirtyComponents 后,事务结束马上就执行 close方法进行了处理了,和之前理解的流程好像不太一致?这时候再回头看mountComponent和updateComponent,它们的参数:@param {ReactReconcileTransaction} transaction,也就是说整个过程都在ReactReconcileTransaction事务中(事件回调同理),自然在其中的生命周期调用setState不用引起重复 render,只会将 state 放入队列和将组件放入 dirtyComponents 中,然后在结束后统一处理
  • ReactReconcileTransaction中 initialize 用于清空回调队列;close 用于触发回调函数componentDidMount、componentDidUpdate 执行
  • 我开始一直比较疑惑的是ReactDefaultBatchingStrategy.batchedUpdates中的ReactDefaultBatchingStrategyTransaction和ReactReconcileTransaction到底是什么关系?我试图找出两个transaction 中 wrapper 是否有 merge 的情况,发现没有。目前大概的理解和结论是这样的:整个生命周期就是一个 transaction,即对应ReactDefaultBatchingStrategy.batchedUpdates,而ReactReconcileTransaction粒度较小,负责单个组件(所以也能看到,前者直接new,而后者利用了对象池)。通过各自 wrapper 可以看到,前者([FLUSH_BATCHED_UPDATES,RESET_BATCHED_UPDATES])负责了全部组件更新 和 callback,后者([SELECTION_RESTORATION, EVENT_SUPPRESSION,ON_DOM_READY_QUEUEING)负责了各自组件自身的问题,如 focus 等。
  • 例证:ReactDom 中调用render(插入过程),实际最终调用了 ReactMount的_renderNewRootComponent,其中执行了ReactUpdates.batchedUpdates(batchedMountComponentIntoNode,componentInstance, container, shouldReuseMarkup, context);(注意出现了batchedUpdates),而batchedMountComponentIntoNode中调用了ReactUpdates.ReactReconcileTransaction.getPooled,这样,嵌套关系就联系起来了
  • 例证: ReactEventListener的dispatchEvent,会调用ReactUpdates.batchedUpdates(handleTopLevelImpl,bookKeeping); 和上述同理
  • 熟悉 React 生命周期的同学一定对父子组件各生命周期的执行顺序很清晰(比如 componentWillMount是从父到子),以上述的理论,是如何保证的么?上文中可以看到,FLUSH_BATCHED_UPDATES的 close方法利调用了runBatchedUpdates,来看这个方法(ReactUpdates.js):
function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;
  // reconcile them before their children by sorting the array.
  dirtyComponents.sort(mountOrderComparator);
  // Any updates enqueued while reconciling must be performed after this entire
  // batch. Otherwise, if dirtyComponents is [A, B] where A has children B and
  // C, B could update twice in a single batch if C's render enqueues an update
  // to B (since B would have already updated, we should skip it, and the only
  // way we can know to do so is by checking the batch counter).
  updateBatchNumber++;
  for (var i = 0; i < len; i++) {
    // If a component is unmounted before pending changes apply, it will still
    // be here, but we assume that it has cleared its _pendingCallbacks and
    // that was is a noop.
    var component = dirtyComponents[i];
    // If performUpdateIfNecessary happens to enqueue any new updates, we
    // shouldn't execute the callbacks until the next render happens, so
    // stash the callbacks first
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;
    ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction, updateBatchNumber);
    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
      }
    }
  }
}
function mountOrderComparator(c1, c2) {
  return c1._mountOrder - c2._mountOrder;
}
  • flushBatchedUpdates在事务ReactUpdatesFlushTransaction中,此事务是对ReactReconcileTransaction和CallbackQueue的封装,结束时置空dirtyComponents 并通知回调
  • performUpdateIfNecessary最终会调用updateComponent,进行更新

diff 算法

  • 传统对于树的 diff 算法,时间复杂度要达到 o(n^3),这对于用户端显然是不能接受的。而 React基于几个基础假设,将时间复杂度优化为 o(n)
  • 假设(策略)
  • Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计
  • 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
  • 对于同一层级的一组子节点,它们可以通过唯一 id进行区分

场景

  • tree diff:只对比同层级节点(注意前文中所有代码中,都是只比较prevRenderedElement和nextRenderedElement)
  • component diff: 如果类型相同则继续比较,如果类型不同则直接卸载再挂载,即上文中提到的shouldUpdateReactComponent(虽然当两个
    component 是不同类型但结构相似时,React diff 会影响性能,但正如 React 官方博客所言:不同类型的component 是很少存在相似 DOM tree 的机会,因此为这种极端情况而做太多比较是不值得的)
  • element diff: 当一组节点处于同一层级时,React 对于每个节点提供了三种操作,分别为INSERT_MARKUP(插入)、MOVE_EXISTING(移动)、 REMOVE_NODE(删除)
  • 上文的代码中,除了关心 type,还关心 key,这也是 diff 算法的关键,如图
    这里写图片描述

  • 首先对新集合的节点进行循环遍历,for (name in nextChildren),如果存在相同节点,则进行操作,是否移动是通过比较child._mountIndex < lastIndex,符合则进行节点移动操作(即在老集合中的位置和 lastIndex 比较),lastIndex 表示访问过的节点在老集合中最右的位置(即最大的位置)。这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置,如果新集合中当前访问的节点比 lastIndex大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比lastIndex 小时,才需要进行移动操作。来看具体过程:

  • 从新集合中取得 B,判断老集合中存在相同节点 B,通过对比节点位置判断是否进行移动操作,B 在老集合中的位置 B._mountIndex = 1,此时 lastIndex = 0,不满足 child._mountIndex < lastIndex 的条件,因此不对 B 进行移动操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),其中
    prevChild._mountIndex 表示 B 在老集合中的位置,则 lastIndex = 1,并将 B的位置更新为新集合中的位置prevChild._mountIndex = nextIndex,此时新集合中 B._mountIndex = 0,nextIndex++ 进入下一个节点的判断
  • 从新集合中取得 A,判断老集合中存在相同节点 A,通过对比节点位置判断是否进行移动操作,A 在老集合中的位置 A._mountIndex = 0,此时 lastIndex = 1,满足 child._mountIndex < lastIndex的条件,因此对 A 进行移动操作 enqueueMove(this, child._mountIndex, toIndex),其中 toIndex 其实就是nextIndex,表示 A 需要移动到的位置;更新 lastIndex =Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 1,并将 A的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中A._mountIndex = 1,nextIndex++ 进入下一个节点的判断。
  • 从新集合中取得 D,判断老集合中存在相同节点 D,通过对比节点位置判断是否进行移动操作,D 在老集合中的位置 D._mountIndex= 3,此时 lastIndex = 1,不满足 child._mountIndex < lastIndex的条件,因此不对 D 进行移动操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则lastIndex = 3,并将 D 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中D._mountIndex = 2,nextIndex++ 进入下一个节点的判断。
  • 从新集合中取得 C,判断老集合中存在相同节点 C,通过对比节点位置判断是否进行移动操作,C 在老集合中的位置 C._mountIndex= 2,此时 lastIndex = 3,满足 child._mountIndex < lastIndex 的条件,因此对 C 进行移动操作 enqueueMove(this, child._mountIndex, toIndex);更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 3,并将 C的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中 C._mountIndex= 3,nextIndex++ 进入下一个节点的判断,由于 C 已经是最后一个节点,因此 diff 到此完成。
  • 当有新的 Component 插入时,逻辑一致,不做具体分析了
  • 当完成集合中所有节点 diff,还需要遍历老集合,如果存在新集合中没有但老集合中有的节点,则删除

代码(ReactMultiChild.js),针对 element diff(tree diff 和 component diff 在之前的代码中已经提到过):

_updateChildren: function (nextNestedChildrenElements, transaction, context) {
      var prevChildren = this._renderedChildren;
      var removedNodes = {};
      var mountImages = [];
      var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements, mountImages, removedNodes, transaction, context);
      if (!nextChildren && !prevChildren) {
        return;
      }
      var updates = null;
      var name;
      // `nextIndex` will increment for each child in `nextChildren`, but
      // `lastIndex` will be the last index visited in `prevChildren`.
      var nextIndex = 0;
      var lastIndex = 0;
      // `nextMountIndex` will increment for each newly mounted child.
      var nextMountIndex = 0;
      var lastPlacedNode = null;
      for (name in nextChildren) {
        if (!nextChildren.hasOwnProperty(name)) {
          continue;
        }
        var prevChild = prevChildren && prevChildren[name];
        var nextChild = nextChildren[name];
        if (prevChild === nextChild) {
          updates = enqueue(updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex));
          lastIndex = Math.max(prevChild._mountIndex, lastIndex);
          prevChild._mountIndex = nextIndex;
        } else {
          if (prevChild) {
            // Update `lastIndex` before `_mountIndex` gets unset by unmounting.
            lastIndex = Math.max(prevChild._mountIndex, lastIndex);
            // The `removedNodes` loop below will actually remove the child.
          }
          // The child must be instantiated before it's mounted.
          updates = enqueue(updates, this._mountChildAtIndex(nextChild, mountImages[nextMountIndex], lastPlacedNode, nextIndex, transaction, context));
          nextMountIndex++;
        }
        nextIndex++;
        lastPlacedNode = ReactReconciler.getHostNode(nextChild);
      }
      // Remove children that are no longer present.
      for (name in removedNodes) {
        if (removedNodes.hasOwnProperty(name)) {
          updates = enqueue(updates, this._unmountChild(prevChildren[name], removedNodes[name]));
        }
      }
      if (updates) {
        processQueue(this, updates);
      }
      this._renderedChildren = nextChildren;
    },

综上,在开发中,保持稳定的结构有助于性能提升,当有一组节点时,除了要设置 key,也要避免将靠后的节点移动到靠前的位置

一些其他的点

interface(ReactClass.js)

var ReactClassInterface = {
  mixins: 'DEFINE_MANY',
  statics: 'DEFINE_MANY',
  propTypes: 'DEFINE_MANY',
  contextTypes: 'DEFINE_MANY',
  childContextTypes: 'DEFINE_MANY',
  // ==== Definition methods ====
  getDefaultProps: 'DEFINE_MANY_MERGED',
  getInitialState: 'DEFINE_MANY_MERGED',
  getChildContext: 'DEFINE_MANY_MERGED',
  render: 'DEFINE_ONCE',
  // ==== Delegate methods ====
  componentWillMount: 'DEFINE_MANY',
  componentDidMount: 'DEFINE_MANY',
  componentWillReceiveProps: 'DEFINE_MANY',
  shouldComponentUpdate: 'DEFINE_ONCE',
  componentWillUpdate: 'DEFINE_MANY',
  componentDidUpdate: 'DEFINE_MANY',
  componentWillUnmount: 'DEFINE_MANY',
  // ==== Advanced methods ====
  updateComponent: 'OVERRIDE_BASE'
};
function validateMethodOverride(isAlreadyDefined, name) {
  var specPolicy = ReactClassInterface.hasOwnProperty(name) ? ReactClassInterface[name] : null;
  // Disallow overriding of base class methods unless explicitly allowed.
  if (ReactClassMixin.hasOwnProperty(name)) {
    !(specPolicy === 'OVERRIDE_BASE') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to override `%s` from your class specification. Ensure that your method names do not overlap with React methods.', name) : _prodInvariant('73', name) : void 0;
  }
  // Disallow defining methods more than once unless explicitly allowed.
  if (isAlreadyDefined) {
    !(specPolicy === 'DEFINE_MANY' || specPolicy === 'DEFINE_MANY_MERGED') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.', name) : _prodInvariant('74', name) : void 0;
  }
}

可以看到,和后端中interface(或是抽象类)还是有区别的,但是可以起到规范和检查的作用,实际项目中可以借鉴

react源码解析3.react源码架构 react源码解析3.react源码架构 视频讲解(高效学习):进入学习 往期文章: 1.开篇介绍和面试题 2.react的设计理念 3.react源码架构 4.源码目录结构和调试 5.jsx&核心api 6.legacy和concurrent模式入口函数 7.Fiber架构 8.render阶段 9.diff算法 10.commit阶段 11.生命周期 12.状态更新流程 13.hooks源码 14.手写hooks 15.scheduler&Lane 16.concurrent模式 17. 阅读详情

相关推荐

React 源码解析

通过这个简化版实现,我们可以看到React设计的核心智慧:在计算机科学原理与工程实践之间找到平衡点。

qq_50708187的博客 978

react中 keep-alive

react中如何实现 keep-alive 效果

大灰狼的小绵羊哥哥的博客 7552

React进阶之React核心源码解析(一)

cline + deeseek => AI工具,上手成本非常低,非常便宜cursor 编辑器带着问题学源码学习方法论:由大到小 对比回答问题。

qq_34306228的博客 1233

前端做自动化测试 —— 用TDD的思想做组件测试

Test-Driven Development(测试驱动开发,以下简述TDD)是一种设计方法论, 原理是在开发功能代码之前, 先编写单元测试用例代码, 通过测试来推动整个开发的进行.

测试界的彭于晏的博客 1172

K-means算法代码实现(python)

K-means算法代码实现以及解决质心选择问题 计算距离 距离通常使用欧几里得距离来衡量 def euclDistance(vector1, vector2): return np.sqrt(sum((vector2 - vector1) ** 2)) 初始化质心 def initCentroids(data, k): numSamples, dim = data.shape ...

江南小帅瓜的博客 1万+

英飞凌 AURIX Development Studio工程移植到Hightec的方法

1. 软件简介 AURIX™ Development Studio是英飞凌针对AURIX TC2XX/TC3XX系列单片机推出了一款免费的软件,集成了iLLD库,内置Tasking compiler和Tasking Debugger,但这两个模块只能用于非商业用途,可供学习和试用。iLLD库是英飞凌为TC2XX和TC3XX Tricore系列单片机提供的底层驱动库,有完善的使用手册,和较好的移植性。 Hightec是一款可以用来开发Aurix系列单片机的编译器,有免费版,可商用,集成GCC编译器,可用UDE

weixin_42967006的博客 7131

React移动端项目-01

>本项目对标`优快云`、`博客园`等竞品,致力成为更加贴近年轻 IT 从业者的科技资讯类应用 >产品关键词:IT、极客、活力、科技、技术分享、前沿动态、内容社交 >用户特点:年轻有活力,对IT领域前言科技信息充满探索欲和学习热情

m0_73442777的博客 1456

Xilinx XDMA IP核配置实战:AXI Memory Mapped vs Stream选型指南(附PCIe调试技巧)

本文深入解析Xilinx XDMA IP核配置中AXI Memory Mapped与AXI Stream两种DMA接口的核心区别与选型策略。针对PCIe高速数据传输场景,详细对比了二者在数据特性、延迟、硬件资源及软件复杂度上的差异,为FPGA工程师在数据采集、实时处理等项目中做出高效设计决策提供实战指南,并附有关键的PCIe调试技巧。

qsc901234的博客 584

React 项目 黑马极客园

React 项目 黑马极客园

打工人 3964

105. replace( )函数

【目录】

weixin_63986098的博客 1万+

【Maxwell16.0】实战指南:从DXF导入到MotorCAD联合仿真的关键技巧

本文详细解析了Maxwell 16.0与MotorCAD进行联合仿真的核心流程与关键技巧。重点阐述了从DXF模型导入、几何修复、一极一槽模型剪裁,到坐标系对齐、MotorCAD参数匹配及结果验证的全过程,旨在帮助工程师高效打通联合仿真链路,规避常见错误,提升电机设计效率。

weixin_29062865的博客 712

React 源码解析之总览

本小书大部分内容来自作者 Jokcy 的 《React 源码解析》: react.jokcy.me/ 本文已同步在我的博客: ruizhengyun.cn/blog/post/2… 感谢 Jokcy 让我深度了解 React。如他所说,在决定阅读 React 源码时认为不会是一件很难的事,但是真正开始阅读之后才发现,事情没那么简单,因为需要足够的耐心、能够独立思考和静下心来(因为你会碰到之前编码...

weixin_34008933的博客 216

若依(RuoYi)项目里,Quartz定时任务踩坑实录:并发控制、事务回滚与异常处理

本文深入解析若依(RuoYi)项目中Quartz定时任务的并发控制、事务回滚与异常处理三大核心问题。通过实战案例揭示@DisallowConcurrentExecution注解的局限性和Redis分布式锁的应用,详细讲解Spring事务集成方案及多数据源事务管理,并提供生产级监控体系搭建指南,帮助开发者规避Quartz在生产环境中的常见陷阱。

weixin_28731223的博客 407

React核心源码解析

本文对比 React15,以及16.8之后的版本来对 React 核心源码进行解析,包含fiber、双缓存、JSX编译、diff算法等内容。

musio777的博客 1822

【花雕学编程】Arduino ESP32 SPP 之使用Servo库控制360度舵机正反转

使用 Servo 库控制 360 度舵机的正反转是 Arduino ESP32 SPP 应用中的一个重要案例,具有简易控制接口、灵活速度调节和高兼容性等特点,广泛应用于机器人、自动化装置和动态艺术等领域。在第三个案例中,使用map()函数将用户输入的速度参数(范围0-100)映射到舵机控制的角度(0-180),使得用户能够以更人性化的方式控制舵机速度。5、创新:Arduino可以让你用电子的方式来表达你的创意和想象,你可以用Arduino来制作各种有趣和有用的项目,如机器人、智能家居、艺术装置等。

雕爷学编程 1468

什么是数据挖掘?数据挖掘到底在解决什么问题?

本文我将借助5W1H的思想来带你从整体上了解一下数据挖掘。比如什么是数据挖掘为什么要做数据挖掘在哪些场景下用数据挖掘怎么做数据挖掘第一个问题,什么是数据挖掘?这个问题看似很简单,但似乎也很难有一个明确的答案。如果非要给数据挖掘一个定义的话,那么我认为数据挖掘就是寻找数据中隐含的知识,并用于产生商业价值。也就是说它是我们在数据中,尤其是在大量的数据中找到一些有价值,甚至是非常有价值的东西的一种手段。

m0_64336780的博客 1万+

react源码解析

React 或者说框架的意义是为了提高代码的可维护性,而不是为了提高性能的,现在所做的提升性能的操作,只是在可维护性的基础上对性能的优化 react中的vdom实际上就是通过 property来进行一个attributes的一个判断,来看它是否有变化 一、导学 react非常纯粹,核心 api 就是 setState,其它所有的内容都围绕组件化来设计,一切基于组件,没有directive 双向绑定和其它API,如果要改变一个ui,就只能通过 setState来进行修改 思想超前:react在1.

rocktanga的博客 2676

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

ReactJS-AdminLTE项目架构解析:从源码到生产部署

ReactJS-AdminLTE是一个基于ReactJS实现的AdminLTE仪表盘项目,它将经典的AdminLTE后台框架以React组件化的方式重新构建,为开发者提供了现代化的管理界面解决方案。本文将带你深入了解这个项目的架构设计、核心组件构成以及从源码到生产环境的完整部署流程。 ## 项目整体架构概览 ReactJS-AdminLTE采用了清晰的模块化架构,主要分为以下几个核心部分:

gitblog_00033的博客 668

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

reactjs源码解析-createElement

const RESERVED_PROPS = { key: true, ref: true, __self: true, __source: true, }; const ReactElement = function(type, key, ref, self, source, owner, props) { const element = { // 此标记允许我们将其唯一标识为React元素 ...

qq_40344104的博客 174

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

React 源码解析之唠叨两句

前言 本小书大部分内容来自作者 Jokcy 的 《React 源码解析》: react.jokcy.me/ 本文已同步在我的博客: ruizhengyun.cn/blog/post/6… 感谢 Jokcy 让我深度了解 React。如他所说,在决定阅读 React 源码时认为不会是一件很难的事,但是真正开始阅读之后才发现,事情没那么简单,因为需要足够的耐心、能够独立思考和静下心来(因为你会碰到之...

weixin_33755557的博客 134

《深入浅出 React 19:AI 视角下的源码解析与进阶》- react19 源码概览

这是React的基础包,提供了所有React组件的基本API,如 React.createElement、React.Component、React.useState等。React Native的渲染器,负责将React组件渲染为原生移动应用UI组件,将React的声明式编程模型带到了移动应用开发中。在对 React 的工作流程有了整体的把握后,我们再来看看 React 19.2.0 的源码仓库中都包含了哪些包。顾名思义,react核心包是react的核心功能,是react的基础,也是react的核心。

waterwong的博客 1468

react源码解析笔记一

0 前言 本React系列文章是基于React最新的代码仓库进行分析,也许和老版本有些许不同,大佬们可酌情观看。 1 Component && Pure Component Component和PureComponent作为所有学习React开发的人的入口,其对我们来说还是比较重要的,所以本篇文章选择从此处作为切入进行分析。 源码位于:ReactBaseClasses.js文件。 该文件更多的类似于抽象类的作用,对很多进行定义,然后提供给外部调用,真正的细节实现并不在此处。 1.1 Comp

我是新手的博客 302

《React源码解析》系列完结!

前言 距离第一篇《React源码解析(一)》已经过去将近4个月的时间,由于是我第一次进行源码解析相关的写作,思路和文笔还不够成熟。一百多天以来,我基于读者反馈反思这几篇文章中的不足,同时也在不断学习借...

weixin_39037804的博客 1800

React源码解析————ReactChildren.js

React源码解析————ReactChildren.js2021SC@SUDSCReactChildren.jsimport 2021SC@SUDSC 2021SC@SUDSC ReactChildren.js 只要是看过源码的同学都知道,这个js文件一共导出了五个方法: export { forEachChildren as forEach, mapChildren as map, countChildren as count, onlyChild as only, toArray,

m0_51744158的博客 485

React源码深度解析终极指南:Under-the-hood-ReactJS完整学习路线图

想要真正掌握React的内部工作机制吗?Under-the-hood-ReactJS项目为你提供了完整的React源码学习路径!这个开源项目通过可视化流程图详细解析了React的挂载和更新两大核心流程,让复杂的源码变得清晰易懂。 ## 🎯 项目核心价值 Under-the-hood-ReactJS是一个专门为React开发者设计的源码学习项目。它通过精美的视觉图表,将React的内部架构和运

gitblog_01402的博客 1056
上一篇: Reactjs源码分析
下一篇: 百度地图框选标注坐标点功能
.NET跨平台
博客等级 码龄15年 6066粉丝 738原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值