2019-12-19

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

前言:hooks出了已有大半年了,关注的公众号也大都推了关于hooks的文章,可是因为工作中一直用的是class,所以一直没有用,也没有学,趁着这段时间项目不那么干,将hooks系统性的学习一下,并做笔记记录一下。

<span id="目录">目录</span>
  1. useState
  2. userEffect
  3. userEffect实现componentWillUnmont
  4. 父子组件传值
  5. userContext
  6. userReducer
  7. useReducer替代Redux案例
  8. useMemo
  9. useRef
  10. useCallBack
  11. 自定义函数

一:<span id="useState">useState</span>

在组件中,我们难免使用state来进行数据的实时响应,这是react框架的一大特性,只需更改state,组件就会重新渲染,试图也会响应更新。<br />
不同于reactclass可以直接定义state,或者是在constructor中使用this.state来直接定义state值,在hooks中使用state需要useState函数,如下:

import React, { useState, useEffect } from 'react';

function Hooks() {
  const [count, setCount] = useState(0);
  const [age] = useState(16);
  useEffect(() => {
    console.log(count);
  });
  return (
    <div>
      <p>小女子芳年{age}</p>
      <p>计数器目前值为{count}</p>
      <button type="button" onClick={() => { setCount(count + 1); }}>点击+1</button>
      <button type="button" onClick={() => { setCount(count - 1); }}>点击-1</button>
    </div>
  );
}

export default Hooks;

在上面的例子中,我们使用了useState定义了两个state变量,countage,其中定义count的时候还定义了setCount,就是用来改变count值的函数。在class类中,改变state是使用setState函数,而在hooks中是定义变量的同时定义一个改变变量的函数。<br />
userState是一个方法,方法返回值为当前state以及更新state的函数,所以,在上面的例子中,我们用const [count, setCount] = useState(0);将count和setCount解构出来,而userState方法的参数就是state的初始值。当然count和与之对应的改变函数名称并不一定非得是setCount,名称可以随便起,只要是一块解构出来的即可。<br />
class组件中,我们可以用setState一次更改多个state值而只渲染一次,同样的,在hooks中,我们调用多个改变state的方法,也只是渲染一次。

二:<span id="userEffect">userEffect</span> 回目录

class组件中,有生命周期的概念,最常用的,我们通常会在componentDidMount这个生命周期中做数据请求,偶尔,我们也会用一些其它的生命周期,像是componentDidUpdatacomponentWillReceiveProps等。在hooks中,没有生命周期的概念,但是,有副作用函数useEffect。<br />
使用useEffect,和使用useState相同,必须得先引入import React, { useState, useEffect } from 'react';,默认情况下,useEffect会在第一次和每次更新之后都会执行,useEffect函数接受两个参数,第一个参数是一个函数,每次执行的就是函数中的内容,第二个函数是个数组,数组中可选择性写state中的数据,代表只有当数组中的state发生变化是才执行函数内的语句。如果是个空数组,代表只执行一次,类似于componentDidUpdata。所以,向后端请求可以写成下面这种方式:

// 页面进来只调用一次
useEffect(()=>{
    axios.get('/getYearMonth').then(res=> {
        console.log('getYearMonth',res);
        setValues(oldValues => ({
            ...oldValues,
            fileList:res.data.msg
        }));
    })
},[]);

effect函数会在浏览器完成画面渲染之后延迟调用<br />
在一个hooks函数中,可以同时存在多个effect函数,所以,当有需求每次更新都执行useEffect中的代码时,可以用一个useEffect请求数据,用其他的useEffect做另外的事情。只需根据第二个参数即可区别不同作用。

//官方示例性能优化
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

三:<span id="userEffect实现componentWillUnmount">userEffect实现componentWillUnmount</span> 回目录

部分情况下,需要在组件卸载是做一些事情,例如移除监听事件等,在class组件中,我们可以在componentWillUNmount这个生命周期中做这些事情,而在hooks中,我们可以通过useEffect第一个函数参数中返回一个函数来实现相同效果。

// 官方示例
useEffect(() => {
    // ...
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
});

个人示例:

import React, { useState, useEffect } from 'react';
import { Switch, Route, Link } from 'react-router-dom';

function Index() {
  useEffect(() => {
    console.log('useEffect:come-index');
    return () => {
      console.log('useEffect:leave-index');
    };
  }, []);
  return <div>这是首页</div>;
}

function List() {
  useEffect(() => {
    console.log('useEffect:come-list');
    return () => {
      console.log('useEffect:leave-list');
    };
  }, []);
  return <div>这是列表页</div>;
}

function HooksEffect() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(count);
    return () => {
      console.log('-------------------');
    };
  }, [count]);

  return (
    <div>
      <p>你点击了{count}次</p>
      <button 
        type="submit" 
        onClick={() => { setCount(count + 1); }}
      >
        点击+1
      </button>
      <ul>
        <li><Link to="/index">首页</Link></li>
        <li><Link to="/list">列表</Link></li>
      </ul>
      <Switch>
        <Route path="/index" exact component={Index} />
        <Route path="/list" component={List} />
      </Switch>
    </div>
  );
}

export default HooksEffect;

在上面的例子中,全部用了清除副作用的return 函数,其中,hooksEffect组件为父组件,listindex为子组件,如果在子组件的useEffect中不使用第二个参数空数组,则父组件的每次更新都会引发子组件的useEffect的调用,在父组件的useEffect函数中,第二个参数数组中为count,代表每次count的变化都会引起useEffect函数的触发以及返回函数的调用。

四:<span id="父子组件传值">父子组件传值</span> 回目录

父子组件传值在实际开发中是必不可少的,在class组件中,我们可以直接给子组件添加属性,然后在子组件通过props即可获取到父组件的值。但是在hooks中,组件都是函数,没有props,所以不能用相同的方式传值<br />
hooks中,组件都是函数,所以我们可以通过参数的方式进行传值,也可以通过content来进行传值,这一小节主要是讲通过参数方式进行传值,案例如下:

import React, { useState } from 'react';

function Show({ count, age, clear }) {
  return (
    <div>
      数量:{count}  
      年龄:{age} 
      <button 
        type="button" 
        onClick={() => { clear(); }}
      >
        复原
      </button>
    </div>
  );
}

function HooksContext() {
  const [count, setCnt] = useState(0);
  const [age, setAge] = useState(16);
  function clear() {
    setCnt(0);
    setAge(16);
  }
  return (
    <div>
      <p>小女子芳年{age}</p>
      <p>你点击了{count}次</p>
      <button 
        type="button" 
        onClick={() => { setCnt(count + 1); 
        setAge(age + 1); }}
      >
        点击+1
      </button>
      <Show count={count} age={age} clear={clear} />
    </div>
  );
}

export default HooksContext;

在上面的案例中,通过给Show组件属性赋值,然后在Show函数组件中以解构参数的方式获取父组件的值。这种传值方式和类组件本质上还是一样的。

五:<span id="userContext">userContext</span> 回目录

使用userContext,不仅可以实现父子组件传值,还可以跨越多个层级进行传值,例如父组件可以给孙子组件甚至重孙子组件进行直接传值等,redux全局状态管理本质上也是对content的一种应用。<br />
hooks中使用content,需要使用createContextuseContext,废话不多说,直接示例展示用法

// context.js  新建一个context
import { createContext } from 'react';

const ShowContext = createContext('aaa');

export default ShowContext;

// HooksContext.jsx  父组件,提供context
import React, { useState } from 'react';
import Show from './Show.jsx';
import ShowContext from './context';

function HooksContext() {
  const [count, setCnt] = useState(0);
  const [age, setAge] = useState(16);
  function clear() {
    setCnt(0);
    setAge(16);
  }
  return (
    <div>
      <p>小女子芳年{age}</p>
      <p>你点击了{count}次</p>
      <button
        type="button"
        onClick={() => { setCnt(count + 1); setAge(age + 1); }}
      >
        点击+1
      </button>
      <ShowContext.Provider value={{ count, age, clear }}>
        <Show />
      </ShowContext.Provider>
    </div>
  );
}

export default HooksContext;

// Show.jsx  子组件,使用context
import React, { useContext } from 'react';
import ShowContext from './context';

function Show() {
  const { count, age, clear } = useContext(ShowContext);
  return (
    <div>
      数量:{count}
      年龄:{age}
      <button
        type="button"
        onClick={() => { clear(); }}
      >
        复原
      </button>
    </div>
  );
}

export default Show;

上面是一个完整的使用content实现父子组件传值的过程,如果Show组件下还有子组件,无论多少层,都可以用useContext直接取到HooksContext父组件提供的值,而context.js文件是新建一个context,新建必须要单独列出来,否则子组件无法使用useContext。<br />
content提供了一种树状结构,被Context.Provider所包裹的所有组件,都可以直接取数据。redux就是利用了context的这种特性实现全局状态管理。在下面的几小节中,我们会讲hookscontext搭配useReducer来实现redux的功能。

六:<span id="userReducer">userReducer</span> 回目录

userReduceruseState的替代方案,它接收一个形如(state,action) => newStatereducer,并返回当前的state以及其配套的dispatch方法。
总的来说呢,userReducer可以接受两个参数,第一个参数就是和redux中的reducer一样的纯函数,第二个参数是state的初始值,并返回当前state以及dispatch。<br />
还是以官方示例的计数器为例

import React, { useReducer } from 'react';

function countReducer(state, action) {
  switch (action.type) {
    case 'add':
      return state + 1;
    case 'minus':
      return state - 1;
    default:
      return state;
  }
}

function HooksEffect() {
  const [count, dispatch] = useReducer(countReducer, 0);

  return (
    <div>
      <p>你点击了{count}次</p>
      <button
        type="button"
        onClick={() => { dispatch({ type: 'add' }); }}
      >
        点击+1
      </button>
      <button
        type="button"
        onClick={() => { dispatch({ type: 'minus' }); }}
      >
        点击-1
      </button>
    </div>
  );
}

export default HooksEffect;

相比起redux还需要connect高阶函数包裹一下才能将dispatch和state注入到props中,hooks中使用reducer更加简洁。在下面一小节中,我们会用案例来实现redux。

七:<span id="useReducer替代Redux案例">useReducer替代Redux案例</span> 回目录

在本小节中,我们会用contextuseReducer来实现redux的效果。依然是使用计数器这个功能,先贴代码,后面会详细讲解:

// count.js  定义context和reducer,导出context和包含reducer的context包裹组件。
import React, { createContext, useReducer } from 'react';

function countReducer(state, action) {
  switch (action.type) {
    case 'add':
      return state + 1;
    case 'minus':
      return state - 1;
    default:
      return state;
  }
}

const ADDCOUNT = 'add';
const MINUSCOUNT = 'minus';

export const CountContext = createContext();
export const CountWrap = (props) => {
  const [count, dispatch] = useReducer(countReducer, 0);
  return (
    <CountContext.Provider
      value={{ count, dispatch, ADDCOUNT, MINUSCOUNT }}
    >
      {props.children}
    </CountContext.Provider>
  );
};

// ReducerToRedux.jsx,连接组件,
import React from 'react';
import Button from './Button';
import Show from './Show';
import { CountWrap } from './count';

function ReducerToRedux() {
  return (
    <div>
      <CountWrap>
        <Show />
        <Button />
      </CountWrap>
    </div>
  );
}

export default ReducerToRedux;

// Show.jsx  显示当前数值的组件
import React, { useContext } from 'react';
import { CountContext } from './count';

function ReducerToRedux() {
  const { count } = useContext(CountContext);
  return (
    <div>现在的计数器值为:{count}</div>
  );
}

export default ReducerToRedux;

// Button.jsx  按钮组件,可以实现计数器的增和减
import React, { useContext } from 'react';
import { CountContext } from './count';

function ReducerToRedux() {
  const { dispatch, ADDCOUNT, MINUSCOUNT } = useContext(CountContext);
  return (
    <div>
      <button
        type="button"
        onClick={() => { dispatch({ type: MINUSCOUNT }); }}
      >点我-1</button>
      <button
        type="button"
        onClick={() => { dispatch({ type: ADDCOUNT }); }}
      >点我+1</button>
    </div>
  );
}

export default ReducerToRedux;

通过reducer和context实现计数器的功能,我们共用了四个文件,当然count.js这个文件本应该拆分成三个文件,常量单独定义一个文件,reducer纯函数也应该单独定一个文件,不过代码不多,就暂时合一块了。<br />
count.js中,我们导出CountContextCountWrap,其中,CoutWrap就是provider,也就是只要被CountWrap包裹过的组件,就可以使用userContent取到传递数据,而CountContext就是用createContext新建的一个content,使用useContext取传递数据的时候会用到。同时,在这个文件中,我们还将从useReducer解构出的countdispatch,以及常量增减通过provider传递给包裹组件,使被包裹的组件可以通过useContext取到这些数据。函数countReducer就是和redux中的reducer一样的纯函数,子组件dispatch action,reducer则是接受当前stateaction,通过判断action,返回新的state。<br />
Button组件中,我们通过countContext取到dispatch及常量,改变count这个数值,在Show组件中,只是展示count,在ReducerToRedux文件中,是做一个连接器,用CountWrap包裹ButtonShow组件。<br />
可能说的有些啰嗦,看上去有些复杂,其实稍一整理,原理很简单,自己写一遍整理清楚逻辑使用上就很简单了。<br />
讲到这里,其实hooks已经能应对绝大部分场景了,下面两小节,我们会讲一下useMemouseRef,用于优化渲染以及处理特殊情况。

八:<span id="useMemo">useMemo</span> 回目录

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。<br />

useMemo是函数式组件官方提供的性能优化的一个方法,接受两个参数,第一个参数是要执行的函数,第二个参数是state中的值或者父组件传下来的值,代表只有当第二个参数的值发生变化时,才执行函数。其中,第二个参数是数组,可以同时优化多个state或者父组件传下来的参数,首次渲染组件是,如果页面用到要优化的值,函数会执行。<br />
我们还是以计数器以及年龄为例

import React, { useState, useMemo } from 'react';

function Show({ count, age, clear }) {
  function ageChange(value) {
    console.log(value);
    return value + 2;
  }
  const myAge = useMemo(() => ageChange(age), [age]);
  return (
    <div>
      数量:{count}  我的年龄:{myAge}
      <button
        type="button"
        onClick={() => { clear(); }}
      >复原</button>
    </div>
  );
}

function HooksUseMome() {
  const [count, setCnt] = useState(0);
  const [age, setAge] = useState(16);
  function clear() {
    setCnt(0);
    setAge(16);
  }
  return (
    <div>
      <p>小女子芳年{age}</p>
      <p>你点击了{count}次</p>
      <button
        type="button"
        onClick={() => { setAge(age + 1); }}
      >点击年龄+1</button>
      <button
        type="button"
        onClick={() => { setCnt(count + 1); }}
      >点击计数器+1</button>
      <Show count={count} age={age} clear={clear} />
    </div>
  );
}

export default HooksUseMome;

在上面的例子中,父组件小女子初始年龄为16岁,而到子组件经过ageChange函数,返回我的年龄永远比小女子年龄大两岁。<br />
但是如果没有useMemo,当父组件的计数器count值发生变化时,子组件的ageChange函数也会执行,这不是我们想要的结果,我们只想当小女子的年龄发生变化时,再执行ageChange函数。所以,用useMemo可以实现我们想要的效果。如上面代码所示const myAge = useMemo(() => ageChange(age), [age]);,使用useMemo,第二个参数是age,这样,只有当age发生变化时,才执行其中的函数。<br />

在类组件中,有shouldComponentDidUpdata生命周期,我们可以在其中做监测,当检测到state值没发生变化时,直接不渲染组件,而useMemo和这个生命周期还有些许不同。它是当检测的state发生变化时而执行某些函数,避免额外的开销,节省性能。

九: <span id="useRef">useRef</span> 回目录

在项目开发中,我们比较少用到ref,一般我们不直接操作DOM,都是通过状态来控制DOM,不过在某些情况下,可能还是会用到ref,这一节我们通过对input输入框数据的双向绑定来认识useRef

import React, { useState, useRef } from 'react';

function HooksUseRef() {
  const [inputValue, setInputValue] = useState();
  const inputRef = useRef(null);

  function inputChangeHandle(e) {
    setInputValue(e.target.value);
  }

  function inputRefChangeHandle() {
    console.log(inputRef.current.value);
  }
  return (
    <div>
      <div>
        <input
          value={inputValue}
          onChange={inputChangeHandle}
          type="text"
        />
        <span>使用state绑定inputValue值</span>
      </div>
      <div>
        <input
          ref={inputRef}
          onChange={inputRefChangeHandle}
          type="text"
        />
        <span>使用Ref绑定inputValue值</span>
      </div>
    </div>
  );
}

export default HooksUseRef;

在上面的案例中,我们如果要取input的值,如果是state双向绑定,可以直接取inputValue,如果是用ref,则可以通过inputRef.current.value取到值
通过const inputRef = useRef(null);,我们获取到的是一个对象,而current属性就是其中的dom元素。

十: <span id="useCallBack">useCallBack</span> 回目录

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

简而言之,useCallBack是用来缓存函数的,在class类中,我们通常在constructor中使用this.fn = this.fn.bind(this)来绑定this,是每次调用的fn都是之前的fn,而不用开辟新的函数。而useCallback同样有此功能,useCallBackuseMemo的不同点在于useMemo相当于缓存state,而useCallBack相当于缓存函数,官方给的解释是这样的useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。.

我们下面还是用计数器和年龄做例子

import React, { useState, useEffect, useCallback } from 'react';

function Show({ countCallBack, ageCallBack }) {
  const [count, setCount] = useState(() => { countCallBack(); });
  const [age, setAge] = useState(() => { ageCallBack(); });

  useEffect(() => {
    setCount(countCallBack());
  }, [countCallBack]);

  useEffect(() => {
    setAge(ageCallBack());
  }, [ageCallBack]);

  return (
    <div>
      数量:{count}  年龄:{age}
    </div>
  );
}

function HooksCallBack() {
  const [count, setCnt] = useState(0);
  const [age, setAge] = useState(16);

  const countCallBack = useCallback(() => {
    return count;
  }, [count]);

  const ageCallBack = useCallback(() => {
    return age;
  }, []);

  return (
    <div>
      <p>小女子芳年{age}</p>
      <p>你点击了{count}次</p>
      <button
        type="button"
        onClick={() => { setAge(age + 1); }}
      >点击年龄+1</button>
      <button
        type="button"
        onClick={() => { setCnt(count + 1); }}
      >点击计数器+1</button>
      <Show countCallBack={countCallBack} ageCallBack={ageCallBack} />
    </div>
  );
}

export default HooksCallBack;

在上面的例子中,只有点击计数器按钮,子组件才会跟着更新,点击年龄按钮子组件则不跟着更新。使用useCallback如果没有依赖,则只会执行一次,只有依赖改变,才会返回新的函数,我们可以根据这个规则实现bind的效果。

十一: <span id="自定义函数">自定义函数</span> 回目录

这一小节,我们做一个监听浏览器窗口的自定义函数,废话不多说,直接上例子:

import React, { useState, useEffect, useCallback } from 'react';

function useWinSize() {
  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
  });

  const resizeHandle = useCallback(() => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    });
  }, []);

  useEffect(() => {
    window.addEventListener('resize', resizeHandle);
    return () => {
      window.removeEventListener('resize', resizeHandle);
    };
  }, []);
  return size;
}

function HooksFunction() {
  let size = useWinSize();
  return (
    <div>
      浏览器窗口尺寸{`${size.width}*${size.height}`}
    </div>
  );
}

export default HooksFunction;


上面的代码就不多解释了,所需要注意的是自定义函数需要以use开头,且后面应该用大写字母与use分隔开。到此呢,hooks先写到这里,基本上也能面对绝大多数的业务场景,其它的hooksAPI等以后开发中如果有用到,再来补充。<br />

在下一节中,我们将会把TS从基础到项目应用整个的梳理出来,分两篇来完成,待续。。。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容