react高階組件

React進(jìn)階之高階組件

前言

本文代碼淺顯易懂,思想深入實(shí)用。此屬于react進(jìn)階用法,如果你還不了解react,建議從文檔開(kāi)始看起。

我們都知道高階函數(shù)是什么, 高階組件其實(shí)是差不多的用法,只不過(guò)傳入的參數(shù)變成了react組件,并返回一個(gè)新的組件.

A higher-order component is a function that takes a component and returns a new component.

形如:

const EnhancedComponent = higherOrderComponent(WrappedComponent);

高階組件是react應(yīng)用中很重要的一部分,最大的特點(diǎn)就是重用組件邏輯。它并不是由React API定義出來(lái)的功能,而是由React的組合特性衍生出來(lái)的一種設(shè)計(jì)模式。如果你用過(guò)redux,那你就一定接觸過(guò)高階組件,因?yàn)閞eact-redux中的connect就是一個(gè)高階組件。

原文https://github.com/sunyongjian/blog/issues/25
歡迎star
另外本次demo代碼都放在 https://github.com/sunyongjian/hoc-demo
clone下來(lái)跑一下加深理解

引入

先來(lái)一個(gè)最簡(jiǎn)單的高階組件

import React, { Component } from 'react';
import simpleHoc from './simple-hoc';

class Usual extends Component {
  render() {
    console.log(this.props, 'props');
    return (
      <div>
        Usual
      </div>
    )
  }
}
export default simpleHoc(Usual);
import React, { Component } from 'react';

const simpleHoc = WrappedComponent => {
  console.log('simpleHoc');
  return class extends Component {
    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}
export default simpleHoc;

組件Usual通過(guò)simpleHoc的包裝,打了一個(gè)log... 那么形如simpleHoc就是一個(gè)高階組件了,通過(guò)接收一個(gè)組件class Usual,并返回一個(gè)組件class。 其實(shí)我們可以看到,在這個(gè)函數(shù)里,我們可以做很多操作。 而且return的組件同樣有自己的生命周期,function,另外,我們看到也可以把props傳給WrappedComponent(被包裝的組件)。 高階組件的定義我都是用箭頭函數(shù)去寫(xiě)的,如有不適請(qǐng)參照arrow function

裝飾器模式

高階組件可以看做是裝飾器模式(Decorator Pattern)在React的實(shí)現(xiàn)。即允許向一個(gè)現(xiàn)有的對(duì)象添加新的功能,同時(shí)又不改變其結(jié)構(gòu),屬于包裝模式(Wrapper Pattern)的一種

ES7中添加了一個(gè)decorator的屬性,使用@符表示,可以更精簡(jiǎn)的書(shū)寫(xiě)。那上面的例子就可以改成:

import React, { Component } from 'react';
import simpleHoc from './simple-hoc';

@simpleHoc
export default class Usual extends Component {
  render() {
    return (
      <div>
        Usual
      </div>
    )
  }
}

是同樣的效果。
當(dāng)然兼容性是存在問(wèn)題的,通常都是通過(guò)babel去編譯的。 babel提供了plugin,高階組件用的是類(lèi)裝飾器,所以用transform-decorators-legacy babel

兩種形式

屬性代理

引入里我們寫(xiě)的最簡(jiǎn)單的形式,就是屬性代理(Props Proxy)的形式。通過(guò)hoc包裝wrappedComponent,也就是例子中的Usual,本來(lái)傳給Usual的props,都在hoc中接受到了,也就是props proxy。 由此我們可以做一些操作

  • 操作props
    最直觀(guān)的就是接受到props,我們可以做任何讀取,編輯,刪除的很多自定義操作。包括hoc中定義的自定義事件,都可以通過(guò)props再傳下去。

    import React, { Component } from 'react';
    
    const propsProxyHoc = WrappedComponent => class extends Component {
    
      handleClick() {
        console.log('click');
      }
    
      render() {
        return (<WrappedComponent
          {...this.props}
          handleClick={this.handleClick}
        />);
      }
    };
    export default propsProxyHoc;
    

    然后我們的Usual組件render的時(shí)候, console.log(this.props) 會(huì)得到handleClick.

  • refs獲取組件實(shí)例
    當(dāng)我們包裝Usual的時(shí)候,想獲取到它的實(shí)例怎么辦,可以通過(guò)引用(ref),在Usual組件掛載的時(shí)候,會(huì)執(zhí)行ref的回調(diào)函數(shù),在hoc中取到組件的實(shí)例。通過(guò)打印,可以看到它的props, state,都是可以取到的。

    import React, { Component } from 'react';
    
    const refHoc = WrappedComponent => class extends Component {
    
      componentDidMount() {
        console.log(this.instanceComponent, 'instanceComponent');
      }
    
      render() {
        return (<WrappedComponent
          {...this.props}
          ref={instanceComponent => this.instanceComponent = instanceComponent}
        />);
      }
    };
    
    export default refHoc;
    
  • 抽離state

    這里不是通過(guò)ref獲取state, 而是通過(guò) { props, 回調(diào)函數(shù) } 傳遞給wrappedComponent組件,通過(guò)回調(diào)函數(shù)獲取state。這里用的比較多的就是react處理表單的時(shí)候。通常react在處理表單的時(shí)候,一般使用的是受控組件(文檔),即把input都做成受控的,改變value的時(shí)候,用onChange事件同步到state中。當(dāng)然這種操作通過(guò)Container組件也可以做到,具體的區(qū)別放到后面去比較。看一下代碼就知道怎么回事了:

    // 普通組件Login
    import React, { Component } from 'react';
    import formCreate from './form-create';
    
    @formCreate
    export default class Login extends Component {
      render() {
        return (
          <div>
            <div>
              <label id="username">
                賬戶(hù)
              </label>
              <input name="username" {...this.props.getField('username')}/>
            </div>
            <div>
              <label id="password">
                密碼
              </label>
              <input name="password" {...this.props.getField('password')}/>
            </div>
            <div onClick={this.props.handleSubmit}>提交</div>
            <div>other content</div>
          </div>
        )
      }
    }
    
    //HOC
    import React, { Component } from 'react';
    
    const formCreate = WrappedComponent => class extends Component {
    
      constructor() {
        super();
        this.state = {
          fields: {},
        }
      }
      onChange = key => e => {
        const { fields } = this.state;
        fields[key] = e.target.value;
        this.setState({
          fields,
        })
      }
      handleSubmit = () => {
        console.log(this.state.fields);
      }
      getField = fieldName => {
        return {
          onChange: this.onChange(fieldName),
        }
      }
      render() {
        const props = {
          ...this.props,
          handleSubmit: this.handleSubmit,
          getField: this.getField,
        }
        return (<WrappedComponent
          {...props}
        />);
      }
    };
    export default formCreate;
    

    這里我們把state,onChange等方法都放到HOC里,其實(shí)是遵從的react組件的一種規(guī)范,子組件簡(jiǎn)單,傻瓜,負(fù)責(zé)展示,邏輯與操作放到Container。比如說(shuō)我們?cè)贖OC獲取到用戶(hù)名密碼之后,再去做其他操作,就方便多了,而state,處理函數(shù)放到Form組件里,只會(huì)讓Form更加笨重,承擔(dān)了本不屬于它的工作,這樣我們可能其他地方也需要用到這個(gè)組件,但是處理方式稍微不同,就很麻煩了。

反向繼承

反向繼承(Inheritance Inversion),簡(jiǎn)稱(chēng)II,本來(lái)我是叫繼承反轉(zhuǎn)的...因?yàn)橛袀€(gè)模式叫控制反轉(zhuǎn)嘛...
跟屬性代理的方式不同的是,II采用通過(guò) 去繼承WrappedComponent,本來(lái)是一種嵌套的關(guān)系,結(jié)果II返回的組件卻繼承了WrappedComponent,這看起來(lái)是一種反轉(zhuǎn)的關(guān)系。
通過(guò)繼承WrappedComponent,除了一些靜態(tài)方法,包括生命周期,state,各種function,我們都可以得到。上栗子:

 // usual
import React, { Component } from 'react';
import iiHoc from './ii-hoc';

@iiHoc
export default class Usual extends Component {

  constructor() {
    super();
    this.state = {
      usual: 'usual',
    }
  }

  componentDidMount() {
    console.log('didMount')
  }

  render() {
    return (
      <div>
        Usual
      </div>
    )
  }
}
//IIHOC
import React from 'react';

const iiHoc = WrappedComponent => class extends WrappedComponent {
    render() {
      console.log(this.state, 'state');
      return super.render();
    }
}

export default iiHoc;

iiHoc return的組件通過(guò)繼承,擁有了Usual的生命周期及屬性,所以didMount會(huì)打印,state也通過(guò)constructor執(zhí)行,得到state.usual。
其實(shí),你還可以通過(guò)II:

渲染劫持

這里HOC里定義的組件繼承了WrappedComponent的render(渲染),我們可以以此進(jìn)行hijack(劫持),也就是控制它的render函數(shù)。栗子:

  //hijack-hoc
  import React from 'react';

  const hijackRenderHoc = config => WrappedComponent => class extends WrappedComponent {
    render() {
      const { style = {} } = config;
      const elementsTree = super.render();
      console.log(elementsTree, 'elementsTree');
      if (config.type === 'add-style') {
        return <div style={{...style}}>
          {elementsTree}
        </div>;
      }
      return elementsTree;
    }
  };

  export default hijackRenderHoc;
  //usual
  @hijackRenderHoc({type: 'add-style', style: { color: 'red'}})
  class Usual extends Component {
    ...
  }

我這里通過(guò)二階函數(shù),把config參數(shù)預(yù)制進(jìn)HOC, 算是一種柯理化的思想。
栗子很簡(jiǎn)單,這個(gè)hoc就是添加樣式的功能。但是它暴露出來(lái)的信息卻不少。首先我們可以通過(guò)config參數(shù)進(jìn)行邏輯判斷,有條件的渲染,當(dāng)然這個(gè)參數(shù)的作用很多,react-redux中的connect不就是傳入了props-key 嘛。再就是我們還可以拿到WrappedComponent的元素樹(shù),可以進(jìn)行修改操作。最后就是我們通過(guò)div包裹,設(shè)置了style。但其實(shí)具體如何操作還是根據(jù)業(yè)務(wù)邏輯去處理的...

[圖片上傳中...(image-936804-1521617092866-1)]

我的應(yīng)用場(chǎng)景

  • 通常我會(huì)通過(guò)高階組件去優(yōu)化之前老項(xiàng)目寫(xiě)的不好的地方,比如兩個(gè)頁(yè)面UI幾乎一樣,功能幾乎相同,僅僅幾個(gè)操作不太一樣,卻寫(xiě)了兩個(gè)耦合很多的頁(yè)面級(jí)組件。當(dāng)我去維護(hù)它的時(shí)候,由于它的耦合性過(guò)多,經(jīng)常會(huì)添加一個(gè)功能(這兩個(gè)組件都要添加),我要去改完第一個(gè)的時(shí)候,還要改第二個(gè)。而且有時(shí)候由于我的記性不好,會(huì)忘掉第二個(gè)... 就會(huì)出現(xiàn)bug再返工。更重要的是由于個(gè)人比較懶,不想去重構(gòu)這部分的代碼,因?yàn)闁|西太多了,花費(fèi)太多時(shí)間。所以加新功能的時(shí)候,我會(huì)寫(xiě)一個(gè)高階組件,往HOC里添加方法,把那兩個(gè)組件包裝一下,也就是屬性代理。這樣新代碼就不會(huì)再出現(xiàn)耦合,舊的邏輯并不會(huì)改變,說(shuō)不定哪天心情好就會(huì)抽離一部分功能到HOC里,直到理想的狀態(tài)。

  • 另一種情況就是之前寫(xiě)過(guò)一個(gè)組件A,做完上線(xiàn),之后產(chǎn)品加了一個(gè)新需求,很奇怪要做的組件B跟A幾乎一模一樣,但稍微有區(qū)別。那我可能就通過(guò)II的方式去繼承之前的組件A,比如它在didMount去fetch請(qǐng)求,需要的數(shù)據(jù)是一樣的。不同的地方我就會(huì)放到HOC里,存儲(chǔ)新的state這樣,再通過(guò)劫持渲染,把不同的地方,添加的地方進(jìn)行處理。但其實(shí)這算Hack的一種方式,能快速解決問(wèn)題,也反映了組件設(shè)計(jì)規(guī)劃之初有所不足(原因比較多)。

  • Container解決不了的時(shí)候甚至不太優(yōu)雅的時(shí)候。其實(shí)大部分時(shí)候包一層Container組件也能做到差不多的效果,比如操作props,渲染劫持。但其實(shí)還是有很大區(qū)別的。比如我們現(xiàn)在有兩個(gè)功能的container,添加樣式和添加處理函數(shù)的,對(duì)Usual進(jìn)行包裝。栗子:

    //usual
    class Usual extends Component {
    
      render() {
        console.log(this.props, 'props');
        return <div>
          Usual
        </div>
      }
    };
    export default Usual;
    //console - Object {handleClick: function}  "props"
    
    import React, { Component } from 'react';
    import Usual from './usual';
    
    class StyleContainer extends Component {
    
      render() {
        return (<div style={{ color: '#76d0a3' }}>
          <div>container</div>
          <Usual {...this.props} />
        </div>);
      }
    }
    
    export default StyleContainer;
    
    import React, { Component } from 'react';
    import StyleContainer from './container-add-style';
    
    class FuncContainer extends Component {
      handleClick() {
        console.log('click');
      }
    
      render() {
        const props = {
          ...this.props,
          handleClick: this.handleClick,
        };
        return (<StyleContainer {...props} />);
      }
    }
    
    export default FuncContainer;
    

    外層Container必須要引入內(nèi)層Container,進(jìn)行包裝,還有props的傳遞,同樣要注意包裝的順序。當(dāng)然你可以把所有的處理都放到一個(gè)Container里。那用HOC怎么處理呢,相信大家有清晰的答案了。

    const addFunc = WrappedComponent => class extends Component {
      handleClick() {
        console.log('click');
      }
    
      render() {
        const props = {
          ...this.props,
          handleClick: this.handleClick,
        };
        return <WrappedComponent {...props} />;
      }
    };
    
    const addStyle = WrappedComponent => class extends Component {
    
      render() {
        return (<div style={{ color: '#76d0a3' }}>
          <WrappedComponent {...this.props} />
        </div>);
      }
    };
    
    const WrappenComponent = addStyle(addFunc(Usual));
    
    class WrappedUsual extends Component {
    
      render() {
        console.log(this.props, 'props');
        return (<div>
          <WrappedComponent />
        </div>);
      }
    }
    

    顯然HOC是更優(yōu)雅一些的,每個(gè)HOC都定義自己獨(dú)有的處理邏輯,需要的時(shí)候只需要去包裝你的組件。相較于Container的方式,HOC耦合性更低,靈活性更高,可以自由組合,更適合應(yīng)付復(fù)雜的業(yè)務(wù)。當(dāng)然當(dāng)你的需求很簡(jiǎn)單的時(shí)候,還是用Container去自由組合,應(yīng)用場(chǎng)景需要你清楚。

注意點(diǎn)(約束)

其實(shí)官網(wǎng)有很多,簡(jiǎn)單介紹一下。

  • 最重要的原則就是,注意高階組件不會(huì)修改子組件,也不拷貝子組件的行為。高階組件只是通過(guò)組合的方式將子組件包裝在容器組件中,是一個(gè)無(wú)副作用的純函數(shù)

  • 要給hoc添加class名,便于debugger。我上面的好多栗子組件都沒(méi)寫(xiě)class 名,請(qǐng)不要學(xué)我,因?yàn)槲覍?shí)在想不出叫什么名了... 當(dāng)我們?cè)赾hrome里應(yīng)用React-Developer-Tools的時(shí)候,組件結(jié)構(gòu)可以一目了然,所以DisplayName最好還是加上。
    [圖片上傳中...(image-1527ba-1521617092866-0)]

  • 靜態(tài)方法要復(fù)制
    無(wú)論P(yáng)P還是II的方式,WrappedComponent的靜態(tài)方法都不會(huì)復(fù)制,如果要用需要我們單獨(dú)復(fù)制。

  • refs不會(huì)傳遞。 意思就是HOC里指定的ref,并不會(huì)傳遞到子組件,如果你要使用最好寫(xiě)回調(diào)函數(shù)通過(guò)props傳下去。

  • 不要在render方法內(nèi)部使用高階組件。簡(jiǎn)單來(lái)說(shuō)react的差分算法會(huì)去比較 NowElement === OldElement, 來(lái)決定要不要替換這個(gè)elementTree。也就是如果你每次返回的結(jié)果都不是一個(gè)引用,react以為發(fā)生了變化,去更替這個(gè)組件會(huì)導(dǎo)致之前組件的狀態(tài)丟失。

     // HOC不要放到render函數(shù)里面
    
     class WrappedUsual extends Component {
    
      render() {
        const WrappenComponent = addStyle(addFunc(Usual));
    
        console.log(this.props, 'props');
        return (<div>
          <WrappedComponent />
        </div>);
      }
    }
    
  • 使用compose組合HOC。函數(shù)式編程的套路... 例如應(yīng)用redux中的middleware以增強(qiáng)功能。redux-middleware解析

    const addFuncHOC = ...
    const addStyleHOC = ...//省略
    
    const compose = (...funcs) => component => {
      if (funcs.lenght === 0) {
        return component;
      }
      const last = funcs[funcs.length - 1];
      return funcs.reduceRight((res, cur) => cur(res), last(component));
    };
    
    const WrappedComponent = compose(addFuncHOC, addStyleHOC)(Usual);
    

    關(guān)于注意點(diǎn),官網(wǎng)有所介紹,不再贅述。鏈接

總結(jié)

高階組件最大的好處就是解耦和靈活性,在react的開(kāi)發(fā)中還是很有用的。
當(dāng)然這不可能是高階組件的全部用法。掌握了它的一些技巧,還有一些限制,你可以結(jié)合你的應(yīng)用場(chǎng)景,發(fā)散思維,嘗試一些不同的用法。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,673評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,610評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,939評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,668評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 56,004評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,173評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,705評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,426評(píng)論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,656評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,833評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,247評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,580評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,371評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,621評(píng)論 2 380

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

  • 在目前的前端社區(qū),『推崇組合,不推薦繼承(prefer composition than inheritance)...
    Wenliang閱讀 77,709評(píng)論 16 124
  • 高階組件是對(duì)既有組件進(jìn)行包裝,以增強(qiáng)既有組件的功能。其核心實(shí)現(xiàn)是一個(gè)無(wú)狀態(tài)組件(函數(shù)),接收另一個(gè)組件作為參數(shù),然...
    柏丘君閱讀 3,102評(píng)論 0 6
  • React高階組件探究 在使用React構(gòu)建項(xiàng)目的過(guò)程中,經(jīng)常會(huì)碰到在不同的組件中需要用到相同功能的情況。不過(guò)我們...
    緋色流火閱讀 2,580評(píng)論 4 19
  • 一個(gè)高階組件就是一個(gè)函數(shù),這個(gè)函數(shù)接受一個(gè)組件作為輸入,然后返回一個(gè)新的組件作為結(jié)果,而且,返回的新組件擁有了輸入...
    DCbryant閱讀 1,002評(píng)論 0 0
  • 高考結(jié)束當(dāng)天我們班級(jí)集會(huì),這次集會(huì)對(duì)于我們來(lái)說(shuō)非常有意義,在一起三年朝夕相處的老同學(xué)要看就要各奔東西了,心里有很多...
    Z奔跑的蝸牛閱讀 332評(píng)論 1 1