VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 网站开发 > JavaScript >
  • JavaScript教程之React组件设计(2)

底层的Virtual Dom(虚拟DOM树)的diff算法有紧密的关系,所以真正决定同步还是异步的其实是Virtual DOMdiff算法。

依赖注入

React中,想做依赖注入(Dependency Injection)其实相当简单。可以通过props来进行传递。但是,如果组件数量很多,并且组件嵌套层次很深的话,这种方式就不太合适。

高阶组件

// inject.jsx
var title = 'React Dependency Injection';
export default function inject(Component) {
  return class Injector extends React.Component {
    render() {
      return (
        <Component
          {...this.state}
          {...this.props}
          title={ title }
        />
      )
    }
  };
}
// Title.jsx
export default function Title(props) {
  return <h1>{ props.title }</h1>;
}
// Header.jsx
import inject from './inject.jsx';
import Title from './Title.jsx';

var EnhancedTitle = inject(Title);
export default function Header() {
  return (
    <header>
      <EnhancedTitle />
    </header>
  );
}

context

React v16.3.0 之前的 Context:

var context = { title: 'React in patterns' };
class App extends React.Component {
  getChildContext() {
    return context;
  }
  // ...
}

App.childContextTypes = {
  title: PropTypes.string
};
class Inject extends React.Component {
  render() {
    var title = this.context.title;
  // ...
  }
}
Inject.contextTypes = {
  title: PropTypes.string
};

之前的 Context 作为一个实验性质的 API,直到 React v16.3.0 版本前都一直不被官方所提倡去使用,其主要原因就是因为在子组件中使用 Context 会破坏 React 应用的分型架构。

这里的分形架构指的是从理想的 React 应用的根组件树中抽取的任意一部分都仍是一个可以直接运行的子组件树。在这个子组件树之上再包一层,就可以将它无缝地移植到任意一个其他的根组件树中。

但如果根组件树中有任意一个组件使用了支持透传的 Context API,那么如果把包含了这个组件的子组件树单独拿出来,因为缺少了提供 Context 值的根组件树,这时的这个子组件树是无法直接运行的。

并且他有一个致命缺陷:任何一个中间传递的组件shouldComponentUpdate 函数返回false,组件都不会得到更新。

新的Context Api

新的Context Api 采用声明式的写法,并且可以透过shouldComponentUpdate 函数返回false的组件继续向下传播,以保证目标组件一定可以接收到顶层组件 Context 值的更新,一举解决了现有 Context API 的两大弊端,也终于成为了 React 中的第一级(first-class) API

新的 Context API 分为三个组成部分:

  1. React.createContext 用于初始化一个 Context

  2. XXXContext.Provider作为顶层组件接收一个名为 value的 prop,可以接收任意需要被放入 Context 中的字符串,数字,甚至是函数。

  3. XXXContext.Consumer作为目标组件可以出现在组件树的任意位置(在 Provider 之后),接收 children prop,这里的 children 必须是一个函数(context =&gt; ())用来接收从顶层传来的 Context

const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton(props) {
  return (
    <ThemeContext.Consumer>
      {theme => <Button {...props} theme={theme} />}
    </ThemeContext.Consumer>
  );
}

事件处理中的this指向问题

class Switcher extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: 'React in patterns' };
  }
  render() {
    return (
      <button onClick={ this._handleButtonClick }>
        click me
      </button>
    );
  }

  _handleButtonClick() {
    console.log(`Button is clicked inside ${ this.state.name }`);
    // 将导致
    // Uncaught TypeError: Cannot read property 'state' of null
  }
}

我们可以通过下面三种方式简单实现this指向的绑定:

  • constructor 中事先绑定 this._buttonClick = this._handleButtonClick.bind(this);
  • 调用时使用箭头函数 <button onClick={ () => this._buttonClick() }>
  • ES7中的绑定操作符 <button onClick={ ::this._buttonClick() }>

给setState传入回调函数

setState() 不仅能接受一个对象,还能接受一个函数作为参数呢,该函数接受该组件前一刻的 state 以及当前的 props 作为参数,计算和返回下一刻的 state。

// assuming this.state.count === 0
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
// this.state.count === 1, not 3

this.setState((prevState, props) => ({
  count: prevState.count + props.increment
}));
// Passing object
this.setState({ expanded: !this.state.expanded });

// Passing function
this.setState(prevState => ({ expanded: !prevState.expanded }));

组件切换技巧

import HomePage from './HomePage.jsx';
import AboutPage from './AboutPage.jsx';
import UserPage from './UserPage.jsx';
import FourOhFourPage from './FourOhFourPage.jsx';

const PAGES = {
  home: HomePage,
  about: AboutPage,
  user: UserPage
};

const Page = (props) => {
  const Handler = PAGES[props.page] || FourOhFourPage;

  return <Handler {...props} />
};

React style

组件分类

基础组件, 布局组件, 排版组件

给无状态的纯UI组件应用样式

请保持样式远离那些离不开state的组件. 比如路由, 视图, 容器, 表单, 布局等等不应该有任何的样式或者css class出现在组件上. 相反, 这些复杂的业务组件应该有一些带有基本功能的无状态UI组件组成.

class SampleComponent extends Component {
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <Heading children='Sign In'/>
        <Input
          name='username'
          value={username}
          onChange={this.handleChange}/>
        <Input
          type='password'
          name='password'
          value={password}
          onChange={this.handleChange}/>
        <Button
          type='submit'
          children='Sign In'/>
      </form>
    )
  }
}

// 表达组件(带样式)
const Button = ({
  ...props
  }) => {
  const sx = {
    fontFamily: 'inherit',
    fontSize: 'inherit',
    fontWeight: 'bold',
    textDecoration: 'none',
    display: 'inline-block',
    margin: 0,
    paddingTop: 8,
    paddingBottom: 8,
    paddingLeft: 16,
    paddingRight: 16,
    border: 0,
    color: 'white',
    backgroundColor: 'blue',
    WebkitAppearance: 'none',
    MozAppearance: 'none'
  }

  return (
    <button {...props} style={sx}/>
  )
}

样式模块(style module)

一般来说, 在组件内写死(hard code)样式应该是要被避免的. 这些有可能被不同的UI组件分享的样式应该被分开放入对应的模块中.

// 样式模块
export const white = '#fff';
export const black = '#111';
export const blue = '#07c';

export const colors = {
  white,
  black,
  blue
};

export const space = [
  0,
  8,
  16,
  32,
  64
];

const styles = {
  bold: 600,
  space,
  colors
};

export default styles
// button.jsx
import React from 'react'
import { bold, space, colors } from './styles'

const Button = ({
  ...props
  }) => {
  const sx = {
    fontFamily: 'inherit',
    fontSize: 'inherit',
    fontWeight: bold,
    textDecoration: 'none',
    display: 'inline-block',
    margin: 0,
    paddingTop: space[1],
    paddingBottom: space[1],
    paddingLeft: space[2],
    paddingRight: space[2],
    border: 0,
    color: colors.white,
    backgroundColor: colors.blue,
    WebkitAppearance: 'none',
    MozAppearance: 'none'
  };

  return (
    <button {...props} style={sx}/>
  )
};

样式函数(Style Functions)

// Modular powers of two scale
const scale = [
  0,
  8,
  16,
  32,
  64
];

// 通过这个函数去取得一部分的样式
const createScaledPropertyGetter = (scale) => (prop) => (x) => {
  return (typeof x === 'number' && typeof scale[x] === 'number')
    ? {[prop]: scale[x]}
    : null
};
const getScaledProperty = createScaledPropertyGetter(scale);

export const getMargin = getScaledProperty('margin');
export const getPadding = getScaledProperty('padding');
// 样式函数的用法
const Box = ({
  m,
  p,
  ...props
  }) => {
  const sx = {
    ...getMargin(m),
    ...getPadding(p)
  };

  return <div {...props} style={sx}/>
};

// 组件用法.
const Box = () => (
  <div>
    <Box m={2} p={3}>
      A box with 16px margin and 32px padding
    </Box>
  </div>
);

常见小坑

state不更新?

class SampleComponent extends Component {
  // constructor function (or getInitialState)
  constructor(props) {
    super(props);
    this.state = {
      flag: false,
      inputVal: props.inputValue
    };
  }

  render() {
    return <div>{this.state.inputVal && <AnotherComponent/>}</div>
  }
}

这样做的危险在于, 有可能组件的props发生了改变但是组件却没有被更新. 新的props的值不会被React认为是更新的数据因为构造器constructor或者getInitialState方法在组件创建之后不会再次被调用了,因此组件的state不再会被更新。 要记住, State的初始化只会在组件第一次初始化的时候发生。

class SampleComponent extends Component {
  // constructor function (or getInitialState)
  constructor(props) {
    super(props);
    this.state = {
      flag: false
    };
  }

  render() {
    return <div>{this.props.inputValue && <AnotherComponent/>}</div>
  }
}

更干净的render函数?

更干净的render函数? 这个概念可能会有点让人疑惑.

其实在这里干净是指我们在shouldComponentUpdate这个生命周期函数里面去做浅比较, 从而避免不必要的渲染.

class Table extends PureComponent {
  render() {
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={this.props.options || []}/>
        )}
      </div>
    );
  }
}

这种写法的问题在于{this.props.options || []} 这种写法会导致所有的Cell都被重新渲染即使只有一个cell发生了改变. 为什么会发生这种事呢?

仔细观察你会发现, options这个数组被传到了Cell这个组件上, 一般情况下, 这不会导致什么问题. 因为如果有其他的Cell组件, 组件会在有props发生改变的时候浅对比props并且跳过渲染(因为对于其他Cell组件, props并没有发生改变). 但是在这个例子里面, 当optionsnull时, 一个默认的空数组就会被当成Props传到组件里面去. 事实上每次传入的[]都相当于创建了新的Array实例. 在JavaScript里面, 不同的实例是有不同的实体的, 所以浅比较在这种情况下总是会返回false, 然后组件就会被重新渲染. 因为两个实体不是同一个实体. 这就完全破坏了React对于我们组件渲染的优化.

const defaultval = [];  // <---  也可以使用defaultProps
class Table extends PureComponent {
  render() {
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={this.props.options || defaultval}/>
        )}
      </div>
    );
  }
}

还是多次重新渲染

class App extends PureComponent {
  render() {
    return <MyInput
      onChange={e => this.props.update(e.target.value)}/>;
  }
}
class App extends PureComponent {
  update(e) {
    this.props.update(e.target.value);
  }

  render() {
    return <MyInput onChange={this.update.bind(this)}/>;
  }
}

在上面的两个坏实践中, 每次我们都会去创建一个新的函数实体. 和第一个例子类似, 新的函数实体会让我们的浅比较返回false, 导致组件被重新渲染. 所以我们需要在更早的时候去bind我们的函数.

class App extends PureComponent {
  constructor(props) {
    super(props);
    this.update = this.update.bind(this);
  }

  update(e) {
    this.props.update(e.target.value);
  }

  render() {
    return <MyInput onChange={this.update}/>;
  }
}

命名

引用命名

React模块名使用帕斯卡命名,实例使用骆驼式命名

// bad
import reservationCard from './ReservationCard';

// good
import ReservationCard from './ReservationCard';

// bad
const ReservationItem = <ReservationCard />;

// good
const reservationItem = <ReservationCard />;

高阶模块命名

// bad
export default function withFoo(WrappedComponent) {
  return function WithFoo(props) {
    return <WrappedComponent {...props} foo />;
  }
}

// good
export default function withFoo(WrappedComponent) {
  function WithFoo(props) {
    return <WrappedComponent {...props} foo />;
  }

  const wrappedComponentName = WrappedComponent.displayName
    || WrappedComponent.name
    || 'Component';

  WithFoo.displayName = `withFoo(${wrappedComponentName})`;
  return WithFoo;
}

属性命名

避免使用DOM相关的属性来用作其他的用途。

// bad
<MyComponent style="fancy" />

// good
<MyComponent