(轉)React學習筆記(乾貨滿滿)

1. React 長什麼樣

React 是 facebook 開源出來的一套前端方案,官網在 https://reactjs.org 。javascript

先看一個簡單的樣子:html

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
</head>
<body>
  <h1>Hello</h1>
  <div id="place"></div>
  <script type="text/javascript">
    var attribute = {
      className: 'text',
      style: {color: 'red'},
      id: 'me',
      href: 'http://' + 's.zys.me'
    };
    var inner = React.createElement('a', attribute, 'React');
    var instance = React.createElement('h1', null, '字符串在中間', inner);
    ReactDOM.render(instance, $('#place')[0]);
  </script>
</body>
</html>
 

從上面能夠看出,它的使用其實跟其它不少框架的方式差很少,即,找到一個 DOM 節點,而後在這個節點上作事,相似 jQuery 的 $(dom).xxx({}) 。前端

在 React.createElement 中,第一個參數是標籤名,第二個是 config ,其中包括了節點屬性(class 由於關鍵詞問題改用 className) ,第三個及之後,是子級節點,或者說子級元素。java

上面兩個 createElement 獲得的最終結果,是:node

 
<h1>
    字符串在中間
    <a class="text" style="color: red" id="me" href="http://s.zys.me">React</a>
</h1>
 

固然,這裏的重點,不是 React.createElement ,而是它返回的那個 instance ,事實上,從源碼 https://unpkg.com/react@16.2.0/umd/react.development.js 中看的話,能夠看出它返回的是一個 ReactElement ,這個 ReactElement 就是整個 React 體系於衆不一樣的地方,不然它直接返回一個 DOM Element 就行了。react

最後一行的 ReactDOM.render 就是處理 ReactElement 的, ReactDOM 的源碼在 https://unpkg.com/react-dom@16.2.0/umd/react-dom.development.js , 這個東西本身實現了一套掛節點的樹結構(所謂的 Virtual DOM),中間隔了一層再去處理真實的 DOM 渲染。這麼作的好處是,由於本身有一套完整的樹結構的,因此,能夠在上面實現不少在原始 DOM 結構上不能作,或者不方便作的事。好比,異構渲染之類的。固然,侷限的地方也很明顯,本質上仍是「節點」,其實沒有一個往上的抽象,暴露的細節仍是不少的,這種狀況下,東西能夠作得比較細,可是付出的成本也比較大,我是這樣預測的。jquery

2. ReactElement 的定義

最開始,已經簡單演示了針對像 a h1 這類原生 DOM 元素的 ReactElement 定義, 天然,「能接受一個肯定值的地方也應該能夠接受一個函數」是 js 的一個慣例吧:webpack

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
</head>
<body>
  <div></div>
  <script type="text/javascript">
    function Component(props) {
      if(props.tag === 'h1') {
        return React.createElement('h1', null, '大標題');
      } else {
        return React.createElement('span', null, '普通文本');
      }
    }
    var instance = React.createElement(Component, {tag: 'h1'});
    ReactDOM.render(instance, $('div')[0]);
  </script>
</body>
</html>
 

上面代碼的過程,大概是:git

  • React.createElement(type, config, children) 返回 ReactElement() 。
  • ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props) ,上面 config 的一些東西,會補爲這裏的幾個參數,及 props 裏的東西。
  • ReactElement() 的調用會返回一個 element ,這是一個比較單純的結構體,裏面會有 typekey ref props _owner $$typeof 等內容。
  • ReactDOM.render(element, container, callback) 是對 renderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) 的調用, element 也轉爲 children 的統一律唸了。
  • 而後是到 DOMRender (從 reactReconciler 來) 的 updateContainer(element, container, parentComponent, callback) ,最外層的調用, container 就是一個 newRoot ,前面的 children概念這裏又變成 element 了。
  • 上面會處理一下 container ,接着到 scheduleTopLevelUpdate(current, element, callback) , current 是從 container 裏取的。
  • scheduleTopLevelUpdate 是會做 scheduleWork(current, expirationTime) ,應該是處理一些同步的事。還有 構造一個 update 的結構體,而後 insertUpdateIntoFiber(current, update) , element被放到 update 中的 partialState: { element: element } 。
  • insertUpdateIntoFiber(fiber, update) , 新的 element 在 update 中,而 fiber 是 container的範疇。這裏只是處理「兩個隊列」的狀態,幹活的仍是在 scheduleWork() 中。
  • 這個地方開始, current 和 element 就分開了,current 繼續進入 scheduleWork(fiber, expirationTime) 進行下一步處理。而 element 只在 insertUpdateIntoFiber 中更新於隊列狀態。
  • scheduleWork() 裏實際上會用 scheduleWorkImpl(fiber, expirationTime, isErrorRecovery) ,這裏開始迭代 fiber ,離真實的節點建立還有一半的路要走吧,中間又各類處理,最後 createInstance() 會建立真實的節點。

反正,我從源碼中找了一長串,仍是沒搞明白 createElement() 第一個參數的 type 能夠是,應該是一個什麼東西。github

從官方的文檔來看 https://reactjs.org/docs/react-api.html#createelement ,這個 type 能夠是三類東西:

  • 原生類節點,的標籤字符串,好比 div , span 。
  • React component , React 組件。
  • React fragment ,這好像是 v16.2.0 加入的新機制,簡單來講它使 React 能夠支持直接渲染「一串」節點,而以前,只能直接渲染「一個」節點,若是是一串的需求,那麼在外面須要包一個節點。我理解大概就是下面這個樣子吧:
 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
</head>
<body>
  <div></div>
  <script type="text/javascript">
    function Component(props) {
      return ['1', React.createElement('h1', null, props.text)];
    }
    var instance = React.createElement(Component, {text: '啊啊啊'});
    ReactDOM.render(instance, $('div')[0]);
  </script>
</body>
</html>
 

React 組件有兩種類型,一種是簡單的「函數式組件」,另外一種是複雜點的「類組件」。

2.1. 函數式組件

函數式組件就是接受 props ,而後返回一個 React element :

 
function Component(props) {
  return React.createElement('h1', null, props.text);
}
 

2.2. 類組件

類組件,是繼承 React.Component ,而後重寫一些方法:

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://unpkg.com/react@16.2.0/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16.2.0/umd/react-dom.development.js"></script>
</head>
<body>
  <div></div>
  <script type="text/javascript">
    class MyComponent extends React.Component {
      render(){
        return React.createElement('h1', null, this.props.text);
      }
    }
    var instance = React.createElement(MyComponent, {text: '哈哈'});
    ReactDOM.render(instance, $('div')[0]);
  </script>
</body>
</html>
 

3. createElement 的 DSL 方案 JSX

前面提過,若是你想經過 createElement 構造一個相似:

 
<h1>
    字符串在中間
    <a class="text" style="color: red" id="me" href="http://s.zys.me">React</a>
</h1>
 

那麼你須要的 js 大概是:

 
var attribute = {
  className: 'text',
  style: {color: 'red'},
  id: 'me',
  href: 'http://' + 's.zys.me'
};
var inner = React.createElement('a', attribute, 'React');
var instance = React.createElement('h1', null, '字符串在中間', inner);
ReactDOM.render(instance, $('#place')[0]);
 

看起來是比較麻煩的,於 React 中引入了一個新的東西,JSX ,即 JavaScript XML ,簡單來講,就是 javascript 和 XML 的混寫,上面的代碼能夠寫成:

 
var inner = <a className="text" style={ {color: 'red'} } id="me" href={'http://' + 's.zys.me'}>React</a>;
var instance = <h1>字符串在中間 {inner}</h1>;
ReactDOM.render(instance, $('#place')[0]);
 

甚至是:

 
var inner = <a className="text" style={ {color: 'red'} } id="me" href={'http://' + 's.zys.me'}>React</a>;
ReactDOM.render(<h1>字符串在中間 {inner}</h1>, $('#place')[0]);
 

能夠看出,比原來直接寫 createElement 的方式要簡潔得多, JSX 除了支持直接寫 XML 風格的標籤,還可使用 {} 處理表達式。不過, JSX 並非瀏覽器的標準,要讓它跑在瀏覽器,須要在發佈前專門編譯到純 js ,平時測試,也能夠直接用 babel 來跑:

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/babel-standalone@6.15.0/babel.js"></script>
</head>
<body>
  <h1>Hello</h1>
  <div id="place"></div>
  <script type="text/babel">
    var inner = <a className="text" style={ {color: 'red'} } id="me" href={'http://' + 's.zys.me'}>React</a>;
    ReactDOM.render(<h1>字符串在中間 {inner}</h1>, $('#place')[0]);
  </script>
</body>
</html>
 

注意,上在的 script 的 type 寫的是 text/babel 哦。

babel 也有命令行的工具,先安裝:

 
npm install -g babel
npm install -g babel-cli
npm install -g babel-preset-react
 

而後對於一個 demo.jsx :

 
var inner = <a className="text" style={ {color: 'red'} } id="me" href={'http://' + 's.zys.me'}>React</a>;
ReactDOM.render(<h1>字符串在中間 {inner}</h1>, $('#place')[0]);
 

可使用 babel 編譯它:

 
babel --presets=react demo.jsx
 

就能夠看到標準的 js 輸出了:

 
var inner = React.createElement(
  "a",
  { className: "text", style: { color: 'red' }, id: "me", href: 'http://' + 's.zys.me' },
  "React"
);
ReactDOM.render(React.createElement(
  "h1",
  null,
  "\u5B57\u7B26\u4E32\u5728\u4E2D\u95F4 ",
  inner
), $('#place')[0]);
 

babel 默認輸出的到標準輸出,能夠經過 -o 輸出到指定文件。

平時的前端項目,通常把 babel 和 webpack 結合使用,在構建過程處理 JSX 格式。

3.1. JSX 中的名字

JSX 中的名字,若是是自定義組件,名字開頭須要大寫,小寫的是 DOM 標籤。

名字中,能夠帶名字空間:

 
<MyComponent.A.B />
 

也能夠是變量值:

 
let My = [MyComponent][0];
return <My />
 

可是,不能帶表達式(下面是錯誤的):

 
<[MyComponent][0] />
 

提示一下, JSX 不是模板,它的行爲,更像是「宏」。本質,仍是 js 代碼。 宏是沒有運行時能力的,天然沒法求值表達式。

4. 使用 state 處理數據單向綁定

前面已經提到過 props 了, props 通常處理初始化後就不改變的數據,對於一個組件中要變化的數據,須要放在 state 中處理,由於 React 專門設計了組件的 setState() 來維護 state (不要直接更改 state ,而是使用 setState()) ,同時 setState() 還處理節點渲染相關的事,經過函數調用,來實現「數據到展示」的單向綁定。

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/babel-standalone@6.15.0/babel.js"></script>
</head>
<body>
  <div id="place"></div>
  <script type="text/babel">

    class MyComponent extends React.Component {
      constructor(props) {
          super(props);
          this.state = {date: (new Date()).valueOf()};
          setInterval(() => this.setState({date: (new Date()).valueOf()}), 1000);
      }

      render(){
        return <h1>Hello {this.props.name} {this.state.date}</h1>;
      }

    }

    ReactDOM.render(<MyComponent name="哈哈" />, $('#place')[0]);
  </script>
</body>
</html>
 

4.1. setState

setState() 也能夠接受一個函數,這個函數的參數第一個是當前的 state ,另外一個是 props :

 
class MyComponent extends React.Component {
  constructor(props) {
      super(props);
      this.state = {date: (new Date()).valueOf()};
      setInterval(() => this.setState((prevState, props) => ({date: prevState.date + 1000})), 1000);
  }

  render(){
    return <h1>Hello {this.props.name} {this.state.date}</h1>;
  }

}

ReactDOM.render(<MyComponent name="哈哈" />, $('#place')[0]);
 

5. 事件處理

React 本身維護了一個節點狀態的樹結構,而且,同時也本身實現了一套事件機制,只是這套事件,跟瀏覽器自身的區別不是很大:

 
class MyComponent extends React.Component {
  constructor(props) {
      super(props);
      this.state = {date: (new Date()).valueOf()};
      setInterval(() => this.setState((prevState, props) => ({date: prevState.date + 1000})), 1000);
  }

  handleClick = () => {
      console.log(this);
  }

  render(){
      return <h1 onClick={this.handleClick}>Hello {this.props.name} {this.state.date}</h1>;
  }

}

ReactDOM.render(<MyComponent name="哈哈" />, $('#place')[0]);
 

注意那個 handleClick 的寫法:

 
handleClick = () => {
    console.log(this);
}
 

5.1. this 的問題

若是不用「箭頭函數」把 this 固定住了,那麼在使用時就須要顯式綁定:

 
handlerClick(){
    console.log(this);
}

render(){
    return <h1 onClick={this.handleClick.bind(this)}>Hello {this.props.name} {this.state.date}</h1>;
}
 

React 本身的事件,是「駝峯式」的名字,好比 onClick ,而 onclick 則是瀏覽器本身的事件了。

其它支持的完整的事件列表,見: https://reactjs.org/docs/events.html

6. 列表與 key

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script crossorigin src="https://unpkg.com/babel-standalone@6.15.0/babel.js"></script>
</head>
<body>
  <div id="place"></div>
  <script type="text/babel">

    class MyComponent extends React.Component {
      constructor(props) {
          super(props);
          this.state = {itemList: [{name: 'abc', id: 'a'}, {name: 'oiii', id: 'b'}] };
      }

      handleClick(o){
        return (e) => {
          console.log(o);
        }
      }

      render(){
        return <div>
          {this.state.itemList.map( (o) => (<div key={o.id} onClick={this.handleClick(o)}>{o.name}</div>) ) }
          </div>
      }

    }

    ReactDOM.render(<MyComponent name="哈哈" />, $('#place')[0]);
  </script>
</body>
</html>
 

對於列表的渲染, React 要求節點上須要添加 key 屬性,不然,會報 warn (是的,不會報錯)。若是沒有明確的 id 之類的標識,可使用序列的索引值 index 。這個 key 屬性的做用, React 會用於標識節點,以便在數據變動時,對應地能夠追蹤到節點的變動狀況。

7. 組件的各狀態 Hook (生命週期)

對於一個 React 組件,準備說,「類組件」,它在渲染及銷燬過程當中,每一個狀態轉換時, React 都預留了相關的 Hook 函數,這些函數,一共 10 個,能夠分紅 4 類:

  • 渲染時
    • constructor(props)
    • componentWillMount()
    • render()
    • componentDidMount() ,從這裏開始, setState() 會觸發從新的 render() 。
  • 修改時
    • componentWillReceiveProps(nextProps) , props 改變時觸發,能夠因爲父組件的變化引發。 setState() 通常不會觸發這個過程。
    • shouldComponentUpdate(nextProps, nextState) ,若是返回 false ,則使 React 「儘可能」 不要從新渲染組件,注意,只是 「儘可能」 而已。若是渲染上有性能問題,則應該考慮其它解決方案。
    • componentWillUpdate(nextProps, nextState) ,上面的 shouldComponentUpdate() 若是返回 false ,則這個過程必定不會觸發,現時,第一次初始化不會觸發這個過程。
    • render()
    • componentDidUpdate(prevProps, prevState) ,第一次初始化不會觸發這個過程。
  • 銷燬時
    • componentWillUnmount()
  • 出錯時
    • componentDidCatch(error, info)

這幾個方法的一些細節:

componentWillReceiveProps

  • 初始化時不會被調用。
  • 能夠在這裏做 setState() 。
  • props 沒變化時也可能被調用。

componentWillUpdate

  • 初始化時不會被調用。
  • 不能在這裏做 setState() , 不然會再次觸發變化流程,形成死循環。
  • shouldComponentUpdate() 爲 false 時不會調用。

componentDidUpdate

  • 初始化時不會被調用。
  • 遠程請求常放在這裏,由於能夠先判斷 props 的相應變化後再決定是否做新的遠程調用。

除了上面講的過程 Hook 函數,及前面用過的 setState() ,組件還有一個 forceUpdate(callback)方法,它的調用能夠觸發 render() ,而且跳過 shouldComponentUpdate() 。

另外,在「類」層面,有兩個屬性, defaultProps 用於定義默認的 props , displayName ,用於定義調試時顯示的名字。

試試各個過程的表現吧:

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script>
</head>
<body>
  <div></div>
  <script type="text/babel">
    class MyComponent extends React.Component {
      constructor(props){
        super(props);
        this.state = {name: '初始化'};
        let that = this;
        setTimeout(function(){
          console.log('2s go ...');
          that.setState({name: '修改了的'});
        }, 2000);
        console.log('constructor');
      }

      componentWillMount(){
        console.log('componentWillMount');
      }

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

      componentWillReceiveProps(nextProps){
        console.log('componentWillReceiveProps');
      }

      shouldComponentUpdate(nextProps, nextState){
        console.log('shouldComponentUpdate');
        return true;
      }

      componentWillUpdate(nextProps, nextSate){
        console.log('componentWillUpdate');
      }

      componentDidUpdate(prevProps, prevState){
        console.log('componentDidUpdate');
      }

      componentWillUnmount(){
        console.log('componentWillUnmount');
      }

      componentDidCatch(error, info){
        console.log('componentDidCatch');
      }

      render(){
        console.log('render');
        if(this.props.error){throw Error()};
        return <h1>{this.state.name}</h1>;
      }

    }
    ReactDOM.render(<MyComponent />, $('div')[0]);

    setTimeout(function(){
      console.log('5s go ...');
      ReactDOM.render(<MyComponent />, $('div')[0]);
    }, 5000);

    setTimeout(function(){
      console.log('10s go ...');
      ReactDOM.render(null, $('div')[0]);
    }, 10000);

  </script>
</body>
</html>
 

上面的代碼會輸出:

 
constructor
componentWillMount
render
componentDidMount
2s go ...
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
5s go ...
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
10s go ...
componentWillUnmount
 

上面過程看起來都很好理解的。

少的那個 componentDidCatch 是用來抓子組件錯誤的:

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script>
</head>
<body>
  <div></div>
  <script type="text/babel">
    class ErrorWrapper extends React.Component {
      constructor(props){
        super(props);
        this.state = {error: null};
      }

      componentDidCatch(error, info){
        console.log('componentDidCatch');
        console.log(error, info);
        this.setState({error: {error: error, info: info}});
      }

      render(){
        if(!!this.state.error) {
          return <h1>Error Here</h1>
        } else {
          return this.props.children || '';
        }
      }
    }

    class MyComponent extends React.Component {
      render(){
        throw Error();
      }
    }

    ReactDOM.render(<ErrorWrapper><MyComponent /></ErrorWrapper>, $('div')[0]);

  </script>
</body>
</html>
 

上面代碼佔, MyComponent 的錯誤,會觸發 ErrorWrapper 的 componentDidCatch ,參數中的 error 是一串調用棧字符串, info 是一個結構,裏面有一個 componentStack 字符串記錄了組件序列。

8. 外圍函數

React 對於 Component 的處理,前面介紹得差很少了,這裏說的外圍函數,是指尚未脫離 DOM 的那幾個工具,好比前面用到的 ReactDOM.render(child, container) 就是一個典型。

  • ReactDOM.unmountComponentAtNode(container) , 從一個節點中刪除組件。
  • ReactDOM.findDOMNode(component) ,找到組件「渲染的內容」所在的 DOM 節點,注意,不是組件本身所在的節點。
  • ReactDOM.createPortal(child, container) ,在 render() 中,取代返回 ReactElement ,而在一個確認的位置渲染出組件。(好比「彈層」的那種狀況)

前 2 個方法是比較好理解的:

 
class MyComponent extends React.Component {
  render(){
    return <h1 onClick={this.clickHandler}>哈哈哈</h1>;
  }

  clickHandler = () => {
    var dom = ReactDOM.findDOMNode(this);
    console.log(dom);
    ReactDOM.unmountComponentAtNode($('div')[0]);
  }
}

ReactDOM.render(<MyComponent />, $('div')[0]);
 

8.1. ReactDOM.createPortal

ReactDOM.createPortal(child, container) 通常用在須要本身建立並維護真實 DOM 節點的地方:

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script>
</head>
<body>
  <div></div>
  <script type="text/babel">

    class MyComponent extends React.Component {
      constructor(props){
        super(props);
        this.dom = document.createElement('div');
      }

      componentDidMount(){
        $('body').append($(this.dom));
      }

      componentWillUnmount(){
        $(this.dom).remove();
      }

      clickHandler = () => {
        var dom = ReactDOM.findDOMNode(this);
        console.log(dom);
        ReactDOM.unmountComponentAtNode($('div')[0]);
      }
      
      render(){
        return ReactDOM.createPortal(<div onClick={this.clickHandler}>{this.props.children}</div>, this.dom);
      }

    }

    ReactDOM.render(<MyComponent><h1>彈出來的東西</h1></MyComponent>, $('div')[0]);

  </script>
</body>
</html>
 

上面的代碼演示了 ReactDOM.createPortal 的使用,須要注意的是,即便一個組件的 render() 實現,不是 ReactElement 而是 ReactDOM.createPortal ,組件自己,仍是須要依附於父組件,或者 ReactDOM.render() 的,即坑位在佔住,這種時候,就有點「它在那裏,它又不在那裏」的感受。

9. DOM 元素本身的那些東西

React 的實現的基礎是本身實現的一套「樹狀節點結構」,而後再把這個結構放到瀏覽器中用真實的 DOM 展示出來。那麼這中間,本身搞的一套,與瀏覽器環境下的一套,之間確定會有少量的差別的,雖然 React 連事件這種都本身實現了一遍。

首先,對於節點的屬性來講, React 的屬性都是「駝峯式」的名字,與 DOM 本身的一些全小寫屬性名不一樣,好比, onclick 變成了 onClick , tabindex 變成了 tabIndex , SVG 中的屬性 React 也支持。

屬性名中的 class ,由於關鍵詞因素,在 React 中變成了 className 。

style 在 React 中做了擴展實現的,給它一個對象值的話,能夠獲得正確的樣式。

onChange 事件在 React 中的實現也做了加強,能夠實時觸發相應的回調。

label 標籤的 for 屬性,在 React 中可使用 htmlFor 代替。

9.1. dangerouslySetInnerHTML

dangerouslySetInnerHTML 是一個特殊的屬性,能夠用於直接添加原始 HTML 內容:

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script>
</head>
<body>
  <div></div>
  <script type="text/babel">

    class MyComponent extends React.Component {
      getHtml = () => {
        return {__html: '<h1 style="color: red">哈哈哈</h1>'};
      }
      
      render(){
        return <div dangerouslySetInnerHTML={this.getHtml()} />
      }

    }

    ReactDOM.render(<MyComponent />, $('div')[0]);

  </script>
</body>
</html>
 

9.2. ref

ref 也是 React 中的一個特殊屬性,用於回調真實 DOM 節點的引用:

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script>
</head>
<body>
  <div></div>
  <script type="text/babel">

    class MyComponent extends React.Component {
      clickHandler = () => {
        console.log(this.h1);
      }

      render(){
        return <h1 ref={(node) => {this.h1 = node}} onClick={this.clickHandler}>哈哈</h1>
      }

    }

    ReactDOM.render(<MyComponent />, $('div')[0]);

  </script>
</body>
</html>
 

ref 能夠用於控制焦點,或者在節點上使用瀏覽器的其它一些 API ,好比音頻,視頻等。

10. 測試工具

React 由於有本身的一套樹狀態,因此,理論上它在測試方面有着得天獨厚的優點,由於即便是在瀏覽器環境,不涉及 DOM 相關 API 的行爲,只須要在那個樹狀態中的去操做,並檢查操做後的狀態就能夠達到測試目的。固然,咱們也不用關心 React 的一些實現細節,如今說測試,會有兩方面的內容。一方面是 React 提供一些測試相關的工具方法,只是工具方法,若是你要創建完整的測試體系,還須要本身解決斷言庫,測試用例組件與調度等問題。另外一方面,是看看非瀏覽器的渲染,若是能夠徹底擺脫瀏覽器環境,完成大部分的測試工做,那將是很美好的一件事。

11. 測試 API

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script>
<script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom-test-utils.development.js"></script>
</head>
<body>
  <div></div>
  <script type="text/babel">

    class MyComponent extends React.Component {
      constructor(props){
        super(props);
        this.state = {n: 0};
      }

      clickHandler = () => {
        this.setState((s) => ({n: s.n + 1}));
      }

      render(){
        return <h1 onClick={this.clickHandler}>{ this.state.n }</h1>
      }

    }

    let component = ReactTestUtils.renderIntoDocument(<MyComponent />);
    console.log(component);
    let node = ReactDOM.findDOMNode(component)
    console.log(component.state);
    ReactTestUtils.Simulate.click(node);
    console.log(component.state);

    console.log(ReactTestUtils.isElement(<MyComponent />));
    console.log(ReactTestUtils.isElementOfType(<MyComponent />, MyComponent));
    console.log(ReactTestUtils.isDOMComponent(ReactTestUtils.renderIntoDocument(<h1>xx</h1>)));
    console.log(ReactTestUtils.isCompositeComponent(component));
    console.log(ReactTestUtils.isCompositeComponentWithType(component, MyComponent));
    console.log(ReactTestUtils.findRenderedDOMComponentWithTag(component, 'h1'));
    console.log(ReactTestUtils.findRenderedComponentWithType(component, MyComponent));

  </script>
</body>
</html>
 

引入額外的資源文件,相關的方法在瀏覽器是徹底能夠用的。

不過,完成的測試體系的話,還須要搭配其它工具來完成, React 的相關 API 只是提供了獲取信息的方法。

11.1. 非瀏覽器渲染

在 nodejs 環境,可使用 react-test-renderer 來完成對相關組件的處理,並按樹結構遍歷結果:

 
const React = require('react');
const TestRenderer = require('react-test-renderer');

class MyComponent extends React.Component {
  constructor(props){
    super(props);
    this.state = {number: 0};
  }
  render(){
    return <h1 onClick={ () => {this.setState((s) => ({number: s.number + 1}))} }>{this.state.number}</h1>;
  }
}


const testRenderer = TestRenderer.create(<MyComponent />);
testRenderer.root.findByType('h1').props.onClick();
console.log(testRenderer.root.findByType('h1').children);
console.log(testRenderer.root.instance.state);
 

這部分完整的 API 在 https://reactjs.org/docs/test-renderer.html 。

在非瀏覽器環境下,「模擬點擊」這種操做是沒有意義的,可是 onClick 自己並無說必定是「點擊」,它只是指明瞭一個回調函數而已,瀏覽環境下是點擊觸發,那麼純編程環境下,直接調用便可。

12. Redux

12.1. 基本概念

Redux 並非 React 必不可少的一部分,它只是隨着 React 出來的一套模式,而且帶了一個這個模式的實現: https://redux.js.org/ 。 簡單來講,這個模式是着眼於「組件狀態及組件間通訊」的,換句話,它定義了一種「相對獨立的組件句柄」。

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>Redux</title>
<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
</head>
<body>

  <script type="text/javascript">
    var store = Redux.createStore(function reducer(state, action){
      console.log('here', state);
      if(action.type === 'HAHA'){ return 'OK' }
      return 'ERROR';
    });
    console.log(store.getState());
    store.subscribe(function render(){
      console.log('subscribe');
      console.log(store.getState());
    })
    store.dispatch({type: 'HAHA'});
  </script>

</body>
</html>
 

Redux 的流程自己很是簡單,如上所示,經過一個 reducer 初始化一個 store , 這個 store 先 subscribe 一些動做,而後使用時在須要的時候 dispatch 出一個 action 就行了。 dispatch 的對應邏輯是 reducer 中的事,而 reducer 執行完就輪到 subscribe 的回調了,就是這樣的一個執行循環。

12.2. React-Redux

React-Redux 是 Redux 在 React 上的一個實現,直觀點說,就是結合了 React 的 Component 機制的一套 Redux 流程。它的核心點,我我的的理解是下面幾點:

  • 首先,組件自己進行兩分類,一類「純組件」,一類「通訊組件」。「純組件」的意思,就是參入 props 以後,它本身就能夠徹底獨立工做的,裏面儘可能不包含業務邏輯。而「通訊組件」的做用,就是在「純組件」之上,加入具體業務邏輯,去完成各「純組件」的調度。
  • 在實現「通訊組件」上,並不推薦你去手寫,而是根據 Redux 的思路, React-Redux提供相應用的包裝 API ,來完成 State -> Props 和 Dispatch -> Props 的這個過程。
  • 進一步說,「純組件」的行爲,徹底由 props 控制,不管是相應的數據輸入,仍是一些事件回調的輸出。狀態性的一些東西,則由上層的「通訊組件」,經過 state 和 dispatch 來維護,上層通訊組件的狀態性的數據,會更新到「純組件」的 props 上,來完成頁面重渲染。
  • 本質上,是實現上的一種垂直抽象分層, Redux 的具體 API 不是重點。

具體代碼:

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/redux.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react-redux.min.js"></script>
</head>
<body>
  <div id="app"></div>
  <script type="text/babel">

    const Title = props => (
      <h1 onClick={props.onClick}>{props.name}</h1>
    )

    const mapStateToProps = state => {
      return {...state};
    }

    const mapDispatchToProps = dispatch => {
      return {
        onClick: id => {
          dispatch({type: "CLICK"});
        }
      }
    }

    const VisibleTitle = ReactRedux.connect( mapStateToProps, mapDispatchToProps )(Title);

    let store = Redux.createStore( (state, action) => {
      if(action.type === 'CLICK'){
        return {name: '點擊以後的'};
      }
      return {name: '初始化的'};
    });

    const App = () => (
      <ReactRedux.Provider store={store}>
        <VisibleTitle />
      </ReactRedux.Provider>
    )

    ReactDOM.render(<App />, document.getElementById('app'));
  </script>
</body>
</html>
 

上面的組件實現的效果很簡單,就是點擊以後,改變文本。可是通過 React-Redux 分拆以後,感受複雜了好多。不過,直觀地也能夠看出,分拆以後, Title 這個組件變「純」了,它的行爲一目瞭解,沒有什麼「點擊以後,改變文本」這類邏輯,只有單純的接受輸入,而後渲染組件。操做上的邏輯,則由外層的 VisibleTitle 來完成。

VisibleTitle 經過它的 state 狀態性數據,來控制下面的 Title 的渲染。 state 則是經過 Redux 的 store 來承接的。進一步, state 到 Title 的 props ,可能只是 Title 須要輸入的一部分(數據部分),另外一部分涉及「反饋」到 state 變化的 props ,則由 Redux 的 dispatch - action 來承接。

說的這兩部分,也就對應着 mapStateToProps 和 mapDispatchToProps ,一個處理數據,一個調度 dispatch 的執行。而後使用 connect 這個現成的方法生成包裹在 Title 之上的 VisibleTitle 。

固然,事情到這裏尚未結束,再上層,還有一個 store , Provider 是 React-Redux 提供的東西,目的是給裏面的組件提供 store 。而獲得 store ,又須要 Reducer , dispatch 出的 action ,及 state 的變化其實是在這裏處理。

12.3. Redux 中間件

Redux 的設計中,全部的狀態變化是經過 dispatch 這一個點完成的(而且這個 API 又很是簡單),天然,圍繞這一個點,就能夠很容易完成「套圈」。

 
let next = store.dispatch;
store.dispatch = function(action){
    console.log(action);
    next(action);
}
 

這都徹底不須要特別設計,你本身就能夠隨便搞。

不過從 Monkeypatching 做爲切入,官方的 https://redux.js.org/docs/advanced/Middleware.html 這份文檔寫得很是好,按部就班,推薦一讀。

最後,官方 API 在這上面的支持,或者說推薦做法,是經過高階函數定義 Middleware ,而後使用 applyMiddleware 掛到 store 裏面去。

 
const logger = store => next => action => {
  console.log('dispatching', action, store.getState());
  let result = next(action);
  console.log('next state', store.getState());
  return result
}
 

這是定義,使用時:

 
import { createStore, combineReducers, applyMiddleware } from 'redux'
let store = createStore( reducer, applyMiddleware(logger) )
 

用前面的簡單例子能夠試試:

 
function logger(store) {
  return function(next){
    return function(action){
      console.log('dispatching', action, store.getState());
      var result = next(action);
      console.log('next state', store.getState());
      return result
    }
  }
}

var store = Redux.createStore(function reducer(state, action){
  if(action.type === 'HAHA'){ return 'OK' }
  return 'ERROR';
}, Redux.applyMiddleware(logger));

store.subscribe(function render(){
  console.log('sub', store.getState());
})
store.dispatch({type: 'HAHA'});
 

12.4. 異步流程與 redux-promise

瞭解了中間件,再來看異步流程,就沒什麼新問題了。

在 Redux 中,state 的維護是在 reducer 中的,從前面的例子能夠看出, reducer 自己的設計,是沒有考慮異步狀況的,它的結構: (state, action) => newState 是同步的。

若是咱們在某個 dispatch 以後,須要異步獲取數據(事實上這很常見),並把這個東西更新到 store 中去,咱們就須要額外作點事,比較很容易想到的一個辦法:

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>Redux</title>
<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
</head>
<body>

  <script type="text/javascript">
    var store = Redux.createStore(function reducer(state, action){
      if(action.type === 'HAHA'){
        setTimeout(function(){
          store.dispatch({type: 'FETCH', payload: '123'});
        }, 3000);
        return 'LOADING';
      }
      if(action.type === 'FETCH'){
        return action.payload;
      }
      return 'ERROR';
    });
    store.subscribe(function render(){
      console.log('sub', store.getState());
    })
    store.dispatch({type: 'HAHA'});
  </script>

</body>
</html>
 

簡單來講,在 dispatch 以後,咱們異步發出請求,並在當前至少有對 dispatch 的引用,而後返回一箇中間狀態的 state ,甚至是沒有任何更改的 state 。在異步響應以後,再次 dispatch ,同時把異步響應的數據放到 action 中傳遞出去。

這種方式,雖然繞了一點,可是自己沒毛病,不過要多定義出來一個 dispatch 類型,是很煩人就是了。

考慮前面講過的「中間件」機制,咱們很容易對 dispatch 的行爲做一些擴展,加入對異步的支持。

先看「中間件」的樣子:

 
const logger = store => next => action => {
  console.log('dispatching', action, store.getState());
  let result = next(action);
  console.log('next state', store.getState());
  return result
}
 

很顯然,對於 dispatch 傳入的 action 這個東西,咱們能夠先判斷它的特徵,或者說狀態,根據既定規則決定在何時做 next(action) 。

事實上, redux-promise 的代碼就幾行:

 
import { isFSA } from 'flux-standard-action';

function isPromise(val) {
  return val && typeof val.then === 'function';
}

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action)
        ? action.then(dispatch)
        : next(action);
    }

    return isPromise(action.payload)
      ? action.payload.then(
          result => dispatch({ ...action, payload: result }),
          error => {
            dispatch({ ...action, payload: error, error: true });
            return Promise.reject(error);
          }
        )
      : next(action);
  };
}
 

作的事,就是當 dispatch 出去的東西,裏面有一個 then 成員,而且這個成員是一個函數的話,那麼 reducer 會返回這個函數的調用結果,而且調用這個函數時傳入的是當前的 dispatch 。

簡單演示一下大概是:

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>Redux</title>
<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
</head>
<body>

  <script type="text/javascript">
    var store = Redux.createStore(function reducer(state, action){
      if(!!action.then){
        return action.then(store.dispatch.bind(store));
      }
      return 'GOT: ' + action.payload;
    });
    store.subscribe(function render(){
      console.log('sub', store.getState());
    })

    store.dispatch({type: '123', payload: 'null'});
    setTimeout(function(){
      store.dispatch({type: 'async', then: function(dispatch){
        setTimeout(function(){
          dispatch({type: 'xxx', payload: 'async data'});
        }, 3000);
      }});
    }, 2000);

  </script>

</body>
</html>
 

12.5. Redux 流程中的重複計算問題

前面一個例中,有一個 mapStateToProps ,把例子改一點點看看:

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/redux.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react-redux.min.js"></script>
</head>
<body>
  <div id="app"></div>
  <script type="text/babel">

    const getName = (name, count) => {
      console.log('getName');
      return name + count;
    }

    const Title = props => (
      <div>
        <h1 onClick={props.onClick}>{props.name}</h1>
        <h1 onClick={props.onOtherClick}>另外的 {props.name}</h1>
      </div>
    )

    const mapStateToProps = (state, props) => {
      console.log('mapStateToProps');
      return {
        name: getName(state.name, state.count)
      }
    }

    const mapDispatchToProps = dispatch => {
      return {
        onClick: () => {
          dispatch({type: "CLICK"});
        },
        onOtherClick: () => {
          dispatch({type: "OTHER_CLICK"});
        }
      }
    }

    const VisibleTitle = ReactRedux.connect( mapStateToProps, mapDispatchToProps )(Title);

    let store = Redux.createStore( (state = {name: '初始值', count: 0}, action) => {
      if(action.type === 'CLICK'){
        return { ...state, name: '點擊以後的', count: state.count + 1};
      }
      if(action.type === 'OTHER_CLICK'){
        return { ...state, other: '什麼東西'};
      }
      return { ...state };
    });

    const App = () => (
      <ReactRedux.Provider store={store}>
        <VisibleTitle />
      </ReactRedux.Provider>
    )

    ReactDOM.render(<App />, document.getElementById('app'));
  </script>
</body>
</html>
 

上面的例子,簡單來講,「純組件」須要一個 name 來顯示,而這個 name 的來自於外套組件的 store 中的 name 和 count 連在一塊兒的。

執行這個例子,能夠看出一個重複執行的問題,當你點擊第二行「另外的」那塊文本時,是作的 onOtherClick 回調,這個過程並不會更改 count 值,也就是說,「純組件」須要的 name 並不會由於這個操做而有所改變。可是, getName() 這個函數卻仍是一遍又一遍地執行了,這就是所謂的重複執行的問題。(若是這樣的函數不少,會影響到頁面性能的,特別是這些函數的邏輯有一點複雜的時候)

其實看到這裏,我個以爲 React 中的這個問題仍是很是尷尬的,它本身內部的虛擬節點,靠本身的 diff 算法實現高效的更新策略,可是外面的 state 自己的維護又成了可能的問題。

回到重複執行的問題,它並無什麼本質的解決辦法,只可能進一步拆分 getName() ,分離變量的狀況下,做一些執行層面的優化,簡單說,先做變量是否變化的判斷,而後再決定是否繼續執行。這樣用額外判斷的成本,換取可能的省略後繼執行的效益。大概是:

 
const getName = ( () => {
  let lastName = null;
  let lastCount = null;
  let lastResult = null;
  return (name, count) => {
    console.log('getName');
    if(name === lastName && count === lastCount){ return lastResult }
    lastName = name;
    lastCount = count;
    lastResult = name + count;
    return lastResult;
  }
})();
 

這個樣子,像是中間加了一個緩存層。

把這個形式封裝一下,就是說先把「變量」的獲取提取出來,而後加一箇中間緩存層。官方推薦的實現是 reselect ,作的事就是這樣的: https://github.com/reactjs/reselect 。

用 reselect 把前面的代碼調整一下,就是:

 
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8" />
<title>React</title>
<script crossorigin src="https://s.zys.me/js/jq/jquery.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react-dom.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/babel.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/redux.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/react-redux.min.js"></script>
<script crossorigin src="https://s.zys.me/js/react/reselect.min.js"></script>
</head>
<body>
  <div id="app"></div>
  <script type="text/babel">

    const getName = Reselect.createSelector(
      [
        (state, props) => {console.log('reselect'); return state.name},
        (state, props) => state.count
      ],
      (name, count) => {
        console.log('getName');
        return name + count
      }
    );

    const Title = props => (
      <div>
        <h1 onClick={props.onClick}>{props.name}</h1>
        <h1 onClick={props.onOtherClick}>另外的 {props.name}</h1>
      </div>
    )

    const mapStateToProps = (state, props) => {
      console.log('mapStateToProps');
      return {
        name: getName(state, props)
      }
    }

    const mapDispatchToProps = dispatch => {
      return {
        onClick: () => {
          dispatch({type: "CLICK"});
        },
        onOtherClick: () => {
          dispatch({type: "OTHER_CLICK"});
        }
      }
    }

    const VisibleTitle = ReactRedux.connect( mapStateToProps, mapDispatchToProps )(Title);

    let store = Redux.createStore( (state = {name: '初始值', count: 0}, action) => {
      if(action.type === 'CLICK'){
        return { ...state, name: '點擊以後的', count: state.count + 1};
      }
      if(action.type === 'OTHER_CLICK'){
        return { ...state, other: '什麼東西'};
      }
      return { ...state };
    });

    const App = () => (
      <ReactRedux.Provider store={store}>
        <VisibleTitle />
      </ReactRedux.Provider>
    )

    ReactDOM.render(<App />, document.getElementById('app'));
  </script>
</body>
</html>
 

Reselect.createSelector 是一個高階函數,它接受的第一個參數,是一個列表,裏面就是分拆出來的,獲取「變量」的方法,第二個參數就是接受變量的主要邏輯。由於變量已經明確拆分出來的,因此天然地,針對變量做判斷並應用緩存機制就好辦了。

這裏注意一下,雖然表面上 getName() 的調用少了,可是卻額外多了 reselect 的調用,本質上來講,只是改善的重複調用的損耗,並無避免它。

 

原文轉載自https://www.zouyesheng.com/react.html#toc3

相關文章
相關標籤/搜索