美文网首页
React高级教程

React高级教程

作者: 我是上帝可爱多 | 来源:发表于2017-07-23 20:34 被阅读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应用中任何数据的改变都应遵从 单一数据源 原则。
    通常情况下,状态应被优先加入到渲染时依赖该值的组件中。但当有其他组件也依赖于该状态时,我们可以将其提升至组件们的最近公共祖先组件中,而不是尝试同步不同组件间的状态。我们应该遵守数据流的从上至下的原则。

    相关文章

      网友评论

          本文标题:React高级教程

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