React 高级指导 笔记

作者: gaoer1938 | 来源:发表于2017-04-12 14:52 被阅读1947次

    深入JSX

    date:20170412
    笔记原文
    其实JSX是React.createElement(component, props, ...children)的语法糖。

    JSX代码:

    <MyButton color="blue" shadowSize={2}>
      Click Me
    </MyButton>
    

    将会被编译为

    React.createElement(
      MyButton,
      {color: 'blue', shadowSize: 2},
      'Click Me'
    )
    

    如果没有包含子标签,你也可以用自闭标签:

    <div className="sidebar" />
    

    将被编译为

    React.createElement(
      'div',
      {className: 'sidebar'},
      null
    )
    

    在线Bable编译器可以测试你的JSX代码将会被编译成的JS代码。

    指定React 元素类型

    JSX标签的第一部分决定了React元素的类型。类型首字母大写说明引用的是React组件,而不是html标签。在当前文件中,应该引入使用的组件。

    React 必须要在上下文中

    例如:

    import React from 'react';
    import CustomButton from './CustomButton';
    
    function WarningButton() {
      // return React.createElement(CustomButton, {color: 'red'}, null);
      return <CustomButton color="red" />;
    }
    

    导入React和CustomButton都是必须的。

    在JSX类型中,使用.表达式

    在JSX中,也可以用点表达式来引用React组件。如果你在一个类型中声明了很多React组件,这会方便引用。

    import React from 'react';
    
    const MyComponents = {
      DatePicker: function DatePicker(props) {
        return <div>Imagine a {props.color} datepicker here.</div>;
      }
    }
    
    function BlueDatePicker() {
      return <MyComponents.DatePicker color="blue" />;
    }
    

    自定义组件名首字母必须大写

    如果类型首字母是小写时,说明它是一个内建的组件就像divspan。类型首字母大写的时候,将会编译为React.createElement(Foo),并且对应于在JS中定义的组件。
    我们建议将自定义组件的首字母大写。如果组件名是小写的时候,那必须在使用之前,赋值给大写变量。
    这是一个错误的案例。

    import React from 'react';
    
    // Wrong! This is a component and should have been capitalized:
    function hello(props) {
      // Correct! This use of <div> is legitimate because div is a valid HTML tag:
      return <div>Hello {props.toWhat}</div>;
    }
    
    function HelloWorld() {
      // Wrong! React thinks <hello /> is an HTML tag because it's not capitalized:
      return <hello toWhat="World" />;
    }
    

    为了修改这个bug,我们要将小写组件名变为大写;

    import React from 'react';
    
    // Correct! This is a component and should be capitalized:
    function Hello(props) {
      // Correct! This use of <div> is legitimate because div is a valid HTML tag:
      return <div>Hello {props.toWhat}</div>;
    }
    
    function HelloWorld() {
      // Correct! React knows <Hello /> is a component because it's capitalized.
      return <Hello toWhat="World" />;
    }
    

    在运行的时候选择类型

    要实现这样的功能,下面的代码是不可以的:

    import React from 'react';
    import { PhotoStory, VideoStory } from './stories';
    
    const components = {
      photo: PhotoStory,
      video: VideoStory
    };
    
    function Story(props) {
      // Wrong! JSX type can't be an expression.
      return <components[props.storyType] story={props.story} />;
    }
    

    要想通过prop来决定渲染哪个组件,必须先要把它赋值给一个大写变量:

    import React from 'react';
    import { PhotoStory, VideoStory } from './stories';
    
    const components = {
      photo: PhotoStory,
      video: VideoStory
    };
    
    function Story(props) {
      // Correct! JSX type can be a capitalized variable.
      const SpecificStory = components[props.storyType];
      return <SpecificStory story={props.story} />;
    }
    

    JSX中的Props

    在JSX中指定props有几种方法

    JavaScript 表达式

    可以在JSX中插入任何的JS表达式,例如:

    <MyComponent foo={1 + 2 + 3 + 4} />
    

    这里MyComponent组件的foo属性九是10,因为表达式已经计算好了。
    iffor语句不是表达式。所以不能直接使用。但是我们可以用变量来存储代码。

    function NumberDescriber(props) {
      let description;
      if (props.number % 2 == 0) {
        description = <strong>even</strong>;
      } else {
        description = <i>odd</i>;
      }
      return <div>{props.number} is an {description} number</div>;
    }
    

    字符串字面量

    我们可以直接将字符串赋值给props。以下两种方法是一样的:

    <MyComponent message="hello world" />
    
    <MyComponent message={'hello world'} />
    

    如果直接传递字符串,HTML的标签要转义。

    <MyComponent message="<3" />
    
    <MyComponent message={'<3'} />
    

    Props默认为 True

    如果不在props中传值,默认是true。以下两个表达式是一致的:

    <MyTextBox autocomplete />
    
    <MyTextBox autocomplete={true} />
    

    通常,我们不建议用这种方法,因为按照ES6对象简写中的语法,{foo}表示的是{foo: foo}而不是{foo:true}。这种逻辑之所以存在,是因为符合HTML的逻辑。

    属性展开

    如果你有一个props对象,并且想要将它传递给JSX,你可以使用...来展开对象。以下代码效果是一致的:

    function App1() {
      return <Greeting firstName="Ben" lastName="Hector" />;
    }
    
    function App2() {
      const props = {firstName: 'Ben', lastName: 'Hector'};
      return <Greeting {...props} />;
    }
    

    这个方法,在构建自定义的组件的时候很有用。但是也会使代码变乱,因为这样做也会把不需要的变量也传递进去。所以这个特性也得谨慎的使用。

    JSX中的子类

    在JSX表达式中,也会有开放的标签和封闭的标签。但是在封闭标签之中的内容会通过props.children传递给组件。以下是传递子类的几种方法。

    字符串字面量

    直接将字符串用标签包裹起来,那么props.children就是该字符串。这个写法很常见。

    <MyComponent>Hello world!</MyComponent>
    

    MyComponent组件里的props.children就是字符串Hello world!。HTML需要转义。

    <div>This is valid HTML & JSX at the same time.</div>
    

    JSX会将行首和行尾的空白字符删掉,也会把空白行删除。和标签毗邻的空行会被删除;在字符串之间的空行也会被删除。以下的代码渲染结果都是一样的。

    <div>Hello World</div>
    
    <div>
      Hello World
    </div>
    
    <div>
      Hello
      World
    </div>
    
    <div>
    
      Hello World
    </div>
    

    JSX后代

    可以直接将JSX元素作为子代传入。在嵌套渲染的时候很有用:

    <MyContainer>
      <MyFirstComponent />
      <MySecondComponent />
    </MyContainer>
    

    两种类型混合也是可以的。

    <div>
      Here is a list:
      <ul>
        <li>Item 1</li>
        <li>Item 2</li>
      </ul>
    </div>
    

    一个React组件不能返回多个React元素,但是一个简单的JSX表达式可以拥有很多个后代。所以如果想要渲染多个视图,可以把这些视图用<div>包裹起来。

    javascript表达式

    可以嵌入任何JavaScript表达式,但是要用{}包裹起来。以下两种写法是一致的:

    <MyComponent>foo</MyComponent>
    
    <MyComponent>{'foo'}</MyComponent>
    

    这个写法对于渲染一个任意长度的列表视图是很有用的。例如:

    function Item(props) {
      return <li>{props.message}</li>;
    }
    
    function TodoList() {
      const todos = ['finish doc', 'submit pr', 'nag dan to review'];
      return (
        <ul>
          {todos.map((message) => <Item key={message} message={message} />)}
        </ul>
      );
    }
    

    js表达式可以和其他类型的混合起来。通常在用变量替代字符串。

    function Hello(props) {
      return <div>Hello {props.addressee}!</div>;
    }
    

    传递函数作为后代

    通常JSX中的js表达式会当作字符串,React元素或者一个数组。然而,props.children就像其他的属性一样,可以传递任何数据。例如,我们可以在自定义的组件中,传递一个回调函数:

    // Calls the children callback numTimes to produce a repeated component
    function Repeat(props) {
      let items = [];
      for (let i = 0; i < props.numTimes; i++) {
        items.push(props.children(i));
      }
      return <div>{items}</div>;
    }
    
    function ListOfTenThings() {
      return (
        <Repeat numTimes={10}>
          {(index) => <div key={index}>This is item {index} in the list</div>}
        </Repeat>
      );
    }
    

    这种用法是不太常见。

    Boolean Null 和Undifined会被忽略

    false,null,undefinedtrue都是合理的后代。但是通常他们不会被渲染。以下代码渲染的结果都是一样的:

    <div />
    
    <div></div>
    
    <div>{false}</div>
    
    <div>{null}</div>
    
    <div>{undefined}</div>
    
    <div>{true}</div>
    

    在条件渲染的时候是很有用的。例如:

    <div>
      {showHeader && <Header />}
      <Content />
    </div>
    

    只有在showHeader为true的时候,才会渲染<Header>
    需要注意的是,0不是false。例如:

    <div>
      {props.messages.length &&
        <MessageList messages={props.messages} />
      }
    </div>
    

    要修改这个bug,可以这么改:

    <div>
      {props.messages.length > 0 &&
        <MessageList messages={props.messages} />
      }
    </div>
    

    相反的,如果想要渲染false,true,null或者undefined的时候,需要先转换为String

    <div>
      My JavaScript variable is {String(myVariable)}.
    </div>
    

    DOM和Refs

    date:20170413
    原文链接
    按照典型的React数据流,[props][react-props]是父控件影响子控件的唯一方式。如果要修改子控件,那就必须用新的prop重新渲染。但有时候我们需要另外的方法才能满足需求。

    什么时候使用Refs

    refs的使用场景:

    • 焦点管理,文本选择或者媒体回放
    • 触发动画
    • 集成第三方库
      我们不应该滥用refs。
      例如,在控制对话框的显示上,我们不应该暴露open()或者close()接口,用isOpen的props属性就好了。

    在DOM元素中添加Ref

    React 通过ref属性来传递一个回调函数,这个回调函数在组件的加载和卸载的时候马上执行。当在HTML元素中使用ref的时候,当前的HTML元素回作为参数传递给它。例如:

    class CustomTextInput extends React.Component {
      constructor(props) {
        super(props);
        this.focus = this.focus.bind(this);
      }
    
      focus() {
        // Explicitly focus the text input using the raw DOM API
        this.textInput.focus();
      }
    
      render() {
        // Use the `ref` callback to store a reference to the text input DOM
        // element in an instance field (for example, this.textInput).
        return (
          <div>
            <input
              type="text"
              ref={(input) => { this.textInput = input; }} />
            <input
              type="button"
              value="Focus the text input"
              onClick={this.focus}
            />
          </div>
        );
      }
    }
    

    以上代码中,React会在转载的时候马上回调ref回调函数,对textInput进行初始化,然后在卸载的时候将textInput设置为null
    使用ref回调是操作DOM的一种固定模式。以上的有关refs的代码也可以简写为ref={input => this.textInput = input}

    在类组件中定义Ref

    ref属性用在通过类定义的组件里的时候,ref获取到的是加载的类组件。例如:

    class AutoFocusTextInput extends React.Component {
      componentDidMount() {
        this.textInput.focus();
      }
    
      render() {
        return (
          <CustomTextInput
            ref={(input) => { this.textInput = input; }} />
        );
      }
    }
    

    这里获取到的就是CustomTextInput组件。
    需要注意的是,这只有在CustomTextInput声明为类的时候才有效果。

    Refs和函数组件

    在函数定义的组件中是不可以使用ref的,因为它不具有实例。
    这个例子就不会有效:

    function MyFunctionalComponent() {
      return <input />;
    }
    
    class Parent extends React.Component {
      render() {
        // 这里就会**出错**
        return (
          <MyFunctionalComponent
            ref={(input) => { this.textInput = input; }} />
        );
      }
    }
    

    你必须将它转换为类函数,就像需要生命周期和state一样。这些都只能出现在类组件的特性。
    但是你可以在函数组件中的DOM元素或者类组件中使用

    function CustomTextInput(props) {
      // textInput must be declared here so the ref callback can refer to it
      let textInput = null;
    
      function handleClick() {
        textInput.focus();
      }
    
      return (
        <div>
          <input
            type="text"
            ref={(input) => { textInput = input; }} />
          <input
            type="button"
            value="Focus the text input"
            onClick={handleClick}
          />
        </div>
      );  
    }
    

    不能滥用Refs

    如果需要在app中使用ref,那就有必要花点时间想想state应该在组件关系中的那个层级。通常是在较高的层级(父级)。参见提升state

    遗留的API:字符串Refs

    以前的版本中可以通过this.refs.textInput来引用,但是这会引起问题。这个api将来会在某个版本移除,所以应该用回调的模式使用ref

    要注意的陷阱

    如果ref通过单行函数定义的时候,在更新的时候会执行两次。第一次是null,第二次正常。这是因为react更新的时候会重新生成一个组件实例,所以之前的实例销毁,设置为null,当创建了新的实例之后,又传递进来。你可以通过定义方法(a bound method,非单行函数)来避免这个问题,但是通常情况下并不影响。


    非控制组件

    date:20170414
    笔记原文
    通常情况下,我们推荐使用控制组件来实现表单。在控制组件中,React组件来处理表单数据,但是在非控制组件中,DOM自己来处理组件。
    在实现非控制组件的时候,我们不用事件监听来获取state的变化,我们使用ref从DOM中获取数据。举个例子,通过非控制组件来获取姓名:

    class NameForm extends React.Component {
      constructor(props) {
        super(props);
        this.handleSubmit = this.handleSubmit.bind(this);
      }
    
      handleSubmit(event) {
        alert('A name was submitted: ' + this.input.value);
        event.preventDefault();
      }
    
      render() {
        return (
          <form onSubmit={this.handleSubmit}>
            <label>
              Name:
              <input type="text" ref={(input) => this.input = input} />
            </label>
            <input type="submit" value="Submit" />
          </form>
        );
      }
    }
    

    因为非控制组件直接保存的是DOM元素,所以有时候会很方便的将React和非React组件结合起来。
    如果你对什么时候使用那种类型的组件还不太清楚的时候,可以参看控制vs非控制组件文章。

    默认值

    在React的生命周期里,表单元素中的value值会覆盖DOM的值。在非控制组件中,常常需要指定初始值,这种情况下,可以直接指定defaultValue属性。

    render() {
      return (
        <form onSubmit={this.handleSubmit}>
          <label>
            Name:
            <input
              defaultValue="Bob"
              type="text"
              ref={(input) => this.input = input} />
          </label>
          <input type="submit" value="Submit" />
        </form>
      );
    }
    

    另外,<input type="checkbox"><input type="radio">支持defaultChecked属性,<select><textarea>支持defaultValue属性。


    性能优化

    date:20170418

    笔记原文

    React使用了一些技术技巧来减少DOM操作的开销。对于大多数应用来说,不需要优化就已经能够快速的呈现用户界面。尽管如此,这里也有几种方法来加速React应用。

    使用发布版本

    如果你在测试app性能,那么确认使用最小化过的发布版本,我一开始选用了Create-React-App的构建工具,所以这里我只记录这个工具的使用。原文中还有其他工具的使用方法。

    • 在Create React App 中,我们使用npm run build命令。
      因为版本有很多调试信息会使得app变慢,所以很有必要使用发布版本来做新能分析和产品发布。

    组件在chrome中的表现

    在开发版本模式,我们使用性能工具能够以图表的方式查看加载组件,更新组件和卸载组件的性能。

    1. 在url后面添加?react_perf
    2. 打开Chrome DevTool TimeLine选项卡,并点击Record
    3. 开始操作app,事件不要超过20s,不然会导致Chrome挂起。
    4. 停止记录。
    5. 追踪到的记录会在User Timing选项卡中显示。
      需要注意到的是这些数值都是相对的,在发布版本中可能会渲染得更快。但是你可以发现一些错误,比如渲染了无关的模块。还可以发现模块更新的频繁程度。
      目前这个功能只能在Chrome,Edge和IE傻姑娘支持,不过可以使用User Timing API来支持更多的浏览器。

    避免视图刷新

    React使用了自己的一套渲染UI的方法。React不需要创建DOM结点,也不需要操作它们,因为操作dom比操作js对象慢。这里用的方法就是虚拟DOM。
    当组件的props或者state变化的时候,react会对比返回的元素和之前的元素,如果不同,就会更新视图。
    有时候,你也可以将这个过程加速:复写生命周期方法shouldComponentUpdate,这个回调会在渲染过程开始之前回调。默认的实现如下,始终返回true,来刷新视图:

    shouldComponentUpdate(nextProps, nextState) {
      return true;
    }
    

    如果有些情况下,你确定不需要刷新的时候,那就直接返回false,来避免刷新。

    使用 shouldComponentUpdate 生命周期方法

    原文在这里用一个例子来说明界面更新的逻辑。这里就简单总结下:如果shouldComponentUpdate返回true,就去对比状态是否改变,如果改变了,就刷新,没有改变就不刷新。shouldComponentUpdate返回false,那就说没有必要刷新了,也不会有比较状态这个步骤了。

    举例

    如果你的组件,只有在props.colorstate.count变化的情况下才需要刷新界面,你必须要在shouldComponentUpdate中检查:

    class CounterButton extends React.Component {
      constructor(props) {
        super(props);
        this.state = {count: 1};
      }
    
      shouldComponentUpdate(nextProps, nextState) {
        if (this.props.color !== nextProps.color) {
          return true;
        }
        if (this.state.count !== nextState.count) {
          return true;
        }
        return false;
      }
    
      render() {
        return (
          <button
            color={this.props.color}
            onClick={() => this.setState(state => ({count: state.count + 1}))}>
            Count: {this.state.count}
          </button>
        );
      }
    }
    

    这种模式很简单,反正就检查下关心的变量。React也提供了实现这个模式的辅助类,React.PureComponent。以下的代码效果与之前一样。

    class CounterButton extends React.PureComponent {
      constructor(props) {
        super(props);
        this.state = {count: 1};
      }
    
      render() {
        return (
          <button
            color={this.props.color}
            onClick={() => this.setState(state => ({count: state.count + 1}))}>
            Count: {this.state.count}
          </button>
        );
      }
    }
    

    大多数情况下,就使用React.PureComponent就可以了,不用自己实现shouldComponentUpdate
    这里原文提到了一个浅对比(shallow comparsion),对比文章后面的内容,我理解为:对比变量的引用,而不是数据。例如数组,并不对比数组中的每个值,而是这个数组引用。
    但是,对于复杂的数据结构就会有问题了。例如有个ListOfWords的控件用于渲染逗号隔开的一组词汇,并放置在WordAdder的控件中。WordAdder的功能就是通过点击按钮,给ListOfWords添加单词。以下的代码存在一个bug:

    class ListOfWords extends React.PureComponent {
      render() {
        return <div>{this.props.words.join(',')}</div>;
      }
    }
    
    class WordAdder extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          words: ['marklar']
        };
        this.handleClick = this.handleClick.bind(this);
      }
    
      handleClick() {
        // This section is bad style and causes a bug
        const words = this.state.words;
        words.push('marklar');
        this.setState({words: words});
      }
    
      render() {
        return (
          <div>
            <button onClick={this.handleClick} />
            <ListOfWords words={this.state.words} />
          </div>
        );
      }
    }
    

    这个问题是由于PrueComponent只是简单的比较了this.props.words,由于数组的引用不变,得出的比较是相等的,尽管数组的内容已经变了。

    不要直接改变数据

    要避免这个问题,最简单的方法就是,避免直接改变props和state的值。例如,上面的handleClick方法可以重写成这样:

    handleClick() {
      this.setState(prevState => ({
        words: prevState.words.concat(['marklar'])
      }));
    }
    

    对于操作数组,ES6有个展开语法,使用起来会比较简单。在Create React App 脚手架里,这个功能是默认可用的,其他脚手架就不知道了。

    handleClick() {
      this.setState(prevState => ({
        words: [...prevState.words, 'marklar'],
      }));
    };
    

    对于对象,我们也可以使用类似的方法来避免突变。例如,以下的方法:

    function updateColorMap(colormap) {
      colormap.right = 'blue';
    }
    

    需要修改为:

    function updateColorMap(colormap) {
      return Object.assign({}, colormap, {right: 'blue'});
    }
    

    现在updateColorMap直接返回一个新的对象,而不是在原来的对象上修改。
    有个JS提议,希望添加对象属性展开语法,那么代码就可以修改为下面的样子了:

    function updateColorMap(colormap) {
      return {...colormap, right: 'blue'};
    }
    

    现在是不可以直接这么写的。

    使用不可变的数据结构

    Immutable.js是另外一种解决问题的方法。它通过结构分享,提供了永久的,不可变的集合。

    • 不可变:一旦集合创建了,就不会再改变了
    • 永久:新的集合可以从以前的集合创建出来。但是原来的集合也是可以使用的。
    • 结构共享:新的集合会使用原来的数据结构,以避免拷贝数据影响细性能。
      这种不可变的特性使得追踪数据变化变得很简单。因为数据的变化总是会导致对象的变化,所以我们就只需要对比引用有没有变化。例如:
    const x = { foo: 'bar' };
    const y = x;
    y.foo = 'baz';
    x === y; // true
    

    这里,尽管y已经改变了,但是和x的引用一样,所以,对比表达式返回的还是true。用immutablejs重写的代码如下:

    const SomeRecord = Immutable.Record({ foo: null });
    const x = new SomeRecord({ foo: 'bar' });
    const y = x.set('foo', 'baz');
    x === y; // false
    

    另外两个类似的库是seamless-immutableimmutability-helper.
    不可变这一机理可以让我们很方便的跟踪数据变化,同时也提升性能有很大的帮助。


    不使用ES6时的React

    date:20170420

    笔记原文

    很多时候,我们会通过类的形式定义组件:

    class Greeting extends React.Component {
      render() {
        return <h1>Hello, {this.props.name}</h1>;
      }
    }
    

    如果你还没有开始使用ES6,那么你可以使用Create-react-class模块:

    var createReactClass = require('create-react-class');
    var Greeting = createReactClass({
      render: function() {
        return <h1>Hello, {this.props.name}</h1>;
      }
    });
    

    ES6的API和createReactClass()是一样的效果。

    定义默认Props

    在类组件中,我们可以将默认的属性像组件的属性一样定义:

    class Greeting extends React.Component {
      // ...
    }
    
    Greeting.defaultProps = {
      name: 'Mary'
    };
    

    如果通过creatReactClass()方法,你需要定义getDefaultProps()函数:

    var Greeting = createReactClass({
      getDefaultProps: function() {
        return {
          name: 'Mary'
        };
      },
    
      // ...
    
    });
    
    

    设置初始状态

    在ES6中,你可以在构造函数中对this.state赋值,来定义初始状态。

    class Counter extends React.Component {
      constructor(props) {
        super(props);
        this.state = {count: props.initialCount};
      }
      // ...
    }
    

    如果通过creatReactClass()方法,你需要定义getInitialState()函数:

    var Counter = createReactClass({
      getInitialState: function() {
        return {count: this.props.initialCount};
      },
      // ...
    });
    

    自动绑定

    在ES6定义的组件中,方法与ES6的类具有相同的语义。这说明,我们不会给实例自动绑定this,我们需要在构造函数中手动的调用.bind(this)来绑定。

    class SayHello extends React.Component {
      constructor(props) {
        super(props);
        this.state = {message: 'Hello!'};
        // This line is important!
        this.handleClick = this.handleClick.bind(this);
      }
    
      handleClick() {
        alert(this.state.message);
      }
    
      render() {
        // Because `this.handleClick` is bound, we can use it as an event handler.
        return (
          <button onClick={this.handleClick}>
            Say hello
          </button>
        );
      }
    }
    

    createReactClass()中,我们就不需要手动绑定了,那都是自动完成的:

    var SayHello = createReactClass({
      getInitialState: function() {
        return {message: 'Hello!'};
      },
    
      handleClick: function() {
        alert(this.state.message);
      },
    
      render: function() {
        return (
          <button onClick={this.handleClick}>
            Say hello
          </button>
        );
      }
    });
    

    这意味着ES6类会在定义事件处理函数上都写些代码,但是这样做的好处是能够提升大型应用的性能。如果这烦人的代码对你没有吸引力,你可以在Babel中开启实验的类属性语法提议

    class SayHello extends React.Component {
      constructor(props) {
        super(props);
        this.state = {message: 'Hello!'};
      }
      // WARNING: this syntax is experimental!
      // Using an arrow here binds the method:
      handleClick = () => {
        alert(this.state.message);
      }
    
      render() {
        return (
          <button onClick={this.handleClick}>
            Say hello
          </button>
        );
      }
    }
    

    需要注意的是,这个功能还在实验阶段,语法可能会改变,或者提议会被放弃。
    如果你想要安全些,你有几个注意的地方:

    • 在构造函数中绑定
    • 使用箭头函数,onClick=>{(e) => this.handleClick(e)}.
    • 使用createReactClass

    Mixins

    注意:
    ES6并不支持Mixins,所以在React的ES6类中,不支持任何的Mixins.
    我们发现在使用Mixins之后会有很多问题,所以我们不推荐使用。
    这个章节只是为了提及这一点。

    有时候,个别组件需要公用一些共同的函数。这涉及到了交叉剪切?(cross-cutting)的概念createReactClass了一使用遗留的mixins机制来实现。
    一个很常见的用法是,组件需要通过interval来更新自己。使用setInterval()很简单,但是要在不需要定时的时候取消定时,来减少内存的消耗。React提供了生命周期方法让你知道组件的创建和销毁。举个例子:

    var SetIntervalMixin = {
      componentWillMount: function() {
        this.intervals = [];
      },
      setInterval: function() {
        this.intervals.push(setInterval.apply(null, arguments));
      },
      componentWillUnmount: function() {
        this.intervals.forEach(clearInterval);
      }
    };
    
    var createReactClass = require('create-react-class');
    
    var TickTock = createReactClass({
      mixins: [SetIntervalMixin], // Use the mixin
      getInitialState: function() {
        return {seconds: 0};
      },
      componentDidMount: function() {
        this.setInterval(this.tick, 1000); // Call a method on the mixin
      },
      tick: function() {
        this.setState({seconds: this.state.seconds + 1});
      },
      render: function() {
        return (
          <p>
            React has been running for {this.state.seconds} seconds.
          </p>
        );
      }
    });
    
    ReactDOM.render(
      <TickTock />,
      document.getElementById('example')
    );
    

    如果在组件中的几个生命周期方法中定义了多个mixins方法,所有的生命周期方法都会按顺序被执行。
    好了,这个Mixins是被废弃了,也不需要做过多的了解。


    不使用JSX时的React

    date:20170421
    原文链接

    在使用React的时候,JSX也不是必须。当你不想配置jsx的编译环境的时候,不使用JSX会尤其方便些。JSX是React.createElement(component,props,...children)的语法糖,所以不使用JSX的时候,可以直接使用这个底层的方法。
    例如,用JSX的代码如下:

    class Hello extends React.Component {
      render() {
        return <div>Hello {this.props.toWhat}</div>;
      }
    }
    
    ReactDOM.render(
      <Hello toWhat="World" />,
      document.getElementById('root')
    );
    

    编译为非JSX版本的代码如下:

    class Hello extends React.Component {
      render() {
        return React.createElement('div', null, `Hello ${this.props.toWhat}`);
      }
    }
    
    ReactDOM.render(
      React.createElement(Hello, {toWhat: 'World'}, null),
      document.getElementById('root')
    );
    

    如果你对JSX如何编译为JavaScript很好奇,那么你可以看看在线Babel编译器.组件可以是字符串,也可以是React.Component的子类,也是可以一个不包含state的函数组件。
    如果你觉得React.createElement输入太多,一个简单的方法是将它赋值给简短的变量:

    const e = React.createElement;
    
    ReactDOM.render(
      e('div', null, 'Hello World'),
      document.getElementById('root')
    );
    

    看吧,这也跟使用JSX一样方便了,对吧。


    视图刷新

    date:20170421
    原文链接
    React提供了一组API使得我们可以不需要完全了解每次界面更新。这使得编码简单化了,但是我们不知道里边的原理。这篇文章就来介绍下为什么我们会选择这么奇特的逻辑。

    动机

    使用React的时候,在某个简单的点上,调用render()会生成一个树状的React元素。当props或者state更新的时候,render()返回新的React元素。这时候React就需要快速有效的找到需要跟新的元素。
    对于这个逻辑问题,有许多普通的解决方法:找到变换UI的最小的操作次数。但是这个逻辑的复杂度是O(n3),n是渲染树中的元素。
    如果我们在React中使用了1000个元素,那么将要进行10亿次比较。开销太大。React基于两个假设,使用的是O(n)的逻辑:
    1. 两个不同的元素渲染不同的界面。
    2. 开发者通过key属性,提示是否要刷新。
    在实际运用中,这些假设几乎在所有的情况下都可行。

    对比差异的逻辑

    获取到两个树之后,React首先会比较两个根元素。之后的对比会根据根元素的不同有些差异。

    不同类型的元素

    如果根目录的元素不一样的时候,React会重新渲染整个树。从<a>变为<img><Article>变为<Comment><Button>变为<div>。所有的这些都会导致重新构建。
    当出去了原来的渲染树的时候,所有的元素会被销毁,回调componentWillUnmount()。新的DOM会插入。新的组件回调componentWIllMount(),然后componentDidMount()。所有以前的元素,将会消失。
    所有根元素下的组件会卸载,状态会销毁。例如:

    <div>
      <Counter />
    </div>
    
    <span>
      <Counter />
    </span>
    

    旧的Counter会销毁,新的Counter会挂载上去。

    相同类型的DOM元素

    对比相同类型的元素的时候,React会查看元素的所有属性,保留相同的,更新变化的。例如:

    <div className="before" title="stuff" />
    
    <div className="after" title="stuff" />
    

    对比上面的两个组件,React只会更新组件的类名。
    当更新样式的时候,React也只会更新变化的部分:

    <div style={{color: 'red', fontWeight: 'bold'}} />
    
    <div style={{color: 'green', fontWeight: 'bold'}} />
    

    这里,React只会更新color属性,不会更新fontWeight属性。
    在父节点更改之后,会遍历子节点。

    相同类型的组件元素

    当组件更新的时候,并不重新创建新的元素。所以组件的状态都会保持。React直接更新组件的Props,而不会去比较它。组件回调componentWillReceiveProps()componentWillUpdate()方法。
    接着,回调render()函数,然后再对比组件中的差异。

    子组件对比逻辑

    默认情况下,React会同时遍历新旧组件的子组件,当对比出差异的时候,直接更改。
    例如,我们在列表底部添加子元素的时候:

    <ul>
      <li>first</li>
      <li>second</li>
    </ul>
    
    <ul>
      <li>first</li>
      <li>second</li>
      <li>third</li>
    </ul>
    

    我们会对比两个<li>first</li>,对比<li>second</li>,然后在插入<li>third</li>。如果你直接在第一项中添加了一项,那么性能就会很糟糕:

    <ul>
      <li>Duke</li>
      <li>Villanova</li>
    </ul>
    
    <ul>
      <li>Connecticut</li>
      <li>Duke</li>
      <li>Villanova</li>
    </ul>
    

    React并不会意识到其中的两项不必更新,所以这个列表的更新就会比较低效。

    keys

    React通过key参数,来解决上述列表更新的问题:通过比较key来决定是否更新列表。

    <ul>
      <li key="2015">Duke</li>
      <li key="2016">Villanova</li>
    </ul>
    
    <ul>
      <li key="2014">Connecticut</li>
      <li key="2015">Duke</li>
      <li key="2016">Villanova</li>
    </ul>
    

    这样,React就可以发现,只有key为2014的元素是新的,另外两个只要移动下位置就可以了。
    实际上,给key赋值也是很简单的。很多时候数据本身带有唯一的id。

    <li key={item.id}>{item.name}</li>
    

    如果没有这个key,你可以添加一个属性,或是进行hash运算,计算出一个唯一的值。键值在列表中必须是唯一的,但是在全局中可以不同。你也可以用数据项的索引来充当键值。如果不重新排序,这个做法没有什么问题,但是要重新排序就会有问题了。

    总结(tradeoffs)

    熟悉React的更新逻辑是很重要的。在触发每个动作之后,React会重新渲染整个app,尽管有些时候UI并没有变化。
    在当前的实现中,你可以解释子树在同级间的移动,但是不知道移动到哪里了。这套算法会重新渲染整个子树。
    由于React的这个实现逻辑是发散式的,如果这个实现逻辑背后的假设并不成立的话,性能就会很差。

    1. 这个算法并不会比较不同类型组件的子组件。如果你的两个子组件很相似,并且经常切换,那么需要将它们合二为一。在实际应用中,我们也没有发现什么问题。
    2. key必须要稳定的,可以预测的以及唯一的。不稳定的key(通过Math.random()生成的key)将会导致很多组件实例和DOM结点没有必要的重建,影响性能,同时状态也不会保留下来。

    上下文

    date:20170422
    笔记原文

    注意:由于在React v15.5中React.PropTypes已经废弃了,所以我们推荐使用prop-types库来定义contextTypes.

    React可以很容易地跟踪数据流。当你查看一个组件的时候,你可以查看传递了哪个prop,这可以很容易找到问题。
    有时候,你不需要通过传递props来传递数据。你可以通过上下文传递数据。

    为什么不使用上下文

    很多app并不需要使用上下文。

    • 如果你想让app更加稳定,就不要使用上下文。这个一个试验功能,可能在以后的版本中废除。
    • 如果你不熟悉状态管理库Redux或者Mobx,就不要使用context。推荐使用Redux,因为很多组件都基于这些三方库。
    • 如果你不是经验丰富的React开发者,就不要用context。
    • 如果你一定要使用context,那么将代码分离,限定在小范围内。这有利于升级API。

    如何使用Context

    假设你有如下的结构:

    class Button extends React.Component {
      render() {
        return (
          <button style={{background: this.props.color}}>
            {this.props.children}
          </button>
        );
      }
    }
    
    class Message extends React.Component {
      render() {
        return (
          <div>
            {this.props.text} <Button color={this.props.color}>Delete</Button>
          </div>
        );
      }
    }
    
    class MessageList extends React.Component {
      render() {
        const color = "purple";
        const children = this.props.messages.map((message) =>
          <Message text={message.text} color={color} />
        );
        return <div>{children}</div>;
      }
    }
    

    在这个例子中,我们通过props传递color参数。如果使用context,那么代码如下:

    const PropTypes = require('prop-types');
    
    class Button extends React.Component {
      render() {
        return (
          <button style={{background: this.context.color}}>
            {this.props.children}
          </button>
        );
      }
    }
    
    Button.contextTypes = {
      color: PropTypes.string
    };
    
    class Message extends React.Component {
      render() {
        return (
          <div>
            {this.props.text} <Button>Delete</Button>
          </div>
        );
      }
    }
    
    class MessageList extends React.Component {
      getChildContext() {
        return {color: "purple"};
      }
    
      render() {
        const children = this.props.messages.map((message) =>
          <Message text={message.text} />
        );
        return <div>{children}</div>;
      }
    }
    
    MessageList.childContextTypes = {
      color: PropTypes.string
    };
    

    通过在MessageList中添加childContextTypesgetChildContext。React自动传递了参数,任何子组件可以通过contextTypes获取参数。
    如果contextTypes没有定义,那么context就会为空。

    父组件与子组件间通讯

    Context可以实现父子组件间的通讯。例如,React Router V4就是按照这个方法来的。

    import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
    
    const BasicExample = () => (
      <Router>
        <div>
          <ul>
            <li><Link to="/">Home</Link></li>
            <li><Link to="/about">About</Link></li>
            <li><Link to="/topics">Topics</Link></li>
          </ul>
    
          <hr />
    
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
          <Route path="/topics" component={Topics} />
        </div>
      </Router>
    );
    

    通过Router组件向下传递了一些信息,每个LinkRoute都可以向Router传递信息。
    当你这样的方式实现逻辑的时候,考虑下是否有个更加清楚的替代方法。例如你可以通过props传递React参数。

    在生命周期函数中引用Context

    如果在组件中定义了contextTypes,以下的生命周期方法,将会获取到context对象。

    • constructor(props,context)
    • componentWillReceiveProps(nextProps,nextContext)
    • shouldComponentUpdate(nextProps,nextState,nextContext)
    • componentWillUpdate(nextProps,nextState,nextContext)
    • componentDidUpdate(preProps,prevState,prevContext)

    在不包含状态的函数组件中引用Context

    如果定义了函数组件的contextTypes参数,也可以使用context。例如下面的Button组件:

    const PropTypes = require('prop-types');
    
    const Button = ({children}, context) =>
      <button style={{background: context.color}}>
        {children}
      </button>;
    
    Button.contextTypes = {color: PropTypes.string};
    

    更新Context

    千万别更新Context。React提供了API来更新上下文。
    当state或者props更新的时候,getChildContext函数会被回调,来更新context中的数据。state的更新使用this.setState方法。这会触发生成新的context,自组件也会获取到新的数据。

    const PropTypes = require('prop-types');
    
    class MediaQuery extends React.Component {
      constructor(props) {
        super(props);
        this.state = {type:'desktop'};
      }
    
      getChildContext() {
        return {type: this.state.type};
      }
    
      componentDidMount() {
        const checkMediaQuery = () => {
          const type = window.matchMedia("(min-width: 1025px)").matches ? 'desktop' : 'mobile';
          if (type !== this.state.type) {
            this.setState({type});
          }
        };
    
        window.addEventListener('resize', checkMediaQuery);
        checkMediaQuery();
      }
    
      render() {
        return this.props.children;
      }
    }
    
    MediaQuery.childContextTypes = {
      type: PropTypes.string
    };
    

    这有个问题是,如果context跟新的时候,子组件如果在shouldComponentUpdate中返回了false就不会自动跟新了。这就会使得自组件使用context失去控制。所以没有基础的方法来更新context这篇博客详细阐述了这个问题的缘由以及如何去绕过这个问题。


    web组件

    date: 20170422

    笔记原文

    React和web组件用于解决不同的问题。web组件可以提供高度封装好的,可以重用的组件。然而React提供了声明好的库。它们相辅相成。开发者可以自由的在web组件中使用React,或者在React中使用web组件。
    很多人会使用React,但是不使用web组件。但是你可能使用了用web组件实现的第三方库。

    在React中使用web组件

    class HelloMessage extends React.Component {
      render() {
        return <div>Hello <x-search>{this.props.name}</x-search>!</div>;
      }
    }
    

    注意:
    web组件可能会提供一组接口。例如videoweb组件需要暴露play()pause()方法。如果要使用这些API,那么你要做下封装,然后可以直接控制DOM结点。如果你使用第三方的web组件,最好的方法是用React来封装web组件。
    web组件触发的事件可能不会被React捕捉到。你需要自己实现事件监听。

    有个容易出错的地方就是在web组件中使用class而不是className.

    function BrickFlipbox() {
      return (
        <brick-flipbox class="demo">
          <div>front</div>
          <div>back</div>
        </brick-flipbox>
      );
    }
    

    在web组件中使用React

    const proto = Object.create(HTMLElement.prototype, {
      attachedCallback: {
        value: function() {
          const mountPoint = document.createElement('span');
          this.createShadowRoot().appendChild(mountPoint);
    
          const name = this.getAttribute('name');
          const url = 'https://www.google.com/search?q=' + encodeURIComponent(name);
          ReactDOM.render(<a href={url}>{name}</a>, mountPoint);
        }
      }
    });
    document.registerElement('x-search', {prototype: proto});
    

    高阶组件

    date:20170426

    笔记原文

    高阶组件(HOC)是React重用组件的高级技术。HOC不是React API的一部分,而是React封装的一种模式。
    其实,HOC是一个能够将一个组件变为另一个组件的函数。

    const EnhancedComponent = higherOrderComponent(WrappedComponent);
    

    一个组件,将属性展示为UI;而一个高阶组件,将一个组件变为另一个组件。HOC在三方库中非常常见。例如Redux的connect和Relay的createContainer.
    在这个文档中,我们将解释为什么HOC会很有用处,并且如何去实现自己的HOC。

    HOC的横切关注点

    注意:
    我们之前会推荐使用mixins来处理横切关注点。我们后来意识到mixins会导致很多bug。详情参见这篇博客,阐述废弃mixins的原因以及如何更改原有代码。

    组件是React里的最初单元。但是有时候你会发现有些模式使用传统的组件并不能满足条件。
    例如,你有一个CommentList组件,监听外部的数据来渲染列表。

    class CommentList extends React.Component {
      constructor() {
        super();
        this.handleChange = this.handleChange.bind(this);
        this.state = {
          // "DataSource" is some global data source
          comments: DataSource.getComments()
        };
      }
    
      componentDidMount() {
        // Subscribe to changes
        DataSource.addChangeListener(this.handleChange);
      }
    
      componentWillUnmount() {
        // Clean up listener
        DataSource.removeChangeListener(this.handleChange);
      }
    
      handleChange() {
        // Update component state whenever the data source changes
        this.setState({
          comments: DataSource.getComments()
        });
      }
    
      render() {
        return (
          <div>
            {this.state.comments.map((comment) => (
              <Comment comment={comment} key={comment.id} />
            ))}
          </div>
        );
      }
    }
    

    后来,你又需要监听博客的提交数据,使用的是相同的模式:

    class BlogPost extends React.Component {
      constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
        this.state = {
          blogPost: DataSource.getBlogPost(props.id)
        };
      }
    
      componentDidMount() {
        DataSource.addChangeListener(this.handleChange);
      }
    
      componentWillUnmount() {
        DataSource.removeChangeListener(this.handleChange);
      }
    
      handleChange() {
        this.setState({
          blogPost: DataSource.getBlogPost(this.props.id)
        });
      }
    
      render() {
        return <TextBlock text={this.state.blogPost} />;
      }
    }
    

    CommentListBlogPost是不同的组件,它们调用各自的方法,监听各自的不同的数据,渲染不同的输出。但是它们的模式是一样的:

    • 在加载组件的时候,对数据源添加监听函数。
    • 在监听函数中,当数据变化的时候调用setState
    • 在卸载的时候,移除监听。
      可以想象,在大型应用中,这种监听数据源,设置更新数据的模式经常会用到。所以我们需要把这部分功能抽象出来,并且在很多个组件中实现公用。这就是高阶组件做的事情。
      我们现在先来实现一个函数,用来生成类似CommentList或者BlogPost的组件。这个函数有两个参数,一个参数是需要监听数据源的组件,另一个参数是数据源。函数名称定义为withSubscription:
    const CommentListWithSubscription = withSubscription(
      CommentList,
      (DataSource) => DataSource.getComments()
    );
    
    const BlogPostWithSubscription = withSubscription(
      BlogPost,
      (DataSource, props) => DataSource.getBlogPost(props.id)
    });
    

    第一个参数就是被包裹的组件,第二个参数就是提过我们感兴趣的数据的函数。
    CommentListWithSubscriptionBlogPostWithSubscription渲染的时候,CommentListBlogPost将会被传入一个data属性:

    // This function takes a component...
    function withSubscription(WrappedComponent, selectData) {
      // ...and returns another component...
      return class extends React.Component {
        constructor(props) {
          super(props);
          this.handleChange = this.handleChange.bind(this);
          this.state = {
            data: selectData(DataSource, props)
          };
        }
    
        componentDidMount() {
          // ... that takes care of the subscription...
          DataSource.addChangeListener(this.handleChange);
        }
    
        componentWillUnmount() {
          DataSource.removeChangeListener(this.handleChange);
        }
    
        handleChange() {
          this.setState({
            data: selectData(DataSource, this.props)
          });
        }
    
        render() {
          // ... and renders the wrapped component with the fresh data!
          // Notice that we pass through any additional props
          return <WrappedComponent data={this.state.data} {...this.props} />;
        }
      };
    }
    

    这里我们看到,HOC并不会改变输入的组件,也不复制组件。而是在容器中将原来的组件包裹起来。HOC是一个不影响任何一方的“纯净”组件。它获取到关于组件的所有参数,并赋值给新的变量data,用来渲染界面。HOC不关心数据的来源,也不关心数据的用法。
    因为withSubscription是一个很平常的函数,你可以添加任意多的参数,来实现你想要的功能。
    withSubscription和被包裹的组件之间是基于props的,所以在需要的时候替换HOC是很简单的。

    不要改变原来的组件,选择包裹

    不要在HOC中改变传递进来的组件:

    function logProps(InputComponent) {
      InputComponent.prototype.componentWillReceiveProps(nextProps) {
        console.log('Current props: ', this.props);
        console.log('Next props: ', nextProps);
      }
      // The fact that we're returning the original input is a hint that it has
      // been mutated.
      return InputComponent;
    }
    
    // EnhancedComponent will log whenever props are received
    const EnhancedComponent = logProps(InputComponent);
    

    这样做会导致一些问题。首先就是输入组件脱离了这个强化组件就不能重用了。更重要的是,如果你传入另一个组件,也会改变componentWillReceiveProps函数,之前HOC的功能就被复写了。而且这个HOC也不支持函数组件,因为函数组件并没有生命周期。
    这样的HOC并不是一个好的实现,使用者必须知道HOC是怎么实现的,才能避免与其他组件产生冲突。
    所以,我们不能改变原来组件,而是重新生成一个组件,将输入组件包裹起来:

    function logProps(WrappedComponent) {
      return class extends React.Component {
        componentWillReceiveProps(nextProps) {
          console.log('Current props: ', this.props);
          console.log('Next props: ', nextProps);
        }
        render() {
          // Wraps the input component in a container, without mutating it. Good!
          return <WrappedComponent {...this.props} />;
        }
      }
    }
    

    这个HOC和之前的直接修改的实现的功能是一样的,但是这个可以避免冲突,并且可以支持函数组件。由于它是纯组件,所以它和其他的HOC是兼容的。
    你会注意到HOC和容器组件很相似。容器组件是为了分离不同级别之间的功能的一种策略。容器管理订阅和状态,传递props给组件。容器组件是HOC的一部分。HOC可以理解为参数化的容器组件。

    公约:给包裹的组件只传递相关的props

    HOC只是添加功能,不能对原有的组件做很大的变化。我们期望输入和输出只有温和的变化。HOC传递的参数,只是需要的参数,不相关的参数就不要传递进去:

    render() {
      // Filter out extra props that are specific to this HOC and shouldn't be
      // passed through
      const { extraProp, ...passThroughProps } = this.props;
    
      // Inject props into the wrapped component. These are usually state values or
      // instance methods.
      const injectedProp = someStateOrInstanceMethod;
    
      // Pass props to wrapped component
      return (
        <WrappedComponent
          injectedProp={injectedProp}
          {...passThroughProps}
        />
      );
    }
    

    这个约定有利于HOC更加灵活和更容易实现重用。

    公约:兼容性最大化

    并不是所有的HOC都是一样的形式,有的只有一个参数,需要被包裹的组件:

    const NavbarWithRouter = withRouter(Navbar);
    

    通常,HOC需要额外的参数。举一个Relay的例子,为了说明数据的依赖,需要传递一个配置对象:

    const CommentWithRelay = Relay.createContainer(Comment, config);
    

    HOC常见的形式如下:

    // React Redux's `connect`
    const ConnectedComment = connect(commentSelector, commentActions)(Comment);
    

    这个形式有点看不清楚,换个写法,你就知道了:

    // connect is a function that returns another function
    const enhance = connect(commentListSelector, commentListActions);
    // The returned function is an HOC, which returns a component that is connected
    // to the Redux store
    const ConnectedComment = enhance(CommentList);
    

    connect是一个高阶组件返回一个高阶组件。这种形式可能会比较迷惑,或者不必要。但是确实是很有用的一个特性。像connect返回的单参数HOC一样,具有Component=>Component这样的特征。这就很容易兼容:

    // Instead of doing this...
    const EnhancedComponent = connect(commentSelector)(withRouter(WrappedComponent))
    
    // ... you can use a function composition utility
    // compose(f, g, h) is the same as (...args) => f(g(h(...args)))
    const enhance = compose(
      // These are both single-argument HOCs
      connect(commentSelector),
      withRouter
    )
    const EnhancedComponent = enhance(WrappedComponent)
    

    有很多第三方库提供这样的compose功能。例如lodash.flowRight,ReduxRamda

    公约:起一个方便调试的名称

    这些HOC生成的容器组件能够在React调试工具显示出来。为了方便调试,我们要给它们取一个方便的名字,以便调试。
    通用的做法就是对原来的组件名称包裹下。例如,高阶组件的名称叫做withSubscription,被包裹的组件叫做CommentList的时候,使用名称WithSubscription(CommentList):

    function withSubscription(WrappedComponent) {
      class WithSubscription extends React.Component {/* ... */}
      WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
      return WithSubscription;
    }
    
    function getDisplayName(WrappedComponent) {
      return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
    

    陷阱

    如果你是新手,那么就得注意这些问题了:

    不能在渲染函数中使用HOCs

    React 差异算法通过比较组件是否相同,来决定是否更新它还是直接销毁它。如果在render函数中返回的组件是和之前的组件是相同的,那么就和新的比较下,更新差异;如果不同就直接卸载。
    通常情况下不需要想太多,但是如果这里遇到了HOC的话,就要小心了:

    render() {
      // A new version of EnhancedComponent is created on every render
      // EnhancedComponent1 !== EnhancedComponent2
      const EnhancedComponent = enhance(MyComponent);
      // That causes the entire subtree to unmount/remount each time!
      return <EnhancedComponent />;
    }
    

    这里不是因为性能的问题,而是重新渲染组件,导致组件状态丢失的问题。
    所以,应该在组件外使用HOC来确保组件只是生成一次。
    若果有时候你需要运用HOC,你也可以在生命周期方法或者构造函数方法中使用。

    静态函数必须要手动拷贝过来

    有时候在React组件中定义静态方法是很有用的。例如,Relay容器就暴露了一个方法,getFragmnet来加快生成GraphQL fragments。
    如果一个组件中使用了HOC,但是源组件又是一个容器组件。这意味着新的组件并没有元组件中的任何静态方法。

    // Define a static method
    WrappedComponent.staticMethod = function() {/*...*/}
    // Now apply an HOC
    const EnhancedComponent = enhance(WrappedComponent);
    
    // The enhanced component has no static method
    typeof EnhancedComponent.staticMethod === 'undefined' // true
    

    为了解决这个问题,你必须将容器里的静态方法手动拷贝出来:

    function enhance(WrappedComponent) {
      class Enhance extends React.Component {/*...*/}
      // Must know exactly which method(s) to copy :(
      Enhance.staticMethod = WrappedComponent.staticMethod;
      return Enhance;
    }
    

    所以你又必须知道你具体要拷贝什么函数。你可以使用hoist-non-react-statics来自动拷贝非react的静态代码:

    import hoistNonReactStatic from 'hoist-non-react-statics';
    function enhance(WrappedComponent) {
      class Enhance extends React.Component {/*...*/}
      hoistNonReactStatic(Enhance, WrappedComponent);
      return Enhance;
    }
    

    另一个解决方法是在容器中分离出静态的方法:

    // Instead of...
    MyComponent.someFunction = someFunction;
    export default MyComponent;
    
    // ...export the method separately...
    export { someFunction };
    
    // ...and in the consuming module, import both
    import MyComponent, { someFunction } from './MyComponent.js';
    
    refs不会被传递

    虽然组件的所有属性都会传递给新的包裹组件。但是不会传递refs。这是因为ref并不是prop,比如key,这些个都会被React单独处理。
    如果你面临这样的问题,最好的办法是如何才能不使用ref。可以尝试用props替代。
    当时有时候又必须使用refs。例如让input获得焦点。一个解决方法是将refs回调方法通过props传递进去,但是要不同的命名:

    function Field({ inputRef, ...rest }) {
      return <input ref={inputRef} {...rest} />;
    }
    
    // Wrap Field in a higher-order component
    const EnhancedField = enhance(Field);
    
    // Inside a class component's render method...
    <EnhancedField
      inputRef={(inputEl) => {
        // This callback gets passed through as a regular prop
        this.inputEl = inputEl
      }}
    />
    
    // Now you can call imperative methods
    this.inputEl.focus();
    

    这并不是一个完美的解决方法。我们希望能够有个库来处理ref,而不是手动处理。我们还在寻找优雅的方法。。。


    差不多一个月的时间,把两篇guide都学习完了。感觉我是在翻译文章。按道理来说,笔记应该是结合自己的相法。在复习的时候可以重拾自己的理解。我觉得我并没有完全理解React。入门?如果没有项目,我也觉得心虚。所以keep coding~

    相关文章

      网友评论

        本文标题:React 高级指导 笔记

        本文链接:https://www.haomeiwen.com/subject/wxemattx.html