资讯专栏INFORMATION COLUMN

React setState 源码解析

kaka / 2765人阅读

摘要:下面是的源码如其名,是一个队列操作,将要变更的统一插入队列,待一一处理。只有控制下的事件周期,会执行切换状态,保证批量操作能被截获并插入堆栈。也就是源码中的。他在初始被赋值为,也就是以下两个对象,在源码被称作为。本文参考源码版本为。

React setState

不知道什么时候开始,很多人开始认为setState是异步操作,所谓的异步操作,就是我们在执行了setState之后,立即通过this.state.xxx不能拿到更新之后的值。这样的认知其实有一种先入为主的意识,也许是受到很多不知名博主的不科学言论导致的错误认知,也有可能是日常开发过程中积累的经验。毕竟大部分开发写setState这样的方法,都是在组件的生命周期(如componentDidMountcomponentWillMount)中,或者react的事件处理机制中,这种教科书式的写代码方式,基本不会碰到有数据异常。

虽然官方文档对setState这种同步行为语焉不详,但是我们可以发现某些情况下,setState是真的可以同步获取数据的。通过本文我们可以了解react这方面的工作原理,对于我们的思考开发方案,解决疑难问题,避免不必要的错误,也许会有不少帮助。

我们先来说结论:

在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理,componentWillMount等生命周期),调用setState不会同步更新this.state;除此之外的setState调用会同步执行this.state。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。

不想看长篇大论的同学,到这里就可以结束了。想了解原理的同学请继续参观。。

用过angular框架的同学也许记得angular的代码模式中有一个$timeout这样的调用方法,和setTimeout功能基本一致,但是setTimeout却不能实时触发UI的更新。这是因为$timeoutsetTimeout添加了对UI更新(脏检查)的处理,在延时结束后立即调用更新方法更新UI的渲染。同样的道理,我们必须使用react指定的方式更新state才能同步UI的渲染,因为react控制下的事件会同步处理UI的更新。而直接使用this.state.xxx = xxx这样的方式仅仅改变了数据,没有改变UI,这就不是React倡导的reactive programing了。

实际上,在react的源码中我们会发现,大部分react控制下的事件或生命周期,会调用batchedUpdates(查看如下代码)。这个方法会触发component渲染的状态isBatchingUpdates。同样的,react的事件监听机制会触发batchedUpdates方法,同样会将isBatchingUpdates状态置为true。

// 更新状态
batchingStrategy.batchedUpdates(method, component);

在组件渲染状态isBatchingUpdates中,任何的setState都不会触发更新,而是进入队列。除此之外,通过setTimeout/setInterval产生的异步调用是可以同步更新state的。这样的讲解比较抽象,我们可以直接根据以下源码开始理解。

setState

下面我们来看下setState在源码中的定义:

/**
 * Sets a subset of the state. Always use this to mutate
 * state. You should treat `this.state` as immutable.
 *
 * There is no guarantee that `this.state` will be immediately updated, so
 * accessing `this.state` after calling this method may return the old value.
 *
 * There is no guarantee that calls to `setState` will run synchronously,
 * as they may eventually be batched together.  You can provide an optional
 * callback that will be executed when the call to setState is actually
 * completed.
 *
 * When a function is provided to setState, it will be called at some point in
 * the future (not synchronously). It will be called with the up to date
 * component arguments (state, props, context). These values can be different
 * from this.* because your function may be called after receiveProps but before
 * shouldComponentUpdate, and this new state, props, and context will not yet be
 * assigned to this.
 *
 * @param {object|function} partialState Next partial state or function to
 *        produce next partial state to be merged with current state.
 * @param {?function} callback Called after state is updated.
 * @final
 * @protected
 */
ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, "setState");
  }
};

根据源码中的注释,有这么一句话。

There is no guarantee that this.state will be immediately updated, so accessing this.state after calling this method may return the old value.

大概意思就是setState不能确保实时更新state,官方从来没有说过setState是一种异步操作,但也没有否认,只是告诉我们什么时候会触发同步操作,什么时候是异步操作。所以我们工作中千万不要被一些民间偏方蒙蔽双眼,多看看源代码,发现原理的同时,还可以发现很多好玩的东西,开源库的好处就是在于我们能在源码中发现真理。

我们在源码的这段注释里也能看到setState的一些有趣玩法,比如

// 在回调中操作更新后的state
this.setState({
    count: 1
}, function () {
    console.log("# next State", this.state);
});

// 以非对象的形式操作
this.setState((state, props, context) => {
    return {
        count: state.count + 1
    }
});

回到正题,源码中setState执行了this.updater.enqueueSetState方法和this.updater.enqueueCallback方法 ,暂且不论enqueueCallback,我们关注下enqueueSetState的作用。

enqueueSetState

下面是enqueueSetState的源码:

/**
   * Sets a subset of the state. This only exists because _pendingState is
   * internal. This provides a merging strategy that is not available to deep
   * properties which is confusing. TODO: Expose pendingState or don"t use it
   * during the merge.
   *
   * @param {ReactClass} publicInstance The instance that should rerender.
   * @param {object} partialState Next partial state to be merged with state.
   * @internal
   */
  enqueueSetState: function (publicInstance, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, "setState");

    if (!internalInstance) {
      return;
    }

    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
}

enqueueSetState如其名,是一个队列操作,将要变更的state统一插入队列,待一一处理。队列数据_pengdingStateQueue会挂载在一个组件对象上internalInstance,对于internalInstance想要了解下的同学,可以参考下react源码中的ReactInstanceMap这个概念。

队列操作完成之后,就开始真正的更新操作了。

enqueueUpdate

更新方法enqueueUpdate的源码如下:

/**
 * Mark a component as needing a rerender, adding an optional callback to a
 * list of functions which will be executed once the rerender occurs.
 */
function enqueueUpdate(component) {
  ensureInjected();

  // Various parts of our code (such as ReactCompositeComponent"s
  // _renderValidatedComponent) assume that calls to render aren"t nested;
  // verify that that"s the case. (This is called by each top-level update
  // function, like setProps, setState, forceUpdate, etc.; creation and
  // destruction of top-level components is guarded in ReactMount.)

  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
}

第一次执行setState的时候,可以进入if语句,遇到里面的return语句,终止执行。如果不是正处于创建或更新组件阶段,则处理update事务。

第二次执行setState的时候,进入不了if语句,将组件放入dirtyComponents。如果正在创建或更新组件,则暂且先不处理update,只是将组件放在dirtyComponents数组中。

enqueueUpdate包含了React避免重复render的逻辑。参考源码中batchedUpdates的调用情况,mountComponentupdateComponent方法在执行的最开始,会调用到batchedUpdates进行批处理更新,这些是react实例的生命周期,此时会将isBatchingUpdates设置为true,也就是将状态标记为现在正处于更新阶段了。之后React以事务的方式处理组件update,事务处理完后会调用wrapper.close(), 而TRANSACTION_WRAPPERS中包含了RESET_BATCHED_UPDATES这个wrapper,故最终会调用RESET_BATCHED_UPDATES.close(), 它最终会将isBatchingUpdates设置为false。

听不懂?听不懂没关系。。我们会一句句剖析。

enqueueUpdatebatchingStrategy的概念我们放一起考虑。

batchingStrategy简单直译叫做批量处理策略。这个是React处理批量state操作时的精髓,源码如下:

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  /**
   * Call the provided function in a context within which calls to `setState`
   * and friends are batched such that components aren"t updated unnecessarily.
   */
  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) {
      callback(a, b, c, d, e);
    } else {
      transaction.perform(callback, null, a, b, c, d, e);
    }
  }
};

enqueueUpdate源码中所述,每次执行更新前,会预先判断isBatchingUpdates是否处理批量更新状态,如我们常见的周期诸如componentWillMountcomponentDidMount,都是处于isBatchingUpdates的批量更新状态,此时执行的setState操作,不会进入if语句执行update,而是进入dirtyComponents的堆栈中。

这就是文章开头所说的栗子,为什么setTimeout执行的setState会同步更新state,而react生命周期中执行的setState只能异步更新的原因。只有react控制下的事件周期,会执行batchedUpdates切换isBatchingUpdates状态,保证批量操作能被截获并插入堆栈。其他事件都和同步执行update方法无异。

执行batchedUpdates之后,会立即将isBatchingUpdates赋值为true,表明此时即将进入更新状态,所有之后的setState进入队列等待。

这里我们以普通的setTimeout为例,执行一次更新。业务代如下:

setTimeout(function () {
    this.setState({
        count: this.state.count + 1
    });
}, 0);

执行时isBatchingUpdates默认是false,所以当我们执行到batchedUpdates这一步的时候,源码中alreadyBatchingUpdates被赋值为false,我们会跳过if进入else条件,执行下一阶段transaction.perform

transaction.perform

perform为我们执行了UI更新的第一步预操作。这里我们会执行一系列更新初始化操作和更新状态的关闭。该方法做了try-catch控制,大量数据操作有可能引发错误exception,perform方法在这里对错误做了截获控制。

/**
 * Executes the function within a safety window. Use this for the top level
 * methods that result in large amounts of computation/mutations that would
 * need to be safety checked. The optional arguments helps prevent the need
 * to bind in many cases.
 *
 * @param {function} method Member of scope to call.
 * @param {Object} scope Scope to invoke from.
 * @param {Object?=} a Argument to pass to the method.
 * @param {Object?=} b Argument to pass to the method.
 * @param {Object?=} c Argument to pass to the method.
 * @param {Object?=} d Argument to pass to the method.
 * @param {Object?=} e Argument to pass to the method.
 * @param {Object?=} f Argument to pass to the method.
 *
 * @return {*} Return value from `method`.
 */
perform: function (method, scope, a, b, c, d, e, f) {
  !!this.isInTransaction() ? "development" !== "production" ? invariant(false, "Transaction.perform(...): Cannot initialize a transaction when there " + "is already an outstanding transaction.") : invariant(false) : void 0;
  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;
}

源码中执行了一些错误的预判,最终我们真正执行的是closeAll方法。关于state的数据更新,从close开始。

close
/**
 * Invokes each of `this.transactionWrappers.close[i]` functions, passing into
 * them the respective return values of `this.transactionWrappers.init[i]`
 * (`close`rs that correspond to initializers that failed will not be
 * invoked).
 */
closeAll: function (startIndex) {
  !this.isInTransaction() ? "development" !== "production" ? invariant(false, "Transaction.closeAll(): Cannot close transaction when none are open.") : invariant(false) : void 0;
  var transactionWrappers = this.transactionWrappers;
  for (var i = startIndex; i < transactionWrappers.length; i++) {
    var wrapper = transactionWrappers[i];
    var initData = this.wrapperInitData[i];
    var errorThrown;
    try {
      // 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
      // wrapper.close threw.
      errorThrown = true;
      if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) {
        wrapper.close.call(this, initData);
      }
      errorThrown = false;
    } finally {
      if (errorThrown) {
        // The closer for wrapper i threw an error; close the remaining
        // wrappers but silence any exceptions from them to ensure that the
        // first error is the one to bubble up.
        try {
          this.closeAll(i + 1);
        } catch (e) {}
      }
    }
  }
  this.wrapperInitData.length = 0;
}

在介绍close之前,我们先了解下两个对象。也就是源码中的this.transactionWrappers。他在初始被赋值为[FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES],也就是以下两个对象,在源码被称作为wrapper

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

源码中我们看到closeAll执行了一次for循环,并执行了每个wrapperclose方法。

RESET_BATCHED_UPDATES的close方法很简单,把isBatchingUpdates更新中这个状态做了一个close的操作,也就是赋值为false,表明本次批量更新已结束。

FLUSH_BATCHED_UPDATES的close方法执行的是flushBatchedUpdates方法。

flushBatchedUpdates
var flushBatchedUpdates = function () {
  // ReactUpdatesFlushTransaction"s wrappers will clear the dirtyComponents
  // array and perform any updates enqueued by mount-ready handlers (i.e.,
  // componentDidUpdate) but we need to check here too in order to catch
  // updates enqueued by setState callbacks and asap calls.
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }

    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();
      CallbackQueue.release(queue);
    }
  }
};

我们暂且不论asap是什么,可以看到flushBatchedUpdates做的是对dirtyComponents的批量处理操作,对于队列中的每个component执行perform更新。这些更新都会执行真正的更新方法runBatchedUpdates

function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;
  !(len === dirtyComponents.length) ? "development" !== "production" ? invariant(false, "Expected flush transaction"s stored dirty-components length (%s) to " + "match dirty-components array length (%s).", len, dirtyComponents.length) : invariant(false) : void 0;

  // Since reconciling a component higher in the owner hierarchy usually (not
  // always -- see shouldComponentUpdate()) will reconcile children, reconcile
  // them before their children by sorting the array.
  dirtyComponents.sort(mountOrderComparator);

  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 performUpdateIfNecessary 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;

    var markerName;
    if (ReactFeatureFlags.logTopLevelRenders) {
      var namedComponent = component;
      // Duck type TopLevelWrapper. This is probably always true.
      if (component._currentElement.props === component._renderedComponent._currentElement) {
        namedComponent = component._renderedComponent;
      }
      markerName = "React update: " + namedComponent.getName();
      console.time(markerName);
    }

    ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);

    if (markerName) {
      console.timeEnd(markerName);
    }

    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
      }
    }
  }
}

runBatchedUpdates中的核心处理是ReactReconciler.performUpdateIfNecessary

/**
   * If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate`
   * is set, update the component.
   *
   * @param {ReactReconcileTransaction} transaction
   * @internal
   */
performUpdateIfNecessary: function (transaction) {
  if (this._pendingElement != null) {
    ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
  }

  if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
    this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
  }
}

在这里我们终于又看到了我们熟悉的_pendingStateQueue,还记得这是什么吗?是的,这就是state的更新队列,performUpdateIfNecessary做了队列的特殊判断,避免导致错误更新。

接下来的这段代码是updateComponent,源码内容比较长,但是我们可以看到很多熟知的生命周期方法的身影,比如说componentWillReceivePropsshouldComponentUpdate,做了component的更新判断。

这部分方法统一归属于ReactCompositeComponentMixin模块,有兴趣了解整个生命周期的同学可以参考下源码中的该模块源码,这里我们不再扩展,会继续讲解state的更新过程。
updateComponent
/**
 * Perform an update to a mounted component. The componentWillReceiveProps and
 * shouldComponentUpdate methods are called, then (assuming the update isn"t
 * skipped) the remaining update lifecycle methods are called and the DOM
 * representation is updated.
 *
 * By default, this implements React"s rendering and reconciliation algorithm.
 * Sophisticated clients may wish to override this.
 *
 * @param {ReactReconcileTransaction} transaction
 * @param {ReactElement} prevParentElement
 * @param {ReactElement} nextParentElement
 * @internal
 * @overridable
 */
updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {
  var inst = this._instance;
  var willReceive = false;
  var nextContext;
  var nextProps;

  // Determine if the context has changed or not
  if (this._context === nextUnmaskedContext) {
    nextContext = inst.context;
  } else {
    nextContext = this._processContext(nextUnmaskedContext);
    willReceive = true;
  }

  // Distinguish between a props update versus a simple state update
  if (prevParentElement === nextParentElement) {
    // Skip checking prop types again -- we don"t read inst.props to avoid
    // warning for DOM component props in this upgrade
    nextProps = nextParentElement.props;
  } else {
    nextProps = this._processProps(nextParentElement.props);
    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 = this._pendingForceUpdate || !inst.shouldComponentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext);

  if ("development" !== "production") {
    "development" !== "production" ? warning(shouldUpdate !== undefined, "%s.shouldComponentUpdate(): Returned undefined instead of a " + "boolean value. Make sure to return true or false.", this.getName() || "ReactCompositeComponent") : void 0;
  }

  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;
  }
}

跳过除了state的其他源码部分,我们可以看到该方法中仍然嵌套了一段对state的更新方法,这个方法就是state更新的终点_processPendingState

_processPendingState

为什么对state中的同一属性做多次setState处理,不会得到多次更新?比如

this.setState({ count: count++ });
this.set

那是因为源码中的多个nextState的更新,只做了一次assign操作,如下源码请查看:

_processPendingState: function (props, context) {
  var inst = this._instance;
  var queue = this._pendingStateQueue;
  var replace = this._pendingReplaceState;
  this._pendingReplaceState = false;
  this._pendingStateQueue = null;

  if (!queue) {
    return inst.state;
  }

  if (replace && queue.length === 1) {
    return queue[0];
  }

  var nextState = _assign({}, replace ? queue[0] : inst.state);
  for (var i = replace ? 1 : 0; i < queue.length; i++) {
    var partial = queue[i];
    _assign(nextState, typeof partial === "function" ? partial.call(inst, nextState, props, context) : partial);
  }

  return nextState;
}

有人说,React抽象来说,就是一个公式
UI=f(state).

的确如此,一个简单的setState执行过程,内部暗藏了这么深的玄机,经历多个模块的处理,经历多个错误处理机制以及对数据边界的判断,保证了一次更新的正常进行。同时我们也发现了为什么setState的操作不能简单的说作是一个异步操作,大家应该在文章中已经找到了答案。

对其他react深层的理解,感兴趣的同学可以多多参考下源码。本文参考react源码版本为15.0.1

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108785.html

相关文章

  • React源码解析React.Component()/PureComponent()

    摘要:只涉及了,其他均没有自己实现。这种组件的复用性是最强的。所以会新建,只继承的原型,不包括,以此来节省内存。 showImg(https://segmentfault.com/img/remote/1460000019783989); 一、React.Component() GitHub:https://github.com/AttackXiaoJinJin/reactExplain/...

    Cristalven 评论0 收藏0
  • Component,PureComponent源码解析

    摘要:首先是创建了一个构造函数,他的原型指到的原型然后创建了一个加上了和一样的属性这里为啥不用。的原型指向的实例修改原型的属性使其正确指向的构造函数,并挂一个的属性。 每次都信誓旦旦的给自己立下要好好学习react源码的flag,结果都是因为某个地方卡住了,或是其他原因没看多少就放弃了。这次又给自己立个flag-坚持看完react源码。为了敦促自己,特开设这样一个专栏来记录自己的学习历程,这...

    luqiuwen 评论0 收藏0
  • React源码解析

    摘要:生命周期当首次挂载组件时,按顺序执行和。由于是通过构造函数进行管理的,所以也是整个生命周期中先开始执行的加载组件若存在,则执行。一组及方法称为一个。因此,允许用户通过来判断该组件是否需要进行算法分析。 生命周期  当首次挂载组件时,按顺序执行 getDefaultProps、getInitialState、componentWillMount、 render 和 componentD...

    Freeman 评论0 收藏0
  • React setState源码实现理解

    摘要:新的值回调函数。官方注解是给组件做个标记需要重新渲染,并且将可选的回调函数添加到函数列表中,这些函数将在重新渲染的时候执行。一共做了两件事一是通过执行方法来更新组件二是若方法传入了回调函数则将回调函数存入队列。 Q1 setState改变状态之后,不会立即更新state值。所以,如果改变state值,react是什么时候进行组件的更新呢?setState()到底做了一些什么呢? A1 ...

    xietao3 评论0 收藏0
  • 剖析 React 源码:render 流程(二)

    摘要:就是,如果你不了解这个的话可以阅读下相关文档,是应用初始化时就会生成的一个变量,值也是,并且这个值不会在后期再被改变。这是我的剖析 React 源码的第三篇文章,如果你没有阅读过之前的文章,请务必先阅读一下 第一篇文章 中提到的一些注意事项,能帮助你更好地阅读源码。 文章相关资料 React 16.8.6 源码中文注释,这个链接是文章的核心,文中的具体代码及代码行数都是依托于这个仓库 热身...

    My_Oh_My 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<