Hooks 作为 React 的新特性,在简单了解后,觉得非常喜欢,更加喜欢的是脸书背后那帮工程师的思想,也许他们也是感受到开发中的一些痛点,但这些痛点可能我们中的大部分程序感觉不明显,甚至没有感觉,这些痛点要在项目足够复杂的情况下才会被放大,他们察觉到了,便以一种工匠式的精神,寻求更优秀的办法,或更简洁以提升开发体验,或模块化,组件化更独立以提升项目可维护度,或运行更快以提升性能。
组件化方式
组件化是目前前端开发的主流方式,因为它更通用,颗粒度更小。在 React 中声明组件有如下两种方式
- 函数式组件
- 类组件
这两种方式的区别很明显,类组件有生命周期,有内部状态,是一个比较全面的东西,而函数式组件只有属性传值,因此适用性没那么广泛,通常用来实现一些比较简单的组件。
这里的选择存在一个比较大的问题,项目开发中不是一蹴而就的,不断迭代递进的,在初期可能觉得这个组件足够简单,因此直接声明为函数式组件,可是后来的需求导致函数式组件不能满足要求,因此就需要改造成一个类组件,这个改造是需要成本的,因此导致一部分工程师声明组件就直接 class 起头了。
如果是 Class 类,React 会用 new 关键字实例化,然后调用该实例的 render 方法,如果是 Func 函数,React 会直接调用它。
入门级组件化
最基础的也是最常用,日常开发中,下面这几种方式应该是都比较熟悉的
- 内部状态(state)
- 外部属性(props)
- 组件引用(refs)
首先第一种就是子组件内部维护自己的状态的,也是最基础的,demo 如下
export class Example1 extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: false
}
}
render() {
return this.state.loading ? 'loading' : 'finish'
}
}
这种方式缺点首当其中的就是不够通用,和外界没有联系,是个独立的个体,因为没有办法传值。这时候我们就会用到第二种,依赖外部属性的方式,简单代码如下
export class Example1 extends React.Component {
render() {
return this.props.loading ? 'loading' : 'finish'
}
}
注意:这里只是一个简单例子,这几种方式都是可以组合使用的,对于 refs 场景,子组件维护内部状态,提供改变状态的方法,此时外部就可以通过 refs 得到组件实例,从而调用组件方法,代码如下:
export class Example1 extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: false
}
}
changeLoading() {
this.setState({
loading: !this.state.loading
})
}
render() {
return this.state.loading ? 'loading' : 'finish'
}
}
// 父组件简单代码
export class App extends React.Component {
changeStatus = () => {
this.refs.example1.changeLoading()
}
render() {
return (
<div>
<Example1 ref="example1"></Example1>
<button onClick={this.changeStatus}>change</button>
</div>
)
}
}
好啦,这几种方式都比较简单和常用,就不废话记载了,这里主要给 hooks 埋下伏笔用来做对比,接下来看看进阶的组件化
对了关于 ref,这里多废话几句,ref 可以挂载到组件上也可以是 dom 元素上
- 挂到组件(class 声明的组件)上的 ref 表示对组件实例的引用。不能在函数式组件上使用 ref 属性,因为它们没有实例
- 挂载到 dom 元素上时表示具体的 dom 元素节点。
同时 ref 属性可以设置为一个回调函数,ref 属性接受一个回调函数,它在组件被加载或卸载时会立即执行。
- 当给 HTML 元素添加 ref 属性时,ref 回调接收了底层的 DOM 元素作为参数。
- 当给组件添加 ref 属性时,ref 回调接收当前组件实例作为参数。
- 当组件卸载的时候,会传入null
- ref 回调会在componentDidMount 或 componentDidUpdate 这些生命周期回调之前执行。
如果通过 ref 引用的是组件,可以想得到真实的 dom 节点,如何处理呢?不管 ref 设置值是回调函数还是字符串,都可以通过ReactDOM.findDOMNode(ref)
来获取组件挂载后真正的 dom 节点。
进阶组件化
这个世界并不总是非黑即白的,总比我们想象的复杂对吧,实际开发中,还有如下进阶的用法
- HOC:一种装饰器模式,它接受一个组件作为参数,然后返回相同的组件,这样就可以额外增加一些功能。
- Render Props:给一个函数传递一个回调函数做为参数,该回调函数就能利用外面函数的执行结果做为参数,执行任何操作。
相同点:
- 两者都能很好的帮助我们重用组件逻辑
- 和回调函数类似,当嵌套层数很多时,会造成回调地狱
不同点:
- HOC 父组件有相同属性名属性传递过来,会造成属性丢失;
- Render Props 你只需要实例化一个中间类,而 HOC 你每次调用的地方都需要额外实例化一个中间类。
备注:这不是 React 特有的,而是衍生出来的一种设计模式
一定是有某种需求才会促使 HOC 的出现,那么它帮助我们解决什么问题呢,增强组件?条件渲染?其实我觉得 HOC 的主要目的是分离业务代码,至于增加组件和条件渲染并不是目的,而是业务分离之后,然后通过 HOC 的方式产生的结果。
举个简单的例子,比如加入购物车功能,很多组件都可能用到,你可以选择在每个组件中都写一份,但这是最 low 的,因为一旦你的加入购物车逻辑发生改变,有多少个组件你就需要修改多少遍,是不是贼痛苦。此时你就会说,这不就是被淘汰了的 mixins 当初做的事情么!其实是对的,HOC 就是替代 mixins 的解决方案。既然说到了 mixins,那就说说这部分历史呗。
在我刚准备学习 react 的时候,mixins 还没有被废弃,当我再次准备深入了解时,这玩意就被淘汰了。
为了实现分离业务逻辑代码,实现组件内部相关业务逻辑的复用,在 React 的迭代中针对类组件中的代码复用依次发布了 Mixin、HOC、Render props 等几个方案。
mixins 有哪些缺点导致他被淘汰呢
- ES6 class 支持的是继承的模式,而不是 mixins
- mixin 会存在覆盖,如果两个 mixin 模块,存在相同生命周期或相同函数名的函数,那么会存在相同函数覆盖问题
- mixins 是数组形式,可以同时使用多个,mixin 中的函数可以调用 setState 方法操作组件中的 state,如果多处修改了相同的 state,会无法确定 state 的更新来源
至于 HOC,我觉得最好的例子就是 redux 了,redux 就是通过 HOC 增强组件,同时业务代码可以抽离出来达到复用,简直就是天然的例子好么。所谓任何技术都不是银弹呀,因此 HOC 存在的问题有
- 属性覆盖问题:如果工厂函数中使用了相同的属性名称,则会被覆盖
- 难以溯源:比较复杂的情形,如果一个组件A被包装过多次,最终组件B会多了很多属性等,但仅仅通过最终组件B无法知道来源于哪个工厂函数。
- 会产生无用的空组件:如果被多次包装,则中间的组件实际并不会被用到
这一节最后在聊聊 Render Prop。也是一种剥离重复使用的逻辑代码,提升组件复用性的解决方案。在被复用的组件中,通过一个名为 render(属性名也可以不是 render,只要值是一个函数即可)的属性,该属性是一个函数,这个函数接受一个对象并返回一个子组件,会将这个函数参数中的对象作为 props 传入给新生成的组件。
这种方法跟直接的在父组件中,将父组件中的state直接传给子组件的区别是,通过Render Props不用写死子组件,可以动态的决定父组件需要渲染哪一个子组件。
Render Props 就是一个函数,做为一个属性被赋值给父组件,使得父组件可以根据该属性去渲染子组件。
我觉得 Render Props 可以说是个很有意思的东西,也可以说是一种聪明的取巧方式,对比 HOC 而言,他们的方向好像是反过来的,HOC 有点像把通用代码抽离处理,然后插拔式的附加在特定组件上,而 Render Props 像是把一个个组件抽离出来,如果该组件需要某组件提供的功能,则作为 render 函数返回值传入即可,render 参数用来传递数据。可能有点抽象,看看我之前博客的例子就可以明白,我这里 copy 过来,内容大概是,我有一个记录鼠标坐标的组件,如果某组件也需要这个功能,就可以不用写重复代码啦
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: "100%" }} onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
);
}
}
<Mouse render={mouse => <Cat mouse={mouse} />} />
// 当你使用Render Props时, 每次传入的render都是一个新的函数, 所以每次浅比较都会导致重新渲染。为了避免这个问题, 你可以将prop定义为一个实例方法
// <Mouse render={this.renderTheCat} />
Render Props有以下几个优点。
- 不用担心props的命名问题
- 可以溯源,子组件的props一定是来自于直接父组件
后续:应用越来越大,组件之间交互越来越复杂,那整个页面的数据逻辑将变得难以管理,这时候为了方便管理应用的状态,你可以选择一些状态管理工具,例如 Redux 等。当你代码中有大量的异步操作时,例如 fetch 请求,你肯定会想到事件监听、回调函数、发布/订阅。回调函数的主流的解决方案是 redux-thunk,而发布/订阅的主流解决方案是 saga。对于简单的业务,虽然有很多页面,嵌套层次也很复杂,你当然可以不用状态管理工具,你可以试着使用 Context,它可以方便你传递数据,它其实就是 Render Props 的一种实现。
Hooks
Hooks 是 React v16.7.0-alpha 推出了新特性,目前还是 alpha 版本,直接通过 create-react-app 创建的 app,截止到目前都是不支持 Hooks 的,这里小小的坑了我一把,使用 npm 更新 react 的版本总是无效,直到使用 yarn 才搞定,还能不能好好玩耍了
yarn add react@next react-dom@next
在 React 里,hooks 就是一系列特殊的函数,使函数组件 (functional component) 内部能够”钩住“ React 内部的 state 和 life-cycles。
首先我们思考:为啥又要造个 Hooks 呢
- 复用有状态组件太麻烦了,目前的复用方式 Render Props 和 HOC,无论是哪一种方法都会造成组件数量增多,组件树结构的修改,而且有可能出现组件嵌套地狱(wrapper hell)的情况。
- 生命周期钩子函数里的逻辑太乱了吧:我们通常希望一个函数只做一件事情,但是生命周期钩子函数中通常做了很多事情,而且这些事情通常互不相关
- class 让人困惑:主要表现在 this 指向问题和无状态组件到有状态组件的改造
Hooks 遵循函数式编程的理念,主旨是在函数组件中引入类组件中的状态和生命周期,并且这些状态和生命周期函数也可以被抽离,实现复用的同时,减少函数组件的复杂性和易用性。
Hooks 主要由三部分组成,State Hooks、Effect Hooks 和 Custom Hooks,先简单了解下对应 api 的使用
useState:用来声明状态变量,该方法创建一个传入初始值,创建一个 state。返回一个标识该 state 当前值,以及更新该 state 的方法。一个函数组件是可以通过 useState 创建多个 state 的。此外 State Hooks 的定义必须在函数组件的最高一级,不能在嵌套,循环等语句中使用。
为啥只能最高一级使用呢?因为一个函数组件可以存在多个 State Hooks,并且 useState 返回的是一个数组,数组的每一个元素是没有标识信息的,完全依靠调用 useState 的顺序来确定哪个状态对应于哪个变量,所以必须保证使用 useState 在函数组件的最外层,此外后面要介绍的 Effect Hooks 的函数 useEffect 也必须在函数组件的最外层。如果你使用了条件或循环语句,第二次渲染时,顺序有可能不对,这样就完全混乱了。
useEffect:以一种极为简化的方式来引入生命周期。useEffect 整合了 componentDidMount 和 componentDidUpdate,也就是说在 componentDidMount 和 componentDidUpdate 的时候都会执行一遍 useEffect 的函数,此外为了实现 componentWillUnmount 这个生命周期函数,useEffect 函数如果返回值是一个函数,这个函数就被定义成在 componentWillUnmount 这个周期内执行的函数。这个新的函数将会在组件下一次重新渲染之后执行。
useEffect(() => {
//componentDidMount 和 componentDidUpdate 周期的函数体
return () => {
//componentWillUnmount 周期的函数体
}
});
useEffect 第二个参数减少不必要的状态更新和渲染,上面我们知道 useEffect 其实包含了 componentDidMount 和 componentDidUpdate,如果仅想在 componentDidMount 的时候被执行,则传递一个空数据即可。此外我们也可以限制为某个具体值发生改变,才去执行函数。
可以将 useState 和 useEffect 的状态和生命周期函数抽离,组成一个新的函数,该函数就是一个自定义的封装完毕的 hooks
hooks 的目标就是让你不再写 class,让 function 一统江湖。
useState 和 useEffect 解决了函数式组件没有状态和生命周期的问题,但是如何才能把可以复用的逻辑抽离出来,变成一个个可以随意插拔的“插销”呢。其实很简单了,将相关 useState 和 useEffect 抽离出来到一个函数中(常取名为use*),返回 stateName 即可,在组件中就可以直接引用这个函数。
其他 Hooks
待学习尾链#3
谈 Hooks
简单看完 Hooks 入门后,内心还是很激动的,在 JS 的发展中,我感觉是有迷茫期的,受 OOP 编程的影响,开发者总想引入类、多态、继承等概念,ES6 之前我们通过 prototype 模拟,ES6 直接提供 class 关键字原生支持。但我们需要知道的是,无论你怎么伪装,JS 中的确是没有类的概念的。
Hooks 给我的感觉是,回归 JS 本质,函数才是一等公民,将面向类编程转换为面向函数编程。JS 学的越深,对他的一些天然特性就特别喜欢了,比如作为一门动态语言,提供了无比的灵活性。函数式编程,不一样的编程体验。
Hooks 引进后, 函数组件和 类组件该如何选择呢?官方类似回复如下
Our goal is for Hooks to cover all use cases for classes as soon as possible. There are no Hook equivalents to the uncommon getSnapshotBeforeUpdate and componentDidCatch lifecycles yet, but we plan to add them soon. It is a very early time for Hooks, so some integrations like DevTools support or Flow/TypeScript typings may not be ready yet. Some third-party libraries might also not be compatible with Hooks at the moment.
意思就是:官方的目标是尽可能快的让 Hooks 去覆盖所有的类组件案例,但是现在 Hooks 还处于一个非常早的阶段,各种调试工具、第三方库等都还没有做好对 Hooks 的支持,而且目前也没有可以取代类组件中 getSnapshotBeforeUpdate 和 componentDidCatch 生命做起的 Hooks,不过很快会加上他们。
Hooks 是否可以代替 render-props 和 higher-order components ?
Often, render props and higher-order components render only a single child. We think Hooks are a simpler way to serve this use case. There is still a place for both patterns (for example, a virtual scroller component might have a renderItem prop, or a visual container component might have its own DOM structure). But in most cases, Hooks will be sufficient and can help reduce nesting in your tree.
意思就是:在大多数案例下,hooks 足够应付且更适合,所以优先考虑 hooks。