React源碼學(xué)習(xí)系列(二)—— ReactDOM.render乳规,初次渲染

概述

上一篇講到React中的元素(ReactElement的“實(shí)例”)會(huì)有一個(gè)type屬性,而該值將決定其被渲染時(shí)的處理結(jié)果合呐。
ReactDOM.render實(shí)際即為React初次將vdom渲染至真實(shí)dom樹的過程暮的,其中包括了創(chuàng)建元素、添加屬性淌实、綁定事件等等操作冻辩。
本篇猖腕,我們就通過ReactDOM.render的源碼來了解一下其處理過程。

ReactDOM.render方法使用

首先看ReactDOM.render的使用方式:

const App = (<h2>Hello World!</h2>)
ReactDOM.render(App, document.querySelector('#app'))

或者

class App extends React.Component {
  render(){
    return (
      <div>
        <h2>Hello World!</h2>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.querySelector('#app'))

根據(jù)我們上一篇的討論恨闪,我們知道上面兩個(gè)例子中ReactDOM.render第一個(gè)參數(shù)傳入的都是ReactElement的“實(shí)例”倘感。

而當(dāng)?shù)谝粋€(gè)參數(shù)傳入一個(gè)字符串類型,如下:

ReactDOM.render('This is String', document.querySelector('#app'))

// Uncaught Error: ReactDOM.render(): Invalid component element. Instead of passing a string like 'div', pass React.createElement('div') or <div />.

可見咙咽,ReactDOM.render第一個(gè)參數(shù)不支持字符串類型老玛,即不會(huì)直接創(chuàng)建 TextNode 插入到第二個(gè)參數(shù)指定的容器中。

接下來钧敞,我們一起進(jìn)入到源碼中查看該方法蜡豹。

源碼結(jié)構(gòu)

查看ReactDOM.js文件,可以看到ReactDOM.render引用ReactMount.jsrender方法,如下:

ReactMount = {
  // ReactDOM.render直接引用此方法
  render: function (nextElement, container, callback) {
    return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
  },
  // 實(shí)際執(zhí)行render的方法
  _renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
    ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');

    // 將傳入的element用TopLevelWrapper包裝犁享,
    // 包裝后的元素余素,標(biāo)記有rootID,并且擁有render方法炊昆,
    // 具體可看TopLevelWrapper的源碼
    var nextWrappedElement = React.createElement(TopLevelWrapper, {
      child: nextElement
    });

    // ReactDOM.render方法調(diào)用時(shí),parentComponent為null
    var nextContext;
    if (parentComponent) {
      var parentInst = ReactInstanceMap.get(parentComponent);
      nextContext = parentInst._processChildContext(parentInst._context);
    } else {
      nextContext = emptyObject;
    }

    // 第一次執(zhí)行時(shí)威根,prevComponent為null凤巨,具體可看此方法源碼
    var prevComponent = getTopLevelWrapperInContainer(container);

    if (prevComponent) {
      var prevWrappedElement = prevComponent._currentElement;
      var prevElement = prevWrappedElement.props.child;

      // 判斷上一次的prevElement和nextElement是否是同一個(gè)組件,或者僅僅是數(shù)字洛搀、字符串敢茁,如果是,則直接update留美,
      // 否則彰檬,重新渲染整個(gè)Element
      if (shouldUpdateReactComponent(prevElement, nextElement)) {
        var publicInst = prevComponent._renderedComponent.getPublicInstance();
        var updatedCallback = callback && function () {
          callback.call(publicInst);
        };
        // 更新vdom
        ReactMount._updateRootComponent(prevComponent, nextWrappedElement, nextContext, container, updatedCallback);
        return publicInst;
      } else {
        ReactMount.unmountComponentAtNode(container);
      }
    }

    var reactRootElement = getReactRootElementInContainer(container);
    var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
    var containerHasNonRootReactChild = hasNonRootReactChild(container);


    var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;
    // 本次為首次渲染,因此調(diào)用ReactMount._renderNewRootComponent
    var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
    if (callback) {
      callback.call(component);
    }
    return component;
  },
  /**
   * Render a new component into the DOM. Hooked by hooks!
   *
   * @param {ReactElement} nextElement element to render
   * @param {DOMElement} container container to render into
   * @param {boolean} shouldReuseMarkup if we should skip the markup insertion
   * @return {ReactComponent} nextComponent
   */
  _renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {

    ReactBrowserEventEmitter.ensureScrollValueMonitoring();
    // 初始化組件實(shí)例谎砾,并增加組件掛載(mount)逢倍、更新(update)、卸載(unmount)等方法
    var componentInstance = instantiateReactComponent(nextElement, false);

    // The initial render is synchronous but any updates that happen during
    // rendering, in componentWillMount or componentDidMount, will be batched
    // according to the current batching strategy.

    ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);

    var wrapperID = componentInstance._instance.rootID;
    instancesByReactRootID[wrapperID] = componentInstance;

    return componentInstance;
  },
}

從以上代碼可以看出景图,當(dāng)調(diào)用ReactDOM.render時(shí)较雕,使用TopLevelWrapper對element進(jìn)行包裝,隨后將其傳入ReactMount._renderNewRootComponent中挚币,在此方法內(nèi)亮蒋,調(diào)用instantiateReactComponent組件的實(shí)例,該實(shí)例擁有mountComponent等掛載妆毕、更新的方法慎玖。

接下來學(xué)習(xí)instantiateReactComponent的源碼,源碼位置位于instantiateReactComponent.js文件笛粘。

/**
 * Given a ReactNode, create an instance that will actually be mounted.
 *
 * @param {ReactNode} node
 * @param {boolean} shouldHaveDebugID
 * @return {object} A new instance of the element's constructor.
 * @protected
 */
function instantiateReactComponent(node, shouldHaveDebugID) {
  var instance;

  if (node === null || node === false) {
    instance = ReactEmptyComponent.create(instantiateReactComponent);
  } else if (typeof node === 'object') {
    var element = node;
    var type = element.type;

    // 代碼塊(1)
    // Special case string values
    if (typeof element.type === 'string') {
      // type為string的趁怔,調(diào)用createInternalComponent方法远舅,
      // 對節(jié)點(diǎn)進(jìn)行處理衰齐,包含屬性柏副、默認(rèn)事件等等
      instance = ReactHostComponent.createInternalComponent(element); // (2)
    } else if (isInternalComponentType(element.type)) {
      // 內(nèi)置type?
      // This is temporarily available for custom components that are not string
      // representations. I.e. ART. Once those are updated to use the string
      // representation, we can drop this code path.
      instance = new element.type(element);

      // We renamed this. Allow the old name for compat. :(
      if (!instance.getHostNode) {
        instance.getHostNode = instance.getNativeNode;
      }
    } else {
      // 其余的均為自定義組件, 通過此方法报破,創(chuàng)建組件實(shí)例
      // 此方法比較復(fù)雜
      instance = new ReactCompositeComponentWrapper(element);
    }
  } else if (typeof node === 'string' || typeof node === 'number') {
    // 字符串或數(shù)字任连,直接調(diào)用 createInstanceForText蚤吹,生成實(shí)例
    instance = ReactHostComponent.createInstanceForText(node);
  } else {
    !false ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Encountered invalid React node of type %s', typeof node) : _prodInvariant('131', typeof node) : void 0;
  }

  // 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.
  // 與diff算法相關(guān),TOREAD...
  instance._mountIndex = 0;
  instance._mountImage = null;

  return instance;
}

結(jié)合注釋細(xì)讀以上代碼随抠,如代碼塊(1)中裁着,根據(jù)nodetype類型來渲染節(jié)點(diǎn),也即本文一開始所提到的type拱她。為更好理解二驰,我們使用以下代碼渲染一個(gè)input元素:

/**
 * 以下JSX相當(dāng)于:
 * const inputEle = React.createElement('input', {
 *  defaultValue: '10',
 *  onClick: () => console.log('clicked')
 * })
*/
const inputEle = (
  <input
    defaultValue="10"
    onClick={() => console.log('clicked')}
  />
)

ReactDOM.render(inputEle, document.getElementById('app'))

根據(jù)我們上一篇所講,inputEleReactElement的一個(gè)實(shí)例秉沼,其type屬性為input桶雀。
因此,在instantiateReactComponent方法中唬复,應(yīng)該執(zhí)行(2)處的分支矗积,即:ReactHostComponent.createInternalComponent(element)
我們查看ReactHostComponent.js文件敞咧,可看到createInternalComponent方法棘捣,代碼如下:

/**
 * Get a host internal component class for a specific tag.
 *
 * @param {ReactElement} element The element to create.
 * @return {function} The internal class constructor function.
 */
function createInternalComponent(element) {
  !genericComponentClass ? process.env.NODE_ENV !== 'production' ? invariant(false, 'There is no registered component for the tag %s', element.type) : _prodInvariant('111', element.type) : void 0;
  return new genericComponentClass(element);
}

即返回genericComponentClass的一個(gè)實(shí)例,而genericComponentClass的來源休建,追尋源碼乍恐,可以找到在ReactDefaultInjection中找到,實(shí)際上將ReactDOMComponent注入進(jìn)來测砂。

ReactDOM源碼中茵烈,作者將各種類型(如ReactEventListener、ReactDOMComponent等)抽象后通過Injection機(jī)制注入邑彪,我的理解是這樣方便未來將類型整體升級(jí)替換瞧毙,并且能一定程度上解耦(只需要保證類型對外提供的接口一致)。不知道是否理解有誤... ...還望指教寄症。

因此instantiateReactComponent的代碼(2)處實(shí)際返回:new ReactDOMComponent(node)宙彪。
接下來閱讀ReactDOMComponent.js文件:
先看ReactDOMComponent這個(gè)方法:

/**
 * Creates a new React class that is idempotent and capable of containing other
 * React components. It accepts event listeners and DOM properties that are
 * valid according to `DOMProperty`.
 *
 *  - Event listeners: `onClick`, `onMouseDown`, etc.
 *  - DOM properties: `className`, `name`, `title`, etc.
 *
 * The `style` property functions differently from the DOM API. It accepts an
 * object mapping of style properties to values.
 *
 * @constructor ReactDOMComponent
 * @extends ReactMultiChild
 */
function ReactDOMComponent(element) {
  var tag = element.type;
  validateDangerousTag(tag);
  this._currentElement = element;
  this._tag = tag.toLowerCase();
  this._namespaceURI = null;
  this._renderedChildren = null;
  this._previousStyle = null;
  this._previousStyleCopy = null;
  this._hostNode = null;
  this._hostParent = null;
  this._rootNodeID = 0;
  this._domID = 0;
  this._hostContainerInfo = null;
  this._wrapperState = null;
  this._topLevelWrapper = null;
  this._flags = 0;
  if (process.env.NODE_ENV !== 'production') {
    this._ancestorInfo = null;
    setAndValidateContentChildDev.call(this, null);
  }
}

_assign(ReactDOMComponent.prototype, ReactDOMComponent.Mixin, ReactMultiChild.Mixin)

以上代碼可以看到,ReactDOMComponent這個(gè)類繼承了ReactMultiChildMixin有巧。
元素掛載時(shí)释漆,實(shí)際調(diào)用:ReactDOMComponent.Mixin中的mountComponent方法,整體源碼如下:

 /**
   * Generates root tag markup then recurses. This method has side effects and
   * is not idempotent.
   *
   * @internal
   * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
   * @param {?ReactDOMComponent} the parent component instance
   * @param {?object} info about the host container
   * @param {object} context
   * @return {string} The computed markup.
   */
  mountComponent: function (transaction, hostParent, hostContainerInfo, context) {
    this._rootNodeID = globalIdCounter++;
    this._domID = hostContainerInfo._idCounter++;
    this._hostParent = hostParent;
    this._hostContainerInfo = hostContainerInfo;

    var props = this._currentElement.props;
    // 調(diào)整props至DOM的合法屬性,并且處理事件
    switch (this._tag) {
      case 'audio':
      case 'form':
      case 'iframe':
      case 'img':
      case 'link':
      case 'object':
      case 'source':
      case 'video':
        this._wrapperState = {
          listeners: null
        };
        transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
        break;
      case 'input':
        ReactDOMInput.mountWrapper(this, props, hostParent);
        props = ReactDOMInput.getHostProps(this, props);
        transaction.getReactMountReady().enqueue(trackInputValue, this);
        transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
        break;
      case 'option':
        ReactDOMOption.mountWrapper(this, props, hostParent);
        props = ReactDOMOption.getHostProps(this, props);
        break;
      case 'select':
        ReactDOMSelect.mountWrapper(this, props, hostParent);
        props = ReactDOMSelect.getHostProps(this, props);
        transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
        break;
      case 'textarea':
        ReactDOMTextarea.mountWrapper(this, props, hostParent);
        props = ReactDOMTextarea.getHostProps(this, props);
        transaction.getReactMountReady().enqueue(trackInputValue, this);
        transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
        break;
    }

    assertValidProps(this, props);

    // We create tags in the namespace of their parent container, except HTML
    // tags get no namespace.
    var namespaceURI;
    var parentTag;
    if (hostParent != null) {
      namespaceURI = hostParent._namespaceURI;
      parentTag = hostParent._tag;
    } else if (hostContainerInfo._tag) {
      namespaceURI = hostContainerInfo._namespaceURI;
      parentTag = hostContainerInfo._tag;
    }
    if (namespaceURI == null || namespaceURI === DOMNamespaces.svg && parentTag === 'foreignobject') {
      namespaceURI = DOMNamespaces.html;
    }
    if (namespaceURI === DOMNamespaces.html) {
      if (this._tag === 'svg') {
        namespaceURI = DOMNamespaces.svg;
      } else if (this._tag === 'math') {
        namespaceURI = DOMNamespaces.mathml;
      }
    }
    this._namespaceURI = namespaceURI;

    var mountImage;
    if (transaction.useCreateElement) {
      var ownerDocument = hostContainerInfo._ownerDocument;
      var el;
      if (namespaceURI === DOMNamespaces.html) {
        if (this._tag === 'script') {
          // Create the script via .innerHTML so its "parser-inserted" flag is
          // set to true and it does not execute
          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 {
          // Separate else branch instead of using `props.is || undefined` above becuase of a Firefox bug.
          // See discussion in https://github.com/facebook/react/pull/6896
          // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
          el = ownerDocument.createElement(this._currentElement.type);
        }
      } else {
        el = ownerDocument.createElementNS(namespaceURI, this._currentElement.type);
      }
      ReactDOMComponentTree.precacheNode(this, el);
      this._flags |= Flags.hasCachedChildNodes;
      if (!this._hostParent) {
        DOMPropertyOperations.setAttributeForRoot(el);
      }
      this._updateDOMProperties(null, props, transaction);
      var lazyTree = DOMLazyTree(el);
      this._createInitialChildren(transaction, props, context, lazyTree);
      mountImage = lazyTree;
    } else {
      var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
      var tagContent = this._createContentMarkup(transaction, props, context);
      if (!tagContent && omittedCloseTags[this._tag]) {
        mountImage = tagOpen + '/>';
      } else {
        mountImage = tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
      }
    }

    switch (this._tag) {
      case 'input':
        transaction.getReactMountReady().enqueue(inputPostMount, this);
        if (props.autoFocus) {
          transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
        }
        break;
      case 'textarea':
        transaction.getReactMountReady().enqueue(textareaPostMount, this);
        if (props.autoFocus) {
          transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
        }
        break;
      case 'select':
        if (props.autoFocus) {
          transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
        }
        break;
      case 'button':
        if (props.autoFocus) {
          transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
        }
        break;
      case 'option':
        transaction.getReactMountReady().enqueue(optionPostMount, this);
        break;
    }

    return mountImage;
  }

閱讀上述代碼篮迎,可以知道React是如何將一個(gè)ReactElement與DOM進(jìn)行映射的(本例子只展示了DOMComponent這種類型男图,自定義組件示姿、textNode這兩種可自行找到源碼閱讀)。
上述方法返回的值將會(huì)被傳入ReactUpdates.batchedUpdates中進(jìn)行掛載逊笆,這部分內(nèi)容較為復(fù)雜栈戳,在未來將進(jìn)一步解讀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末难裆,一起剝皮案震驚了整個(gè)濱河市子檀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌乃戈,老刑警劉巖褂痰,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異症虑,居然都是意外死亡缩歪,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門谍憔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匪蝙,“玉大人,你說我怎么就攤上這事韵卤∑郏” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵沈条,是天一觀的道長。 經(jīng)常有香客問我诅炉,道長蜡歹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任涕烧,我火速辦了婚禮月而,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘议纯。我一直安慰自己父款,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布瞻凤。 她就那樣靜靜地躺著憨攒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪阀参。 梳的紋絲不亂的頭發(fā)上肝集,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音蛛壳,去河邊找鬼杏瞻。 笑死所刀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的捞挥。 我是一名探鬼主播浮创,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼砌函!你這毒婦竟也來了斩披?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對情侶失蹤胸嘴,失蹤者是張志新(化名)和其女友劉穎雏掠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體劣像,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乡话,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了耳奕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绑青。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖屋群,靈堂內(nèi)的尸體忽然破棺而出闸婴,到底是詐尸還是另有隱情,我是刑警寧澤芍躏,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布邪乍,位于F島的核電站,受9級(jí)特大地震影響对竣,放射性物質(zhì)發(fā)生泄漏庇楞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一否纬、第九天 我趴在偏房一處隱蔽的房頂上張望吕晌。 院中可真熱鬧,春花似錦临燃、人聲如沸睛驳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乏沸。三九已至,卻和暖如春溃论,著一層夾襖步出監(jiān)牢的瞬間屎蜓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國打工钥勋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留炬转,地道東北人辆苔。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像扼劈,于是被迫代替她去往敵國和親驻啤。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容

  • 原教程內(nèi)容詳見精益 React 學(xué)習(xí)指南荐吵,這只是我在學(xué)習(xí)過程中的一些閱讀筆記骑冗,個(gè)人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,813評(píng)論 1 18
  • 安裝: 概述 React起源于FaceBook的內(nèi)部項(xiàng)目,因?yàn)樵摴緦κ袌錾纤械腏avaScript MVC框架...
    姒沝無痕閱讀 714評(píng)論 0 0
  • 1.晚上七點(diǎn)先煎,在南京南站無聊地等車贼涩,在人來人往中看見一個(gè)姑娘,米白風(fēng)衣薯蝎,長發(fā)飄飄遥倦,微胖,一邊疾步如飛占锯,一邊焦急地盯...
    不成文女孩閱讀 277評(píng)論 4 4
  • “合昏尚知時(shí)消略,鴛鴦不獨(dú)宿堡称。但見新人笑,那聞舊人哭艺演∪唇簦” 最近有個(gè)朋友跟我說,他感覺她女朋友對他不上心了胎撤,是不是變心了...
    叫我老唐閱讀 1,508評(píng)論 0 1
  • 每個(gè)人心中都有自己所向往的地方啄寡,都有自己所認(rèn)定的美好的地方。當(dāng)然哩照,我向往的地方一一深圳,但這就是我向往的地方懒浮。 每...
    憧憬你的邂逅閱讀 1,647評(píng)論 0 2