新手零基础入门react(上)【近1w字 一文入门】

213 阅读22分钟

温馨提示:目前还没有更新ES6相关知识,所以文中几个传送门无效(url为空)。另外,建议结合示例代码理解,代码中做了详细注释,每一小节后有一个总结,核心内容用’’特殊显示(但手机上显示效果不好,建议手机切换深色模式或电脑阅读)。

简介与前置知识

首先介绍一下周边生态:React.js用于绑定事件、React Native用于开发app应用、React VR用于开发VR/全景应用,本文主要对react.js相关基础知识作对应笔记。

如何使用react.js?有两种方式,第一种方式是在script标签中来引入react,这种方式弊端是性能差;第二种方式是通过脚手架工具来编码。

什么是脚手架?脚手架编译出来的代码才可以在浏览器中运行。常用脚手架工具有grunt、gulp、webpack,其中,react官方提供的脚手架工具是Create-react-app。

使用Create-react-app,首先要安装node.js

创建react应用及安装所需依赖

安装好node.js之后,我们可以使用create-react-app脚手架工具来创建我们第一个react应用:

npx create-react-app my-app
cd my-app
npm start

注意:如果安装过程出现比如timeout network error,切换网络比如手机热点,即可解决问题。

若创建成功,localhost:3000即可查看。

打开桌面创建的my-app项目,发现缺少node_moldels文件夹。命令行切换至my-app目录,然后执行yarn install命令安装项目所需依赖。

注意:如果项目中没有yarn.lock文件,而是package.json文件,则执行npm install命令安装依赖。

项目结构解读及精简化初始项目代码

  • package.json文件中scripts属性定义一些命令start、build、test等。可以通过npm/yarn start命令启动项目,而npm build执行失败是因为npm start会自动转换为npm run start,所以要执行build命令,应该采用npm run build命令。

  • gitignore文件,如果项目代码想要上传到git仓库,而项目中某些文件不想上传,就可以将该文件写到gitignore文件中。

  • node_modules文件夹,包括项目所需依赖。

  • public目录下favicon.ico文件,控制浏览器网页标签小图标(可以搜索怎样生成favicon.ico文件,更改项目小图标);public目录下index.html中title控制浏览器网页标签名字;public目录下index.html文件中name="viewpoint"的meta标签,控制移动端屏幕显示,比如页面宽度是否自适应设备屏幕,是否允许用户缩放页面等;name="theme-color"的meta标签,设置网页主题颜色;rel="manifest"的link标签。上述name=viewpoint,name=theme-color,rel=mainfest标签,包括public目录下manifest.json文件可直接删除(在以下代码示例中用不到)。

  • src目录下index.js文件为入口文件。为什么index.js文件中可以导入React和ReactDom?因为项目中自动安装了React和ReactDom,见package.json文件中dependencies。为什么index.js文件中可以import './index.css'文件?因为底层webpack实现了。import App from './App'这里App即App.js文件,而不是App.css文件,因为默认先查找App.js文件。index.js文件中serviceWorker、serviceWorker.unregister()对应代码/代码,包括serviceWorker.js文件直接删除(在以下代码示例中同样用不到)。

  • App.js文件中import logo from './logo.svg'为页面中转动的react小图标,render()函数中return中img标签为使用导入的logo.svg小图标,导入代码import logo from './logo.svg'img标签,包括App.css文件、App.js文件中div中内容全部删除(以下代码示例同样用不到)。

  • 此外,src文件夹中除App.js和index.js文件外其他文件可以全部删除。

最后,App为自定义的一个组件,index.js是入口文件,public目录下html文件为div DOM所在位置,下面理一下三者之间逻辑:

App.js文件reader()中渲染的内容并不会挂载到页面上,需要在index.js文件中import App from './App',然后在index.js文件中ReactDOM.render(,document.getElementById('root')),才会渲染到页面中。可以理解为需将中生成的文本挂载到id名为root的标签上才可以渲染到页面上。

react中组件

关于import React,{ Component } from 'react';代码解读:

import React,{ Component } from 'react';

// 等价于:
import React from 'react';
import { Component } from 'react';
// 其中React是一个变量,react中所有值都放入React变量中;
// 而{ Component }是从react中取一部分

// 等价于:
import React from 'react';
const { Component } = React;
// 其中const { Component } = React 是ES6中的解构赋值

// 等价于
import React from 'react';
const Component = React.Component;

ES6中解构赋值相关知识传送门

为什么需要导入React?render()函数中return中类似html的标签是JSX语法,需要引入React才能正确在浏览器中渲染JSX语法的标签。

项目运行流程:public中index.html文件中有一个空的id名为root的div标签;src中App.js中定义App这个组件,然后export导出App组件;src中index.js文件import引入在App.js中定义的App组件,然后通过ReactDOM将App组件挂载到id名为root的div标签上。

react中如何渲染两个组件?

render()函数中不能同时写入两个标签,可以用一个div标签包裹两个或两个以上组件,如下:

// 一个组件
ReactDOM.render(<App />,document.getElementById('root'));

// 两个组件,假设在src新建一个Test.js的文件,Test.js中定义一个Test的组件,需将Test组件渲染到id名为root的div标签中
ReactDOM.render(<div><App /><Test /></div>,document.getElementById('root'));

JSX语法

react中挂载DOM与原生js挂载DOM区别:

// 原生js挂载DOM,需要用''包裹div标签
var root = document.getElementById('root');
root.append('<div>hello world</div>');

// react中挂载DOM,直接返回div标签即可
render(){
    return(){
        <div>
            hello world
        </div>
    }
}

JSX中两种标签,一种是普通的html标签,比如<div></div>;另一种是组件标签,比如<App />。注意组件标签首字母必须大写

class TodoList extends Component{

    constructor(props){
        super(props);
        
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleKeyUp = this.handleKeyUp.bind(this);
        
        this.state = {
            inputValue: 'hello world',
            list:['learn react','learn Component','learn react-dom']
        }
    }
   
   handleInputChange(e){
       console.log(e);
       this.setState({
           inputValue:e.target.value
       })
    }
   
   handleKeyUp(e){
       console.log(e.keyCode)
       // e.target.value !== '',空内容回车不会产生列表项
       if(e.keyCode ===13 && e.target.value !== ''){
           const list = [...this.state.list,this.state.inputValue];
           this.setState({
               // this.setState中,list:list 可简写为 list
               list,
               inputValue:''
           })
       }
   }
   
   handleItemClick(index){
       alert(index);
       const list = [...this.state.list];
       list.splice(index,1);
       this.setState({list});
   }
   
   getListItems(){
       return this.state.list.map((value,index) => {
           return (
               <li 
                   key={index} 
                   onClick={this.handleItemClick.bind(this,index)}
                   {/*
                   在这里若使用{value},则当在输入框输入如"<h1>标题</h1>"时,则不会转义
                   而使用dangerouslySetInnerHTML={__html:value}时,可以转义显示一个加粗的"标题"
                   */}
                   dangerouslySetInnerHTML={{__html:value}}
               >
               </li>
           )
       })
   }
   
   render(){
       return(
           <Fragment>
               {/*
                   使用label标签实现鼠标移动到“请输入内容”,光标聚焦到input输入框
                   label标签中使用for='myinput',input标签中使用id='myinput'
                   注意JSX语法中为避免上述的for与循环的for混淆,应该使用htmlFor
               */}
               <label htmlFor='myinput'>请输入内容:</label>
               <input 
                   id='myinput'
                   value={this.state.inputValue}
                   {/*
                       每次触发函数都会调用bind函数消耗性能可以将bind(this)写入到constructor构造函数中
                       onChange={this.handleInputChange.bind(this)}
                   */}
                   onChange={this.handleInputChange}
                   onKeyUp={this.handleKeyUp}
               />
               <ul>
                   {/* 
                       将循环生成li标签逻辑提取出来,放入getListItems函数中
                       JSXu语法中调用函数,this.getListItems如下:
                   */}
                   {this.getListItems}
               </ul>
           </Fragment>
   }
}

export default Todolist;

总结:

  1. JSX语法中,给标签添加class属性会与定义组件class Todolist中class相混淆,因此react中,给标签增加样式属性时,应该使用className而不是class。JSX中给标签添加样式属性:class -> className
  2. map遍历li标签逻辑提取出来,放入一个函数中。JSX语法中调用函数:{this.getListItem}
  3. reactDOM中触发事件函数时bind(this)可以提取到constructor中,可以提高性能(bind(this)底层原理,用户每次触发事件时,都会生成一个新的函数,而bind(this)写到constructor中,只会在创建组件时执行一次)。标签中this.handleInputChange.bind(this) -> 标签中this.handleInputChange与constructor中this.handleInputChange=this.handleInputChange.bind(this),事实上,将handleInputChange方法this强制绑定至TodoList。
  4. label标签实现input标签中光标聚焦,label标签中htmlFor='id名'。input标签中id='id名',注意在JSX中为了避免与循环的for混淆,这里将for改为htmlFor。
  5. dangerouslySetInnerHTML={{__html:value}},实现在input框中输入html标签,可以转义该标签内容。
  6. e.target.value,获取事件绑定标签的value值。事件目标元素除value属性外,还有如name、checked、type等属性。键盘码'e.keyCode',其中13为回车键键盘码
  7. const list = [...this.state.list,this.state.inputValue],'...'展开运算符与在展开运算符中添加元素,展开元素运算符常用于复制数组。
  8. splice(index,1),从索引index位置删除一个数组元素,并重新将数组拼接。map(),this.state.list.map((value,index) => { 执行函数或运算 })。
  9. 输入框内容变化'onChange',输入框键盘按键弹起'onKeyUp'。

ES6中数组方法传送门

react中数据驱动的设计思想与事件绑定

react是数据驱动框架,不直接操作DOM,而是操作数据。

那么render函数中标签如何接受构造函数constructor中state数据?

class TodoList extends Component{
    // constructor是类的构造函数,props、super(props)、this.state
    constructor(props){
        super(props);
        this.state = {
            inputValue: 'hello world',
            list:[]
        }
    }
    
   render(){
       return(
           <Fragment>
               {/* 如何在 input 输入框中接受构造函数中的 state 数据?
                   原生 JS 中:
                   <input value=this.state.inputValue />
                   而在react中需加{}包裹,如下:
               */}
        
               <input value={this.state.inputValue}/>
               <ul>
                   <li>learn React</li>
                   <li>learn Component</li>
               </ul>
           </Fragment>
       );
   }
}

export default Todolist;

如何在render函数标签中修改构造函数constructor中state数据?

class Todolist extends Component{

    constructor(props){
        super(props);
        this.state = {
            inputValue: 'hello world',
            list:[]
        }
    }
   
   // e事件对象 执行handleInputChange触发
   handleInputChange(e){
       console.log(e);
       // console.log(e.target.value);
       
       // 执行下面这行代码出现'state' undefined报错,具体见下面看如何解决该报错的
       // this.state.inputValue = e.target.value;
       
       // 分析:1.'state' undefined报错说明state找不到,打印state
       // console.log(this.state);
       // 打印this.state还是出现'state' undefined,说明是this的问题
       // 2.打印this,没有报错,控制台输出undefined
       // 说明this指向的不是当前组件,而是undefined 
       // console.log(this);
       
       //  解决:让this.handleInputChange中this绑定组件中this
       // onChange={this.handleInputChange}改为
       // onChange={this.handleInputChange.bind(this)}
       
       // react中改变构造函数中state不能直接this.state修改,需要使用setState函数
       // 错误修改:this.state.inputValue = e.target.value;
       this.setState({
           inputValue:e.target.value
       })
    }
   
   render(){
       return(
           <Fragment>
               <input 
                   value={this.state.inputValue}
                   {/*
                   原生js中为input绑定事件onchange而在react中需要驼峰状onChange
                   */}
                   onChange={this.handleInputChange.bind(this)}
               />
               <ul>
                   <li>learn React</li>
                   <li>learn Component</li>
               </ul>
           </Fragment>
       );
   }
}

export default Todolist;

总结:

  1. 组件中事件函数从constructor取数据用this.state,而事件函数修改constructor中数据使用this.setState({}),而不是直接在事件函数中this.state.value=e.target.value。
  2. JSX语法中,属性等于js表达式或者js变量时,需要给js表达式或js变量嵌套{}。
  3. 事件函数中this默认指向的是undefined值,若想将事件函数中this指向组件,则需要bind(this)绑定
  4. react中事件函数继承定义组件类的属性,子组件接受来自父组件的数据。对于事件函数中出现错误,比如this.state.inputValue中state出现'undefined'报错,应该考虑的是"逐层向上"确定错误位置(见上述19-28行代码分析);而对于constructor中出现错误,可以考虑的是来自外部或父组件的数据是否正确引用和接受。基于此,对于继承性的编程语言可以得出的一个debug方法论是:"由里向外 由子向父"查询错误源

react与原生js写代码区别,react中考虑数据的操作,而原生js代码考虑的是DOM中的操作。可以理解为render中普通html标签/组件标签构造组件结构,constructor中存放标签中及事件函数中需要用到的数据,事件函数中执行数据变换逻辑和更改数据。

this指向问题传送门

list列表新增删除功能

用map循环生成li标签,列表项数据放在state.list数组中:

class Todolist extends Component{
    constructor(props){
        super(props);
        this.state = {
            inputValue: 'hello world',
            list:['learn React','learn Component','learn react-dom']
        }
    }
    
   render(){
       return(
           <Fragment>
               <input value={this.state.inputValue}/>
               <ul>
                   {
                       {/*
                           在ul标签中写js循环表达式必须用{}包裹
                           es6中数组循环map,map接受value和index两个参数
                           map语法要求每次循环要return一个结果,因此应该是return <li></li>,而不是直接写<li></li>
                       */}
                       this.state.list.map((value,index) =>{
                           {/*
                               下面这样写会出现each child in array or iterator should have a unique "key" prop警告
                               return <li>{value}</li>
                               做如下修改,注意index值每次循环是不唯一的,因此可以key={index}
                           */}
                           return <li key={index}>{value}</li>
                       })
                   }
               </ul>
           </Fragment>
       );
   }
}

export default Todolist;

新增列表项,为input标签绑定键盘弹起事件onKeyUp:

class Todolist extends Component{

    constructor(props){
        super(props);
        this.state = {
            inputValue: 'hello world',
            list:['learn react','learn Component','learn react-dom']
        }
    }

   handleInputChange(e){
       console.log(e);
       this.setState({
           inputValue:e.target.value
       })
    }
   
   handleKeyUp(e){
       // keyCode键盘码,回车的键盘码是13
       console.log(e.keyCode)
       if(e.keyCode ===13){
           // ...es6中中数组展开运算符,实际上相当于将list数组中内容全部copy
           // 下面这行代码等价于 const list = ['learn react','learn Component','learn react-dom'];
           // const list = [...this.state.list];
           // 追加新元素
           const list = [...this.state.list,this.state.inputValue];
           // 更新state中list数组
           this.setState({
               // list:list
               // es6语法中如果对象键和值变量名字相同,则只需要写一个就行,等价如下:
               list,
               // 更新list同时,将inputValue中内容设置为空
               inputValue:''
           })
       }
   }
   render(){
       return(
           <Fragment>
               <input 
                   value={this.state.inputValue}
                   onChange={this.handleInputChange.bind(this)}
                   onKeyUp={this.handleKeyUp.bind(this)}
               />
               <ul>
                   {
                       this.state.list.map((value,index) => {
                           return <li key={index}>{value}</li>
                       })
                   }
               </ul>
           </Fragment>
       );
   }
}

export default Todolist;

点击列表项实现删除功能:

class Todolist extends Component{

    constructor(props){
        super(props);
        this.state = {
            inputValue: 'hello world',
            list:['learn react','learn Component','learn react-dom']
        }
    }

   handleInputChange(e){
       console.log(e);
       this.setState({
           inputValue:e.target.value
       })
    }
   
   handleKeyUp(e){
       console.log(e.keyCode)
       if(e.keyCode ===13){
           const list = [...this.state.list,this.state.inputValue];
           this.setState({
               list,
               inputValue:''
           })
       }
   }
   
   handleItemClick(index){
       // 先在当前函数内复制this.state.list的内容,然后修改list,最后更新list,而不是直接更改this.state.list中内容,避免出现一些错误
       alert(index);
       const list = [...this.state.list];
       // 删除list中index下标开始的内容,删除1项
       list.splice(index,1);
       this.setState({list});
   }
   
   render(){
       return(
           <Fragment>
               <input 
                   value={this.state.inputValue}
                   onChange={this.handleInputChange.bind(this)}
                   onKeyUp={this.handleKeyUp.bind(this)}
               />
               <ul>
                   {
                       this.state.list.map((value,index) => {
                           {/*
                               如果要想将下面这行代码return多行,加个()即可
                               return <li key={index}>{value}</li>
                           */}
                           return (
                               <li 
                                   key={index} 
                                   <!--
                                       如何将当前点击的index传递出去bind中添加index如下-->
                                   onClick={this.handleItemClick.bind(this,index)}>
                                   {value}
                               </li>
                           )
                       })
                   }
               </ul>
           </Fragment>
       );
   }
}

export default Todolist;

总结:

  1. 事件绑定中onClick={this.handleItemClick.bind(this,index)},将事件处理函数中的 this 绑定到当前组件实例上。未bind之前,严格模式下this.handleItemClick中this指向undefined(非严格模式下,指向触发事件的 DOM 元素,这里是li标签),bind(this)中this指向当前组件实例,index作为第一个参数传递给handleItemClick方法
  2. 遍历数组的map方法中一定要return
  3. 遍历列表项时,需添加key={index},不添加key属性会出现'each child in array or iterator should have a unique "key" prop'警告,key属性中为'index'理由是每次遍历的key值应该不一样,而每个列表项索引值是不会相同的。

es6中bind方法传送门

组件拆分与组件传值

父组件TodoList所在TodoList.js文件中内容:

import React,{ Component } from 'react';
// 从同级目录TodoItem.js文件中导入子组件TodoItem
// 注意./TodoItem.js可以省略后缀.js,直接写成./TodoItem即可
import TodoItem from './TodoItem';

class Todolist extends Component{

    constructor(props){
        super(props);
        
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleKeyUp = this.handleKeyUp,bind(this);
        // 3.子组件中调用父组件传递过来的handleItemClick方法时,会出现handleItemClick方法中this指向的是子组件TodoItem
        // 这时可以强制将handleItemClick方法this指向绑定到父组件上:
        this.handleItemClick = this.handleItemClick.bind(this);
        
        this.state = {
            inputValue: 'hello world',
            list:['learn react','learn Component','learn react-dom']
        }
    }

   handleInputChange(e){
       console.log(e);
       this.setState({
           inputValue:e.target.value
       })
    }
   
   handleKeyUp(e){
       console.log(e.keyCode)
       if(e.keyCode ===13 && e.target.value !== ''){
           const list = [...this.state.list,this.state.inputValue];
           this.setState({
               list,
               inputValue:''
           })
       }
   }
   
   handleItemClick(index){
       alert(index);
       const list = [...this.state.list];
       list.splice(index,1);
       this.setState({list});
   }
   
   getListItems(){
       return this.state.list.map((value,index) => {
           {/*
               父组件如何传值到子组件?
               父组件中设置任意变量比如content={value};
               子组件中通过this.props.content接受value值;
               
               最后,返回子组件标签<TodoList />
           */}
           return (
               <TodoItem 
                   content={value} 
                   key={index} 
                   {/* 
                       子组件如何改变父组件中内容点击item实现删除功能):
                       
                       1.因为不能在父组件中引入的子组件中绑定事件比如下面<TodoItem />中绑定onClick事件,
                       需要在定义TodoItem子组件里面绑定onClick事件,TodoItem子组件中重写handleItemClick函数;
                       
                       在子组件新写的handleItemClick函数中要实现子组件改变父组件中内容,即改变父组件中list数据,
                       一个思路是直接将父组件内容传递给子组件,然后在子组件中修改,但是不建议这种方式;
                      
                       2.那怎么做?父组件不仅可以将值传递给子组件,还可以将父组件中方法传递给子组件;
                       将handleItemClick方法和index值传递给子组件,定义属性deleteFunction={this.handleItemClick}和index={index}
                   */}
                   index={index}
                   deleteFunction={this.handleItemClick}
               />
           )
           {/*
               原始内容:
               return (
               <li 
                   key={index} 
                   onClick={this.handleItemClick.bind(this,index)}
                   dangerouslySetInnerHTML={{__html:value}}>
               </li>
               )
           */}
       })
   }
   
   render(){
       return(
           <Fragment>
               <label htmlFor='myinput'>请输入内容:</label>
               <input 
                   id='myinput'
                   value={this.state.inputValue}
                   onChange={this.handleInputChange}
                   onKeyUp={this.handleKeyUp}
               />
               <ul>
                   {this.getListItems}
               </ul>
           </Fragment>
       );
   }
}

export default Todolist;

将生成item逻辑抽离出来,单独建一个TodoItem.js文件,实现TodoItem组件:

import React,{ Component } from 'react';

class TodoItem extends Component{
    
    constructor(props){
        super(props);
        this.handleItemClick = this.handleItemClick.bind(this)
    }
    
    handleItemClick(){
        console.log(this);
        // 2.接收父组件传递过来deleteFunction和index属性,通过this.props接受
        // 因为deleteFunction函数传递过来的是父组件的handleItemClick方法,而该方法调用了index值
        // 所以handleItemClick(index) -> this.props.deleteFunction(this.props.index),如下:
        // this.props.deleteFunction(this.props.index);
        
        // 这时点击item会出现cannot read property 'list' of undefined错误,
        // 这是因为this.props.deleteFunction(this.props.index)调用handleItemClick方法时,
        // 由于this.props.deleteFunction()是在TodoItem子组件中执行的,所以this.props.index中this指向子组件
        
        // 3.这时需要在父组件constructor构造函数中强制将handleItemClick方法绑定到父组件TodoList中
        
        // 解构赋值将下面这行代码再简化
        // this.props.deleteFunction(this.props.index);
        const {deleteFunction,index} = this.props;
        deleteFunction(index);
    }
    
    render(){
        // 子组件接收父组件传递过来值:
        // return <li>{this.props.content}</li>
        
        // ES6语法,等价于下面两行代码:
        const { content } = this.props; // 这行代码也等价于const content = this.props.content;
        // 1.在子组件中绑定click事件
        return <li onClick={this.handleItemClick}>{content}</li>
        
    }
}

export default TodoItem;

总结:

  1. 父组件通过属性形式向子组件传值,属性名自定义命名,子组件通过this.props接收
  2. 父组件通过属性形式向子组件传递方法,deleteFunction={this.handleItemClick}
  3. 子组件要想改变父组件中数据,可以调用父组件中方法(注意方法中参数this指向指向的是子组件,要在父组件中将方法bind(this),绑定到父组件上)。不建议直接将父组件中数据传递给子组件,子组件来更改该数据。

react一些核心特性:

  1. 声明式开发,只需要定义JSX模版,数据发生变化,页面自动发生变化,而原生js是命令式开发,需要直接操作DOM。
  2. 可以与其他框架并存,有一套与其他框架解耦的机制,比如rsc目录下index.js文件将TodoList组件挂载到public目录下index.html文件中的id名为root的div中。所以整个src目录下代码之和id为root的div有关系。这是如果在public目录下index.html文件中在定义一个div,可以用其他框架来操控这个div。
  3. 组件化
  4. 单向数据流,指的是父组件可以直接改变子组件的数据,而子组件不可以直接改变父组件的数据,只能通过调用父组件的方法来改变父组件的数据,本质上是父组件自己改变了自己的数据。
  5. 函数式编程,方便前端进行自动化测试。

Props,State和render函数之间关系

在counter.js中定义Counter组件:

import React,{ Component, Fragment } from 'react';

class Counter extends Component{

    constructor(props){
        super(props);
        this.handleBtnClick = this.handleBtnClick.bind(this);
        this.state = {
            counter:1;
        }
    }
    
    handleBtnClick(){
        const newCounter = this.state.counter+1;
        this.setState({
            counter:newCounter
        })
    }
  
    render(){
        return(
            <Fragment>
                <button onclick={this.handleBtnClick}>增加</button>
                <div>{this.state.counter}</div>
            </Fragment>
        )
    }
}

export default Counter;

总结:

  1. render函数return中最外层加一个Fragment占位符,作用是可以使用多组标签
  2. 当组件初次创建时,render函数会执行一次;当state数据发生变化时,render函数会执行。
  3. 对于子组件,当props中数据发生变化时,render函数会执行

react中ref使用

如何在react中直接操作DOM?

counter.js中内容:

import React,{ Component, Fragment } from 'react';

class Counter extends Component{

    constructor(props){
        super(props);
        this.handleBtnClick = this.handleBtnClick.bind(this);
        this.state = {
            counter:1;
        }
    }
    
    handleBtnClick(){
        console.log(this.buttonElem.clientTop);
        const newCounter = this.state.counter+1;
        /*
        console.log(this.divElem.innerHTML);
        this.setState({
            counter:newCounter
        })
        console.log(this.divElem.innerHTML);
        发现这里上下打印值一致,这说明setState是异步的
        */
        
        // 那么如何使两次打印出来结果不一致?将setState作如下更改:
        this.setState(() => {
            return {
                counter:newCounter
            }
        },() => {
            console.log(this.divElem.innerHTML);
        })
    }
    render(){
        return(
            <Fragment>
                {/*
                    例如,这里需要直接操作button标签,在button标签中定义一个ref,它接受一个函数,函数接受的参数button就是JSX标签真实对应的DOM
                    用this.buttonElem存储该真实DOM button
                    
                    ref写在html标签上,获取的是dom节点
                    ref写在组件标签上,获取的是组件的js实例
                */}
                <button 
                    onclick={this.handleBtnClick}
                    ref={(button) => {this.buttonElem = button}}
                >
                    增加
                </button>
                {/*
                    如果将ref写在下面div中:
                    <div ref={(div) => {this.divElem = div}}>{this.state.counter}</div>
                    然后在handleBtnClick中this.setState上下各打印一次
                */}
                <div>{this.state.counter}</div>
            </Fragment>
        )
    }
}

export default Counter;

总结:

  1. ref写在html标签上,获取到的是dom;ref写在组件标签上获取到的是实例<button ref={(button) => {this.buttonElem = button}} >,ref接受一个函数,函数中参数及该标签真实DOM。
  2. setState是异步的。若要想在setState中数据更新前后两次分别使用更新前和更新后的数据,在setState前后使用是无效的;可以在setState中写两个函数,前一个函数用于更新数据,后一个函数用于使用更新后的数据(见上述17-32行代码)。

react中的生命周期函数(钩子函数)

生命周期函数:组件在某一个时刻自动执行的函数。

constructor是生命周期函数,但是不是react中的生命周期函数,因为ES6中所有类都具有constructor。

react中生命周期函数:在react中,组件在某一个时刻自动执行的函数

render函数是react中一个生命周期函数,因为render函数只在react中有,并且render函数当数据发生变化时会自动执行。

除render外,react中其他生命周期函数:

1ba0bcdaceb026a1a7776b82e254c7c.jpg

  1. Initialization初始化时,初始化一些数据,比如props和state。
  2. Mounting页面挂载时,依次执行componentWillMount、render、componentDidMount
  3. Updation数据发生变化时,有两条执行路径。分别是props变化和states变化。
  4. states变化时,依次执行shouldComponentUpdate、componentWillUpdate、render、componentDidUpdate,其中shouldComponentUpdate应该return一个布尔值
  5. states数据发生变化,不希望页面依次执行上述4个生命周期函数,进而重新将页面渲染一遍,那么可以将shouldComponentUpdate返回一个false
  6. 子组件中props因为父组件数据发生变化而变化时,会依次执行componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render、componentDidUpdate
  7. 若父组件中render嵌套子组件,且父组件中数据发生变化则这种父子组件嵌套结构的生命周期函数执行顺序为:父组件中Initialization中生命周期函数、父组件中Mounting中生命周期函数、父组件中states中shouldComponentUpdate、componentWillUpdate、render、写在render中子组件props变化的生命周期函数、父组件的componentDidUpdate。
  8. 组件被销毁时(组件即将从页面中移除时),执行componentWillUnmount

发送ajax请求时,一般将请求放在componentDidMount生命周期函数中,如下:

先安装axios:npm install axios --save

import React,{ Component } from 'react';
// 这个包的作用是发送ajax请求时需要使用axios
import  axios from 'axios';

class Counter extends Component{
    
    render(){
        return(
            <div>hello world</div>
        )
    }
    
    // 一般将ajax数据请求放在componentDidMount生命周期函数中
    // 不建议将ajax请求放在constructor或componentWillMount中发送
    componentDidMount(){
        // promise存放的是ajax的过程
        const promise = axios.get('http://www.dell-lee.com/react/api/demo.json');
        // 请求发送成功,执行promise.then中函数
        // res为请求回来的结果
        promise.then((res) => {
            console.log(res);
        })
    }
    
}

export default Counter;

总结:

  1. const promise = axios.get('http://www.dell-lee.com/react/api/demo.json'),axios.get()发送请求,promise存放ajax的过程。
  2. promise.then((res) => { console.log(res); }),使用promise.then()获取请求结果,其中res为请求结果。

Ant Design组件库使用

网址:ant.design/docs/react/…

菜单栏Ant Design of react,按步骤安装使用即可。

新的ant design文档中没有看到以下第二步。

  1. 安装包:npm install antd --save
  2. 在index.js中引入样式文件:import 'antd/dist/antd.css',在index.js中引入样式文件原因是index.js中绑定的div下所有组件都可以使用该样式文件。
  3. 在index.js中引入
import React from 'react';
import { Button } from 'antd';
  1. 选择一个组件,点击以下<>按钮展开代码,直接复制对应组件即可

image.png

index.js中文件:

import React from 'react';
import ReactDom from 'react-dom';
import Counter from './counter';
import 'antd/dist/antd.css';

ReactDom.render(<Counter />,document.getElementById('root'));

counter.js中文件:

import React,{ Component } from 'react';
import { Button } from 'antd';

class Counter extends Component{
    
    render(){
        // 可以在ant design组件基础上自己再添加样式
        return (
            <Button type='primary'>按钮</Button>
        )
    }
}

export default Counter;

react中前端路由

命令行安装前端路由:npm install react-router-dom --save

index.js中内容:

import React from 'react';
import ReactDom from 'react-dom';
import { BrowerRouter , Route, Link } from 'react-router-dom';
import NewList from './newList';
import NewButton from './newButton';
import 'antd/dist/antd.css';

class Entry extends Component {
     render(){
         return(
             <BrowserRouter>
                 {/*
                     BrowserRouter中只能有一个子组件,因此将两个Router组件包在一个div标签中;
                     
                     注意下面path中路径是路由,比如localhost:3000/list,而import中路径是文件路径;
                     
                     BrowserRouter定义一个路由,Route是一个路由项,Link是两个路由比如button和list之间的跳转;
                 */}
                 <div>
                     <Router path='/list' component={NewList}>
                     <Router path='/button' component={NewButton}>
                 </div>
             </BrowserRouter>
         )
     }
}

ReactDom.render(<Entry />,document.getElementById('root'));

newList.js中内容:

import React, { Component } from 'react';
// import中名字List与组件名List相同会冲突报错,因此组件名改为newList
import { List } from 'antd';

const data = [
  'Racing car sprays burning fuel into crowd.',
  'Japanese princess to wed commoner.',
  'Australian walks 100km after outback crash.',
  'Man charged over missing wedding girl.',
  'Los Angeles battles huge wildfires.',
];

class NewList extends Component {
    render(){
        
        // console.log(this.props.location.search);
        // 打印this.props,在打印的props-location-search中可以看到携带过来的参数
        // 这种/list?a=123路由中传递参数在search中格式为'?a=123',需要自己转化一下数据的格式才能拿到123这个数据
        
        // 如何直接拿到传递过来参数?可以在index.js中Router的path='/list/:id'
        console.log(this.props.match.params.id);
    
        return(
            <List
              header={<div>Header</div>}
              footer={<div>Footer</div>}
              bordered
              dataSource={data}
              renderItem={(item) => (
                <List.Item>
                  <Typography.Text mark>[ITEM]</Typography.Text> {item}
                </List.Item>
              )}
            />
        )
    }
}

export default NewList;

newButton.js中内容:

import React, { Component } from 'react';
import { Button } from 'antd';
// 如果要想在button页跳转到list页,先导入Link,然后用Link标签包裹Button组件
import { Link } from 'react-router-dom';


class NewButton extends Component {
    render(){
        
        return(
            <Link to='/list/:id'>
                <Button type="primary">Primary Button</Button>
            </Link>
        )
    }
}

export default NewButton;

总结:

  1. 使用react中路由,要先安装react-router-dom这个模块,然后在入口文件中import { BrowerRouter , Route, Link } from 'react-router-dom',导入BrowerRouter、Route、Link;入口文件render函数中将需使用路由子组件放在Browerouter中,Route定义路由项,<BrowserRouter> <Router path='/list' component={NewList}></BrowserRouter>
  2. BrowserRouter帮助创建路由,它内部只能有一个元素,当有多个路由项时,可以用一个div包裹多个Route路由项
  3. 组件间跳转通过Link实现,Link中有一个to属性,作用是跳转到哪里。比如,在newButton.js文件中希望点击button跳转到list路由,首先在该文件中import { Link } from 'react-router-dom'引入link,再用link标签包裹button按钮<Link to='/list/:id'> <Button type="primary">Primary Button</Button> </Link>
  4. 路由跳转有三种传值方式:一种是/list?123,这种方式需要进行格式转换,不方便拿到数据;一种是/list/123,这种方式控制台拿不到数据,只能在路由中看到;一种是/list/:id,通过this.props.match.params.id可以拿到数据