react 精华内容总结----郭子墨

1. lodash实现padStart 时间格式化

为了在项目中使用 lodash,请先用 npm 完成对应的库安装。

import padStart from 'lodash/padStart'; const ms2Time = (milliseconds) => { let time = milliseconds; const ms = milliseconds % 1000; time = (milliseconds - ms) / 1000; const seconds = time % 60; time = (time - seconds) / 60; const minutes = time % 60; const hours = (time - minutes) / 60; const result = padStart(hours, 2, '0') + ":" + padStart(minutes, 2, '0') + ":" + padStart(seconds, 2, '0') + "." + padStart(ms, 3, '0'); return result; }

 

2.

如果定义构造函数 constructor,一定要记得通过 super 调用父类 React.Component 的构造函数,不然,功能会不正常。

React官方网站上的代码示例是这样调用super函数:

  constructor(props) {
    super(props); //目前可行,但有更好的方法
  }

在早期版本中,React.Component 的构造函数参数有两个,第一个是 props,第二个是 context,如果忽略掉 context 参数,那么这个组件的 context 功能就不能正常工作,不过,现在React的行为已经变了,第二个参数传递不传递都能让context正常工作,看起来React.Component 的构造函数只有第一个参数被用到,但是,没准未来还会增加新的参数呢,所以,以不变应万变的方法,就是使用扩展操作符(spread operator)来展开 arguments,这样不管 React 将来怎么变,这样的代码都正确。

  constructor() {
    super(...arguments); //永远正确!
  }

扩展操作符的作用,在 React 开发中会经常用到,在 JSX 中展开 props 的时候会用到。

属性初始化方法

不过,其实我们也可以完全避免编写 constructor 函数,而直接使用属性初始化(Property Initializer),也就是在 class 定义中直接初始化类的成员变量。

不用 constructor,可以这样初始化 state,效果是完全一样的:

class StopWatch extends React.Component {
  state = {
    isStarted: false,
    startTime: null,
    currentTime: null,
    splits: [],
  }
}

3.组件化样式

添加 styled-jsx 支持

要使用 styled-jsx,必须要修改 webpack 配置,一般来说,对于用 create-react-app 创建的应用,需要用 eject 方法来“弹射”出配置文件,只是,eject 指令是不可逆的,不到万不得已,我们还是不要轻易“弹射”。

一个更简单的方式,是使用 react-app-rewired,不需要 eject,轻轻松松就能够修改 create-react-app 产生应用的配置方法。

首先,我们在项目中安装 react-app-rewired 和 styled-jsx。如果读者使用的是npm v5之前的版本,最好添加--save参数用于修改package.json,如果使用npm v5之后版本,则无需添加--save参数。

npm install react-app-rewired styled-jsx

然后,打开 package.json 文件,找到 scripts 这个部分,应该是下面这样:

   "scripts": {
     "start": "react-scripts start",
     "build": "react-scripts build",
     "test": "react-scripts test --env=jsdom",
     "eject": "react-scripts eject"
   }

当我们在命令行执行 npm start 时,执行的就是 scripts 部分定义的指令,可以看到都是执行 react-scripts

在这里还可以看到 eject 指令的定义,我们做这个修改,恰恰就是为避免使用 eject

我们修改 scripts 部分的代码如下:

   "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test --env=jsdom",
    "eject": "react-scripts eject"

修改的方法其实就是把 startbuild 和 test 对应脚本中的 react-scripts 替换为 react-app-rewired,之后,当用 npm 执行这些指令的时候,就会使用 react-app-rewired。

react-app-rewired 扩展了 react-scripts 的功能,可以从当前目录的 config-overrides.js 文件中读取配置,扩充 react-scripts 的功能。

我们需要让 react-scripts 支持 styled-jsx,对应只需要在项目根目录增加一个 config-overrides.js 文件,内容如下:

const { injectBabelPlugin } = require('react-app-rewired');

module.exports = function override(config, env) {
  config = injectBabelPlugin(['styled-jsx/babel'], config);

  return config;
};

上面 config-overrides.js 文件中的逻辑很简单,就是把 styled-jsx/babel 注入到 react-scripts 的基本配置中去,然后,我们的应用就支持 styled-jsx 了。

使用 styled-jsx 定制样式

有了 styled-jsx 中,我们就可以在 JSX 中用 style jsx 标签直接添加 CSS 规则。

比如,我们要给 MajorClock 中的 h1 增加 CSS 规则,可以这样使用:

const MajorClock = ({milliseconds=0}) => {
  return (
    <React.Fragment>
      <style jsx>{`
        h1 {
          font-family: monospace;
        }
      `}</style>
      <h1>
        {ms2Time(milliseconds)}
      </h1>
    </React.Fragment>
  );
};

注意紧贴 style jsx 内部的是一对大括号,大括号代表里面是一段 JavaScript 的表达式,再往里,是一对符号,代表中间是一段多行的字符串,也就是说,style jsx 包裹的是一个字符串表达式,而这个字符串就是 CSS 规则。

在 MajorClock 中用 style jsx 添加的 CSS 规则,只作用于 MajorClock 的 JSX 中出现的元素,不会影响其他的组件。

你可以尝试在其他组件中添加 h1 元素,也可以尝试在其他组件中添加 style jsx 标签来定制 h1的样式,会发现和 MajorClock 完全是井水不犯河水,互不影响。

我在 StopWatch 中添加一个 h1 元素,内容就是“秒表”,然后用 style jsx 把 h1 的颜色设为绿色,代码如下:

  render() {
    return (
      <Fragment>
        <style jsx>{`
          h1 {
            color: green;
          }
        `}</style>
        <h1>秒表</h1>
        <MajorClock
          milliseconds={this.state.currentTime - this.state.startTime}
          activated={this.state.isStarted}
        />
   ... 

界面效果如下,可以看到,StopWatch 中的 h1 字体不是 monospace,MajorClock 中的 color 也不是绿色。

 

动态 styled jsx

更妙的是,我们还可以动态修改 styled jsx 中的值,因为 styled jsx 的内容就是字符串,我们只要修改其中的字符串,就修改了样式效果。

比如,我们让 MajorClock 在开始计时状态显示红色,否则显示黑色,修改代码如下:

const MajorClock = ({milliseconds=0, activated=false}) => {
  return (
    <React.Fragment>
      <style jsx>{`
        h1 {
          color: ${activated? 'red' : 'black'};
          font-family: monospace;
        }
      `}</style>
      <h1>
        {ms2Time(milliseconds)}
      </h1>
    </React.Fragment>
  );
};

在 style jsx 中,color 后面的值不是固定的,利用 ES6 的字符串模板功能,我们可以根据 activated 的值动态决定是 red 还是 black

 

 

4.聪明组件和傻瓜组件

PureComponent

因为傻瓜组件一般没有自己的状态,所以,可以像上面的 Joke 一样实现为函数形式,其实,我们可以进一步改进,利用 PureComponent 来提高傻瓜组件的性能。

函数形式的 React 组件,好处是不需要管理 state,占用资源少,但是,函数形式的组件无法利用 shouldComponentUpdate

看上面的例子,当 RandomJoke 要渲染 Joke 时,即使传入的 props 是一模一样的,Joke 也要走一遍完整的渲染过程,这就显得浪费了。

好一点的方法,是把 Joke 实现为一个类,而且定义 shouldComponentUpdate 函数,每次渲染过程中,在 render 函数执行之前 shouldComponentUpdate 会被调用,如果返回 true,那就继续,如果返回 false,那么渲染过程立刻停止,因为这代表不需要重画了。

对于傻瓜组件,因为逻辑很简单,界面完全由 props 决定,所以 shouldComponentUpdate 的实现方式就是比较这次渲染的 props 是否和上一次 props 相同。当然,让每一个组件都实现一遍这样简单的 shouldComponentUpdate 也很浪费,所以,React 提供了一个简单的实现工具 PureComponent,可以满足绝大部分需求。

改进后的 Joke 组件如下:

class Joke extends React.PureComponent {
  render() {
    return (
      <div>
        <img src={SmileFace} />
        {this.props.value || 'loading...' }
      </div>
    );
  }
}

值得一提的是,PureComponent 中 shouldComponentUpdate 对 props 做得只是浅层比较,不是深层比较,如果 props 是一个深层对象,就容易产生问题。

比如,两次渲染传入的某个 props 都是同一个对象,但是对象中某个属性的值不同,这在 PureComponent 眼里,props 没有变化,不会重新渲染,但是这明显不是我们想要的结果。

React.memo

虽然 PureComponent 可以提高组件渲染性能,但是它也不是没有代价的,它逼迫我们必须把组件实现为 class,不能用纯函数来实现组件。

如果你使用 React v16.6.0 之后的版本,可以使用一个新功能 React.memo 来完美实现 React 组件,上面的 Joke 组件可以这么写:

const Joke = React.memo(() => (
    <div>
        <img src={SmileFace} />
        {this.props.value || 'loading...' }
    </div>
));

React.memo 既利用了 shouldComponentUpdate,又不要求我们写一个 class,这也体现出 React 逐步向完全函数式编程前进。

 

5.高阶组件

用高阶组件抽取共同逻辑

接下来,我们对 withDoNothing 进行一些改进,让它实现“只有在登录时才显示”这个功能。

假设我们已经有一个函数 getUserId 能够从 cookies 中读取登录用户的 ID,如果用户未登录,这个 getUserId 就返回空,那么“退出登录按钮“就需要这么写:

const LogoutButton = () => {
  if (getUserId()) {
    return ...; // 显示”退出登录“的JSX
  } else {
    return null;
  }
};

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

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

上面两个组件明显有重复的代码,我们可以把重复代码抽取出来,形成 withLogin 这个高阶组件,代码如下:

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

  return NewComponent;
};

如此一来,我们就只需要这样定义 LogoutButton 和 ShoppintCart

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

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

你看,我们避免了重复代码,以后如果要修改对用户是否登录的判断逻辑,也只需要修改 withLogin,而不用修改每个 React 组件。

高阶组件的高级用法

高阶组件只需要返回一个 React 组件即可,没人规定高阶组件只能接受一个 React 组件作为参数,完全可以传入多个 React 组件给高阶组件。

比如,我们可以改进上面的 withLogin,让它接受两个 React 组件,根据用户是否登录选择渲染合适的组件。

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

有了上面的 withLoginAndLogout,就可以产生根据用户登录状态显示不同的内容。

const TopButtons = withLoginAndLogout(
  LogoutButton,
  LoginButton
);

链式调用高阶组件

高阶组件最巧妙的一点,是可以链式调用。

假设,你有三个高阶组件分别是 withOnewithTwo 和 withThree,那么,如果要赋予一个组件 X 某个高阶组件的超能力,那么,你要做的就是挨个使用高阶组件包装,代码如下:

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

很自然,我们可以避免使用中间变量 X1 和 X2,直接连续调用高阶组件,如下:

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

对于 X 而言,它被高阶组件包装了,至于被一个高阶组件包装,还是被 N 个高阶组件包装,没有什么差别。而高阶组件本身就是一个纯函数,纯函数是可以组合使用的,所以,我们其实可以把多个高阶组件组合为一个高阶组件,然后用这一个高阶组件去包装X,代码如下:

const hoc = compose(withThree, withTow, withOne);
const SuperX = hoc(X);

在上面代码中使用的 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)))
}

React 组件可以当做积木一样组合使用,现在有了 compose,我们就可以把高阶组件也当做积木一样组合,进一步重用代码。

假如一个应用中多个组件都需要同样的多个高阶组件包装,那就可以用 compose 组合这些高阶组件为一个高阶组件,这样在使用多个高阶组件的地方实际上就只需要使用一个高阶组件了。

不要滥用高阶组件

高阶组件虽然可以用一种可重用的方式扩充现有 React 组件的功能,但高阶组件并不是绝对完美的。

首先,高阶组件不得不处理 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;
};

每个高阶组件都这么写,就会非常的麻烦。

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

其次,高阶组件支持嵌套调用,这是它的优势。但是如果真的一大长串高阶组件被应用的话,当组件出错,你看到的会是一个超深的 stack trace,十分痛苦。

最后,使用高阶组件,一定要非常小心,要避免重复产生 React 组件,比如,下面的代码是有问题的:

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

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

正确的写法是下面这样,自始至终只有一个 EnhancedFoo 组件类被创建:

const EnhancedFoo = withExample(Foo);

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

总之,高阶组件是重用代码的一种方式,但并不是唯一方式,在下一小节,我们会介绍一种更加精妙的方式。

 

6.render props 模式  调用组件时候可以传递function

 

所谓 render props,指的是让 React 组件的 props 支持函数这种模式。因为作为 props 传入的函数往往被用来渲染一部分界面,所以这种模式被称为 render props。

一个最简单的 render props 组件 RenderAll,代码如下:

const RenderAll = (props) => {
  return(
     <React.Fragment>
     	{props.children(props)}
     </React.Fragement>
  );
};

这个 RenderAll 预期子组件是一个函数,它所做的事情就是把子组件当做函数调用,调用参数就是传入的 props,然后把返回结果渲染出来,除此之外什么事情都没有做。

使用 RenderAll 的代码如下:

      <RenderAll>
        {() => <h1>hello world</h1>}
      </RenderAll>

使用场景:

传递 props

和高阶组件一样,render props 可以做很多的定制功能,我们还是以根据是否登录状态来显示一些界面元素为例,来实现一个 render props。

下面是实现 render props 的 Login 组件,可以看到,render props 和高阶组件的第一个区别,就是 render props 是真正的 React 组件,而不是一个返回 React 组件的函数。

const Login = (props) => {
  const userName = getUserName();

  if (userName) {
    const allProps = {userName, ...props};
    return (
      <React.Fragment>
        {props.children(allProps)}
      </React.Fragment>
    );
  } else {
    return null;
  }
};

当用户处于登录状态,getUserName 返回当前用户名,否则返回空,然后我们根据这个结果决定是否渲染 props.children 返回的结果。

当然,render props 完全可以决定哪些 props 可以传递给 props.children,在 Login 中,我们把 userName 作为增加的 props 传递给下去,这样就是 Login 的增强功能。

一个使用上面 Login 的 JSX 代码示例如下:

  <Login>
    {({userName}) => <h1>Hello {userName}</h1>}
//  这部分就是children 相当于给Login 上写一个children 属性 把children传递过去 props.children
//相当于属性是一个函数
  </Login>

对于名为“程墨Morgan”的用户登录,上面的 JSX 会产生 <h1>Hello 程墨Morgan</h1>

 

7. 

不局限于 children

在上面的例子中,作为 render 方法的 props 就是 children,在我写的《深入浅出React和Redux》中,将这种模式称为“以函数为子组件(function as child)”的模式,这可以算是 render props 的一种具体形式,也就利用 children 这个 props 来作为函数传递。

实际上,render props 这个模式不必局限于 children 这一个 props,任何一个 props 都可以作为函数,也可以利用多个 props 来作为函数。

我们来扩展 Login,不光在用户登录时显示一些东西,也可以定制用户没有登录时显示的东西,我们把这个组件叫做 Auth,对应代码如下:

const Auth= (props) => {
  const userName = getUserName();

  if (userName) {
    const allProps = {userName, ...props};
    return (
      <React.Fragment>
        {props.login(allProps)}
      </React.Fragment>
    );
  } else {
    <React.Fragment>
      {props.nologin(props)}
    </React.Fragment>
  }
};

使用 Auth 的话,可以分别通过 login 和 nologin 两个 props 来指定用户登录或者没登录时显示什么,用法如下:

  <Auth
    login={({userName}) => <h1>Hello {userName}</h1>}
    nologin={() => <h1>Please login</h1>}
  />
 

依赖注入

render props 其实就是 React 世界中的“依赖注入”(Dependency Injection)。

所谓依赖注入,指的是解决这样一个问题:逻辑 A 依赖于逻辑 B,如果让 A 直接依赖于 B,当然可行,但是 A 就没法做得通用了。依赖注入就是把 B 的逻辑以函数形式传递给 A,A 和 B 之间只需要对这个函数接口达成一致就行,如此一来,再来一个逻辑 C,也可以用一样的方法重用逻辑 A。

在上面的代码示例中,Login 和 Auth 组件就是上面所说的逻辑 A,而传递给组件的函数类型 props,就是逻辑 B 和 C。

 

8.高阶组件被包裹的组件 和render props中被包裹组件的传递

render props 和高阶组件的比较

我们来比对一下这两种重用 React 组件逻辑的模式。

首先,render props 模式的应用,就是做一个 React 组件,而高阶组件,虽然名为“组件”,其实只是一个产生 React 组件的函数。

render props 不像上一小节中介绍的高阶组件有那么多毛病,如果说 render props 有什么缺点,那就是 render props 不能像高阶组件那样链式调用,当然,这并不是一个致命缺点。

render props 相对于高阶组件还有一个显著优势,就是对于新增的 props 更加灵活。还是以登录状态为例,假如我们扩展 withLogin 的功能,让它给被包裹的组件传递用户名这个 props,代码如下:

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

  return NewComponent;
};

这就要求被 withLogin 包住的组件要接受 userName 这个props。可是,假如有一个现成的 React 组件不接受 userName,却接受名为 name 的 props 作为用户名,这就麻烦了。我们就不能直接用 withLogin 包住这个 React 组件,还要再造一个组件来做 userName 到 name 的映射,十分费事。

对于应用 render props 的 Login,就不存在这个问题,接受 name 不接受 userName 是吗?这样写就好了:


<Login>
  {
    (props) => {
      const {userName} = props;
      return <TheComponent {...props} name={userName} />
    }
  }
</Login>


相当于
<Login children = (props) => {
      const {userName} = props;
      return <TheComponent {...props} name={userName} />
    } />
 

login的孩子是一个组件
 

所以,当需要重用 React 组件的逻辑时,建议首先看这个功能是否可以抽象为一个简单的组件;如果行不通的话,考虑是否可以应用 render props 模式;再不行的话,才考虑应用高阶组件模式。

这并不表示高阶组件无用武之地,在后续章节,我们会对 render props 和高阶组件分别讲解具体的实例。

 

9.提供者模式使用context 进行多层组件传递数据

所谓 Context 功能,就是能够创造一个“上下文”,在这个上下文笼罩之下的所有组件都可以访问同样的数据。

在 React v16.3.0 之前,React 虽然提供了 Context 功能,但是官方文档上都建议尽量不要使用,因为对应的 API 他们并不满意,觉得迟早要废弃掉。即使如此,依然有很多库和应用使用 Context 功能,可见对这个需求的呼声有多大。

当 React 发布 v16.3.0 时,终于提供了“正式版本”的 Context 功能 API,和之前的有很大不同,当然,这也带来一些问题,我在后面会介绍。

提供者模式的一个典型用例就是实现“样式主题”(Theme),由顶层的提供者确定一个主题,下面的样式就可以直接使用对应主题里的样式。这样,当需要切换样式时,只需要修改提供者就行,其他组件不用修改。

为了方便比对,这里我会介绍提供者模式用不同 Context API 的实现方法。不过,你如果完全不在意老版本 React 如何实现的,可以略过下面一段。

React v16.3.0 之前的提供者模式

在 React v16.3.0 之前,要实现提供者,就要实现一个 React 组件,不过这个组件要做两个特殊处理。

  1. 需要实现 getChildContext 方法,用于返回“上下文”的数据;
  2. 需要定义 childContextTypes 属性,声明“上下文”的结构。

下面就是一个实现“提供者”的例子,组件名为 ThemeProvider

class ThemeProvider extends React.Component {
  getChildContext() {
    return {
      theme: this.props.value
    };
  }

  render() {
    return (
      <React.Fragment>
        {this.props.children}
      </React.Fragment>
    );
  }
}

ThemeProvider.childContextTypes = {
  theme: PropTypes.object
};

在上面的例子中,getChildContext 只是简单返回名为 value 的 props 值,但是,因为 getChildContext 是一个函数,它可以有更加复杂的操作,比如可以从 state 或者其他数据源获得数据。

对于 ThemeProvider,我们创造了一个上下文,这个上下文就是一个对象,结构是这样:

{
  theme: {
    //一个对象
  }
}

接下来,我们来做两个消费(也就是使用)这个“上下文”的组件,第一个是 Subject,代表标题;第二个是 Paragraph,代表章节。

我们把 Subject 实现为一个类,代码如下:

class Subject extends React.Component {
  render() {
    const {mainColor} = this.context.theme;
    return (
      <h1 style={{color: mainColor}}>
        {this.props.children}
      </h1>
    );
  }
}

Subject.contextTypes = {
  theme: PropTypes.object
}

在 Subject 的 render 函数中,可以通过 this.context 访问到“上下文”数据,因为 ThemeProvider 提供的“上下文”包含 theme 字段,所以可以直接访问 this.context.theme

千万不要忘了 Subject 必须增加 contextTypes 属性,必须和 ThemeProvider 的 childContextTypes 属性一致,不然,this.context 就不会得到任何值。

读者可能会问了,为什么这么麻烦呢?为什么要求“提供者”用 childContextTypes 定义一次上下文结构,又要求“消费者”再用 contextTypes 再重复定义一次呢?这不是很浪费吗?

React 这么要求,是考虑到“上下文”可能会嵌套,就是一个“提供者”套着另一个“提供者”,这时候,底层的消费者组件到底消费哪一个“提供者”呢?通过这种显示的方式指定。

不过,实话实说,这样的 API 设计的确麻烦了一点,难怪 React 官方在最初就不建议使用。

上面的 Subject 是一个类,其实也可以把消费者实现为一个纯函数组件,只不过访问“上下文”的方式有些不同,我们用纯函数的方式实现另一个消费者 Paragraph,代码如下:

const Paragraph = (props, context) => {
  const {textColor} = context.theme;
  return (
    <p style={{color: textColor}}>
      {props.children}
    </p>
  );
};

Paragraph.contextTypes = {
  theme: PropTypes.object
};

从上面的代码可以看到,因为 Paragraph 是一个函数形式,所以不可能访问 this.context,但是函数的第二个参数其实就是 context

当然,也不要忘了设定 Paragraph 的 contextTypes,不然参数 context 也不会是上下文。

最后,我们看如何结合”提供者“和”消费者“。

我们做一个组件来使用 Subject 和 Paragraph,这个组件不需要帮助传递任何 props,代码如下:

const Page = () => (
  <div>
    <Subject>这是标题</Subject>
    <Paragraph>
      这是正文
    </Paragraph>
  </div>
);

上面的组件 Page 使用了 Subject 和 Paragraph,现在我们想要定制样式主题,只需要在 Page 或者任何需要应用这个主题的组件外面包上 ThemeProvider,对应的 JSX 代码如下:

  <ThemeProvider value={{mainColor: 'green', textColor: 'red'}} >
    <Page />
  </ThemeProvider>

最后,看到的效果如下:

 

Theme Provider

 

 

当我们需要改变一个样式主题的时候,改变传给 ThemeProvider的 value 值就搞定了。

React v16.3.0 之后的提供者模式

到了 React v16.3.0 的时候,新的 Context API 出来了,这套 API 毫不掩饰自己就是“提供者模式”的实现,命名上就带 “Provider” 和 “Consumer”。

还是上面的样式主题的例子,首先,要用新提供的 createContext 函数创造一个“上下文”对象。

const ThemeContext = React.createContext();

这个“上下文”对象 ThemeContext 有两个属性,分别就是——对,你没猜错——Provider 和 Consumer

const ThemeProvider = ThemeContext.Provider;
const ThemeConsumer = ThemeContext.Consumer;

创造“提供者”极大简化了,都不需要我们创造一个 React 组件类。

使用“消费者”也同样简单,而且应用了上一节我们介绍的 render props 模式,比如,Subject 的代码如下:

class Subject extends React.Component {
  render() {
    return (
      <ThemeConsumer>
        {
          (theme) => (
            <h1 style={{color: theme.mainColor}}>
              {this.props.children}
            </h1>
          )
        }
      </ThemeConsumer>
    );
  }
}

上面的 ThemeConsumer 其实就是一个应用了 render props 模式的组件,它要求子组件是一个函数,会把“上下文”的数据作为参数传递给这个函数,而这个函数里就可以通过参数访问“上下文”对象。

在新的 API 里,不需要设定组件的 childContextTypes 或者 contextTypes 属性,这省了不少事。

可以注意到,Subject 没有自己的状态,没必要实现为类,我们用纯函数的形式实现 Paragraph,代码如下:

const Paragraph = (props, context) => {
  return (
    <ThemeConsumer>
      {
        (theme) => (
          <p style={{color: theme.textColor}}>
            {props.children}
          </p>
          )
      }
    </ThemeConsumer>
  );
};

实现 Page 的方式并没有变化,而应用 ThemeProvider 的代码和之前也完全一样:

  <ThemeProvider value={{mainColor: 'green', textColor: 'red'}} >
    <Page />
  </ThemeProvider>

两种提供者模式实现方式的比较

通过上面的代码,可以很清楚地看到,新的 Context API 更简洁,但是,也并不是十全十美。

在老版 Context API 中,“上下文”只是一个概念,并不对应一个代码,两个组件之间达成一个协议,就诞生了“上下文”。

在新版 Context API 中,需要一个“上下文”对象(上面的例子中就是 ThemeContext),使用“提供者”的代码和“消费者”的代码往往分布在不同的代码文件中,那么,这个 ThemeContext 对象放在哪个代码文件中呢?

最好是放在一个独立的文件中,这么一来,就多出一个代码文件,而且所有和这个“上下文”相关的代码,都要依赖于这个“上下文”代码文件,虽然这没什么大不了的,但是的确多了一层依赖关系。

为了避免依赖关系复杂,每个应用都不要滥用“上下文”,应该限制“上下文”的使用个数。

不管怎么说,新版本的 Context API 才是未来,在 React v17 中,可能就会删除对老版 Context API 的支持,所以,现在大家都应该使用第二种实现方式。

 

 

10.

很多界面都有 Tab 这样的元件,我们需要一个 Tabs 组件和 TabItem 组件,Tabs 是容器,TabItem 是一个一个单独的 Tab,因为一个时刻只有一个 TabItem 被选中,很自然希望被选中的 TabItem 样式会和其他 TabItem 不同。

这并不是一个很难的功能,首先我们想到的就是,用 Tabs 中一个 state 记录当前被选中的 Tabitem 序号,然后根据这个 state 传递 props 给 TabItem,当然,还要传递一个 onClick 事件进去,捕获点击选择事件。

按照这样的设计,Tabs 中如果要显示 One、Two、Three 三个 TabItem,JSX 代码大致这么写:

      <TabItem active={true} onClick={this.onClick}>One</TabItem>
      <TabItem active={false} onClick={this.onClick}>Two</TabItem>
      <TabItem active={false} onClick={this.onClick}>Three</TabItem> 

上面的 TabItem 组件接受 active 这个 props,如果 true 代表当前是选中状态,当然可以工作,但是,也存在大问题:

  1. 每次使用 TabItem 都要传递一堆 props,好麻烦;
  2. 每增加一个新的 TabItem,都要增加对应的 props,更麻烦;
  3. 如果要增加 TabItem,就要去修改 Tabs 的 JSX 代码,超麻烦。

我们不想要这么麻烦,理想情况下,我们希望可以随意增加减少 TabItem 实例,不用传递一堆 props,也不用去修改 Tabs 的代码,最好代码就这样:

    <Tabs>
      <TabItem>One</TabItem>
      <TabItem>Two</TabItem>
      <TabItem>Three</TabItem>
    </Tabs>

如果能像上面一样写代码,那就达到目的了。

像上面这样,Tabs 和 TabItem 不通过表面的 props 传递也能心有灵犀,二者之间有某种神秘的“组合”,就是我们所说的“组合组件”。

传递值通过改造tabs的children 重新渲染children

实现方式

上面我们说过,利用 Context API,可以实现组合组件,但是那样 TabItem 需要应用 render props,至于如何实现,读者可以参照上一节的介绍自己尝试。

在这里,我们用一种更巧妙的方式来实现组合组件,可以避免 TabItem 的复杂化。

我们先写出 TabItem 的代码,如下:

const TabItem = (props) => {
  const {active, onClick} = props;
  const tabStyle = {
    'max-width': '150px',
    color: active ? 'red' : 'green',
    border: active ? '1px red solid' : '0px',
  };
  return (
    <h1 style={tabStyle} onClick={onClick}>
      {props.children}
    </h1>
  );
};

TabItem 有两个重要的 props:active 代表自己是否被激活,onClick 是自己被点击时应该调用的回调函数,这就足够了。TabItem 所做的就是根据这两个 props 渲染出 props.children,没有任何复杂逻辑,是一个活脱脱的“傻瓜组件”,所以,用一个纯函数实现就可以了。

接下来要做的,就看 Tabs 如何把 active 和 onClick 传递给 TabItem。

我们再来看一下使用组合组件的 JSX 代码:

    <Tabs>
      <TabItem>One</TabItem>
      <TabItem>Two</TabItem>
      <TabItem>Three</TabItem>
    </Tabs>

没有 props 的传递啊,怎么悄无声息地把 active 和 onClick 传递给 TabItem 呢?

Tabs 虽然可以访问到作为 props 的 children,但是到手的 children 已经是创造好的元素,而且是不可改变的,Tabs 是不可能把创造好的元素再强塞给 children 的。

怎么办?

办法还是有的,如果 Tabs 并不去渲染 children,而是把 children 拷贝一份,就有机会去篡改这份拷贝,最后渲染这份拷贝就好了。

我们来看 Tabs 的实现代码:

class Tabs extends React.Component {
  state = {
    activeIndex:  0
  }

  render() {
    const newChildren = React.Children.map(this.props.children, (child, index) => {
      if (child.type) {
        return React.cloneElement(child, {
          active: this.state.activeIndex === index,
          onClick: () => this.setState({activeIndex: index})
        });
      } else {
        return child;
      }
    });

    return (
      <Fragment>
        {newChildren}
      </Fragment>
    );
  }
}

在 render 函数中,我们用了 React 中不常用的两个 API:

  1. React.Children.map
  2. React.cloneElement

使用 React.Children.map,可以遍历 children 中所有的元素,因为 children 可能是一个数组嘛。

使用 React.cloneElement 可以复制某个元素。这个函数第一个参数就是被复制的元素,第二个参数可以增加新产生元素的 props,我们就是利用这个机会,把 active 和 onClick 添加了进去。

这两个 API 双剑合璧,就能实现不通过表面的 props 传递,完成两个组件的“组合”。

 

从上面的代码可以看出来,对于组合组件这种实现方式,TabItem 非常简化;Tabs 稍微麻烦了一点,但是好处就是把复杂度都封装起来了,从使用者角度,连 props 都看不见。

所以,应用组合组件的往往是共享组件库,把一些常用的功能封装在组件里,让应用层直接用就行。在 antd 和 bootstrap 这样的共享库中,都使用了组合组件这种模式。

如果你的某两个组件并不需要重用,那么就要谨慎使用组合组件模式,毕竟这让代码复杂了一些。

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值