React的平凡之路,或许我们才刚刚上路

3,226 阅读15分钟

大家一起来

React是一个用于构建用户界面的JS库,核心专注于视图,目的实现组件化开发

所谓组件化开发,其实就像堆积木一样,每个组件都包含了自己的逻辑和样式,然后再组合到一起完成一个复杂的页面

组件化的主要特点就是:可组合、可复用、可维护

那么废话不多说,让我们直接进入今天的主题,运用官方推荐的脚手架来搭建一个react项目开始学习吧

create-react-app启动react项目

第一步:全局安装create-react-app脚手架

npm i create-react-app -g

第二步:创建react项目

create-react-app 项目名  
// 如:create-react-app react123

通过以上操作就会自动创建一个名为react123的react项目了,在创建的过程中,脚手架会自动为你安好react的核心包,react和react-dom,在此过程完成后即可进入第三步了

第三步:进入项目并启动服务

cd react123 && npm start

通过上面的三步曲,就会自动弹出浏览器访问一个localhost:3000(默认为3000端口)的react页面了

现在,让我们通过进入创建好的react项目中,先去看一下搭建好的结构是什么样的?

下面就为大家展示一番:

根据上图画圈所示,public目录下的index.html表示的是项目的主静态文件(包含依赖的节点),打开后发现一大堆内容,其实都可以删掉,只需要留一个root即可

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

说完public后,再说一下src目录,这里面的文件也都可以删掉(除了index.js主入口文件),这里就和webpack4里默认的入口文件比较类似了

先来了解下jsx

在src下的index.js中,我们就可以开启React之旅了,React主要用的是Facebook自己开发的jsx语法,这是一种js和xml的集合体,直接上代码

// 首先引入react的核心包
// 这里的命名必须叫React,如果不是的话会报错
import React from 'react';  
// 这里主要用到render方法,所以直接将其解构出来使用
import { render } from 'react-dom';   

// jsx语法通过babel来转义,通过预设的babel-preset-react来进行转义
// 将jsx语法解析成js来使用
let ele = (
    <h1 className="big">
        hi, <span>baby</span>
    </h1>
);

我们可以将写好的jsx语法直接放到babel官网来解析一下

// 解析后的代码如下
React.createElement(
  "h1",
  { className: "big" },
  "hi, ",
  React.createElement(
    "span",
    null,
    "baby"
  )
);

通过React.createElement来创建一个虚拟DOM,其中包含三个参数,分别是type、props和children

  • type: 对应的标签h1
  • props: 对应的属性{ className: "big" }
  • children: 对应的子节点 -> 字符串'hi, ' 和一个新的React.createElement( "span", null, "baby" )

其实按照React.createElement创建的对象来看,打印出来后的结构是这样滴

根据上面打印的结构来看,其实它们都是react.element上的实例,那么就来针对打印后的对象进行一个主要的抽离,来看看是何方神圣吧

{
    type: 'h1', 
    props: {
        children: [
            'hi',
            {
                type: 'span',
                props: {
                    children: 'baby'
                }
            }
        ],
        className: 'big'
    }
}

在我们转化完对象后,就可以通过ReactDOM提供的render方法去渲染这样的一个对象,最后将其渲染到页面中

上面写好的ele代码块,其实就是React.createElement的语法糖,毕竟每次写那么一大坨React.createElement的话,十有八九会写出错的(我觉得可能是100%会出错,哈哈)

所以我们看到这里可以简单的梳理一下整个渲染的执行顺序

jsx语法 -> createElement格式 -> 转化成对象 -> 对象转换成真实dom - > render方法渲染

import React from 'react';  
import { render } from 'react-dom';   

// React.createElement的语法糖
let ele = (
    <h1 className="big">
        hi, <span>baby</span>
    </h1>
);
render(ele, window.root); // 将ele渲染到root上

下图是将虚拟dom渲染到root节点下的情况

上面写的代码不多,主要就是简单写了个jsx的语法,然后通过render进行了渲染而已。

在写ele的时候,我们发现和正常的html还是很相像的,但是我在这里要说一下, 其实它们不一样,接下来就看看有哪些地方不一样吧!

jsx和html写法的区别

  1. className 它会转化成 class
  2. htmlFor 它要转化成for属性 label for
  3. jsx元素可以嵌套
  4. 相邻的react元素,必须要加一层包裹起来
  5. jsx里面可以写js,{}里面可以写js语句
  6. 只支持多行注释,{/* ... */},不过很少写注释
  7. style标签必须写成一个对象,如:{ background: 'skyblue' }

说了这几点区别,直接开撸,没有代码更让人直观的喜爱了,hurry up go go go

import React from 'react';
import { render } from 'react-dom';
// 提示:下面注释标记的数字对应上面区别的序号,序号5和6就不写了

let singer = '周杰伦';
let style = { backgroundColor: '#0cc', color: '#fff', fontSize: '14px' };
let album = '跨时代';
let arr = ['晴天', '阴天', '下雨天'];

let ele = (
    /* 4.相邻的react元素外层必须包起来(h1,label,input,p它们都是相邻的),
        vue是用一个template标签包起来,
        可以写一个div包起来,不过会产生一个多余的div标签
        react直接采用React.Fragment来包裹最佳
    */
    <React.Fragment>
        { /*1.渲染后会转成<h1 class="song">烟花易冷</h1>*/ }
        <h1 className="song">
            烟花易冷-
            {/* 3.jsx元素可以嵌套 */}
            <span>{singer}</span>
        </h1>
        {/* 2.htmlFor会转成<label for="inp"></label>
            点击label标签会自动获取到input焦点 */}
        <label htmlFor="inp">获得焦点</label>
        <input type="text" id="inp" />
        {/* 7.style必须是对象的形式,如style={{color: '#fff'}} */}
        <p style={style}>{album}</p>
        {arr.map((item, index) =>{ 
            return <li key={index}>{item}</li>
        })}
    </React.Fragment>
);

render(ele, window.root);

按照上面的代码可以直观的看到一些具体的区别,那么心急吃不了臭豆腐,赶紧开始继续往下写吧。就这点东西才不是react的两把刷子呢,下面有请react的组件闪亮登场!

组件

在react中组件分为两种,一种是函数组件,一种是类组件,那么如何区分是不是组件呢?这里有一个标准,首字母要大写

what?是的,没错,小写的就是jsx元素了,哈哈,不信你来看

函数组件

import React from 'react';
import { render } from 'react-dom';

// 此为函数组件
function Song(props) {
    return (
        <div className="wrap">
            <h1>晴天-{props.singer}</h1>
            <p>为你翘课的那一天,花落的那一天</p>
            <p>教室的哪一间,我怎么看不见</p>
        </div>
    );
}
// 组件可以通过属性传递数据
// render方法不会一直渲染,只会渲染一次
render(<Song singer="周杰伦" />, window.root)

当然有些人可能不信,如果我把Song写成song,然后再渲染,能有什么问题呢?

其实没有问题,那是不可能的,因为人家react了解你,会给你一个报错告诉你是不是想使用一个react组件进行渲染,那就把首字母大写

官方说的,童叟无欺了,哈哈

继续回到函数组件的探讨中,函数组件是个无this,无生命周期,无状态的三无产品。所以可想而知使用频率也是不如类组件的。

举个栗子:

import React from 'react';
import { render } from 'react-dom';
// 函数组件Clock
function Clock(props) {
    return <p>{props.date}</p>
}

setInterval(() => {
    render(<Clock date={new Date().toLocaleString()} />, window.root);
}, 1000);

可以实时的更新时间,不过由于函数组件没有状态,而且只会渲染一次,所以还要在外层加一个setInterval定时器,就有点不伦不类了

更重要的是,状态的更改都是通过属性由我们传递过去的,没有自己的状态,刷新的时候都依赖传递,这样很不好,组件的特点就是复用

所以我们就要隆重的请出组件中的巨无霸,类组件闪亮登场

类组件

还是上面的栗子,我们改成类组件的形式

// 由于类组件需要继承react的Component,所以直接解构出来使用
import React, { Component } from 'react';
import { render } from 'react-dom';

class Clock extends Component { // 继承Component
    constructor() {
        super();    // 继承后可以使用setState方法
        // 设置组件的默认状态
        this.state = { date: new Date().toLocaleString(), pos: '北京' }
    }
    // 需要提供一个render方法返回dom对象
    render() {
        return (
            <React.Fragment>
                <p>当前时间:{this.state.date}</p>
                <p>坐标:
                    <span style={{color: 'skyblue'}}>{this.state.pos}</span>
                </p>
            </React.Fragment>
        );
    }
    // 一不小心就展示了目前的第一个生命周期了
    // componentDidMount生命周期
    // 当组件渲染完成后调用此生命周期
    componentDidMount() {
        setInterval(() => {
            // 重新设置date的状态值,
            // this.setState可以更改状态刷新页面
            this.setState({date: new Date().toLocaleString()});
        }, 1000);
    }
}

render(<Clock />, window.root);

上面通过类组件完成了同样的效果,接下来我们好像还少些什么?事件?没错,我们写js怎么能不添加事件呢,下面就来说一下如何添加事件

接着上面的代码,我们继续写,再敲一遍,不怕累,熟能生巧,是胜利

import React, { Component } from 'react';
import ReactDOM, { render } from 'react-dom';

class Clock extends Component {
    constructor() {
        super();
        this.state = { date: new Date().toLocaleString(), pos: '北京' };
        // 强制绑定this
        this.handleClick = this.handleClick.bind(this);
    }
    /* 绑定方法有几种方式 方法中可能会用到this
        1.箭头函数 (不提倡,会产生个新函数,楼下2层也是)
        2.bind绑定 this.handleClick.bind(this) 
        3.在构造函数中绑定this (官方推荐)
        4.ES7语法可以解决this指向 handleClick = () => {} (更推荐,哈哈)
    */
    handleClick = () => {
        console.log(this) // 直接绑给一个函数的话,此时的this是undefined
        
        // 在方法里,我们来移除一下这个组件
        // 移除组件我们就用到了ReactDOM的方法
        ReactDOM.unmountComponentAtNode(window.root);
    }
    render() {
        // 这里绑定个click事件,用的是驼峰写法onClick
        // 后面要跟js语法,onClick={}
        return (<React.Fragment>
            <p onClick={this.handleClick}>当前时间:{this.state.date}</p>
            <p>坐标:
                <span style={{color: 'skyblue'}}>{this.state.pos}</span>
            </p>
        </React.Fragment>)
    }
    componentDidMount() {
        this.timer = setInterval(() => {
            this.setState({ date: new Date().toLocaleString() });
        }, 1000);
    }
    componentWillUnmount() {
        clearInterval(this.timer);
    }
}

render(<Clock />, window.root);

执行以上代码后,点击当前时间的p标签后,就会在root节点下移除掉该组件(dom)了,不过可能大家会忽略掉一些常见的问题,下面我就来解释一下

  • 当点击移除组件后,定时器仍在执行,setState还在更新着date的状态,所以会有报错的
  • 下面就着重解释一下这个问题
class Clock extends Component {
    // 主要看这里(气质)
    componentDidMount() {
        // 和以往清除定时器的做法一样,我们直接将timer挂到this实例上
        this.timer = setInterval(() => {
            this.setState({date: new Date().toLocaleString()})
        });
    }
    // 这个生命周期是组件将要被卸载的时候调用
    componentWillUnmount() {
        // 一般卸载组件后要移除定时器和绑定的方法
        clearInterval(this.timer);  
    }
}

好了,上面的报错问题就这样迎刃而解了。不过,说的总比唱的好听,为什么要将timer放到this上?放到this.state里的timer状态不行吗?

放到this.state上的话说明,当前页面要依赖这个状态,状态变了视图可以刷新,setState是可以更新页面的

然而将timer直接放到当前实例上(this),即时删掉(清空)了,也不影响页面

组件讲了这么多,让我们做个小结吧

  • 组件有两个数据源
    • 一个是属性 外界传递的
    • 一个是状态 自己拥有的

小结是完毕了,那就喝杯水继续往下看吧,我们发现react的状态很有分量啊,state状态改变后可以刷新视图改变,屌不屌!吊炸天啊简直,那么我们就单独针对这个state来说上一说吧

state状态

苍白的文字,不如真实的代码看的带劲,上代码

// 这是一个很常见很常见的栗子,计数器

import React, { Component } from 'react';
import { render } from 'react-dom';

class Counter extends Component {
    constructor() {
        super();
        this.state = { count: 1 };
    }
    handleClick = () => {
        // 写到这里就可以实现点击一次button就加1一次了
        // this.setState({ count: this.state.count + 1 });
        
        // this.setState也可以接受一个函数,它的第一个参数就是上一次的状态
        // 
        this.setState(prevState => ({ count: prevState.count + 1 }));
        this.setState(prevState => ({ count: prevState.count + 1 }));
        this.setState(prevState => ({ count: prevState.count + 1 }));
    }
    render() {
        return (
            <div>
                <span>计数器: </span>
                {this.state.count}
                <button onClick={this.handleClick}>点我</button>
            </div>
        );
    }
}

render(<Counter></Counter>, window.root);

state有个特点:多个状态可以批量更新?就是说如果我上面写了三个this.setState去改变count,那按常理来说,状态就应该改变三次,点一次button就加3了

其实不然,react不会这么做,如果状态改变一次,就刷新一次页面的话,那就疯狂了,react是把状态(count)先记住,最后再一起刷新页面

于是乎就用上面重新设置的setState来保留count的状态,最后再一次性的改变。说白了就是如果更新时下一次的状态依赖于上一次的状态,就得写成函数的形式

好了,下面再来说说组件间的通信吧,先来个父子组件直接的通信

组件间的通信

组件间的通信,父子组件之间就是通过属性传递的,父 -> 子 -> 孙子

单向数据流,数据方向是单向的,子不能改父的属性 下面来看个小demo

父组件 songs.js


import React, { Component } from 'react';
import { render } from 'react-dom';
import axios from 'axios';  // axios用于请求
import List from './list';  // 子组件
import Audio from './audio';  // 子组件

import './css/songs.css';

class Songs extends Component {
    constructor() {
        super();
        this.state = { songs: [], mp3Url: '', isPlay: false };
    }
    // 子传父通信 -> 父提供一个方法,子调用后将参数回传
    // chooseSong为选择歌曲地址填入audio的src的方法
    chooseSong = (url, isPlay) => { 
        // 这里子组件回传了属性更改了isPlay的状态
        // 对应修改了List的同级组件Audio接收的played属性值
        this.setState({mp3Url: url, isPlay});
    }
    render() {
        return (
            <div className="songs-box">
                <ul>
                    {this.state.songs.map((item, index) => (
                        /* 
                            通过传递属性的方式进行通信
                            这里将item解构出来,然后List子组件按照需要去拿数据
                        */ 
                        <List key={index} {...item} choose={this.chooseSong}></List>
                    ))}
                </ul>
                <Audio url={this.state.mp3Url} played={this.state.isPlay}></Audio>
            </div>
        );
    }
    async componentDidMount() {
        // 在react中发送ajax请求 现在我们用axios
        // axios封装了RESTFul 基于promise的 不支持jsonp 可用在服务端
        let { data } = await axios.get('http://musicapi.leanapp.cn/search?keywords=林俊杰');
        this.setState({ songs: data.result.songs });
    }
}

render(<Songs></Songs>, window.root);

未播放效果

播放效果
剩余的代码我就不一一展示了,仅仅是个简单的小demo,功能很多都未完善,有兴趣的同学可以去研究一下继续写写吧,贴个地址吧

通过上面的小demo,其实可以发现组件间的通信主要有三种方式

  • 第一种方式是通过属性传递,父 -> 子 -> 孙子 (父传子)
    • 单向数据流,数据方向是单向的,子不能改父的属性
  • 第二种方式是父写好了一个方法,传递给儿子 (子传父)
    • 儿子调用这个方法,在这个方法中可以去更改状态
  • 第三种方式是同级组件传递
    • 同级组件想要传递数据,可以找到共同的父级,没有父级就创造父级

受控组件和非受控组件

接下来了解一下受控组件与非受控组件的两个概念,他们指的是表单元素

既然是涉及到了表单元素,那我们期待的双向数据绑定应该也是该登场的了

双向数据绑定

import React, { Component } from 'react';
import { render } from 'react-dom';

class Input extends Component{
    constructor(){
        super();
        this.state = {val: '你好'};
    }
    // 通过onChange事件调用该方法后,每次更改输入后val的状态值
    handleChange = (e) =>{ //e是事件源
        let val = e.target.value;
        this.setState({val});
    };
    render(){
        return (<div>
            <input type="text" value={this.state.val} onChange={this.handleChange}/>
            {this.state.val}
        </div>)
    }
}
render(<Input />, window.root);

双向数据绑定主要通过的是监听onChange事件,然后每次都将新输入的value值更改对应val的状态,从而再赋给input的value属性,做到了数据双向传递的实现,果然很高级,哈哈

受控组件

受状态控制的组件,必须要有onChange方法,否则不能使用

受控组件可以赋予默认值

import React, { Component } from 'react';
import { render } from 'react-dom';

class App extends Component{
    constructor(){
        super();
        this.state = {a: '破风', b: '激战'};
    }
    // name表示的就是当前状态改的是哪一个
    // e表示的是事件源
    handleChange = (e) => { //处理多个输入框的值映射到状态的方法
        let name = e.target.name;
        this.setState({ [name]: e.target.value });
    }
    render(){
        return (
            <form>
                 <input type="text"
                    required={true}
                    value={this.state.a}
                    onChange={this.handleChange}
                    name="a"
                />
                <input type="text"
                    required={true}
                    value={this.state.b}
                    onChange={this.handleChange}
                    name="b"
                />
                <input type="submit" />
                <p>{this.state.a}</p>
                <p>{this.state.b}</p>
            </form>
        )
    }
}

render(<App></App>, window.root);

最后再来看一下非受控组件

非受控组件

所谓非受控组件有三个特点:

  1. 可以操作dom,获取真实dom
  2. 可以和第三方库结合
  3. 不需要对当前输入的内容进行校验,也不需要默认值
import React, { Component } from 'react';
import { render } from 'react-dom';

// 1.函数的方式 ref
// 2.React.createRef()  v16.3+
class App extends Component {
    constructor() {
        super();
        this.aaa = React.createRef();
    }
    componentDidMount() {
        // this.aaa.focus();    // 对应1
        this.aaa.current.focus(); // 对应2
    }
    render() {
        return (<div>
            {/* 1.<input type="text" ref={input=>this.aaa = input} />*/}
            {/* 2.会自动的将当前输入框 放在this.aaa.current */}
            <input type="text" ref={this.aaa} />
        </div>)
    }
}
render(<App />, window.root);

上面的栗子呢,就是一进入页面后就会自动获取输入框的焦点了,之后大家可以试着敲敲看吧

时间不早,节目刚好

虽然react还有很多的内容要讲,而且还没有涉及到生命周期等内容

不过,大家放心,该来的总会来,一个都不能少,在之后的文章里,我会继续学习和分享的

那么今天就到这里了,谢谢大家不辞辛劳的观看,各位,安啦!!!