后端到前端react实战

1.背景

近期工作原因再次拾起react开发,已经将近两年没做过react的开发了,再次开始还是多少有很多需要重新熟悉的,但好在没间断过前端开发,所以还是比较快上手的,但是难点在于怎么样教会同组同事们学会react,快速上手。根据个人经验总结出必须要会的部分,梳理文档如下。

2.入门

按照个人经验推荐学习顺序如下
1.官网文档手册:https://react.docschina.org/
2.生命周期学习
3.概念学习
4.hooks学习

3.生命周期

基本学习一个框架,我们首先我们需要了解其生命周期,之前跟大家普及vue的生命周期,就是两年前使用react做业务,踩过很多关于生命周期的坑。我们后端学习spring都还要学习其生命周期,前端也是一样,能更好地控制接口调用时机,组件展示的时机等等。

简单理解React为其组件定义了生命周期的三个状态。针对这三个状态提供了7种钩子函数,方便使用者在不同状态之前或者之后设置一些事件监听或者逻辑处理。

Mounting:组件正在被插入到DOM节点中
Updating:组件正在被重新渲染,是否被更新取决于该组件是否有改变
Unmouting:组件正在从DOM节点中移出
针对以上三个状态,都分别提供了两种钩子函数,用于在进入这个状态之前(will函数),活着离开这个状态之后(did函数)调用,理解了上面的状态,就会非常容易明白函数名和函数的调用时机了。

3.1React 生命周期(旧)

image.png

react旧版生命周期包含三个过程:

1、挂载过程
constructor()
componentWillMount()
componentDidMount()

2、更新过程
componentWillReceiveProps(nextProps)
shouldComponentUpdate(nextProps,nextState)
componentWillUpdate (nextProps,nextState)
render()
componentDidUpdate(prevProps,prevState)

3、卸载过程
componentWillUnmount()

其具体作用分别为:
1、constructor()
完成了React数据的初始化。

2、componentWillMount()
组件已经完成初始化数据,但是还未渲染DOM时执行的逻辑,主要用于服务端渲染。

3、componentDidMount()
组件第一次渲染完成时执行的逻辑,此时DOM节点已经生成了。

4、componentWillReceiveProps(nextProps)
接收父组件新的props时,重新渲染组件执行的逻辑。

5、shouldComponentUpdate(nextProps, nextState)
在setState以后,state发生变化,组件会进入重新渲染的流程时执行的逻辑。在这个生命周期中return false可以阻止组件的更新,主要用于性能优化。

6、componentWillUpdate(nextProps, nextState)
shouldComponentUpdate返回true以后,组件进入重新渲染的流程时执行的逻辑。

7、render()
页面渲染执行的逻辑,render函数把jsx编译为函数并生成虚拟dom,然后通过其diff算法比较更新前后的新旧DOM树,并渲染更改后的节点。

8、componentDidUpdate(prevProps, prevState)
重新渲染后执行的逻辑。

9、componentWillUnmount()
组件的卸载前执行的逻辑,比如进行“清除组件中所有的setTimeout、setInterval等计时器”或“移除所有组件中的监听器removeEventListener”等操作。

3.2React生命周期(新)

image.png

react16.4后使用了新的生命周期,使用getDerivedStateFromProps代替了旧的componentWillReceiveProps及componentWillMount。使用getSnapshotBeforeUpdate代替了旧的componentWillUpdate。

使用getDerivedStateFromProps(nextProps, prevState)的原因:
旧的React中componentWillReceiveProps方法是用来判断前后两个 props 是否相同,如果不同,则将新的 props 更新到相应的 state 上去。在这个过程中我们实际上是可以访问到当前props的,这样我们可能会对this.props做一些奇奇怪怪的操作,很可能会破坏 state 数据的单一数据源,导致组件状态变得不可预测。

而在 getDerivedStateFromProps 中禁止了组件去访问 this.props,强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去访问this.props并做其他一些让组件自身状态变得更加不可预测的事情。

使用getSnapshotBeforeUpdate(prevProps, prevState)的原因:
在 React 开启异步渲染模式后,在执行函数时读到的 DOM 元素状态并不总是渲染时相同,这就导致在 componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。

而getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说在 getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与componentDidUpdate 中一致的。

4.概念

4.1JSX语法

使用JSX语法,可以定义简洁而且较为熟知的树状语法结构。其实它的基本语法规则也很简单:遇到HTML标签(<开头,并且第一个字母是小写,如<div>),就用HTML规则解析;遇到代码块(以{开口)就用JavaScript规则解析,遇到组件(<开头,并且第一个字母是大写,如<Comment>),就是我们的React组件的类名了,所以写组件类的时候,别忘了类名以大写字母开头。

 var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
          <h1>Comments</h1>
          <CommentList data={this.props.data} />
          <CommentForm />
      </div>
    );
  }
});

4.2Components

Component就是其实就是React的核心思想,它通过把代码封装成组件的形式,然后每调用一次就会通过React的工厂方法来生成这个组件类的实例,并且根据注入的props或者state的值来输出组件。

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        Hello, world! I am a CommentBox.
      </div>
    );
  }
});
ReactDOM.render(
  <CommentBox />,
  document.getElementById('content')
);

ReactDOM.render是React的最基本用法,用于将模版转为HTML语言,并插入指定的DOM节点中。下面两小节代码将展示如何将值传到组件中,组件如何获取。

4.3Props

通过Props属性,组件能够读取到从父组件传递过来的数据,然后通过这些标记渲染一些标记,所有在父组件中传过来的属性都可以通过this.props.propertyName来获取到,其中有一个特殊的属性this.props.children,通过它你可以获取到组件的所有子节点,如下例所示:

var data = [
        {author: "Pete Hunt", text: "This is one comment"},
        {author: "Jordan Walke", text: "This is *another* comment"}
      ];
      var Comment = React.createClass({
        render: function() {
          return (
            <div className="comment">
              <h2 className="commentAuthor">
                {this.props.author}
              </h2>
              {this.props.children}
            </div>
          );
        }
      });
      var CommentList = React.createClass({
        render: function() {
          return (
            <div className="commentList">
              <Comment author="Pete Hunt">This is one comment</Comment>
              <Comment author="Jordan Walke">This is *another* comment</Comment>
            </div>
          );
        }
      });
      var CommentBox = React.createClass({
        render: function() {
          return (
            <div className="commentBox">
                <h1>Comments</h1>
                <CommentList data={this.props.data} />
            </div>
          );
        }
      });
      ReactDOM.render(
        <CommentBox data={data} />,
        document.getElementById('content')
      );

image

绿色框是由Comment组件负责生成的,它被它的父组件CommentList(图中的蓝色框)调用了两次,所以根据props中获取的不同的数据实例化了两次。接着组件CommentList又被顶层组件CommentBox所包括(图中的红色框)。React就是通过这样的方式,将组件与组件之间的关系建立起来的,通过组合,可以做出各式各样的我们需要的页面。同时,由于这些模块化的组件,使得我们可以只关注传入组件和改变组件的数据,基本数据对了,组件对数据的渲染也就对了。同时我们也可以在后续的开发中,将一些通用的组件抽出来,代码结构清晰有调理。随着开发的不断深入和代码的不断累积,这种优势就会越来越明显。

4.4State

在上一节的例子中,在组件CommentList中传给Comment组件是写死的。我们知道,可以通过父组设置属性,然后子组件中通过props获取。但是如果子组件中的数据会不断地改变(或者通过定时器,或者通过回调,或者通过Ajax),子组件如何通过数据的变化来不断地重新渲染呢?答案是State。

state和props一样,都是用来描述组件的特性。只不过不同的是,对于props属性,组件只会在对象实例的时候渲染并返回render函数,而对state的设置则在组件的生命周期内都有效,只要setState了,组件就会重新渲染并返回render。换而言之,就是如果你在组件实例以后再对props进行更新,react并不能保证你的更新会反应到VDOM甚至DOM上,而setState就可以。所以我们一般将哪些定义了以后就不再改变的特性放在props中,而随着用户交互或者定时触发产生变化的一些特性,那放在state中将是更好的选择。
现在,我们添加一个可以供用户输入的两个输入框和一个按钮,让用户来输入自己的名字和评论内容,点击提交后页面将会显示他们的评论。

      //修改CommentList
      var CommentList = React.createClass({
        render: function() {
          var commentNodes = this.props.data.map(function (comment) {
            return (
              <Comment author={comment.author}>
                {comment.text}
              </Comment>
            );
          });
          return (
            <div className="commentList">
              {commentNodes}
            </div>
          );
        }
      });
      //创建CommentForm组件,用于用户输入提交
      var CommentForm = React.createClass({
        handleSubmit: function(e) {
          e.preventDefault();
          var author = this.refs.author.value.trim();
          var text = this.refs.text.value.trim();
          if (!text || !author) {
            return;
          }
          this.props.onCommentSubmit({author: author, text: text});
          this.refs.author.value = "";
          this.refs.text.value = "";
          alert("Submit!");
          return;
        },
        render: function() {
          return (
            <form className="commentForm" onSubmit={this.handleSubmit}>
              <input type="text" placeholder="Your name" ref="author" />
              <input type="text" placeholder="Say something..." ref="text" />
              <input type="submit" value="Post" />
            </form>
          );
        }
      });
      var data = [
        {author: "YYQ", text: "这是一条评论"},
        {author: "wuqke", text: "这是另外一条评论"}
      ];
      var Comment = React.createClass({
        render: function() {
          return (
            <div className="comment">
              <h3 className="commentAuthor">
                {this.props.author}说:
              </h3>
              <span>{this.props.children}</span>
            </div>
          );
        }
      });
      var CommentList = React.createClass({
        render: function() {
          var commentNodes = this.props.data.map(function (comment) {
            return (
              <Comment author={comment.author}>
                {comment.text}
              </Comment>
            );
          });
          return (
            <div className="commentList">
              {commentNodes}
            </div>
          );
        }
      });
      var CommentForm = React.createClass({
        handleSubmit: function(e) {
          e.preventDefault();
          var author = this.refs.author.value.trim();
          var text = this.refs.text.value.trim();
          if (!text || !author) {
            return;
          }
          this.props.onCommentSubmit({author: author, text: text});
          this.refs.author.value = "";
          this.refs.text.value = "";
          alert("Submit!");
          return;
        },
        render: function() {
          return (
            <form className="commentForm" onSubmit={this.handleSubmit}>
              <input type="text" placeholder="Your name" ref="author" />
              <input type="text" placeholder="Say something..." ref="text" />
              <input type="submit" value="Post" />
            </form>
          );
        }
      });
      var CommentBox = React.createClass({
        getInitialState: function() {
          return {data: []};
        },handleCommentSubmit: function(comment) {
          var ndata = this.state.data;
          ndata.push(comment);
          this.setState({data:ndata});
        },
        componentDidMount: function() {
          this.setState({data:this.props.data})
        },
        render: function() {
          return (
            <div className="commentBox">
              <h1>Comments</h1>
              <CommentList data={this.state.data} />
              <CommentForm  onCommentSubmit={this.handleCommentSubmit}/>
            </div>
          );
        }
      });
      ReactDOM.render(
        <CommentBox data={data} />,
        document.getElementById('content')
      );
      //修改CommentBox组件,当回调函数被触发的时候,将comment添加到data中并且setState更新data
      var CommentBox = React.createClass({
        getInitialState: function() {
          return {data: []};
        },handleCommentSubmit: function(comment) {
          var ndata = this.state.data;
          ndata.push(comment);
          this.setState({data:ndata});
        },
        componentDidMount: function() {
          this.setState({data:this.props.datas})
        },
        render: function() {
          return (
            <div className="commentBox">
              <h1>Comments</h1>
              <CommentList data={this.state.data} />
              <CommentForm  onCommentSubmit={this.handleCommentSubmit}/>
            </div>
          );
        }
      });

image

----------------------------------------

image

上面的例子,在CommentBox的render中添加了一个CommentForm组件,用于获取用户的输入,同时添加了一个函数handleCommentSubmit(comment),函数接收comment参数,做的事情很简单,就是和原来的数据data合并,并通过setState()更新数据data,该组件发现state变化以后,就会去重新渲染组件,最后在执行render函数,最终将变化反映在VDOM和DOM上。这让我们可以只关注数据的变化,而不必去考虑太多DOM节点是否被更新的问题。

其中最难理解的就是props和state。简单区别理解我总结如下:
1.props不可变, state可变会触发组件渲染并且state值更新是异步的,异步的,异步的!
因为:
state是组件自己管理数据,控制自己的状态,可变,多是在组件中创建,一般是在constructor中初始化state。每次setState都是异步更新的。
props是外部传入的数据参数,不可变,所有的react组件都必须像纯函数一样保护他们的props不被修改。
没有state的叫做无状态组件,有state的叫做有状态组件;
多用props,少用state。也就是多写无状态组件。

2.props一般用于父组件向子组件通信,在组件之间通信使用。state一般用于组件内部的状态维护,更新组建内部的数据,状态,更新子组件的props等。父组件的state常常转变子组件的props。

3.props是传递给组件的(类似于函数的形参),而state是在组件内部被组件自己管理的(类似于在一个函数内声明的变量)。

5.hook

Hook 是一些可以让你在函数组件里“钩子函数” React state 及生命周期等特性的函数。在系统没有调用该函数之前,钩子程序就先捕获该消息,钩子函数先得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,还可以强制结束消息的传递。简单来说,就是把系统的程序拉出来变成我们自己执行代码片段。
  要实现钩子函数,有两个步骤:
  1. 利用系统内部提供的接口,通过实现该接口,然后注入进系统(特定场景下使用)
  2.动态代理(使用所有场景)

常用的hooks有以下几种:

  • useState,useEffect,useRef,useContext
  • useSelector,useDispatch
  • 自定义hooks

5.1 使用 State Hook

官方文档

示例:点击按钮,使得按钮的文案由"点击前"变为“点击后”。

传统的写法:

import React, { Component } from "react";
 
export default class Button extends Component {
    constructor() {
        super();
        this.state = {
            buttonText: "点击前"
        };
    }
    handleClick() {
        this.setState({
            'buttonText': "点击后"
        });
    }
    render() {
        const { buttonText } = this.state;
        return <button onClick={() => this.handleClick()}>{buttonText}</button>;
    }
}

使用hooks的写法:函数组件不需要构造函数,可以通过调用 useState 来初始化 state

import React, { useState } from "react";
 
export default function Button() {
    const [buttonText,setButtonText] = useState("点击前");
    const handleClick = () => {
        setButtonText("点击后");
    }
    return <button onClick={handleClick}>{buttonText}</button>;
}

5.2.使用 Effect Hook

官方文档

useEffect 拥有两个参数,第一个参数作为回调函数会在浏览器布局和绘制完成后调用,因此它不会阻碍浏览器的渲染进程。
第二个参数是一个数组

  • 当数组存在并有值时,如果数组中的任何值发生更改,则每次渲染后都会触发回调。
  • 当它不存在时,每次渲染后都会触发回调。
  • 当它是一个空列表时,回调只会被触发一次,类似于 componentDidMount。

示例:新建一个定时器,每秒数量+1

传统的写法:

import React, { Component } from 'react';
class Count extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 1,
        }
    }
    componentDidMount() {
        // 新建一个定时器实现+1操作
        this.timer = setInterval(() => {
            this.setState({
                count: this.state.count - 0 + 1
            })
        }, 1000);
    }
    componentWillUnmount() {
        // 组件销毁时清除定时器,否则会造成内存泄漏
        clearInterval(this.timer)
    }
    render() {
        return <div>
            {this.state.count}
        </div>
    }
}
export default Count;

hooks的写法:

import React, { useState, useEffect } from 'react';
 
const Count = (props) => {
    const [count, setCount] = useState(0);
 
    useEffect(() => {
        let timer = setInterval(() => {
            setCount(count + 1)
        }, 1000);
        // 当在 useEffect 的回调函数中返回一个函数时,这个函数会在组件卸载前被调用
        return () => clearInterval(timer)
        // count发生变化时再次执行
    }, [count]);
 
    return <div>
        {count}
    </div>
}
export default Count;

5.3 使用 useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

示例:获取某个元素

传统写法:

import React, { Component } from 'react'
 
export default class Test extends Component {
    componentDidMount() {
        console.log(this.refs.box)
    }
    render() {
        return (
            <div ref="box">
                this is a box
            </div>
        )
    }
}

hooks写法:

import React, { useRef, useEffect } from "react";
 
export default function Test() {
    // 初始化一个useRef
    const box = useRef(null);
 
    useEffect(() => {
        // 通过.current获取
        console.log(box.current)
    }, [])
 
    return (
        <div ref={box}>this is a box</div>
    );
}

5.4 使用 自定义hook

官方文档
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

示例:封装一个组件,实现鼠标滑过显示鼠标所在位置

传统方式:定义一个高阶组件,使用时将组件作为参数传入高阶组件

/**
 * 定义一个高阶组件
 * 返回鼠标所在位置
 */
 
import React from 'react'
export default (Component) => {
    return class WrappedComponent extends React.Component {
        constructor(props) {
            super(props)
            this.state = {
                positionX: 0,
                positionY: 0
            }
        }
        componentDidMount() {
            document.addEventListener('mousemove', (e) => {
                this.setState({
                    positionX: e.clientX,
                    positionY: e.clientY
                })
            }) // 在这里我们更新鼠标的位置,并存储在state中去,然后通过props传递给被传入的组件
        }
        render() {
            return (
                <Component {...this.props} {...this.state} {...{ 'x': 1 }} />
                //props:这里返回的是WrappedComponent这个组件,所以本应该传递给Component组件的props,我们应该通过WrappedComponent传递下去
                //state: WrappedComponent可以操作自己的状态,我们可以将这些状态通过props的方式传递给Component组件
            )
        }
 
    }
}
/**
 * 使用时将组件作为参数传入高阶组件
 */
import React from 'react';
import mousePositionHoc from './hoc';
class MousePoint extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return (
            <div>
                <span>x:{this.props.positionX}</span>
                <span>y:{this.props.positionY}</span>
            </div>
        )
    }
}
export default mousePositionHoc(MousePoint)

hooks:自定义一个hooks,使用时直接引入。

/**
 * 自定义一个hooks
 * 返回鼠标所在位置
 */
import React, { useState, useEffect } from 'react'
 
const useMousePosition = () => {
    const [position, setPosition] = useState({ x: 0, y: 0 })
    useEffect(() => {
        const updateMouse = (e) => {
            setPosition({ x: e.clientX, y: e.clientY })
        }
        document.addEventListener('mousemove', updateMouse)
    }, [])
    return position
}
 
export default useMousePosition
 
 
//使用时引入自定义hooks
import React from 'react'
import useMousePosition from './hooks'
 
export default function App() {
    const position = useMousePosition()
    return (
        <div>
            <div>x: {position.x}</div>
            <div>y: {position.y}</div>
        </div>
    )
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容