React渲染過程源碼分析

什么是虛擬DOM(Virtual DOM)

在傳統(tǒng)的開發(fā)模式中,每次需要進(jìn)行頁面更新的時候都需要我們手動的更新DOM:

image

在前端開發(fā)中,最應(yīng)該避免的就是DOM的更新,因?yàn)镈OM更新是極其耗費(fèi)性能的,有過操作DOM經(jīng)歷的都應(yīng)該知道,修改DOM的代碼也非常冗長,也會導(dǎo)致項(xiàng)目代碼閱讀困難。在React中,把真是得DOM轉(zhuǎn)換成JavaScript對象樹,這就是我們說的虛擬DOM,它并不是真正的DOM,只是存有渲染真實(shí)DOM需要的屬性的對象。

image

虛擬DOM的好處

雖然虛擬DOM會提升一定得性能但是并不明顯,因?yàn)槊看涡枰碌臅r候Virtual DOM需要比較兩次的DOM有什么不同,然后批量更新,這也是需要資源的。

Virtual真實(shí)的好處其實(shí)是,他可以實(shí)現(xiàn)跨平臺,我們所熟知的react-native就是基于VirtualDOM來實(shí)現(xiàn)的。

Virtual DOM實(shí)現(xiàn)

現(xiàn)在我們根據(jù)源碼來分析一下Virtual DOM的構(gòu)建過程。

JSX和React.createElement

在看源碼之前,現(xiàn)在回顧一下React中創(chuàng)建組件的兩種方式。

1.JSX

function App() {
  return (
    <div>Hello React</div>
  );
}

2.React.createElement

const App = React.createElement('div', null, 'Hello React');

這里多說一句其實(shí)JSX只不過是React.createElement的語法糖,在編譯的時候babel會將JSX轉(zhuǎn)換成為使用React.createElement的形式,因?yàn)镴SX語法更加符合我們?nèi)粘i_發(fā)的習(xí)慣,所以我們在寫React的時候更多的是使用JSX語法進(jìn)行編寫。

React.createElement都做了什么

下面粘貼一段React.createElement的源碼來分析:

ReactElement.createElement = function(type, config, children) {
  //初始化參數(shù)
  var propName;
  var props = {};
  var key = null;
  var ref = null;
  var self = null;
  var source = null;

  if (config != null) {
    // 如果存在config,則提取里面的內(nèi)容
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 將新添加的元素更新到新的props中
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

    //如果只有一個children參數(shù),那么指直接賦值給children
    //否則合并處理children
  var childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    var childArray = Array(childrenLength);
    for (var i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  // 如果某個prop為空,且存在默認(rèn)的prop,則將默認(rèn)的prop賦值給props
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  
 //返回一個ReactElement實(shí)例對象,這個可以理解就是我們說的虛擬DOM
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
};

ReactElement與其中的安全機(jī)制

看到這里我們不禁好奇上述代碼中返回的ReactElement到底是個什么東西呢?其實(shí)ReactElement就只是我們常說的虛擬DOM,ReactElement主要包含了這個DOM節(jié)點(diǎn)的類型(type)、屬性(props)和子節(jié)點(diǎn)(children)。ReactElement只是包含了DOM節(jié)點(diǎn)的數(shù)據(jù),還沒有注入對應(yīng)的一些方法來完成React框架的功能。

現(xiàn)在來看一下ReactElement的源碼部分

var ReactElement = function (type, key, ref, self, source, owner, props) {
  var element = {
    // react中防止XSS注入的變量,也是標(biāo)志這個是react元素的變量,稍后會講
    $$typeof: REACT_ELEMENT_TYPE,

    // 構(gòu)建屬于這個元素的屬性值
    type: type,
    key: key,
    ref: ref,
    props: props,

    // 記錄一下創(chuàng)建這個元素的組件
    _owner: owner,
  };

  return element;
};

上述代碼可以看出來,ReactElement其實(shí)就是裝有各種屬性的一個大對象而已。

$$typeof

首先我們現(xiàn)在控制臺打印一下react.createElement的結(jié)果:

image

WHAT???這個變量是什么???

其實(shí)$$typeof是為了安全問題引入的變量,什么安全問題呢?那就是XSS

我們都知道React.createElement方法的第三個參數(shù)是允許用戶輸入自定義組件的,那么設(shè)想一下,如果前端允許用戶輸入下面一段代碼:

var input = "{"type": "div",  "props": {"dangerouslySetInnerHTML": {"__html": "<script>alert('hey')</script>"}}}""

//然后我們開始用輸入的值創(chuàng)建ReactElement,就變成了下面這個樣子

React.createElement('div', null, input);

至此XSS注入就達(dá)成目的啦。

那么$$typeof這個變量是怎么做到安全認(rèn)證的呢???

var REACT_ELEMENT_TYPE =
  (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
  0xeac7;
  
  ReactElement.isValidElement = function (object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
};

首先typeof是Symbol類型的變量,是無法通過json對象轉(zhuǎn)成字符串,所以就如果只是簡單的json拷貝,是沒有辦法通過ReactElement.isValidElement的驗(yàn)證的,ReactElement.isValidElement會將不帶有typeof變量的元素全部丟掉不用。

React的render過程

現(xiàn)在通過源碼來看一下react中從定義完組件之后render到頁面的過程。

1.ReactDOM.render

當(dāng)我們想要將一個組件渲染到頁面上需要調(diào)用ReactDOM.render(element,container,[callback])方法,現(xiàn)在我們就從這個方法入手一步一步來看源碼:

var ReactDOM = {
  findDOMNode: findDOMNode,
  render: ReactMount.render,
  unmountComponentAtNode: ReactMount.unmountComponentAtNode,
  version: ReactVersion
};

從上面代碼我們可以看到,我們經(jīng)常調(diào)用的ReactDOM.render,其實(shí)是在調(diào)用ReactMount的render方法。所以我們現(xiàn)在來看ReactMount中的render方法都做了些什么。

/src/renderers/dom/client/ReactMount.js

  render: function (nextElement, container, callback) {
    return ReactMount._renderSubtreeIntoContainer(
      null,
      nextElement,
      container,
      callback,
    );
  }

2._renderSubtreeIntoContainer

現(xiàn)在我們終于找到了源頭,那就是_renderSubtreeIntoContainer方法,我們在來看一下它是怎么樣定義的,可以根據(jù)下面代碼中的注釋一步一步的來看:

  _renderSubtreeIntoContainer: function (
    parentComponent,
    nextElement,
    container,
    callback,
  ) {
    // 檢驗(yàn)傳入的callback是否符合標(biāo)準(zhǔn),如果不符合,validateCallback會throw出
    //一個錯誤(內(nèi)部調(diào)用了node_modules/fbjs/lib/invariant有invariant方法)
    ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');

    // 此處的TopLevelWrapper,只不過是將你傳進(jìn)來的type,進(jìn)行一層包裹,并賦值ID,并會在TopLevelWrapper.render方法中返回你傳入的值
    // 具體看源碼,,所以個這東西只是一個包裹層
    var nextWrappedElement = React.createElement(TopLevelWrapper, {
      child: nextElement,
    });

    //判斷之前是否渲染過此元素,如果有返回此元素,如果沒有返回null
    var prevComponent = getTopLevelWrapperInContainer(container);

    if (prevComponent) {
      var prevWrappedElement = prevComponent._currentElement;
      var prevElement = prevWrappedElement.props.child;
      // 判斷是否需要更新組件
      if (shouldUpdateReactComponent(prevElement, nextElement)) {
        var publicInst = prevComponent._renderedComponent.getPublicInstance();
        var updatedCallback =
          callback &&
          function () {
            callback.call(publicInst);
          };
        // 如果需要更新則調(diào)用組件更新方法,直接返回更新后的組件
        ReactMount._updateRootComponent(
          prevComponent,
          nextWrappedElement,
          nextContext,
          container,
          updatedCallback,
        );
        return publicInst;
      } else {
        // 不需要更新組件,那就把之前的組件卸載掉
        ReactMount.unmountComponentAtNode(container);
      }
    }

    // 返回當(dāng)前容器的DOM節(jié)點(diǎn),如果沒有container返回null
    var reactRootElement = getReactRootElementInContainer(container);
    // 返回上面reactRootElement的data-reactid
    var containerHasReactMarkup =reactRootElement && !!internalGetID(reactRootElement);
    // 判斷當(dāng)前容器是不是有身為react元素的子元素
    var containerHasNonRootReactChild = hasNonRootReactChild(container);
    // 得到是否應(yīng)該重復(fù)使用的標(biāo)記變量
    var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;



    // 將一個新的組件渲染到真是得DOM上
    var component = ReactMount._renderNewRootComponent(
      nextWrappedElement,
      container,
      shouldReuseMarkup,
      nextContext,
    )._renderedComponent.getPublicInstance();

    // 如果有callback函數(shù)那就執(zhí)行這個回調(diào)函數(shù),并且將其this只想component
    if (callback) {
      callback.call(component);
    }

    // 返回組件
    return component;
  },

根據(jù)上面的注釋可以很容易理解上面的代碼,現(xiàn)在我們總結(jié)一下_renderSubtreeIntoContainer方法的執(zhí)行過程:

1.校驗(yàn)傳入callback的格式是否符合規(guī)范
2.用TopLevelWrapper包裹層(帶有reactID)包裹傳入的type,這里說明一下,react.createElement這個方法的type值可以有三種分別是,原生標(biāo)簽的標(biāo)簽名字符串('div'、'span')、react component 、react fragment
3.判斷是否渲染過此次準(zhǔn)備渲染的元素,如果渲染過,則判斷是否需要更新。
    3.1 如果需要更新則調(diào)用更新方法,并且直接將更新后的組件返回
    3.2 如果不需要更新,則卸載老組件
4.如果沒渲染過,則處理shouldReuseMarkup變量
5.調(diào)用ReactMount._renderNewRootComponent將組將更新到DOM(此函數(shù)后面會分析)
6.返回組件

ReactMount._renderNewRootComponent(渲染組件,批次裝載)

上面說到其實(shí)在_renderSubtreeIntoContainer方法中,最后使用了ReactMount._renderNewRootComponent進(jìn)行進(jìn)行組件的渲染,接下來我們看一下該方法的源碼:

  _renderNewRootComponent: function (
    nextElement,
    container,
    shouldReuseMarkup,
    context,
  ) {
    // 監(jiān)聽window上面的滾動事件,緩存滾動變量,保證在滾動的時候頁面不會觸發(fā)重排
    ReactBrowserEventEmitter.ensureScrollValueMonitoring();
    //獲取組件實(shí)例
    var componentInstance = instantiateReactComponent(nextElement, false);

    // 批處理,初始化render的過程是異步的,但是在render的時候componentWillMount或者componentDidMount生命中其中
    // 可能會執(zhí)行更新變量的操作,這是react會將這些操作通過當(dāng)前批次策略,統(tǒng)一處理。
    ReactUpdates.batchedUpdates(
      batchedMountComponentIntoNode, // *
      componentInstance,
      container,
      shouldReuseMarkup,
      context,
    );

    var wrapperID = componentInstance._instance.rootID;
    instancesByReactRootID[wrapperID] = componentInstance;
    // 返回實(shí)例
    return componentInstance;
  }

還是先來總結(jié)一下上面代碼的過程:

1.監(jiān)聽滾動事件,緩存變量,避免滾動帶來的重排
2.初始化組件實(shí)例
3.批量執(zhí)行更新操作
react四大類組件

在上面代碼執(zhí)行過程的2中調(diào)用instantiateReactComponent創(chuàng)建了,組件的實(shí)例,其實(shí)組件類型有四種,具體看下圖:

image

在這里我們還是看一下它的具體實(shí)現(xiàn),然后分析一下過程:

function instantiateReactComponent(node, shouldHaveDebugID) {
  var instance;

  if (node === null || node === false) {
    // 空組件
    instance = ReactEmptyComponent.create(instantiateReactComponent);
  } else if (typeof node === 'object') {
    var element = node;
    if (typeof element.type === 'string') {
      // 原生DOM
      instance = ReactHostComponent.createInternalComponent(element);
    } else if (isInternalComponentType(element.type)) {
      instance = new element.type(element);
    } else {
      // react組件
      instance = new ReactCompositeComponentWrapper(element);
    }
  } else if (typeof node === 'string' || typeof node === 'number') {
    // 文本字符串
    instance = ReactHostComponent.createInstanceForText(node);
  } else {
    
  }
  return instance;
}

1.node為空時初始化空組件ReactEmptyComponent.create(instantiateReactComponent)
2.node類型是對象時,即是DOM標(biāo)簽或者自定義組件,那么如果element的類型是字符串,則初始化DOM標(biāo)簽組件ReactNativeComponent.createInternalComponent,否則初始化自定義組件ReactCompositeComponentWrapper
3.當(dāng)node是字符串或者數(shù)字時,初始化文本組件ReactNativeComponent.createInstanceForText
4.其他情況不處理
批次裝載

在_renderNewRootComponent代碼中有一個方法后面我是打了星號的,batchedUpdate方法的第一個參數(shù)其實(shí)是個callback,這里也就是batchedMountComponentIntoNode,從方法名就可以很容易看出來他是一個批次裝載組件的方法,他是定義在ReactMount上面的,來看一下他的具體實(shí)現(xiàn)吧。

function batchedMountComponentIntoNode(
  componentInstance,
  container,
  shouldReuseMarkup,
  context,
) {

  // 在batchedMountComponentIntoNode中,使用transaction.perform調(diào)用mountComponentIntoNode讓其基于事務(wù)機(jī)制進(jìn)行調(diào)用
  var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
    !shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement,
  );
  transaction.perform(
    mountComponentIntoNode,
    null,
    componentInstance,
    container,
    transaction,
    shouldReuseMarkup,
    context,
  );
  ReactUpdates.ReactReconcileTransaction.release(transaction);
}

事務(wù)機(jī)制以后再進(jìn)行分析,這里就直接來看mountComponentIntoNode是如何將組件渲染成DOM節(jié)點(diǎn)的吧。

mountComponentIntoNode(生成DOM)

mountComponentIntoNode這個函數(shù)主要就是裝載組件,并且將其插入到DOM中,話不多說,直接上源碼,然后根據(jù)源碼一步步的分析:

/**
 * Mounts this component and inserts it into the DOM.
 *
 * @param {ReactComponent} componentInstance The instance to mount.
 * @param {DOMElement} container DOM element to mount into.
 * @param {ReactReconcileTransaction} transaction
 * @param {boolean} shouldReuseMarkup If true, do not insert markup
 */

function mountComponentIntoNode(
  wrapperInstance,
  container,
  transaction,
  shouldReuseMarkup,
  context,
) {
  var markup = ReactReconciler.mountComponent(
    wrapperInstance,
    transaction,
    null,
    ReactDOMContainerInfo(wrapperInstance, container),
    context,
  );
  wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
  ReactMount._mountImageIntoNode(
    markup,
    container,
    wrapperInstance,
    shouldReuseMarkup,
    transaction,
  );
}

可以看到mountComponentIntoNode方法首先調(diào)用了ReactReconciler.mountComponent方法,而在ReactReconciler.mountComponent方法中其實(shí)是調(diào)用了上面四種react組件的mountComponent方法,前面的就不說了,我們直接來看一下四種組件中的mountComponent方法都干了什么吧。

 /src/renderers/dom/shared/ReactDOMComponent.js
  mountComponent: function (
    transaction,
    hostParent,
    hostContainerInfo,
    context,
  ) {
    var props = this._currentElement.props;
    switch (this._tag) {
      case 'audio':
      case 'form':
      case 'iframe':
      case 'img':
      case 'link':
      case 'object':
      case 'source':
      case 'video':

      ....

    // 創(chuàng)建容器
    var mountImage;
      var ownerDocument = hostContainerInfo._ownerDocument;
      var el;
      if (this._tag === 'script') {
          var div = ownerDocument.createElement('div');
          var type = this._currentElement.type;
          div.innerHTML = `<${type}></${type}>`;
          el = div.removeChild(div.firstChild);
        } else if (props.is) {
          el = ownerDocument.createElement(this._currentElement.type, props.is);
        } else {
          el = ownerDocument.createElement(this._currentElement.type);
        }
      }

      // 更新props,第一個參數(shù)是上次的props,第二個參數(shù)是最新的props,如果上一次的props為空那么就是新建狀態(tài)
      this._updateDOMProperties(null, props, transaction);
      // 生成DOMLazyTree對象
      var lazyTree = DOMLazyTree(el);
      // 處理孩子節(jié)點(diǎn)
      this._createInitialChildren(transaction, props, context, lazyTree);
      mountImage = lazyTree;
    
    // 返回容器
    return mountImage;
  }

總結(jié)一下上述代碼的執(zhí)行過程,在這里我只截取了初次渲染時候執(zhí)行的代碼:
1.對特殊的標(biāo)簽進(jìn)行處理,并且調(diào)用方法給出相應(yīng)警告
2.創(chuàng)建DOM節(jié)點(diǎn)
3.調(diào)用_updateDOMProperties方法來處理props
4.生成DOMLazyTree
5.通過DOMLazyTree調(diào)用_createInitialChildren處理孩子節(jié)點(diǎn)。然后返回DOM節(jié)點(diǎn)

下面我們來看一下這個DOMLazyTree方法都干了些什么,還是上源碼:

function queueChild(parentTree, childTree) {
  if (enableLazy) {
    parentTree.children.push(childTree);
  } else {
    parentTree.node.appendChild(childTree.node);
  }
}

function queueHTML(tree, html) {
  if (enableLazy) {
    tree.html = html;
  } else {
    setInnerHTML(tree.node, html);
  }
}

function queueText(tree, text) {
  if (enableLazy) {
    tree.text = text;
  } else {
    setTextContent(tree.node, text);
  }
}

function toString() {
  return this.node.nodeName;
}

function DOMLazyTree(node) {
  return {
    node: node,
    children: [],
    html: null,
    text: null,
    toString,
  };
}

DOMLazyTree.queueChild = queueChild;
DOMLazyTree.queueHTML = queueHTML;
DOMLazyTree.queueText = queueText;

從上述代碼可以看到DOMLazyTree其實(shí)就是一個用來包裹節(jié)點(diǎn)信息的對象,里面有孩子節(jié)點(diǎn),html節(jié)點(diǎn),文本節(jié)點(diǎn),并且提供了將這些節(jié)點(diǎn)插入到真是DOM中的方法,現(xiàn)在我們來看一下在_createInitialChildren方法中它是如何來使用這個lazyTree對象的:

  _createInitialChildren: function (transaction, props, context, lazyTree) {
    var innerHTML = props.dangerouslySetInnerHTML;
    if (innerHTML != null) {
      if (innerHTML.__html != null) {
        DOMLazyTree.queueHTML(lazyTree, innerHTML.__html);
      }
    } else {
      var contentToUse = CONTENT_TYPES[typeof props.children]
        ? props.children
        : null;
      var childrenToUse = contentToUse != null ? null : props.children;
      if (contentToUse != null) {
        if (contentToUse !== '') {
          DOMLazyTree.queueText(lazyTree, contentToUse);
        }
      } else if (childrenToUse != null) {
        var mountImages = this.mountChildren(
          childrenToUse,
          transaction,
          context,
        );
        for (var i = 0; i < mountImages.length; i++) {
          DOMLazyTree.queueChild(lazyTree, mountImages[i]);
        }
      }
    }
  }

判斷當(dāng)前節(jié)點(diǎn)的dangerouslySetInnerHTML屬性、孩子節(jié)點(diǎn)是否為文本和其他節(jié)點(diǎn)分別調(diào)用DOMLazyTree的queueHTML、queueText、queueChild.

ReactCompositeComponent

在實(shí)例調(diào)用mountComponent時,在這里額外的說一下這個函數(shù)的執(zhí)行過程,ReactCompositeComponent也就是我們說的react自定義組件,起主要的執(zhí)行過程如下:

1.處理props、contex等變量,調(diào)用構(gòu)造函數(shù)創(chuàng)建組件實(shí)例
2.判斷是否為無狀態(tài)組件,處理state
3.調(diào)用performInitialMount生命周期,處理子節(jié)點(diǎn),獲取markup。
4.調(diào)用componentDidMount生命周期

在performInitialMount函數(shù)中,首先調(diào)用了componentWillMount生命周期,由于自定義的React組件并不是一個真實(shí)的DOM,所以在函數(shù)中又調(diào)用了孩子節(jié)點(diǎn)的mountComponent。這也是一個遞歸的過程,當(dāng)所有孩子節(jié)點(diǎn)渲染完成后,返回markup并調(diào)用componentDidMount.

渲染DOM

在上述mountComponentIntoNode中最后一步是執(zhí)行_mountImageIntoNode方法,在該方法中核心的渲染方法就是insertTreeBefore,我們直接來看這個方法的源碼,然后進(jìn)行分析:


var insertTreeBefore = function(
  parentNode,
  tree,
  referenceNode,
) {

  if (
    tree.node.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE ||
    (tree.node.nodeType === ELEMENT_NODE_TYPE &&
      tree.node.nodeName.toLowerCase() === 'object' &&
      (tree.node.namespaceURI == null ||
        tree.node.namespaceURI === DOMNamespaces.html))
  ) {
    insertTreeChildren(tree);
    parentNode.insertBefore(tree.node, referenceNode);
  } else {
    parentNode.insertBefore(tree.node, referenceNode);
    insertTreeChildren(tree);
  }
}

function insertTreeChildren(tree) {
  if (!enableLazy) {
    return;
  }
  var node = tree.node;
  var children = tree.children;
  if (children.length) {
    for (var i = 0; i < children.length; i++) {
      insertTreeBefore(node, children[i], null);
    }
  } else if (tree.html != null) {
    setInnerHTML(node, tree.html);
  } else if (tree.text != null) {
    setTextContent(node, tree.text);
  }
}

1.該方法首先就是判斷當(dāng)前節(jié)點(diǎn)是不是fragment節(jié)點(diǎn)或者Object插件
2.如果滿足條件1,首先調(diào)用insertTreeChildren將此節(jié)點(diǎn)的孩子節(jié)點(diǎn)渲染到當(dāng)前節(jié)點(diǎn)上,再將渲染完的節(jié)點(diǎn)插入到html
3.如果不滿足1,是其他節(jié)點(diǎn),先將節(jié)點(diǎn)插入到插入到html,再調(diào)用insertTreeChildren將孩子節(jié)點(diǎn)插入到html

在此過程中已經(jīng)一次調(diào)用了setInnerHTML或setTextContent來分別渲染html節(jié)點(diǎn)和文本節(jié)點(diǎn)。

結(jié)尾

上述文章就是react的初次渲染過程分析,如果有哪些地方寫的不對,歡迎在評論中討論。本文代碼采用的react15中的代碼,和react最新版本代碼會有一些的出入

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容