react 学习笔记

3. JSX

JSX是对JavaScript语言的一个扩展语法, 用于生产React“元素”,建议在描述UI的时候使用。

  1. 可以在JSX中插入任何表达式(注意只能是表达式,for循环之类的代码段就无法插入),只要用花括号括起来,如:
const element = (
  ` <h1> 
    Hello, {formatName(user)}!  // formatName是一个函数
  </h1>` 
);
  1. 在属性中,插入string的时候,可以用引号,插入js表达式的时候,用花括号,不要同时使用引号和花括号,否则属性可能会被当做字符串常量解析:如
    const element = <div tabIndex="0"></div>;
    或者
    const element = <img src={user.avatarUrl}></img>;
  2. 在JSX中声明孩子节点
const element = (
  <div>
    <h1>Hello!</h1>
    <h2>Good to see you here.</h2>
  </div>
);
  1. JSX可以防止注入攻击
    在rendering JSX之前,React DOM会忽略注入的值,所以可以保证不会注入任何在应用中没有明确声明的东西,在rendering之前,所有的东西都被转变为string,这有助于防止 XSS (cross-site-scripting) 攻击。

  2. 在React.createElement()调用时,才会编译JSX,下面两段代码是对等的
    <pre>
    const element = ( <h1 className="greeting"> Hello, world! </h1> );
    </pre>

    <pre>
    const element = React.createElement( 'h1', {className: 'greeting'}, 'Hello, world!' );</pre>
    注意:React DOM使用驼峰式命名HTML变量,如class变成className。

4. react 渲染元素

React的elements(元素)就是普通对象,components(组件)由elements组成,通过const element = <h1>Hello, world</h1>;就可以创建一个element

render用法如下

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(
    element,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);
  1. 使用ReactDOM.render()渲染一个React元素
  2. React elements是不可变( immutable)的,React DOM会将新创建的element以及它的children和前一个element相比,只改变DOM中需要改变的部分。上栗中,虽然每隔一秒都会调用ReactDOM.render(),实际只会调用一次ReactDOM.render(),通过stateful components可以了解上面例子是如何封装的
  3. 改变UI的唯一方法就是创建一个新的element,然后把它传递给ReactDOM.render()

5 react components和props

components将UI分割成独立的、可复用的块。从概念上来说,components像Js的函数,它接受任意输入,然后返回在显示在屏幕上的element

1. 函数和类表示的components

函数式component

function Welcome(props) {
  return<h1>Hello, {props.name}</h1>;
} 

es6对象定义的component

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
2. element也可以是用户定义的component
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;`
ReactDOM.render(
  element,
  document.getElementById('root')
);

注意:component的首个字母大写

3. Components可以引用多个其他的components
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
function App() {
  return (
    <div>
      <Welcome name="Sara" />
      <Welcome name="Cahal" />
      <Welcome name="Edite" />
    </div>
  );
}

注意:components必须返回一个根元素,这就是为什么上面栗子中,添加一个<div>元素来包含<Welcome />

4. 有时候需要把一个component分割成多个更小的components,如:
function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

上述代码包含多层嵌套,需要修改时就会非常微妙,这种情况下,上述代码就适合分割成多块。
如果一个代码可能会多次复用,或者本身太复杂,就适合分割成多个可服用的components

5. props应该是只读的

function sum(a, b) { return a + b;}
以上代码是一个“prue”函数,因为它没有修改输入值,所有的React component都应该是“prue”函数,不要修改props

6. react 状态和生命周期(State and Lifecycle)

State和props类似,但是它是私有的,并且完全由component控制,对比一下分别用state和props实现的一个实时闹钟

a:props

function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}
function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}
setInterval(tick, 1000);

b:state


class Clock extends React.Component {  // 对象继承至React.component
  constructor(props) {
    super(props);
    this.state = {date: new Date()};  // 构造函数提供初始值,state是一个对象
  }
  componentDidMount() {  // 生命周期钩子,在component在DOM中渲染完成之后执行
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }
  componentWillUnmount() { // 生命周期钩子,在component即将销毁时执行
    clearInterval(this.timerID);
  }
  tick() {
    this.setState({  // 修改component的state
      date: new Date()
    });
  }
  render() {  // class返回一个render
    return (  // render返回一个“(element)”
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2> // Js表达式中使用this.state
      </div>
    );
  }
}
ReactDOM.render(
  <Clock />,  // clock中移除date
  document.getElementById('root')
);`
正确使用state
  1. 使用setState修改state
  2. setState的callback函数可以传入两个参数:prevState, props(前一个状态、参数),正确使用
this.setState((prevState, props) => ({ 
  counter: prevState.counter + props.increment
}));
  1. state的修改会被合并(merge),执行浅拷贝的merge。可以理解为执行一个Object.assign(oldState, newState)操作

  2. 可以将state作为props传递给child component(孩子组件),state只会向下传递

<FormattedDate date={this.state.date} />
function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

7. react 事件处理器

React元素时间处理与DOM元素的不同

  1. 命名事件方式不同,react->驼峰式,DOM->小写
  2. react传递函数给事件处理器,DOM传递string
  3. DOM调用return false来阻止默认行为,在react中,必须显示调用preventDefault

对比

<button onclick="console.log('The link was clicked.'); return false">
  Click me
</button>
function ActionLink() {
  function handleClick(e) {
    e.preventDefault();  // 显式调用preventDefault
    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}> // 驼峰式handleClick,{}传递function
      Click me
    </a>
  );
}

注意:上述代码e是SyntheticEvent

在react中,通常不使用addEventListener,替代方法是在elements第一次渲染时提供一个listener

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // This binding is necessary to make `this` work in the callback
    this.handleClick = this.handleClick.bind(this); // 注意此处不绑定this,在真正使用的时候,this会变成undefined
  }

  handleClick() {
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

ReactDOM.render(
  <Toggle />,
  document.getElementById('root')
);

8. 根据条件渲染组件

1 if

可以使用js的if 或者条件运算符 创建符合当前状态的元素
如:通过判断用户是否登陆来创建一个Greeting组件

function Greeting(props) {
  const isLoggedIn = props.isLoggedIn;
  if (isLoggedIn) {
    return <UserGreeting />;
  }
  return <GuestGreeting />;
}

ReactDOM.render(
  // Try changing to isLoggedIn={true}:
  <Greeting isLoggedIn={false} />,
  document.getElementById('root')
);

创建一个登陆登出组件

function LoginButton(props) {
  return (
    <button onClick={props.onClick}>
      Login
    </button>
  );
}

function LogoutButton(props) {
  return (
    <button onClick={props.onClick}>
      Logout
    </button>
  );
}

class LoginControl extends React.Component {
  constructor(props) {
    super(props);
    this.handleLoginClick = this.handleLoginClick.bind(this);
    this.handleLogoutClick = this.handleLogoutClick.bind(this);
    this.state = {isLoggedIn: false};
  }

  handleLoginClick() {
    this.setState({isLoggedIn: true});
  }

  handleLogoutClick() {
    this.setState({isLoggedIn: false});
  }

  render() {
    const isLoggedIn = this.state.isLoggedIn;

    let button = null;
    if (isLoggedIn) {
      button = <LogoutButton onClick={this.handleLogoutClick} />;
    } else {
      button = <LoginButton onClick={this.handleLoginClick} />;
    }

    return (
      <div>
        <Greeting isLoggedIn={isLoggedIn} />
        {button}
      </div>
    );
  }
}

ReactDOM.render(
  <LoginControl />,
  document.getElementById('root')
);
2. &&

&&操作符编写内联的if逻辑,在JSX中插入表达式

function Mailbox(props) {
  const unreadMessages = props.unreadMessages;
  return (
    <div>
      <h1>Hello!</h1>
      {unreadMessages.length > 0 &&
        <h2>
          You have {unreadMessages.length} unread messages.
        </h2>
      }
    </div>
  );
}

const messages = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
  <Mailbox unreadMessages={messages} />,
  document.getElementById('root')
);
3. 内联的条件表达式
render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      {isLoggedIn ? (
        <LogoutButton onClick={this.handleLogoutClick} />
      ) : (
        <LoginButton onClick={this.handleLoginClick} />
      )}
    </div>
  );
}

9 react lists和keys

1 rendering多个components

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
  <li>{number}</li>
);
ReactDOM.render(
  <ul>{listItems}</ul>, 
  document.getElementById('root')
);
key
function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li key={number.toString()}>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);`

代码中,<li key={number.toString()}>如果没有key换成<li>,就会有一个warning信息。在创建elements列表时,key是一个特殊的属性。

  1. 一个array创建的elements列表中,item的key应该是独一无二的。
  2. 注意:react根据keys判断列表的items(项)是否改增删改
  3. element的key应该是稳定的,当你array的item没有一个稳定的标识,你或许可以使用item的index作为element的key。
  4. 对于需要重新排序的array,不建议使用index作为key,这样渲染会比较慢
  5. keys应该在array包裹的环境内使用,例如
function ListItem(props) {

    // Wrong! There is no need to specify the key here:
    //return <li key={value.toString()}>
    // Correct! There is no need to specify the key here:
  return (<li> {props.value} </li>);
}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // Wrong! The key should have been specified here:
    // <ListItem value={number} />
    // Correct! Key should be specified inside the array.
    <ListItem key={number.toString()} value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);
  1. jsx允许在{}中注入任何形式的表达式,上面代码NumberList可以改写成:
  function NumberList(props) {
  const numbers = props.numbers;
  return (
    <ul>
      {numbers.map((number) =>
        <ListItem key={number.toString()}  value={number} />
      )}
    </ul>
  );
}

10. react 表单

controlled component

一个input表单element(元素),如果它的value(值)是由React控制的,我们就叫它controlled component。
在react中,一个可变状态通常都保存在React的state中,state只能通过setState修改

栗子:input
class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

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

通过表单可以很方便的验证和修改用户的输入,如
handleChange(event) { this.setState({value: event.target.value.toUpperCase()}); }

官网地址:https://facebook.github.io/react/docs/forms.html

11. react state 提升

有时候,多个不同的component之间可能需要共享一个state,但是,在react中,state是私有的,这种情况下,可以通过提升state来实现共享,既将state提升至它们共同的最近的component祖先中(从上至下的数据流)

1:state提升需要写一个共同的模板,可能需要写更多的代码
2:如果有些数据,既可以放在props又可以放在state中,那么建议不要放在state中

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};
function toCelsius(fahrenheit) {
  return (fahrenheit - 32) * 5 / 9;
}

function toFahrenheit(celsius) {
  return (celsius * 9 / 5) + 32;
}
function tryConvert(temperature, convert) {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return '';
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}
function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}
class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.props.onTemperatureChange(e.target.value, this.props.scale);
  }

  render() {
    const temperature = this.props.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}
class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleCommonChange = this.handleCommonChange.bind(this)
    this.state = {temperature: '', scale: 'c'};
  }

  handleCommonChange(temperature, scale) {
    this.setState({scale, temperature});
  }

  render() {
    const scale = this.state.scale;
    const temperature = this.state.temperature;
    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
    const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;

    return (
      <div>
        <TemperatureInput
          scale="c"
          temperature={celsius}
          onTemperatureChange={this.handleCommonChange} />
        <TemperatureInput
          scale="f"
          temperature={fahrenheit}
          onTemperatureChange={this.handleCommonChange} />
        <BoilingVerdict
          celsius={parseFloat(celsius)} />
      </div>
    );
  }
}

相关链接:https://facebook.github.io/react/docs/lifting-state-up.html

12. react 组合与集成

react有强大的组合模型,在react建议使用组合来代替继承(避免不同组件之间共用部分代码),本章节介绍一下新手在开发过程中经常遇到的,用组合代替继承的案例

  1. props的参数也可以是组件,如left={<Contacts />}。可以实现插槽功能
function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}
function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

SplitPane组件有两个插槽left和right。<Contacts /> 和 <Chat />都是对象,可以像其他数据一样传递给props,实现插槽的功能

  1. 通过props.children获取孩子节点。可以通过props.children实现“特例”,例如SignUpDialog是Dialog的特例
function Dialog(props) {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
      {props.children}
    </FancyBorder>
  );
}

class SignUpDialog extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSignUp = this.handleSignUp.bind(this);
    this.state = {login: ''};
  }

  render() {
    return (
      <Dialog title="Mars Exploration Program"
              message="How should we refer to you?">
        <input value={this.state.login}
               onChange={this.handleChange} />
        <button onClick={this.handleSignUp}>
          Sign Me Up!
        </button>
      </Dialog>
    );
  }

  handleChange(e) {
    this.setState({login: e.target.value});
  }

  handleSignUp() {
    alert(`Welcome aboard, ${this.state.login}!`);
  }
}

官网:https://facebook.github.io/react/docs/composition-vs-inheritance.html

进阶篇 PropTypes

JavaScript的扩展语言: FlowTypeScript可以检测语法,在react中,可以使用PropTypes对props做类型检查,propTypes只在开发模式下做检查,如果提供的props是一个无效值,会console出一个warning

  1. React.PropTypes.element.isRequired,可以为组件指定必须项
MyComponent.propTypes = {
  children: React.PropTypes.element.isRequired
};

2:defaultProps:为props指定一个默认值

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}
// Specifies the default values for props:
Greeting.defaultProps = {
  name: 'Stranger'
};
  1. React.PropTypes:不同检测器的文档例子
MyComponent.propTypes = {
  // You can declare that a prop is a specific JS primitive. By default, these
  // are all optional.
  optionalArray: React.PropTypes.array,
  optionalBool: React.PropTypes.bool,
  optionalFunc: React.PropTypes.func,
  optionalNumber: React.PropTypes.number,
  optionalObject: React.PropTypes.object,
  optionalString: React.PropTypes.string,
  optionalSymbol: React.PropTypes.symbol,

  // Anything that can be rendered: numbers, strings, elements or an array
  // (or fragment) containing these types.
  optionalNode: React.PropTypes.node,

  // A React element.
  optionalElement: React.PropTypes.element,

  // You can also declare that a prop is an instance of a class. This uses
  // JS's instanceof operator.
  optionalMessage: React.PropTypes.instanceOf(Message),

  // You can ensure that your prop is limited to specific values by treating
  // it as an enum.
  optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),

  // An object that could be one of many types
  optionalUnion: React.PropTypes.oneOfType([
    React.PropTypes.string,
    React.PropTypes.number,
    React.PropTypes.instanceOf(Message)
  ]),

  // An array of a certain type
  optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),

  // An object with property values of a certain type
  optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),

  // An object taking on a particular shape
  optionalObjectWithShape: React.PropTypes.shape({
    color: React.PropTypes.string,
    fontSize: React.PropTypes.number
  }),

  // You can chain any of the above with `isRequired` to make sure a warning
  // is shown if the prop isn't provided.
  requiredFunc: React.PropTypes.func.isRequired,

  // A value of any data type
  requiredAny: React.PropTypes.any.isRequired,

  // You can also specify a custom validator. It should return an Error
  // object if the validation fails. Don't `console.warn` or throw, as this
  // won't work inside `oneOfType`.
  customProp: function(props, propName, componentName) {
    if (!/matchme/.test(props[propName])) {
      return new Error(
        'Invalid prop `' + propName + '` supplied to' +
        ' `' + componentName + '`. Validation failed.'
      );
    }
  },

  // You can also supply a custom validator to `arrayOf` and `objectOf`.
  // It should return an Error object if the validation fails. The validator
  // will be called for each key in the array or object. The first two
  // arguments of the validator are the array or object itself, and the
  // current item's key.
  customArrayProp: React.PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
    if (!/matchme/.test(propValue[key])) {
      return new Error(
        'Invalid prop `' + propFullName + '` supplied to' +
        ' `' + componentName + '`. Validation failed.'
      );
    }
  })
};

14 进阶篇 深入JSX

JSX是React.createElement(component, props, ...children) 的语法糖

指定React元素类型

JSX标签是一个React元素类型,大写的类型表示JSX标签是一个React组件

  1. 因为JSX编译时需要调用React.createElement,所以React必须在JSX代码的作用域内
  2. 可以使用点记法标识一个JSX类型(适用于一个模块导出多个component组件)
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" />;
}
  1. 用户自定义的组件必须大写
  2. React元素不能是普通表达式,如果想使用普通表达式,可以先赋值给一个大写的变量
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} />;
  // Correct! JSX type can be a capitalized variable.
  const SpecificStory = components[props.storyType];
  return <SpecificStory story={props.story} />;
}
JSX中的props
  1. js表达式:prop可以是任何{}包裹的Js表达式,如:<MyComponent foo={1 + 2 + 3 + 4} />
  2. 可以使用string字面量,此时HTML是非转义的
// JSX表达式等效
<MyComponent message="hello world" />
<MyComponent message={'hello world'} />
  1. props默认值是"true",
// 这两种JSX表达式等效
<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />

4:扩展属性,props是一个对象,想要传递给JSX,可以使用……作为扩展操作来传递全部props对象

// 这两种JSX表达式等效
function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}
JSX中的孩子

在组件开口和闭合标签之间的内容是一种特殊的props:props.children,有几种特殊的props.children

  1. 字符串字面量
    此时,props.children是字符串Hello world!
<MyComponent>Hello world!</MyComponent>
  1. JSX孩子
    可以将JSX元素作为孩子,可以混用不同类型的JSX元素或者String作为孩子,这和HTML是类似的,在展现嵌套的组件时非常有用
<MyContainer>
  <MyFirstComponent />
  <MySecondComponent />
</MyContainer>
  1. JSX表达式
    可以传递任何表达式,只要使用{}包裹就可以
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>
  );
}
  1. 函数作为孩子
// 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>
  );
}
  1. false, null, undefined, and true都是有效的children(孩子),不过他们不会被渲染,这在根据条件渲染React元素时非常有用
// 有些假值,例如0仍然会被渲染
<div>
  {props.messages.length > 0 &&
    <MessageList messages={props.messages} />
  }
</div>

反之,如果你想要渲染false, true, null, or undefined 之类的假值,需要先转化为string

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

6:JSX的孩子是一种特殊的props:props.children,可以是字符串字面量、JSX、表达式、函数
7:false, null, undefined, and true都是有效孩子,不会被渲染,需要渲染时,使用{String(myVariable)}转化为string

15. 进阶篇 Refs and the DOM

典型的React数据流中,父子组件交互的唯一方法就是props。有时候需要在典型数据流以外的地方修改子元素,React提供了Refs(引用),用ref来获取组件或者HTML元素的引用

Refs使用场景

1:处理焦点、文本选择、媒体播放
2:触发强制性动画
3:集成第三方DOM库

给DOM添加Refs

可以在任何component上设置一个ref属性,ref有一个回调函数,会在组件mounted(安装完成)和unmounted(卸载完成)的时候立即执行。当ref用在HTML元素上时,ref回调函数接收DOM元素作为入参,如

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>
    );
  }
}

如上例,使用ref回调函数在类上面设置一个属性来获取DOM元素是一种常用的形式

给类组件( Class Component)添加Refs

模拟CustomerTextInput安装完成后被点击的效果

class AutoFocusTextInput extends React.Component {
  componentDidMount() {
    this.textInput.focus();
  }

  render() {
    return (
      <CustomTextInput
        ref={input => { this.textInput = input; }} />
    );
  }
}
引用和方法组件(Functional Components)

方法组件没有实例,所以不适合使用ref属性,可以将方法组件转化为类组件以使用ref,正如使用生命周期方法和state
当引用一个DOM元素或者类组件时,可以在方法组件中使用ref,如

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>
  );  
}
a:不要过度使用Refs
b:String refs有点争议,以后版本可能会遗弃,建议使用回调函数
c:callback如果是内联函数,会调用两次,一次null一次是DOM元素,因为每次渲染都会产生一个新的实例,可以通过在类上绑定一个方法作为ref的回调函数来避免

16 进阶篇 Uncontrolled Components

通常,建议使用controlled Components来实现form,在controlled Components中,表单数据都是React组件处理的,相反,Uncontrolled Components中表单数据都是DOM自己处理的

写一个uncontrolled组件,与其为每一个state变化写一个事件处理器不如使用ref获取DOM表单的值,如:例子中获取一个uncontrolled组件的名字

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>
    );
  }
}

了解更多在一个特殊的情况下应该使用uncontrolled还是controlled组件,查看this article on controlled versus uncontrolled inputs可能会有帮助

在React的渲染周期,form元素的value属性可能会修改DOM的值,在一个uncontrolled组件中,会希望React指定初始值,但是不希望之后值被不受控制的修改,这种情况下可以使用defaultValue代替value

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>
  );
}

同理,在checkboxradio中,使用defaultChecked,select使用defaultValue

17. 进阶篇 优化

优化React应用的一些方法

使用构建工具

1:对单应用,.min.js版本工具
2:对Brunch,build命令使用-p
3:对Browserify,启动在NODE_ENV=production
4:创建React App,执行npm run build,然后跟着提示走
5:对Rollup,在 commonjs插件之前使用replace 插件,那么只在开发模式下需要的模块就不会被导入,完整设置例子see this gist.
6:对webpack,在生成环境需要添加插件
<pre>new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } }), new webpack.optimize.UglifyJsPlugin()</pre>

使用chrome的Timeline剖析组件

1:在app路径query字符串后添加?react_perf(如http://localhost:3000/?react_perf)
2:打开Chrome DevTools Timeline然后点击Record
3:执行你想要剖析的操作,不要记录超过20分钟,否则chrome可能会停止
4:停止记录
5:在用户 Timing标签下,React事件会被分类
在生成环境中渲染会相对快点,目前只有 Chrome, Edge, 和IE 浏览器支持这个特性

Avoid Reconciliation(避免重新渲染)

对于已经渲染过的UI,React构建和保存一份内部表现,它包含了组件返回的React元素,这个表现使React可以避免创建DOM节点,并且在有必要的时候还可以获取已经存在的DOM节点,由于操作JavaScript对象会比操作DOM更加快,有时候把这个内部表现叫做“virtual DOM”(虚拟DOM),在React Native中,virtual DOM也以同样的方式工作

当一个组件的state或者props变化的时候,React通过将新返回的元素与前一个被渲染的元素进行比较来决定是否需要修改真实的DOM,当他们不相等时,React会修改DOM

有时候通过重写生命周期函数shouldComponentUpdate可以加速组件的渲染,shouldComponentUpdate在重新渲染操作即将开始前执行,默认的函数实现是返回true,React就会重新渲染组件
<pre>shouldComponentUpdate(nextProps, nextState) { return true; }</pre>

如果你知道在什么情况下,你的组件不需要修改,你可以在shouldComponentUpdate中返回一个false,那么对组件的重新渲染就不会执行

shouldComponentUpdate操作

下图是组件的子树,对每一个节点,SCU表示shouldComponentUpdate的返回值,vDOMEq表示需要渲染React元素与前一个已经被渲染的元素是否相等

should-component-update.png

由于C2节点的shouldComponentUpdate返回false,React不会去渲染C2,因此C4和C5的shouldComponentUpdate都不会触发

对于C1和C3,shouldComponentUpdate都返回true,因此React走到叶子节点并且检查他们,C6的shouldComponentUpdate返回true,并且与已经渲染的元素不相等,React必须修改DOM

最后一个是C8,shouldComponentUpdate返回true,但是由于C8返回的React元素与前一个已经渲染的元素相等,所以React还是不会修改DOM

总结:只有遍历C6节点时,React会修改DOM,对于C8,由于被比较的两个React元素相等不会修改,对于C2的子树和C7,由于 shouldComponentUpdate没有返回true,React甚至不会比较元素,render方法也不会被调用

例子
下例中,只有props.colorstate.count变化时component才会重新渲染,可以修改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>
    );
  }
}

可以使用类似的模式来对所有的props和state做一个“浅比较”来决定组件是否需要修改

大多数时候可以使用React.PureComponent来代替shouldComponentUpdateReact.PureComponent会做一个“浅比较”,所以对于props或者state修改前后,浅比较值依旧相等的情况,不要使用React.PureComponent

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>
    );
  }
}

PureComponent会对this.props.words的新旧值做一个简单的比较,由于代码中对words的修改是一个数组的修改,修改前后的值做浅比较依旧相等,ListOfWords不会被重新渲染。

上述代码可以修改如下
1:由于concat返回一个新的数组,可以使用concat对words重新赋值。

words: prevState.words.concat(['marklar'])

2:es6的扩展语法,对words重新赋值

words: [...prevState.words, 'marklar']

对于对象的修改,可以使用object.assign

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

或者给对象添加扩展属性

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

Using Immutable Data Structures

Immutable.js是另一个解决方案,它通过结构化共享提供一个不变化、持久化的集合

不可变:一个集合一旦创建后,它的数据就不会被改变
持久化:通过set做了修改,会根据原先的集合创建一个新的集合
结构化共享:新的集合会尽可能多的共享原先集合相同的部分

Immutable对改变的追踪更加廉价,

const x = { foo: 'bar' };
const y = x;
y.foo = 'baz';
x === y; // true

类似的Immutable代码

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

20. 进阶篇 Reconciliation

React提供了API的说明,因此在每次修改的时候,你不需要知道做了什么,本文解释了React的Diffing Algorithm(差分算法)

动机

创建一颗React元素的树时,在state或props变化的下一个瞬间,你知道render()方法会返回一颗不同的React元素的树,React需要考虑如何有效的修改UI来最大程度的匹配现在的树
针对这个算法问题,有一些解决方案可以使用最少的操作来将一颗树转为另一颗,但是,这个state of the art algorithms 有 O(n3) 的复杂度,n是树种元素的个数

如果展示1000个元素,就要进行10亿次比较,消耗太大。基于以下两点假想,React实现一个启发式的O(n) 的算法
1:两个不同的元素会生成两个不同的树
2:开发者可以通过key暗示在不同的渲染中,哪个孩子元素是稳定不变的

实践表明,这些假想基本上在所有的实际例子中都是正确的

Diffing算法

当分裂两棵树时,React首先比较两个根元素。不同类型的根元素会产生不同的行为

不同类型的元素

一旦根元素不同,React就会销毁一个旧的树并且从头构建一颗新树,如从<a><img>, 从 <Article><Comment>, 或者从 <Button><div>,其中任何一个根元素的变化都会重新构建一颗全新的树

销毁一颗树,旧的DOM节点就会销毁,Component实例会调用componentWillUnmount()事件。当构建一棵新树,新的DOM节点会插入DOM,Component会依次执行componentWillMount()componentDidMount(),所有与旧的树有关联的state都会消失

根下面的任何组件也将被卸载,它们的state也会被销毁,例如

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

旧的Counter以及它的state被销毁,React重新安装一个新的Counter

相同类型的DOM元素

当比较两个相同类型的DOM元素,React查看两者的属性,保留相同的底层DOM节点,并且只更新已经更改的属性,例如<div className="before" title="stuff" /> <div className="after" title="stuff" />

通过比较,React知道只更改底层DOM节点的className
处理DOM节点后,React对孩子节点进行同样的递归操作

相同类型的Component元素

当一个Component改变了,改变前后Component的对象实例是同一个,从而在渲染过程中维持一个state,React更新底层组件的props来匹配新的元素,然后调用底层实例的componentWillReceiveProps()componentWillUpdate()
然后render()方法被调用,使用差分算法(diff algorithm)对先前的结果和新的结果进行递归

孩子的递归

默认的,当递归一个DOM孩子节点时,React会同时递归新旧孩子列表,一旦有不同的地方,React就会生成一个改变,
例如,在尾部添加一个节点

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

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

React会比较前两个<li>树,然后插入第三个<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>这个完整的子树,这个低效率是一个问题

keys

为了解决这个问题,React支持一个key属性,如果孩子节点有key,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知道“2014”这个元素是一颗新的树,而“2015”和“2016”这两个key对应的元素只是移动了。

可以添加一个ID属性或者对部分内容做哈希算法生成一个key,这个key只需要在与它的兄弟节点的key不同,而不需要全局独一无二

可以将item在数组中的index作为key,不过如果这个数组会重新排序,重新排序可能会很慢

Tradeoffs

记住,reconciliation算法只是一个实现细节。React可以在每次操作后重新渲染整个App,结果是一样的,我们经常启发式的改进以使通常的用户用例更快
因为React依赖启发式,如果背后的假想不成立,性能会受损
1:这个算法不会试着匹配不同组件类型的子树
2:key应该是稳定的,可预测的,和独一无二的,不稳定的key(例如通过Math.random()生成)会导致很多组件实例和DOM节点不必要的重建,会导致孩子组件的性能降低和state丢失。

生命周期事件

componentWillUnmount():在旧的DOM节点即将销毁时调用
componentWillMount():即将安装新的DOM时调用
componentDidMount():新的DOM安装完成是调用
componentWillReceiveProps():组件即将更新props时调用
componentWillUpdate():组件接收到新的props或者state但还没有render()时被执行
在React中,根据React的组件,很容易就能够跟踪数据流。有时候,你需要通过Component组件来传递数据,但是又不想手动的沿着每一层级传递,你可以通过“context”API直接获取

为什么不使用Context

大型的应用不需要使用context,context会破坏应用的稳定性。context是一个突破性的API,在将来的React版本中可能会突破

如果你不熟悉Redux 或者 MobX,不要使用context。在许多实际应用中,将这些库与React绑定来管理组件的状态是一个不错的选择,也就是说Redux比context更适合解决这个问题

如果你不是有经验的React开发人员,请不要使用context,通常使用props和state来实现功能会更好

如果你不顾这些警告,坚持使用上下文,尝试将context的使用隔离在一小片区域,并且尽可能的不使用context,以便API改变时,应用更容易升级

context的使用

const PropTypes = require('prop-types');

class Button extends React.Component {
  render() {
    return (
      <button style={{background: this.context.color}}>
        {this.props.children}
      </button>
    );
  }
}

// 不定义contextTypes,context将为空
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 {
  // MessageList定义getChildContext
  getChildContext() {
    return {color: "purple"};
  }

  render() {
    const children = this.props.messages.map((message) =>
      <Message text={message.text} />
    );
    return <div>{children}</div>;
  }
}

// MessageList定义childContextTypes
MessageList.childContextTypes = {
  color: PropTypes.string
};

1:提供context的组件需要定义childContextTypesgetChildContext
2:context提供者的所有子组件,通过定义contextTypes都可以获取context(未定义contextTypes,context为一个空对象)

React父子组件耦合

通过context可以创建父子组件通信的API


class Menu extends React.Component {
  getChildTypes() {
    return {
      onItemClick: this.onItemClick
    }
  }
  onItemClick(val) {
    console.log(this)
    alert(val)
  }
  render() {
    return (
      <ul>{this.props.children}</ul>
    )
  }
}

Menu.childContextTypes = {
  onItemClick: React.PropTypes.func
}

class MenuItem extends React.Component {
  render() {
    return (
      <li onClick={() => this.context.onItemClick(this.props.children)}>
        {this.props.children}
      </li>
    )
  }
}

MenuItem.contextTypes = {
  onItemClick: React.PropTypes.func
}

class App extends React.Component {
    render() {
        return (
          <Menu>
            <MenuItem>aubergine</MenuItem>
            <MenuItem>butternut squash</MenuItem>
            <MenuItem>clementine</MenuItem>
          </Menu>
       )
    }
}

Menu创建了一个API:onItemClick,并且传递给子组件。

生命周期函数中的context

如果在组件中定义了contextTypes,那么它的生命周期将会收到一个额外的参数:context

<li>constructor(props, context)

<li>componentWillReceiveProps(nextProps, nextContext)

<li>shouldComponentUpdate(nextProps, nextState, nextContext)

<li>componentWillUpdate(nextProps, nextState, nextContext)

<li>componentDidUpdate(prevProps, prevState, prevContext)

没有state的函数组件引用Context

在没有state的函数组件中,也可以使用context

const PropTypes = require('prop-types');

const Button = ({children}, context) =>
  <button style={{background: context.color}}>
    {children}
  </button>;

Button.contextTypes = {color: PropTypes.string};

Context的修改

React有修改context的API,但是不要使用

当state或者props变化时,就会调用getChildContext方法,然后调用this.setState修改context中的数据,子组件会获取到一个新生成的context

问题是,如果context的值改变了,如果父组件的shouldComponentUpdate返回false,则使用该值的后代不会更新,修改context回事组件完全失控,This blog post解释了为什么避开这个问题

22 Web Components

React和Web Components是为了解决不同的问题而创建,Web组件提供了高度封装的、可复用的组件,而React提供了一个保持数据和DOM同步的声明库,它们相互互补,你可以在React中使用Web组件,也可以在Web组件中使用React

  1. 在React中使用Web Component,如
class HelloMessage extends React.Component {
  render() {
    return <div>Hello <x-search>{this.props.name}</x-search>!</div>;
  }
}
  1. 在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});

转载地址:https://facebook.github.io/react/docs/web-components.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,565评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,021评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,003评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,015评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,020评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,856评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,178评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,824评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,264评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,788评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,913评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,535评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,130评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,102评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,334评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,298评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,622评论 2 343