react高阶组件

1,636 阅读6分钟

此篇文章准备从高阶组件的概念出发,通过应用场景结合例子来学习react的高阶组件。 ##何为高阶组件 在开发 React 组件过程中,很容易发现这样一种现象,某些功能是多个组件通用的,而最直接的想法就是要把这部分共用逻辑提取出来重用。当然,这里有个现成的思路,那就是将这些逻辑写成React 组件来进行复用。不过,有些情况下,这些共用逻辑单独无法使用,它们只是对其他组件的功能加强。 这时候,我们就可以利用“高阶组件(HoC)”这种组件设计模式来解决问题。


##高阶组件的基本形式 “高阶组件”虽然名为“组件”,其实是一个函数,只不过这个函数比较特殊,它接受至少一个 React 组件为参数,并且能够返回一个全新的 React 组件作为结果,换句话说说,这个新产生的 React 组件是对作为参数的组件的包装。 一个最简单的高阶组件是这样:

const withDoNothing = (Component) => {
  const NewComponent = (props) => {
    return <Component {...props} />;
  };
  return NewComponent;
};

上面的函数withDoNothing就是一个高阶组件,作为一项业界通用的代码规范,高阶组件的命名一般都带with前缀,命名中后面的部分代表这个高阶组件的功能。 对于上面这个高阶组件的例子我们能够总结出高阶组件的基本套路:

  1. 高阶组件不能去修改作为参数的组件,高阶组件必须是一个纯函数,不应该有任何副作用。
  2. 高阶组件返回的结果必须是一个新的 React 组件,这个新的组件的 JSX 部分肯定会包含作为参数的组件。
  3. 高阶组件一般需要把传给自己的 props 转手传递给作为参数的组件。

利用高阶组件提取公共逻辑

这里有一个这样的场景:对于一个电商类网站,“退出登录”按钮、“购物车”这些模块,就只有用户登录之后才显示。 实现上述的逻辑,“退出登录按钮“会这么写:

//假设有一个函数 getUserId 能够从 cookies 中读取登录用户的 ID,如果用户未登录,这个 getUserId 就返回空
const LogoutButton = () => {
  if (getUserId()) {
    return ...; // 显示”退出登录“的JSX
  } else {
    return null;
  }
}

同样,“购物车”的代码就是这样:

const ShoppingCart = () => {
  if (getUserId()) {
    return ...; // 显示”购物车“的JSX
  } else {
    return null;
  }
};

上面的两个组件很明显有相同的逻辑,这个时候就可以发挥高阶组件的作用了。我们定义一个withLogin的高阶组件,其内部实现代码如下:

const withLogin = (Component) => {
  const NewComponent = (props) => {
    if (getUserId()) {
      return <Component {...props} />;
    } else {
      return null;
    }
  }

  return NewComponent;
};

于是,“购物车”和“退出按钮”的模块就可以变成这样:

const LogoutButton = withLogin((props) => {
  return ...; // 显示”退出登录“的JSX
});

const ShoppingCart = withLogin(() => {
  return ...; // 显示”购物车“的JSX
});

这样一来,我们只需要关注各自内部实现就ok了。


高阶组件的进阶

####1:接受多个组件参量 还是前面的例子,我们来改进上面的 withLogin,定义为withLoginAndLogout,让它接受两个 React 组件,根据用户是否登录选择渲染合适的组件。

const withLoginAndLogout = (ComponentForLogin, ComponentForLogout) => {
  const NewComponent = (props) => {
    if (getUserId()) {
      return <ComponentForLogin {...props} />;
    } else {
      return <ComponentForLogout{...props} />;
    }
  }
  return NewComponent;
};

这样一来,我们就可以产生根据用户登录状态显示不同的内容

const TopButtons = withLoginAndLogout(
  LogoutButton,
  LoginButton
);

####2:链式调用高阶组件 高阶组件的链式调用赋予了高阶组件新的生命。 假设,有三个高阶组件分别是 withOne、withTwo 和 withThree,那么,如果要赋予一个组件 X某些高阶组件的超能力,那么,你要做的就是挨个使用高阶组件包装,代码如下:

const X1 = withOne(X);
const X2 = withTwo(X1);
const X3 = withThree(X2);
const SuperX = X3; //最终的SuperX具备三个高阶组件的超能力

当然,我们可以简化直接连续调用高阶组件,如下:

const SuperX = withThree(withTwo(withOne(X)));

对于 X 而言,它被高阶组件包装了,至于被一个高阶组件包装,还是被 N 个高阶组件包装,没有什么差别。 那么我们是不是可以往更深的角度去思考? 因为高阶组件本身就是一个纯函数,纯函数是可以组合使用的,所以,我们其实可以把多个高阶组件组合为一个高阶组件,然后用这个组合高阶组件去包装X。 在上面代码中使用的compose,是函数式编程中应用的一种方法,作用就是把多个函数组合为一个函数,所以一个应用中多个组件都需要多个高阶组件包装,那就可以用compose组合这些高阶组件为一个高阶组件,这样在使用多个高阶组件的地方实际上就只需要使用一个高阶组件了。这里提供一个实现compose的方法作为参考。

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

reduce的语法解释。


##高阶组件的限制 所有的事物都有两面性,看待高阶组件的时候也是需要辨证得使用。 1:高阶组件不得不处理 displayName,不然debug 会很痛苦。当 React 渲染出错的时候,靠组件的 displayName 静态属性来判断出错的组件类,而高阶组件总是创造一个新的 React 组件类,所以,每个高阶组件都需要处理一下 displayName。 比如:

const withExample = (Component) => {
  const NewComponent = (props) => {
    return <Component {...props} />;
  }
  
  NewComponent.displayName = `withExample(${Component.displayName || Component.name || 'Component'})`;
  
  return NewCompoennt;
};

2:对于 React 生命周期函数,高阶组件不用怎么特殊处理,但是,如果内层组件包含定制的静态函数,这些静态函数的调用在 React 生命周期之外,那么高阶组件就必须要在新产生的组件中增加这些静态函数的支持,这更加麻烦。 3:避免重复产生 React 组件 不要这样使用高阶组件

const Example = () => {
  const EnhancedFoo = withExample(Foo);
  return <EnhancedFoo />
}

如果像上述使用,每一次渲染 Example,都会用高阶组件产生一个新的组件,虽然都叫做 EnhancedFoo,但是对 React 来说是一个全新的东西,在重新渲染的时候不会重用之前的虚拟 DOM,会造成极大的浪费。 正确的写法是下面这样:

const EnhancedFoo = withExample(Foo);

const Example = () => {
  return <EnhancedFoo />
}