React高级教程

2017-07-23  本文已影响51人  我是上帝可爱多

其实我的技术栈并不是react,我只是一个从3个月java经验的后台转到前台的新手,差不多一年时间我也看了很多,但是我的方向比较杂,什么都会一点,现在主要研究的是angular2,但是我是去年就接触过react的,所以今天想写点什么。。

在编写react组件时,我们需要处理web事件响应,我们会采用以下几种方式:

1.使用箭头函数

class MyComponent extends React.Component {

  render() {
    return (
      <button onClick={(event)=>{console.log(event.target.nodeName + ' clicked');}}>
          Click
      </button>
    );
  }
}

对于这种响应事件比较简单的可以写在标签内,但是逻辑本身比较复杂,就会导致render函数显得比较臃肿,看不出组件内容结构。

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {number: 0};
  }

  handleClick() {
      this.setState({
      number: ++this.state.number
    });
  }

  render() {
    return (
      <div>
          <div>{this.state.number}</div>
        <button onClick={()=>{this.handleClick();}}>
          Click
        </button>
      </div>
    );
  }
}

这种方式最大的问题是,当组件的层级越低时,性能开销就越大,因为任何一个上层组件的变化都可能会触发这个组件的render方法。这种方式也有一个好处,就是不需要考虑this的指向问题,因为这种写法保证箭头函数中的this指向的总是当前组件。

2.使用组件方法

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {number: 0,count:0};
    // 想想不要这段代码会怎样
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    let count = this.state.count
    +new Date()%2==0 && count++
    //这里的this并不是只想当前这个类的对象
    this.setState({
      number: this.state.number += count
    });
  }

  render() {
    return (
      <div>
          <div>{this.state.number}</div>
        <button onClick={this.handleClick}>
          Click
        </button>
      </div>
    );
  }
}

有没有发现这回我们在onclick里面直接写的就是函数了,因为ES6 语法的缘故,ES6 的 Class 构造出来的对象上的方法默认不绑定到 this 上,需要我们手动绑定。

有没有能解决上面麻烦写法的方式呢?

3.属性初始化语法(property initializer syntax)

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {number: 0,count:0,
    style:{background:yellow}
  };
  }

  //此时再也不用bind来做任何函数绑定了
  handleClick = () => {
    let count = this.state.count
    let background;
    +new Date()%2==0 ? count++ && background = 'green' : background = 'yellow'
    //这里的this并不是只想当前这个类的对象
    this.setState({
      number: this.state.number += count
    });
  }

  render() {
    return (
      <div>
          <div style={this.state.style}>{this.state.number}</div>
        <button onClick={this.handleClick}>
          Click
        </button>
      </div>
    );
  }
}

请原谅我上面代码装逼了,我在div的style里面绑定了state里面对应的style,每次点击根据当前时间戳作出反应,改变背景颜色。需要注意的是如果你是使用官方脚手架Create React App 创建的应用,那么这个特性是默认支持的.

所以在使用回调函数传参时,我们可以这样写

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      list: [1,2,3,4],
      current: 1
    };
  }

  handleClick = (item) =>  {
    this.setState({
      current: item
    });
  }

  render() {
    return (
      <ul>
          {this.state.list.map(
            (item)=>(
            <li className={this.state.current === item ? 'current':''} 
            onClick={this.handleClick(item)}>{item}
            </li>
            )
          )}
      </ul>
    );
  }
}

下面我们来探讨以下react的高阶组件如何书写,对于有一定函数式编程经验的人来说好理解些。高阶组件的定义是类比于高阶函数的定义。高阶函数接收函数作为参数,并且返回值也是一个函数。类似的,高阶组件接收React组件作为参数,并且返回一个新的React组件。

假设我有2个组件需要从localstorage里面取出数据出来渲染。

import React, { Component } from 'react'

class MyComponent1 extends Component {

  componentWillMount() {
      let data = localStorage.getItem('data');
      this.setState({data});
  }

  render() {
    return (
    <ul>
    this.state.data.map( (item) => (<li> item </li>))
    </ul>
  )
  }
}

class MyComponent2 extends Component {
   getInitialState(){
       return {'data':localStorage.getItem('data');}
  }
   render() {
    return (
    <div>
         this.state.data   //这里的props.data和上面是一样的,从localstorage里取出来的
    </div>
  )
  }
}


设想一下,如果很多组件都需要从localstorage连去除这样的数据,那我们不得写死。。。所以高阶组件就在这个时候派上用场。

import React, { Component } from 'react'

function withPersistentData(WrappedComponent) {
  return class extends Component {
    getDefaultprops(){
         return {
             'name':'sumail'
       }
    }
    componentWillMount() {
      let data = localStorage.getItem('data');
        this.setState({data});
    }

    render() {
      // 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data} i am {this.props.name}</div>
  }
}

const MyComponentWithPersistentData = withPersistentData(MyComponent2)
export default MyComponentWithPersistentData

withPersisitentData函数接受一个react组件作为参数,返回另外一个组件,这个返回的组件相当于时参数组件的父组件。所以任何需要从localstorage中取出data属性的组件都可以通过这个方法加工一下。

考虑如下代码,这样写会有什么问题。

import React, { Component } from 'react'

class person extends Component{
      constructor(props){
           super(props)
           this.setState({'title':'person detail'})
      }
      
      render(){
        let create = (items) => {
           return class extends Component{
                  componentWillMount() {
                     let data = items.map( item => item.age > 18 ? Object.assign(item,{'type':'adult'}):
                     Object.assign(item,{'type':'child'}))
                     this.setState({data});
          }     
           render(){
                 return (
                   <ul>
                      {data.map( item => 
                    (<li>I am {item.name} ,a {item.type} </li>)
                    )
                   }
                   </ul>
      )
     } 
  }
   }
  //create方法结尾

    return <div>
                   <h1> here is list of {this.state.title}</h1>
                  {create(this.props.details)}
           <div>
}
}

我们上面同样写了一个高阶组件,不过不同的是这回我把它写在了render函数里面这样会有什么问题呢?

注意事项

不要在组件的render方法中使用高阶组件,尽量也不要在组件的其他生命周期方法中使用高阶组件。因为高阶组件每次都会返回一个新的组件,在render中使用会导致每次渲染出来的组件都不相等(===),于是每次render,组件都会卸载(unmount),然后重新挂载(mount),既影响了效率,又丢失了组件及其子组件的状态。高阶组件最适合使用的地方是在组件定义的外部,这样就不会受到组件生命周期的影响了。

下面呈上一个高阶函数的经典例子

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const fn1 = s => s.toLowerCase();
const fn2 = s => s.split('').reverse().join('');
const fn3 = s => s + '!'

const newFunc = pipe(fn1, fn2, fn3);
const result = newFunc('Time'); // emit!

其实高阶组件还支持装饰器的写法,下面我们看个例子。

import React, { Component } from 'react';

const simpleHoc = WrappedComponent => {
  console.log('simpleHoc');
  return class extends Component {
    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}
export default simpleHoc;

如何使用装饰器呢?

import React, { Component } from 'react';
import simpleHoc from './simple-hoc';

@simpleHoc
export default class Usual extends Component {
  render() {
    return (
      <div>
        Usual
      </div>
    )
  }
}

学过python有没有觉得很赞。。。

refs获取组件实例

当我们包装Usual的时候,想获取到它的实例怎么办,可以通过引用(ref),在Usual组件挂载的时候,会执行ref的回调函数,在hoc中取到组件的实例。通过打印,可以看到它的props, state,都是可以取到的。

import React, { Component } from 'react';

const refHoc = WrappedComponent => class extends Component {

  componentDidMount() {
    console.log(this.instanceComponent, 'instanceComponent');
  }

  render() {
    return (<WrappedComponent
      {...this.props}
      ref={instanceComponent => this.instanceComponent = instanceComponent}
    />);
  }
};
如果WrappedComponent是一个input输入框,我们可以调用this.instanceComponent.focus()使他聚焦

关于高阶组件我们以一个例子作为结束。

// 普通组件Login
import React, { Component } from 'react';
import formCreate from './form-create';

//如果用的比较熟,可以使用这种方式来写
@formCreate
export default class Login extends Component {
  render() {
    return (
      <div>
        <div>
          <label id="username">
            账户
          </label>
          <input name="username" {...this.props.getField('username')}/>
        </div>
        <div>
          <label id="password">
            密码
          </label>
          <input name="password" {...this.props.getField('password')}/>
        </div>
        <div onClick={this.props.handleSubmit}>提交</div>
        <div>other content</div>
      </div>
    )
}
}

看看formCreate里面是怎么写的


const formCreate = WrappedComponent => class extends Component {

  constructor() {
    super();
    this.state = {
      fields: {},
    }
  }
  onChange = key => e => {
    const { fields } = this.state;
    fields[key] = e.target.value;
    this.setState({
      fields,
    })
  }
  handleSubmit = () => {
    console.log(this.state.fields);
  }
  getField = fieldName => {
    return {
      onChange: this.onChange(fieldName),
    }
  }
  render() {
    const props = {
      ...this.props,
      handleSubmit: this.handleSubmit,
      getField: this.getField,
    }
    return (<WrappedComponent
      {...props}
    />);
  }
};
export default formCreate;

我们注意{...this.props.getField('username')}这种写法其实是在标签上加了onChange = function (){}
函数内容如下:
e => {
const { fields } = this.state;
fields[key] = e.target.value;
this.setState({
fields,
})
在表单元素每次作出change时都会有事件响应,都会改变state的值,在提交时,会打印出fields的值。

最后一个环节我们来讲一下:

状态提升

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}

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

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

  render() {
    const temperature = this.state.temperature;
    return (
      <fieldset>
        <legend>Enter temperature in Celsius:</legend>
        <input
          value={temperature}
          onChange={this.handleChange} />
        <BoilingVerdict
          celsius={parseFloat(temperature)} />
      </fieldset>
    );
}
}

上面是一个对温度作出检测的组件,判断水是否沸腾。
现在有了一个新需求。除了摄氏度的input外,还需要提供一个华氏度的input,且要求两者保持一致。

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};

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

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

  render() {
    const temperature = this.state.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

//现在可以将Calculator修改为渲染两个不同的温度输入框:
class Calculator extends React.Component {
  render() {
    return (
      <div>
        <TemperatureInput scale="c" />
        <TemperatureInput scale="f" />
      </div>
    );
  }
}

现在我们有两个输入框,但当向其中一个输入框输入数据时,另一个并不会随之改变。而我们的需求是两个输入框保持同步。
另外,在Calculator中也无法显示BoilingVerdict。这是因为当前温度的状态已经被隐藏至了TemperatureInput中,Calculator无法知道当前温度。

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

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

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

我们需要一个转换函数用于摄氏度和华氏度之间的转化。

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();
}
//这个函数接收2个参数,一个是温度,一个是条件(摄氏度,华氏度)

lass Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
    this.state = {temperature: '', scale: 'c'};
  }

  handleCelsiusChange(temperature) {
    this.setState({scale: 'c', temperature});
  }

  handleFahrenheitChange(temperature) {
    this.setState({scale: 'f', 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.handleCelsiusChange} />
        <TemperatureInput
          scale="f"
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange} />
        <BoilingVerdict
          celsius={parseFloat(celsius)} />
      </div>
    );
  }
}

这个状态改变的关键所在是作出了类型判断,2个温度组件个又一个对应的onchange处理程序,从而来改变父组件的state。

小结

React应用中任何数据的改变都应遵从 单一数据源 原则。
通常情况下,状态应被优先加入到渲染时依赖该值的组件中。但当有其他组件也依赖于该状态时,我们可以将其提升至组件们的最近公共祖先组件中,而不是尝试同步不同组件间的状态。我们应该遵守数据流的从上至下的原则。

上一篇下一篇

猜你喜欢

热点阅读