React(一)

813 阅读11分钟

state和props的区别

  • props 属性 只读 组件对外的接口
  • state 状态 可变 组件对内的接口

注意不要使用push、pop、shift、unshift、splice等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而concat、slice、filter会返回一个新的数组

给setState传递一个对象和传递一个函数的区别

因为setState是异步合并更新的,所以setState传递一个对象,如果要更新那些依赖于当前的state的state时,不能确保每次的调用都是最新的state,导致给了我们一个错误的值。 而传递一个函数可以在函数访问到当前的state的值,确保他们是建立在另一个之上的

代码分割的方式

  1. import()
  2. React.lazy
  3. 异常捕获边界
  4. React.lazy和React Router第三方库,来配置基于路由的代码分割

什么是高阶组件(HOC)

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。

高阶组件是参数为组件,返回值为新组件的函数。

例如 Redux 的 connect 和 Relay 的 createFragmentContainer。

// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
  // ...并返回另一个组件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ...负责订阅相关的操作...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... 并使用新数据渲染被包装的组件!
      // 请注意,我们可能还会传递其他属性
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}
const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

注意事项

  • 不要在render方法中使用HOC

React 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。

这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有子组件的状态丢失。

在极少数情况下,你需要动态调用 HOC。你可以在组件的生命周期方法或其构造函数中进行调用。

  • 务必复制静态方法

当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法。

// 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 现在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);

// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
  1. 使用 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;
}
  1. 额外导出这个静态方法
// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...单独导出该方法...
export { someFunction };

// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from './MyComponent.js';
  • Refs不会被传递

ref实际上并不是一个prop- 就像key一样,它是由 React 专门处理的。如果将ref添加到 HOC 的返回组件中,则ref引用指向容器组件,而不是被包装组件。

Context及其API

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递props。主要应用场景在于很多不同层级的组件需要访问同样一些的数据。

React.createContext :创建一个Context对象。

const MyContext = React.createContext(defaultValue);

Context.Provider : 每个Context对象都会返回一个Provider React组件,它允许消费组件订阅context的变化。

<MyContext.Provider value={/* 某个值 */}>

Class.contextType :挂载在class上的contextType属性会被重赋值为一个由React.createContext()创建的Context对象

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* 基于 MyContext 组件的值进行渲染 */
  }
}
MyClass.contextType = MyContext;

Context.Consumer :React组件可以订阅到context变更

<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

Context.displayName :context对象接受一个名为displayName的property,类型为字符串。

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中

Refs转发

Ref 转发是一项将ref自动地通过组件传递到其一子组件的技巧。

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

在高阶组件中转发refs

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      const {forwardedRef, ...rest} = this.props;

      // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // 注意 React.forwardRef 回调的第二个参数 “ref”。
  // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
  // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

什么是Portal

Portal提供了一种将子节点渲染到存在于父组件以外的DOM节点的优秀的方案。

ReactDOM.createPortal(child, container)

第一个参数(child)是任何可渲染React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

什么是Profiler

Profiler 测量渲染一个React应用多久渲染一次以及渲染一次的“代价”。

注意:Profiler增加了额外的开支,应该在需要时才去使用它。

它需求两个prop:一个是id(string),一个是当组件树种的组件“提交”更新的时候被React调用的回调函数onRender(function)。

onRender函数的各个prop:

  • id: string - 发生提交的 Profiler 树的 id。 如果有多个 profiler,它能用来分辨树的哪一部分发生了“提交”。
  • phase: "mount" | "update" - 判断是组件树的第一次装载引起的重渲染,还是由 props、state 或是 hooks 改变引起的重渲染。
  • actualDuration: number - 本次更新在渲染 Profiler 和它的子代上花费的时间。 这个数值表明使用 memoization 之后能表现得多好。(例如 React.memo,useMemo,shouldComponentUpdate)。 理想情况下,由于子代只会因特定的 prop 改变而重渲染,因此这个值应该在第一次装载之后显著下降。
  • baseDuration: number - 在 Profiler 树中最近一次每一个组件 render 的持续时间。 这个值估计了最差的渲染时间。(例如当它是第一次加载或者组件树没有使用 memoization)。
  • startTime: number - 本次更新中 React 开始渲染的时间戳。
  • commitTime: number - 本次更新中 React commit 阶段结束的时间戳。 在一次 commit 中这个值在所有的 profiler 之间是共享的,可以将它们按需分组。
  • interactions: Set - 当更新被制定时,“interactions” 的集合会被追踪。(例如当 render 或者 setState 被调用时)。

性能优化

  1. 使用生产版本
  2. 单文件构建
  3. 通过安装terser-brunch插件,来获取最高效的Brunch生产构建
  4. 使用chrome performance 标签分析组件
  5. 使用开发者工具中的分析器对组件进行分析
  6. 虚拟化长列表(react-windowreact-virtualized
  7. shouldComponentUpdate的优化
  8. 不可变(immutable)数据的力量

React 定义组件的三种方式

  • 无状态函数式组件 为了创建纯展示组件,这种组件只负责根据传入的props来展示,不涉及到要state状态的操作

    特点:

    1. 组件不会被实例化,整体渲染性能得到提升
    2. 组件不能访问this对象
    3. 组件无法访问生命周期的方法
    4. 无状态组件只能访问输入的props,同样的props会得到同样的渲染结果,不会有副作用
  • React.createClass React.createClass是react刚开始推荐的创建组件的方式,这是ES5的原生的JavaScript来实现的React组件

    存在的问题:

    1. React.createClass会自绑定函数方法(不像React.Component只绑定需要关心的函数)导致不必要的性能开销,增加代码过时的可能性。
    2. React.createClass的mixins不够自然、直观;React.Component形式非常适合高阶组件(Higher Order Components--HOC),它以更直观的形式展示了比mixins更强大的功能,并且HOC是纯净的JavaScript,不用担心他们会被废弃。
  • React.Component React.Component是以ES6的形式来创建react的组件的,是React目前极为推荐的创建有状态组件的方式

React.Component与React.createClass区别

  • 声明默认属性不同

无论是函数组件还是 class 组件,都拥有defaultProps属性:

class Greeting extends React.Component {
  // ...
}

Greeting.defaultProps = {
  name: 'Mary'
};

如果使用React.createClass方法创建组件,那就需要在组件中定义 getDefaultProps()函数:

const Greeting = React.createClass({
  getDefaultProps: function() {
    return {
      name: 'Mary'
    };
  },

  // ...

});
  • 初始化State不同

如果使用 ES6 的 class 关键字创建组件,你可以通过给this.state 赋值的方式来定义组件的初始 state:

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

如果使用React.createClass方法创建组件,你需要提供一个单独的 getInitialState方法,让其返回初始 state:

const Counter = React.createClass({
  getInitialState: function() {
    return {count: this.props.initialCount};
  },
  // ...
});
  • 自动绑定this

React.Component创建的组件,其成员函数不会自动绑定this,需要开发者手动绑定

有三种手动绑定方法:

  1. 在构造函数中完成绑定
  constructor(props) {
       super(props);
       this.handleClick = this.handleClick.bind(this); //构造函数中绑定
  }
  1. 在调用时使用method.bind(this)来完成绑定
    <div onClick={this.handleClick.bind(this)}></div> //使用bind来绑定
  1. 使用arrow function来绑定
    <div onClick={()=>this.handleClick()}></div> //使用arrow function来绑定

React.createClass创建的组件,其每一个成员函数的this都有React自动绑定

  • React.createClass支持Mixins,而React.Component不支持

Mixins(混入)是面向对象编程OOP的一种实现,其作用是为了复用共有的代码,将共有的代码通过抽取为一个对象,然后通过Mixins进该对象来达到代码复用。

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], // 使用 mixin
  getInitialState: function() {
    return {seconds: 0};
  },
  componentDidMount: function() {
    this.setInterval(this.tick, 1000); // 调用 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')
);

Diffing算法

  • 对比不同类型的元素 当根节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树。
  • 对比同一类型的元素 当对比两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。
  • 对比同类型的组件元素 当一个组件更新时,组件实例保持不变,这样 state 在跨越不同的渲染时保持一致
  • 对子节点进行递归 在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。 在子元素列表末尾新增元素时,更新开销比较小。比如:
<ul>
  <li>first</li>
  <li>second</li>
</ul>

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

React 会先匹配两个<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 不会意识到应该保留<li>Duke</li><li>Villanova</li>,而是会重建每一个子元素 。这种情况会带来性能问题。

为了解决以上问题,React 支持key属性。当子元素拥有key时,React 使用 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 知道只有带着 '2014' key 的元素是新元素,带着 '2015' 以及 '2016' key 的元素仅仅移动了。

什么是Render Props

术语“render prop” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术.。 具有render prop的组件接受一个返回 React 元素的函数,并在组件内部通过调用此函数来实现自己的渲染逻辑。

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

更具体地说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop

可以使用带有 render prop 的常规组件来实现大多数高阶组件 (HOC)。

Typescript获取一个库的声明文件方式

为了能够显示来自其他包的错误和提示,编译器依赖于声明文件。声明文件提供有关库的所有类型信息。

  • Bundled:该库包含了自己的声明文件。
  • DefinitelyTyped :是一个庞大的声明仓库,为没有声明文件的 JavaScript 库提供类型定义。这些类型定义通过众包的方式完成,并由微软和开源贡献者一起管理。
npm i --save-dev @types/react
  • 局部声明:有时,你要使用的包里没有声明文件,在 DefinitelyTyped 上也没有。在这种情况下,我们可以创建一个本地的定义文件。

何为受控组件和非受控组件

  • 受控组件:数据是由React组件来管理的
  • 非受控组件:数据将交由DOM节点来处理(使用Ref)。