函数式组件的崛起

一.Class Component

Class 无疑是应用最广泛的 React 组件形式,例如:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

具有最完整的生命周期支持,新旧林林总总 13 个:

// Mounting
constructor()
render()
UNSAFE_componentWillMount()
componentDidMount()

// Updating
static getDerivedStateFromProps()
shouldComponentUpdate()
getSnapshotBeforeUpdate()
UNSAFE_componentWillReceiveProps()
UNSAFE_componentWillUpdate()
componentDidUpdate()

// Unmounting
componentWillUnmount()

// Error Handling
static getDerivedStateFromError()
componentDidCatch()

此外,还有 State、Props、Context、Ref 等特性。这些加持让 Class 成为了具备完整组件特性的唯一选项,尽管Class 也存在许多问题,但它无可替代

P.S.关于各个生命周期的含义及其作用,见React | 黯羽轻扬

二.Function Component

另一种组件形式是函数,输入 Props,输出 React Element,例如:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

作为最简单的 React 组件形式:

The simplest way to define a component is to write a JavaScript function.

简单到连生命周期都没有,State 就更不用说了。这些限制决定了函数式组件只能用作非常简单的View Component,担不起重任。在相当长的一段时间里,仅供“教学”使用:

Classes have some additional features that we will discuss in the next sections. Until then, we will use function components for their conciseness.

自 React 16 起,才逐步对函数式组件进行了增强:

  • createRef/forwardRef:React 16.3 之后,函数式组件支持 Ref 了

  • React.memo:React 16.6 之后,函数式组件也迎来了“shouldComponentUpdate”

当然,最重要的增强自然是Hooks

Hooks 让函数式组件也能拥有状态、生命周期等 Class 组件特性(如 state, lifecycle, context, ref 等等)

P.S.关于 Hooks 的详细信息,见React Hooks 简介

三.Function Component with Hooks

简单来讲,有了 Hooks 之后,函数式组件将拥有与 Class 组件几乎相同的表达力,包括各种生命周期、State 等

If you write a function component and realize you need to add some state to it, previously you had to convert it to a class. Now you can use a Hook inside the existing function component.

例如:

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useState Hook 让函数式组件拥有了 State,类似的,生命周期、Context、Ref、组件实例属性等特性都通过类似的 Hook 方式提供了支持,具体如下:

Hook 特性 类比 Class
useState State this.state
this.setState(newState)
useEffect 生命周期 componentDidMount
componentDidUpdate
useContext Context this.context
useReducer State Redux Reducer 式的 State 管理
useCallback Function Props this.myMethod.bind(this)
useMemo 性能优化 避免重复计算
useRef Ref createRef
useImperativeHandle 组件实例属性/方法 forwardRef
useLayoutEffect 生命周期 同步componentDidMount
同步componentDidUpdate
useDebugValue 调试 Hooks 状态可视化(类似于从 React DevTools 看this.state

四.Migrate Class to Hooks

当然,没必要也不太可能将现有的 Class 组件重构到 Hooks:

There is no rush to migrate to Hooks. We recommend avoiding any “big rewrites”, especially for existing, complex class components.

We intend for Hooks to cover all existing use cases for classes, but we will keep supporting class components for the foreseeable future. At Facebook, we have tens of thousands of components written as classes, and we have absolutely no plans to rewrite them.

这里只是说 Hooks 与 Class 特性的对应关系,这种类比有助于理解 Hooks

constructor()

构造函数中最关键的操作应该是声明/初始化this.state,通过 State Hook 来完成:

class Example extends React.Component {
  constructor() {
    this.state = {
      count: 0
    };
  }
}

等价于:

function Example() {
  // 声明一个初始值为0的state变量
  const [count, setCount] = React.useState(0);
}

其语法格式为:

const [state, setState] = useState(initialState);

其中const [state, setState] = xxx是一种解构赋值语法(具体见destructuring(解构赋值)_ES6 笔记 5),等价于:

const stateVariable = useState(initialState); // Returns a pair
const state = stateVariable[0]; // First item in a pair
const setState = stateVariable[1]; // Second item in a pair

返回状态值(state)和对应的 Setter(setState),调用 Setter 会引发组件更新(类似于 Class 里的this.setState

初始值initialState仅作用于首次渲染(通过返回值state取出),之后state保持更新

特殊的,如果需要多个状态变量,就多调几次useState

function ExampleWithManyStates() {
  // Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}

当然,状态值也可以是对象或数组,但不会像this.setState()一样进行 merge

Unlike this.setState in a class, updating a state variable always replaces it instead of merging it.

例如:

function ExampleWithManyStates() {
  const [profile, setProfile] = useState({
    age: 42,
    favorite: 'banana',
    todos: [{ text: 'Learn Hooks' }]
  });

  setProfile({
    todos: []
  });
  // 相当于Class中的
  this.setState({
    age: undefined,
    favorite: undefined,
    todos: []
  });
  // 而不是
  this.setState({
    todos: []
  });
}

render()

函数式组件本身就是个render()函数,将 Props、State 等数据注入到视图中,并注册事件处理逻辑:

class Example extends React.Component {
  /* 略去state初始化部分 */
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

等价于:

function Example() {
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

与 Class 组件不同的是,函数式组件的 State 值通过 State Hook 来获取(上例中的count),而不是this.state。相应的,this.setState()也通过useState()返回的 Setter 来完成

UNSAFE_componentWillMount()

首次渲染时在render()之前触发,与constructor()功能有些重叠,可以参考前述constructor()部分

componentDidMount()

componentDidMount中通常会有一些带副作用的操作,在函数式组件中可以用 Effect Hook 替代:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }
  /* 略去render部分 */
}

等价于:

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });
  /* 略去return部分 */
}

Effect Hook 在组件每次渲染结束时触发,因此相当于 Class 组件的componentDidMountcomponentDidUpdatecomponentWillUnmount

语法格式为:

useEffect(didUpdate);

表示组件需要在每次(包括首次)渲染后做点事情:

The function passed to useEffect will run after the render is committed to the screen.

如果需要区分 mounting 和 updating(componentDidMountcomponentDidUpdate),可以通过声明依赖来完成,具体见Tip: Optimizing Performance by Skipping Effects

而对于只需要执行/清理一次的副作用,声明它不依赖任何组件状态即可(useEffect(didUpdate, [])),此时等价于componentDidMountcomponentWillUnmount

然而,由于Fiber 调度机制Effect Hook 并不是同步触发的。因此如果需要读取 DOM 状态的话,用同步的LayoutEffect Hook

P.S.所以,严格来讲,LayoutEffect Hook 才是与componentDidMountcomponentDidUpdate等生命周期等价的 Hooks API。但出于性能/用户体验考虑,建议优先使用 Effect Hook

特殊的,有一些需要做相应清理工作的副作用,比如取消订阅外部数据源(避免内存泄漏):

class FriendStatus extends React.Component {
  /* 略去state初始化部分 */
  componentDidMount() {
    // 订阅
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentWillUnmount() {
    // 取消订阅
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  /* 略去render部分,及handleStatusChange */
}

等价于:

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    // 订阅
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // 取消订阅
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

如上例,Effect Hook 提供了 Disposable 机制来支持清理操作,但 Hooks 的运行机制决定了每次 render 之后都会触发清理工作

Effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time.

如果反复订阅存在性能影响的话,同样可以通过声明依赖的方式来解决(将来可能会在编译时自动找出依赖)

另外,类似于多次useState(),同样可以通过多次useEffect()将不同的 Effect 分离开:

Just like you can use the State Hook more than once, you can also use several effects. This lets us separate unrelated logic into different effects.

例如:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  // DOM操作Effect
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  // 数据订阅Effect
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

static getDerivedStateFromProps()

getDerivedStateFromProps是用来替代componentWillReceiveProps的,应对 state 需要关联 props 变化的场景

(摘自二.如何理解 getDerivedStateFromProps

函数式组件中,对于 props 变化引发 state 变化的场景,可以直接通过 State Hook 来完成,例如记录滚动方向:

class ExampleComponent extends React.Component {
  state = {
    isScrollingDown: false,
    lastRow: null,
  };

  static getDerivedStateFromProps(props, state) {
    if (props.currentRow !== state.lastRow) {
      return {
        isScrollingDown: props.currentRow > state.lastRow,
        lastRow: props.currentRow,
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

等价于:

function ScrollView({row}) {
  let [isScrollingDown, setIsScrollingDown] = useState(false);
  let [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) {
    // Row changed since last render. Update isScrollingDown.
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}

shouldComponentUpdate()

函数式组件中,用React.memo代替

getSnapshotBeforeUpdate()

暂时(2019/06/23)还没有可替代的 Hooks API,但很快会加

UNSAFE_componentWillReceiveProps()

如前述,componentWillReceivePropsgetDerivedStateFromProps都用 State Hook 代替,见constructor()部分

UNSAFE_componentWillUpdate()

componentWillUpdate一般可以用componentDidUpdate代替,如果需要读取 DOM 状态的话,用getSnapshotBeforeUpdate代替:

Typically, this method can be replaced by componentDidUpdate(). If you were reading from the DOM in this method (e.g. to save a scroll position), you can move that logic to getSnapshotBeforeUpdate().

因此,componentWillUpdate一般可以用 Effect Hook 或 LayoutEffect Hook 代替,见componentDidMount()部分

componentDidUpdate()

如前述,componentDidUpdate可以用 Effect Hook 代替,见componentDidMount()部分

componentWillUnmount()

如前述,componentWillUnmount可以用 Effect Hook 代替,见componentDidMount()部分

static getDerivedStateFromError()

暂时(2019/06/23)还没有可替代的 Hooks API,但很快会加

componentDidCatch()

暂时(2019/06/23)还没有可替代的 Hooks API,但很快会加

Context

函数式组件中同样能够访问 Context,并且读取方式更简单:

// 声明
const {Provider, Consumer} = React.createContext(defaultValue);
// 写
<Provider value={/* some value */}>
// 读
<Consumer>
  {value => /* render something based on the context value */}
</Consumer>

等价于:

// 声明
const MyContext = React.createContext(defaultValue);
const Provider = MyContext.Provider;
// 写
<Provider value={/* some value */}>
// 读
const value = useContext(MyContext);

Ref

Ref 也提供了类似的支持:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }

  render() {
    return <input type="text" ref={this.inputRef} />;
  }
}

等价于:

function MyComponent() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return <input type="text" ref={inputRef} />;
}

即:

const refContainer = useRef(initialValue);
// 等价于
const refContainer = React.createRef();

Instance Variables

有意思的是,Ref 还能用来保留组件实例状态(相当于this.xxx),例如:

function Timer() {
  const intervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });
}

更进一步的,可以通过this.mounted来实现componentDidUpdate

function FunctionComponent(props) {
  // Flag for strict Update lifecycles
  const mounted = useRef();

  useEffect(() => {
    if (mounted.current) {
      // componentDidUpdate
    }
  });
  useEffect(() => {
    mounted.current = true;
  }, []);
  // ...
}

Instance Method

我们可以通过 Ref 引用 Class 组件实例,进而访问其实例方法:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  // 实例方法
  focus() {
    this.inputRef.current.focus();
  }
  render() {
    return <input type="text" ref={this.inputRef} />;
  }
}

class App extends React.Component {
  componentDidMount() {
    this.myComponent.focus();
  }
  render() {
    return <MyComponent ref={ins => this.myComponent = ins} />;
  }
}

等价于:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} />;
}
FancyInput = forwardRef(FancyInput);

class App extends React.Component {
  componentDidMount() {
    this.myComponent.focus();
  }
  render() {
    return <FancyInput ref={ins => this.myComponent = ins} />;
  }
}

五.在线 Demo

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code