【墙裂推荐】Talking about hooks(上)

简介: 从React16.8开始,Hooks API正式被React支持,而就在最近,Vue作者尤雨溪翻译并发布了一篇自己的文章《Vue Function-based API RFC》,并在全文开头强调这是Vue 3.0最重要的RFC,并在文中提到Function-based API 受 React Hooks 的启发,提供了一个全新的逻辑复用方案。

从React16.8开始,Hooks API正式被React支持,而就在最近,Vue作者尤雨溪翻译并发布了一篇自己的文章《Vue Function-based API RFC》,并在全文开头强调这是Vue 3.0最重要的RFC,并在文中提到


Function-based API 受 React Hooks 的启发,提供了一个全新的逻辑复用方案。


可以简单的理解为,React 和 Vue 为了解决相同的问题,基于不同的技术实现了相似的API。所以本文也将结合两种框架各自的特点,简单讲讲个人对Hooks的理解。


在未来版本的规划里,React并不如Vue激进,React的文档里专门提到


并没有从 React 中移除 class的计划。


而Vue却采取了不同的升级策略,做好了抛弃大部分历史语法的准备


  • 兼容版本:同时支持新 API 和 2.x 的所有选项;


  • 标准版本:只支持新 API 和部分 2.x 选项。


为什么我们不再需要Class Component?


为了回答这个问题,我们先看看之前和现在的React组件划分产生了哪些变化。

1. 既然本来就有函数组件,开始为什么引入class组件?


早期的React组件可以依据“有没有状态(state)”分为


// 无状态组件
const Welcome = (props) => <h1>Hello, {props.name}</h1>;
// 有状态组件
class Welcome extends React.Component {
    constructor(props) {
        super(props);
        this.state = {name: 'KuaiGou'};
    }
    render() {
        return <h1>Hello, {this.state.name}</h1>;
    }
}


虽然class也可以不添加状态,但想要使一个函数组件具有状态,不得不将其转换成class组件。


直观来看,好像造成这种差异是因为在class里,我们能通过this保存和访问“状态(state)”,而函数组件在其作用域内难以维持“状态(state)”,因为再次函数运行会重置其作用域内部变量,这种差异导致了我们“不得不”使用class至今。


看来如何解决函数组件保存state的成了移除class这种“难以理解”的关键。


2. 那Hook是如何保留组件状态的?


这就是我看见Hook API产生的第一个疑问。其实在React里,这并不是问题,熟悉React Fiber的同学应该知道,事实上state是保存到Fiber上的属性memoizedState上的,而并不算是class的this.state上。那状态问题就迎刃而解了,如果函数组件同样访问Fiber上的memoizedState属性,就可以解决这个问题。


基于Fiber架构,解决这个问题非常容易,将memoizedState看作一个普通的变量,那么Hook的原理就容易理解和实现了。


在文章[译] 理解 React Hooks中提到


记住,在 Hooks 的实现中也没有什么“魔术”。就像 Jamie 指出的那样,它像极了这个:


let hooks = null;
export function useHook() {
    hooks.push(hookData);
}
function reactsInternalRenderAComponentMethod(component) {
    hooks = [];
    component();
    let hooksForThisComponent = hooks;
    hooks = null
}


如Fiber一样,React实际上使用链表代替了数组这种数据结构,依次执行Hook,有兴趣的同学可以去看下React源码。


可是,class目前也能良好的支撑业务迭代,到底有什么动力去重新学习Hooks?


3. 为什么我们需要Hooks?


针对这个问题,React文档提到了下面三点:


  • 在组件之间复用状态逻辑很难


  • 复杂组件变得难以理解


  • 难以理解的 class


其实我觉得第三点就是来凑数的,毕竟React推出至今一直用着class,再难用各位也都会了,会者不难难者不会嘛(反正对于刚入前端坑那时候的我来说,没有啥是容易的)。


那就回答下一个问题,目前基于class实现的生命周期函数,是否真的会造成逻辑难以复用?


答案是NO


无论高阶组件或是render props,都提供了很好的方式来达到聚合业务逻辑的目的,业务逻辑并不会被生命周期“分割”。


那到底是哪里引入了复杂度?熟悉套娃的同学...呸


熟悉Ajax、Promise的等异步API的同学可能还记得“回调地狱”。类似的,高阶函数、render props等也极容易造成“嵌套地狱”,结合装饰器、函数式的compose等嵌套起来才是真的爽...一直嵌套一直爽...


但是,无论是什么地狱肯定是不好的,那一起来看最后一个问题。


复杂组件变得难以理解


之所以回避前两个问题,是因为我个人认为,无论是class还是HOC,它们都很好的解决了它们需要解决的问题,虽然生命周期函数将很多业务逻辑拆分的七零八碎,但是HOC却依旧能把它们集合在一起,仅考虑保留生命周期而言,就像Function-based一样(这是后话)。


所以我们换一个思路不难发现,真正的问题是在于它们在抽象业务逻辑的时候貌似引入了不必要的概念,才使得逻辑复用困难和难以理解。


这些概念导致了过多的嵌套,加深了组件层级,层级之间互相牵扯,就像我现在兜里的耳机线一样。


Hook独特之处在于化繁为简。


真正繁琐的是层级与层级之间的关系,我将借用React文档关于自定义Hook的例子说明这个问题


import React, { useState, useEffect } from 'react';
// 通过friendID订阅好友状态
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });
  return isOnline;
}


相关文章