关于前端:一文概述从状态复用到Hooks

5次阅读

共计 10462 个字符,预计需要花费 27 分钟才能阅读完成。

测试一下 Hooks 的熟练程度

为什么不能在 for 循环、if 语句里应用 Hooks

React.memo、React.useCallback、React.usememo 的作用,以及比照

useState 中的值是个对象,扭转对象中的值,组件会渲染吗?如果用 React.memo() 包裹住呢

Hooks 的(实现)原理是什么?

Hooks 的实质是什么?为什么?

React Hooks,它带来了哪些便当?

React Hooks 当中的 useEffect 是如何辨别生命周期钩子的

useEffect(fn, []) 和 componentDidMount 有什么差别


答复得如何?在理解一个概念前,纳闷越多,了解就越深

是什么

React Hooks 是 React 16.8 推出的新个性。它能够让你在不编写 class 的状况下应用 state 以及其余的 React 个性

为什么会有 Hooks

咱们肯定要有个概念,即 React 的实质是什么?它的特色是 UI=f(data)、所有皆组件、申明式编程。那么,既然是 UI=f(data),data(数据)通过 function 来驱动 UI 视图变动。在业务中,你不能简略只展现,也需交互,交互就会更新状态,React 是通过 setState 来扭转状态。但这仅限于类组件,所以在 Hooks 呈现之前,函数式组件用来渲染组件(也称它为木偶组件),类组件用来管制状态

而后,为了让状态能更好的复用,提出了 Mixins、render props 和 高阶组件。诚然,render props、高阶组件能尽管能解决,然而会带来副作用——组件会造成“嵌套天堂”

以及类组件自身的生命周期会使得简单的组件变得难以了解、class 语法的学习老本等等,形成了 React 团队提出 hooks——让函数式组件领有状态治理

官网也论述过设计 Hooks 的三大动机:

  1. 在组件之间复用状态逻辑很难
  2. 简单组件变得难以了解
  3. 难以了解的 class

状态复用的试验

Mixins 时代

在笔者尚未应用 React 之前就存在,现已被淘汰

Mixins(混入)是一种通过扩大收集性能的形式,它实质上是将一个对象的属性拷贝到另一个对象上,不过你能够拷贝任意多个对象的任意个办法到一个新对象下来,这是继承所不能实现的。它的呈现次要就是为了解决代码复用问题

这里不对其做剖析,React 官网文档在 Mixins Considered Harmful 一文中提到了 Mixins 带来的危害:

  • Mixins 可能会相互依赖,互相耦合,不利于代码保护
  • 不同的 Mixins 中的办法可能会互相抵触
  • Mixins 十分多时,组件是能够感知到的,甚至还要为其做相干解决,这样会给代码造成滚雪球的复杂性

Render Props

指一种在 React 组件之间应用一个值为函数的 prop 共享代码的简略技术

具备 render prop 的组件承受一个返回 React 元素的函数,并在组件外部通过调用此函数来实现本人的渲染逻辑

<DataProvider render={data=> (<h1>Hello, {data.target}</h1>
)}>

具体可在官网理解

HOC(高阶组件)

HOC 的原理其实很简略,它就是一个函数,并且它承受一个组件作为参数,并返回一个新的组件,把复用的中央放在高阶组件中,你在应用的时候,只须要做不同用途

打个比方:就如同给你一瓶水,你在渴的时候就会喝它;你在耍帅的时候拿它摆 POSE;你在他人须要的时候给他喝帮忙人 …

Writing is cheap. Show me code

function Wrapper(WrappedComponent) {
    return class extends React.Component {componentDidMount() {console.log('我是一瓶水')
        }
        render() {
            return (
                <div>
                    <div className="title">{this.props.title}</div>    
                    <WrappedComponent {...this.props} />
                </div>    
            )
        }
    }
}
import "./styles.css";
import React from "react";
import Wrapper from "./Wrapper";

class A extends React.Component {render() {return <div> 喝它 </div>;}
}

class B extends React.Component {render() {return <div> 耍帅摆 POSE</div>;}
}

class C extends React.Component {render() {return <div> 帮忙他人 </div>;}
}

const AA = Wrapper(A);
const BB = Wrapper(B);
const CC = Wrapper(C);

export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <AA title="我是普通人" />
      <BB />
      <CC />
    </div>
  );
}

这样就很显著的看出 HOC 的益处,”一瓶水“是独特代码,A、B、C 解决业务代码,而后将 A、B、C 传入 HOC(一瓶水)中,返回了一个新的组件 AA、BB、CC。雷同的代码失去了专用

各位能够返回这里查看 demo

HOC 的用途不单单是代码复用,还能够做权限管制、打印日志等。但它的缺点也没显著,当大量应用 HOC 后,会产生大量的嵌套,使得嵌套变得艰难;并且 HOC 会劫持 props,在不恪守约定的状况下可能会造成抵触

总结下 HOC:

  • 用法:创立一个函数,该函数接管一个组件作为输出,除了组件还能够传递其余的参数,基于该组件返回一个不同的组件
  • 长处:代码复用,逻辑复用
  • 毛病:因为嵌套使得调试难度变高;会劫持 props,或者造成抵触

Hooks 的入世

前有状态复用的不给力(Mixins 被淘汰,render props、HOC 的副作用又大),后有类组件的简单组件难以了解、保护(过多的生命周期),class 属性造成的 this 指向又麻烦。于是乎,Hooks 大喊一声:我来也

它起码有三个益处

  • 逻辑复用

    • 秒杀 render props、hoc
  • 业务代码更聚合

    • 秒杀类组件
  • 写法简洁

    • 秒杀类组件

useState

作用:让函数组件具备维持状态的能力,代替类组件的 constructor 初始化状态

例子:

const Counter = () => {const [count, setCount] = useState(0)
    return (<div onClick={() => setCount(count+1)}>{count}</div>
    )
}

特点:逻辑复用

在应用 useState 时,会呈现两个衍生问题:

一:Capture Value 个性

在函数式组件与类组件有何不同中曾介绍过,函数式组件能捕捉渲染时所用的值。并举例组件中点三下加,setTimeout 3 秒后弹出数字,在点两次加,3 秒后展现 3,而不是 5。而类组件却能取得最新的数据,这是为什么?

因为函数式组件有 Capture Value 的个性。而从源码的角度看,每次调用 setXX 会引发 re-render 从而重渲染组件

如果想取得最新值,能够通过 useRef 来将值保留在内存中

二:useState 中的值是个对象,扭转对象中的值,组件会渲染吗?怎么优化?

个别咱们用 useState 尽量恪守繁多值,但难免会遇到一些非凡状况,如果值是个对象,扭转对象中的其中一个属性,其余属性不变,那么援用其余属性的组件是否会渲染呢?

const DemoSon = (props) => {console.log("render", props);
  return <div>{props.name}</div>;
};

const Demo = () => {const [data, setData] = useState({foo: { name: "johan", bar: { baz: 1} } });
  const handleClick = () => {
    setData({
      ...data,
      foo: {
        ...data.foo,
        bar: {baz: 2}
      }
    });
  };

  return (<div onClick={handleClick}>
      {data.foo.bar.baz}
      <DemoSon name={data.foo.name} />
    </div>
  );
};

点击 div,批改 baz 的值,DemoSon 是否会渲染呢?答案是会的,为什么会渲染?因为你的援用值产生了变动,生成了新的虚构 DOM,渲染到视图上时,子组件就会渲染。如何优化,让数据不变的组件不反复渲染?我感觉有两种形式,一拆分 data,拆分成 foo 对象和 name,因为 setData 并不扭转 name,所以 DemoSon 不会渲染,还有一种是通过 memo 包裹住 DemoSon,因为 memo 能防止从新渲染

可查看线上 demo

useEffect

作用:解决副作用,代替类组件的 componentDidMount、componentDidUpdate、componentWillUnmount

应用形式:

// 没有第二个参数
// mount 阶段和 update 阶段都执行
useEffect(fn)

// 第二个参数为空数组
// 当 mount 阶段会执行
useEffect(fn,[])

// 第二个参数为依赖项
// 当依赖项(deps)数据更新时会执行
useEffect(fn, [deps])

// 革除副作用 
useEffect(() => {const subscription = props.source.subscribe();
  return () => {
    // 革除订阅
    subscription.unsubscribe();};
});

PS:以上正文中的 mount 阶段,即组件加载时;update 指数据(包含 props、state)变动时

在应用 useEffect 时,会面临几个问题:

1. useEffect(fn, []) 和 componentDidMount 有什么区别?

尽管 useEffect(fn, []) 和 componentDidMount 都能够示意组件加载时执行,但从细节上两者有所不同。要谈起细节需从源码中聊起,具体可看 React 源码魔术师卡颂的这篇——[useEffect(fn, [])和 cDM 有什么区别?](https://mp.weixin.qq.com/s?__…) 理解,这里我讲下我的了解

源码中把虚构 DOM 和虚构 DOM 渲染到实在 DOM 分为两个阶段。虚构 DOM 存在内存中,在 JSX 中对数据增删改,虚构 DOM 会对对应的数据打上标签,这个阶段称为 render 阶段;把虚构 DOM 映射到实在 DOM 的操作被称为 commit 阶段,它负责把这些标签转换为具体的 DOM 操作

在 render 阶段

  • 插入 DOM 元素被打上 Placement 标签;
  • 更新 DOM 元素被打上 Update 标签;
  • 删除 DOM 元素被打上 Deletion 标签;
  • 更新 Ref 属性被打上 Ref 标签
  • useEffect 回调被打上 Passive 标签

而 commit 阶段分为三个子阶段

  • 渲染视图前(before mutation 阶段)
  • 渲染视图时(mutation 阶段)
  • 渲染视图后(layout 阶段)

被打上 Placement 标签的,会在 mutation 阶段时执行对应的 appendChild 操作,意味着 DOM 节点被插入到视图中,接着在 layout 阶段调用 componentDidMount

而被打上 Passive 标签的,它会在 commit 阶段的三个子阶段执行实现后再异步调用 useEffect 的回调函数

由此可见,它们的调用 调用机会 是不同的,useEffect(fn,[]) 是在 commit 阶段执行完当前异步调用回调函数,而 componentDidMount 会在 commit 阶段实现视图更新(mutation 阶段)后再 layout 阶段同步调用

hooks 中也有一个和 componentDidMount 调用机会雷同的 hooks——useLayoutEffect

其次 useEffect(fn, []) 会捕捉 props 和 state,而 componentDidMount 并不会。应用 useEffect(fn, []) 的会第哦啊函数会拿到初始的 props 和 state,这个情理和 capture value 是一个情理

总结:两点不同,一、执行机会不同;二、useEffect(fn, []) 会对 props 和 state 进行捕捉

下文会用 demo 阐明 capture value 个性

2. 每一次渲染都有它本人的 props 和 state

先讨论一下渲染(rendering),咱们来看一个计数器组件 Counter

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

  return (
    <div>
      <p> 点击 {count} 次 </p>
      <button onClick={() => setCount(count + 1)}>
        点击
      </button>
    </div>
  );
}

第一次渲染时,count 的初始值从 useState(0) 中获取。当调用 setCount(count + 1),React 从新渲染组件,此时 count 的值就成 1。如下所示:

// Mount 第一次渲染
function Counter() {
  const count = 0; // 默认从 useState 中取得
  // ...
  <p> 点击 {count} 次 </p>
  // ...
}

// Update 点击 1 次
function Counter() {
  const count = 1; // 通过 setCount 批改 count
  // ...
  <p> 点击 {count} 次 </p>
  // ...
}

// Update 点击 2 次
function Counter() {
  const count = 2; //  通过 setCount 批改 count
  // ...
  <p> 点击 {count} 次 </p>
  // ...
}

每当咱们更新状态时,React 会从新渲染组件。每次渲染取得此刻(快照)的 count 状态

而在类组件中并不是捕捉值

举个例子:

class ClassDemo extends React.Component {state = {    count: 0};  componentDidMount() {    setInterval(() => {this.setState({ count: this.state.count + 1});    }, 1000);  }  render() {    return <div> 我是 Class Component, {this.state.count}</div>;  }}

页面上的 count 会每隔一秒钟加 1,而换成函数式组件

const FunctionDemo = () => {  const [count, setCount] = useState(0);  useEffect(() => {    const id = setInterval(() => {setCount(count + 1);    }, 1000);    return () => clearInterval(id);  }, []);  const handleClick = () => {    setCount(count + 1);  };  return <div onClick={handleClick}> 我是 Function Component, {count}</div>;};

永远是 1

这就是 hooks 的 capture value,相似例子在函数式组件与类组件有何不同介绍过

可返回线上 demo 查看

useLayoutEffect

作用:同步执行副作用

大部分状况下,应用 useEffect 就能够帮咱们解决副作用,然而如果想要同步调用一些副作用,比方对 DOM 的操作,就须要应用 useLayoutEffect,useLayoutEffect 中的副作用会在 DOM 更新之后同步执行

与类组件中的 componentDidMount 成果统一,都是在 commit 阶段实现视图更新(mutation 阶段)后在 layout 阶段同步调用

useCallback

作用:记忆函数,防止函数从新生成。在函数传递给子组件时,能够防止子组件反复渲染

例子:

const memoizedCallback = useCallback(() => {doSomething(a, b);  },  [a, b],);

可缓存的援用

在类组件中常困扰人的是 this 绑定问题

  1. render 办法中应用 bind
class App extends React.Component {handleClick() {console.log('this >', this);    }    render() {        return (            <div onClick={this.handleClick.bind(this)}>test</div>        )    }}
  1. render 办法中应用箭头函数
class App extends React.Component {handleClick() {console.log('this >', this);    }    render() {        return (            <div onClick={e => this.handleClick(e)}>test</div>        )    }}
  1. 构造函数中 bind
class App extends React.Component {constructor(props) {super(props);        this.handleClick = this.handleClick.bind(this);    }    handleClick() {        console.log('this >', this);    }    render() {        return (        <div onClick={this.handleClick}>test</div>        )    }}

4. 在定义阶段应用箭头函数绑定

class App extends React.Component {handleClick = () => {console.log('this >', this);    }    render() {        return (            <div onClick={this.handleClick}>test</div>        )    }}

前三种都会因 App 组件的 props 或 state 变动而从新触发渲染,使其渲染新的 handleClick。第四种将 handleClick 抽离出赋值为变量,通过 this 指向存储函数,起到了缓存作用

而函数式组件肯定会渲染

function App() {    const handleClick = () => {console.log('Click');    }    return (<div onClick={handleClick}>test</div>    )}

然而 useCallback 能缓存函数,让它”记住“

function App() {    const handleClick = useCallback(() => {console.log('Click');    }, [])    return (<div className="App">          <Demo handleClick={handleClick} />        </div>    )}

但应用 useCallback 必须应用 shouldComponentUpdate 或者 React.memo 来疏忽同样的参数反复渲染

所以独自应用 useCallback 是不能的,它须要和 React.memo 配合

function Demo(props) {return <div onClick={props.handleClick}>test</div>;}const MemoDemo = memo(Demo);function App() {    const handleClick = useCallback(() => {console.log('Click');    }, [])    return (<div className="App">          <Demo handleClick={handleClick} />        </div>    )}

然而 useCallback 会使代码可读性变差,所以尽量不必 useCallback

不必 useCallback,那怎么进步性能呢?

useMemo

作用:记忆组件。代替类组件的 shouldComponentUpdate

useCallback 的性能齐全能够由 useMemo 所取代,如果你想通过 useMemo 返回一个记忆函数也是齐全能够的

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

例子:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

默认状况下,如果 React 父组件从新渲染,它蕴含的所有子组件都会从新渲染,即便子组件没有任何变动

useMemo 和 useCallback 承受的参数都是一样,都是在其依赖项发生变化后执行,都是返回缓存的值,区别在于 useMemo 返回的是函数运行的后果,useCallback 返回的是函数

useMemo 返回的是一个值,用于防止在每次渲染时都进行高开销的计算

useCallback VS useMemo

相同点:useCallback 和 useMemo 都是性能优化的伎俩,相似于类组件的 shouldComponentUpdate,在子组件中应用 shouldComponentUpdate,判断该组件的 props 和 state 有没有变动,从而防止每次父组件 render 时从新渲染子组件

区别:useCallback 和 useMemo 的区别是 useCallback 返回一个函数,当把它返回的这个函数作为子组件应用时,能够防止每次父组件更新时从新渲染这个子组件

memo

作用:防止从新渲染

只有当 props 扭转时会从新渲染子组件

被 memo 包裹住后,当 props 不变时,子组件就不会渲染

React.memo() 办法能够避免子组件不必要渲染,从而提供组件性能。

对于性能优化 Dan 曾写过一篇文章:在你写 memo()之前,其实在咱们应用 useCallback、useMemo、memo 前无妨试试 state 下移和内容晋升。目标就是让不必渲染的组件不反复渲染

useRef

作用:

保留援用值,跟 createRef 相似。咱们习惯用 ref 保留 DOM

应用 useRef 保留和更新一些数据是有肯定益处的,它能够不通过内存来保留数据,使得这些数据再重渲染时不会被革除掉

它不仅仅是用来治理 DOM ref 的,它还相当于 this,能够寄存任何变量,很好的解决闭包带来的不方便性

如果咱们想利用一般的变量再重渲染过程中追踪数据变动是不可行的,因为每次组件渲染时它都会被从新初始化。然而,如果应用 ref 的话,其中的数据能在每次组件渲染时放弃不变。

例子:

const [count, setCount] = useState<number>(0)const countRef = useRef<number>(count)

在函数式组件与类组件有何不同介绍过应用办法

其余

useContext:缩小组件层级

useReducer:类 redux 的办法,useState 是基于它扩张的

ForwardRef:转发 ref

useImperativeHandle:透传 Ref,父组件获取子组件内的办法

自定义 Hooks

因为 useState 和 useEffect 是函数调用,因为咱们能够将其组合成本人的 Hooks

function MyResponsiveComponent() {    const width = useWindowWidth();    return (<p> Window width is {width}</p>        )}function useWindowWidth() {    const [width, setWidth] = useState(window,innerWidth);    useEffect(() => {        const handleResize = () => setWidth(window.innerWidth)        window.addEventListener('resize', handleResize)        return () => {            window.removeEventListener('resize', handleResize)        }    })    return width;}

自定义 Hooks 让不同的组件共享可重用的状态逻辑。留神状态自身是不共享的。每次调用 Hook 都只申明了其本身的独立状态

React Hooks 的有余

尽管实现了大多数类组件的性能,然而还无奈实现 getSnapshotBeforeUpdate 和 componentDidCatch 这两个 API

附录:应用规定

Hooks 的实质就是 JavaScript 函数,在应用它时须要恪守两条规定

只在最顶层应用 Hook

不要在循环,条件或嵌套函数中调用 Hook,确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。恪守这条规定,你就能确保 Hook 在每次渲染中都依照同样的程序被调用。这让 React 可能在屡次的 useState 和 useEffect 调用之间放弃 hook 状态的正确

只在 React 函数中调用 Hook

不要再一般的 JavaScript 函数中调用 Hook,你能够:

  • 在 React 的函数组件中调用 Hook
  • 在自定义 Hook 中调用其余 Hook

参考资料

  • 【React 深刻】从 Mixin 到 HOC 再到 Hook
  • useEffect 残缺指南
  • useCallback、useMemo 剖析以及差异
  • 十个案例学会 React Hooks
  • useEffect, useCallback, useMemo 三者有何区别
  • React hooks 最佳实际【更新中】
  • React Hooks 万字总结
  • 「React 万字根底全面分析」
  • Separation of concerns with React hooks
  • 如何去正当应用 React hook?
  • 应用 React.memo() 进步组件性能
  • react-render-always-rerenders
  • [useEffect(fn, [])和 cDM 有什么区别?](https://mp.weixin.qq.com/s?__…)
  • 在你写 memo()之前
正文完
 0