hiccLoghicc log by wccHipo日志

React 16.x 新特性, Suspense, Hooks, Fiber

toc

Intro

对于眼下2019来说,React, Vue,Angular已成三国鼎立之势,Vue忙着3.0, Angular忙着Ivy, React则用Hooks再次改变了我们书写代码的方式。

React 在官方博客中公布了16.x的一系列计划:

9E0B1216-C047-498A-B407-4B6ED170E503

现在(2019-4-13), 前两个已经发布,本文着重在前两个部分,甚至可以说重点在于Hooks, 大爱Hooks啊。

React.lazy, Suspense

React 16.6.0 引入了lazySuspenseReact.lazy函数可以渲染一个动态的import作为一个组件。Suspense悬停组件,它会在内容还在加载的时候先渲染fallback。它们组合就能实现之前主要是使用loadable-components,来异步加载组件,Code-Splitting。

import React, {lazy, Suspense} from 'react'; const OtherComponent = lazy(() => import('./OtherComponent')); function MyComponent() { return ( <Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense> ); }

可以查看React lazy Demo

需要注意的是:

  • 暂时不支持SSR,loadable支持
  • React.lazy函数只支持动态default组件导入
  • 它们组合出来代码分片使用Webpack, Babel时候,仍然需要配置Babel插件: "plugins": ["@babel/plugin-syntax-dynamic-import"]
  • Suspense目前只支持Code-Splitting, 数据异步获取的支持需要到2019年中……

React.memo

React.memo基本就是React为函数组件提供的PrueComponent或者shouldComponentUpdate功能。下面的例子:

const MyComponent = React.memo(function MyComponent(props) { /* only rerenders if props change */ });

静态属性contextType

React 16.3 正式引入了Context API, 来方便跨组件共享数据,基本使用方式,按照官方例子:

const ThemeContext = React.createContext('light'); class ThemeProvider extends React.Component { state = {theme: 'light'}; render() { return ( <ThemeContext.Provider value={this.state.theme}> {this.props.children} </ThemeContext.Provider> ); } } class ThemedButton extends React.Component { render() { return ( <ThemeContext.Consumer> {theme => <Button theme={theme} />} </ThemeContext.Consumer> ); } }

可以发现消费组件需要按照函数的方式来调用,很不方便,因此新的语法可以赋值给class组件的静态属性contextType,以此能够在各个生命周期函数中得到this.context:

class MyClass extends React.Component { static contextType = MyContext; componentDidMount() { let value = this.context; /* perform a side-effect at mount using the value of MyContext */ } componentDidUpdate() { let value = this.context; /* ... */ } componentWillUnmount() { let value = this.context; /* ... */ } render() { let value = this.context; /* render something based on the value of MyContext */ } }

重头戏React Hooks

React 在版本16.8中发布了Hooks,可以在函数式组件中使用state和其他的React 功能。

React官方文档Introducing Hooks – React花了8个章节来讲述Hooks😬,一定要读一读,本文不会那么详尽,只是试图做一些融汇和贯通。

为什么需要hooks?

React从发布以来就是以单项数据流、搭积木的书写方式迅速流行,然后为了解决日益复杂的业务:

  • 有状态的Class组件势必变得臃肿,难懂。
  • 相同的逻辑在不同生命周期函数中重复,也容易漏写。
  • 更复杂的模式,例如render props 和higher-order components, 为了逻辑的复用容易形成组件嵌套地狱。

更进一步来说,Class组件this加上生命周期函数的方式,难写,难读,易出错,而且AOT,树摇,Component Folding等先进的编译优化手段效果不好……

因此实际上Hooks就是为函数式组件赋能,以此来优化上述问题

useState

useState的语法可能略微奇怪,但是却异常好用.

const [state, setState] = useState(initialState);
  • 不像this.stateuseState可以多次使用
  • this.state会自动合并对象,useState不会
  • useState的中setState直接传值,同样也可以传一个函数,以此在函数中获取到上次的state
  • useState的初始值如果需要一个耗时函数计算时候,给useState传入函数,这样只会在初次调用。
  • 最重要的是,React内部使用数组的方式来记录useState,请不要在循环、条件或者嵌套函数中调用useState,其实所有的Hooks你应该只在函数的顶层调用

Demo react-useState - CodeSandbox

useEffect

可以在useEffect里面做一些,获取,订阅数据,DOM等“副作用”,它也可以实现于Class Component中的componentDidMountcomponentDidUpdatecomponentWillUnmount的调用,使用类似官方的例子:

import React, { useState, useEffect } from 'react'; function FriendStatus(props) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }

一个函数内就搞定了,componentDidMount—> componentDidUpdate—>componentWillUnmount(注意Effect函数返回的函数),易读,精简。

自定义Hook

记住,Hooks就是一些常规的JavaScript函数,只是约定以use开头命名(方便阅读和Eslint)。因此Hooks自然就可以按照函数一样组合使用。

实际上这才是React Hooks真正释放想象,提高生产力的地方。

import { useEffect, useState } from 'react'; const useWindowSize = () => { const [state, setState] = useState<{ width: number; height: number }>({ width: window.innerWidth , height: window.innerHeight, }); useEffect(() => { const handler = () => { setState({ width: window.innerWidth, height: window.innerHeight, }); }; window.addEventListener('resize', handler); return () => window.removeEventListener('resize', handler); }, []); return state; };

更多的自定义Hooks可以查看: GitHub - streamich/react-use: React Hooks — 👍

硬核的useEffect

在你高兴太早之前,useEffect还有可选的第二个参数,可以穿入一个useEffect内函数所依赖值的数组。

实际上所有的故事,所有的纠结都发生在这个参数😱。

使用useEffect来替代生命周期函数

useEffect默认会在每次渲染后调用,如果你传传入一个[],效果就和componentDidMount类似。

import { EffectCallback, useEffect } from 'react'; const useMount = (effect: EffectCallback) => { useEffect(effect, []); };

自然类似componentWillUnmount可以:

const useUnmount = (fn: () => void | undefined) => { useEffect(() => fn, []); };

不过Hook也没有覆盖所有的生命周期,getSnapshotBeforeUpdatecomponentDidCatch暂时没有对应的Hook

Capture Value props

来看如下的代码

const FunName = () => { const [name, setName] = useState("init name"); function log() { setTimeout(() => { console.log("FunName after 3000 ", name); }, 3000); } return ( <div> <h2>Fun name log</h2> <input value={name} onChange={e => setName(e.target.value)} /> <button onClick={log}>delay console.log</button> </div> ); }; class ClassNameView extends React.Component { state = { name: "init name" }; log = () => { setTimeout(() => { console.log("ClassName after 3000 ", this.state.name); }, 3000); }; render() { return ( <div> <h2>class name log</h2> <input value={this.state.name} onChange={e => this.setState({ name: e.target.value })} /> <button onClick={this.log}>delay console.log</button> </div> ); } }

两个功能一样的组件,一个函数组件,一个Class组件,在按钮点击后3000ms之内两者的行为却不一样。

React-Hooks-Capture-value

类似同样的组件,使用父组件的props

const FunName = () => { function log() { setTimeout(() => { console.log("FunName after 3000 ", name); }, 3000); } return ( <div> <h2>Fun name log</h2> <button onClick={log}>delay console.log</button> </div> ); }; class ClassNameView extends React.Component { log = () => { setTimeout(() => { console.log("ClassName after 3000 ", this.state.name); }, 3000); }; render() { return ( <div> <h2>class name log</h2> <button onClick={this.log}>delay console.log</button> </div> ); } } // 父组件 function App() { const [name, setName] = useState("init name"); return ( <div className="App"> <h1>Hooks Capture props</h1> <input value={name} onChange={e => setName(e.target.value)} /> <FunName name={name} /> <ClassNameView name={name} /> </div> ); }

同样行为不一样

React-Hooks-Capture-props

普通javascript函数也有如下的行为:

function sayHi(person) { const name = person.name; setTimeout(() => { alert('Hello, ' + name); }, 3000); } let someone = {name: 'Dan'}; sayHi(someone); someone = {name: 'Yuzhi'}; sayHi(someone); someone = {name: 'Dominic'}; sayHi(someone); //执行结果: //Hello, Dan //Hello, Yuzhi //Hello, Dominic

也就是函数组件的行为才是“正确的”行为,而Class组件行为的原因在于React会修改,this.statethis.props使其指向最新的状态。

使用useRef获取旧的props或者最新的state

useRef一般用作获取DOM的引用,根据官方文档:

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

可变的对象会存在于组件的整个生命周期,因此可以用来保存值,保证拿到最新的值。

function Counter() { const [count, setCount] = useState(0); const prevCount = usePrevious(count); return ( <h1> Now: {count}, before: {prevCount} </h1> ); } function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; }

告诉 React 如何对比 Effects

一般而言你需要将effects所依赖的内部state或者props都列入useEffect第二个参数,不多不少的告诉React 如何去对比Effects, 这样你的组件才会按预期渲染。

当然日常书写难免遗漏,这个ESlint 插件exhaustive-deps规则可以辅助你做这些事情。

这里不再展开说,但是从我日常项目来看,这点还是需要费些心思的。

使用useCallback来缓存你的函数

useCallback会根据传入的第二个参数来“记住”函数。 可以用它来避免函数被作为callback传入子组件时不必要渲染。

而且函数组件内的函数,如果需要在被不同的生命周期中调用,最好使用useCallback来处理,这样一方面拿到正确的值,一方面保证性能的优化。

function SearchResults() { const [query, setQuery] = useState('react'); const getFetchUrl = useCallback(() => { return 'https://siet.com/search?query=' + query; }, [query]); useEffect(() => { const url = getFetchUrl(); // ... Fetch data and do something ... }, [getFetchUrl]); }

总结来说Hooks的:

  • 更彻底的函数化编程,粒度更细,也更精简
  • 状态复用共享不会产生嵌套
  • Hooks可以调用Hooks
  • 更容易将组件的状态和UI分离。

可以更快速让大家写出,稳健,易测试,更易读的代码,enjoy~~

Fiber

如果说Hooks改变了开发者如何写业务代码,那么Fiber就是React改变了如何渲染。简单来说,就是React 将任务切片,分优先级,然后按照一定策略来调度渲染,一举改变之前递归,不可打断式渲染。

更详尽的分析,等我搞懂了,再来说道~~~