React[16x]框架

React[16x]框架

(1)React 简介

A JavaScript library for building user interfaces (用于构建用户界面的JavaScript库)
React生态:React + React-Router + Redux + Axios + Babel + Webpack

(1.1)React 背景

React 起源于 Facebook 的内部项目,因为该公司对市场上所有JavaScript MVC框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。

React的社区也是非常强大的,随着React的普及也衍生出了更多有用的框架,比如ReactNativeReact VRReact从13年开始推广,现在已经推出16RC(React Fiber)这个版本,性能和易用度上,都有很大的提升。

(1.2) 什么是React

\(~~~~\)React是一个简单的javascript UI库,用于构建高效、快速的用户界面。它是一个轻量级库,因此很受欢迎。它遵循组件设计模式声明式编程范式函数式编程概念,以使前端应用程序更高效。它使用虚拟DOM来有效地操作DOM。它遵循从高阶组件到低阶组件的单向数据流

声明式编程

\(~~~~\)声明式编程是一种编程范式,它关注的是你要做什么,而不是如何做。它表达逻辑而不显式地定义步骤。这意味着我们需要根据逻辑的计算来声明要显示的组件。它没有描述控制流步骤。声明式编程的例子有HTML、SQL

函数式编程

\(~~~~\)函数式编程是声明式编程的一部分javascript中的函数是第一类公民,这意味着函数是数据,你可以像保存变量一样在应用程序中保存、检索和传递这些函数
函数式编程有些核心的概念,如下:

  • 不可变性(Immutability)
  • 纯函数(Pure Functions)
  • 数据转换(Data Transformations)
  • 高阶函数 (Higher-Order Functions)
  • 递归
  • 组合

React 中一切都是组件。 我们通常将应用程序的整个逻辑分解为小的单个部分。 我们将每个单独的部分称为组件。 通常,组件是一个javascript函数,它接受输入,处理它并返回在UI中呈现的React元素。

(1.3) React 和 Vue 的对比

这是前端最火的两个框架,虽然说React是世界使用人数最多的框架,但是就在国内而言Vue的使用者很有可能超过React。两个框架都是非常优秀的,所以他们在技术和先进性上不相上下。

React.js相对于Vue.js它的灵活性和协作性更好一点,所以我在处理复杂项目或公司核心项目时,React都是我的第一选择。而Vue.js有着丰富的API,实现起来更简单快速,所以当团队不大,沟通紧密时,我会选择Vue,因为它更快速更易用,当然Vue也完全胜任于大型项目。

(2) React 的优缺点

(2.1) React 优点

  • 1.声明式设计 −React采用声明范式,可以轻松描述应用。

  • 2.高效 −React通过对DOM的模拟,最大限度地减少与DOM的交互。

  • 3.灵活 −React可以与已知的库或框架很好地配合。

  • 4.JSX − JSX JavaScript 语法的扩展。React 开发不一定使用 JSX ,但我们建议使用它。

  • 5.组件 − 通过 React 构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。

  • 6.单向响应的数据流 − React 实现了单向响应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单。

应用学习方面:

  • 生态强大:现在没有哪个框架比React的生态体系好的,几乎所有开发需求都有成熟的解决方案。
  • 上手简单: 你甚至可以在几个小时内就可以上手React技术,但是他的知识很广,你可能需要更多的时间来完全驾驭它。
  • 社区强大:你可以很容易的找到志同道合的人一起学习,因为使用它的人真的是太多了。

(3) React 和 React Native

React Native 看起来很像 React只不过其基础组件是原生组件而非 web 组件。要理解 React Native 应用的基本结构,首先需要了解一些基本的 React 的概念,比如 JSX语法、组件、state状态以及props属性。

(3.1) React Native开发特点

  • 一次学习,随处编写:使用React Native可以为iOSAndroid操作系统开发应用程序,不同平台上的代码根据平台会有一些微小的区别。
  • 混合开发:React Native代码开发的模块与原生代码开发的模块可以双向通信、无缝衔接;
  • 高效的移动应用开发:

(1)独特的UI实现框架
(2)组件化开发
(3)跨平台移植代码迅速
(4)自动匹配不同屏幕大小的手机

  • 高效的移动应用开发调试
  • 高效的应用热更新
  • 有效降低移动应用安装包体积
  • 学习门槛低、开发难度低

使用React Native开发的代价
为了得到React Native开发的优点,使用React Native开发的APP也需要付出一定的代价。

(1)内存消耗大

使用React Native开发的程序运行所需的内存比原生代码开发的程序略多。

(2)运行速度

使用React Native开发的代码运行速度比原生代码略慢。
React React Native 除了在编码表现层都使用 JSX 语法外,在 ReactReact Native 的底层都有 Virtual DOMDOM 之间的映射与转换,以实现了页面组件高效更新的前端表现。

(3.2) React Native与React的关系及特点

**React NativeReact的关系及特点 : **

React是基础框架,是一套基础设计实现理念,开发者不能直接使用它来开发移动应用或网页。在React之上发展出了React.js框架用来开发网页,发展出来React Native用来开发移动应用。底层原理是相同的,都是使用js实现虚拟dom树来驱动页面的渲染,react是驱动HTML dom的渲染,react native是驱动原生组件的渲染。

**React.js : **

目的 是为了使前端的V层更具组件化,能更好的复用,它能够使用简单的html标签创建更多的自定义组件标签,内部绑定事件,同时可以让你从操作dom中解脱出来,只需要操作数据就会改变相应的dom

二者都是基于组件(component)开发,然后组件和组件之间通过props传递方法,每个组件都有一个状态(state),当某个方法改变了这个状态值时,整个组件就会重绘,从而达到刷新。另外,说到重绘就要提到虚拟dom了,就是用js模拟dom结构,等整个组件的dom更新完毕,它会有一个diff的过程,对比出哪些组件发生了变化,然后才渲染到页面,简单来说只更新了相比之前改变了的部分,而不是全部刷新,所以效率很高。

虚拟DOM(Virtual DOM)的机制:在浏览器端用Javascript实现了一套DOM API。基于React进行开发时所有的DOM构造都是通过虚拟DOM进行,每当数据变化时,React都会重新构建整个DOM树,然后React将当前整个DOM树和上一次的DOM树进行对比,得到DOM结构的区别,然后仅仅将需要变化的部分进行实际的浏览器DOM更新。而且React能够批处理虚拟DOM的刷新,在一个事件循环(Event Loop)内的两次数据变化会被合并。

后续将详细学习研究React ative

(4) React[16x]入门

(4.1) React开发环境搭建

在搭建React开发环境前需要安装Node(自行百度)

(4.1.1) 安装Node.js

使用React.js是可以用最原始的<Script>标签进行引入的,但是这实在是太low了,工作中也不会有这种形式进行引入。所以在学习的时候,我们就采用脚手架形式的安装。这就需要我们安装Node.js,用里边的npm来进行安装。
安装Node只需要进入Node网站,进行响应版本的下载,然后进行双击安装就可以了。

Node中文网址:http://nodejs.cn/ (建议你在这里下载,速度会快很多)

注意:一定要正确下载对应版本,版本下载错误,可是没有办法使用

Node.js 安装好以后,如果是Windows系统,可以使用 Win+R打开运行,然后输入cmd,打开终端(或者叫命令行工具)。

node -v

如果正确出现版本号,说明Node安装成功
检查npm环境,正确出现版本号,说明npm也是没问题的

npm -v

(4.1.2) React脚手架搭建

使用npm命令来安装脚手架工具:

npm install -g create-react-app

create-react-appReact官方出的脚手架工具.

补充知识:

npm install moduleName # 安装模块到项目目录下

npm install -g moduleName # -g 的意思是将模块安装到全局,具体安装到磁盘哪个位置,要看 npm config prefix 的位置。

npm install --save moduleName # --save 的意思是将模块安装到项目目录下,并在package文件的dependencies节点写入依赖。

npm install --save-dev moduleName # --save-dev 的意思是将模块安装到项目目录下,并在package文件的devDependencies节点写入依赖。

(4.2) 创建React项目

1. 准备工作

创建文件夹 reactdom ,作为项目路径

2. 创建项目
通过脚手架创建React项目命令:项目名demo01

create-react-app demo01   //用脚手架创建React项目

3. 启动项目
这个有一个小坑,启动项目一定要先进入项目目录,而不是我们创建的reactdom,
使用一下命令运行即可:

cd demo01   //等创建完成后,进入项目目录
npm start   //预览项目,如果能正常打开,说明项目创建成功

(4.2.1) 项目目录介绍

1. 项目初始化目录:

2. 项目顶层目录

先从进入项目根目录说起,也就是第一眼看到的文件(版本不同,可能稍有不同)

  • README.md : 这个文件主要作用就是对项目的说明,已经默认写好了一些东西,你可以简单看看。如果是工作中,你可以把文件中的内容删除,自己来写这个文件,编写这个文件可以使用Markdown的语法来编写。
  • package.json :这个文件是webpack配置和项目包管理文件,项目中依赖的第三方包(包的版本)和一些常用命令配置都在这个里边进行配置,当然脚手架已经为我们配置了一些了,目前位置,我们不需要改动。如果你对webpack了解,对这个一定也很熟悉。
  • gitignore : 这个是git的选择性上传的配置文件,比如一会要介绍的node_modules文件夹,就需要配置不上传。
  • node_modules :这个文件夹就是我们项目的依赖包,到目前位置,脚手架已经都给我们下载好了,你不需要单独安装什么。
  • public :公共文件,里边有公用模板和图标等一些东西。
  • src : 主要代码编写文件,这个文件夹里的文件对我们来说最重要,都需要我们掌握。

3. public文件夹

这个文件都是一些项目使用的公共文件,也就是说都是共用的,我们就具体看一下有那些文件吧。

  • favicon.ico : 这个是网站或者说项目的图标,一般在浏览器标签页的左上角显示。

  • index.html :_ 首页的模板文件_,我们可以试着改动一下,就能看到结果。

  • mainifest.json移动端配置文件,这个会在以后的课程中详细讲解。

4. src文件夹

这个目录里边放的是我们开放的源代码,我们平时操作做最多的目录。

  • index.js : 这个就是项目的入口文件

  • index.css :这个是index.js里的CSS文件

  • app.js : 这个文件相当于一个方法模块,也是一个简单的模块化编程

  • serviceWorker.js : 这个是用于写移动端开发的,PWA必须用到这个文件,有了这个文件,就相当于有了离线浏览的功能。

(4.3) Hello World和组件的编写

先把src目录里的文件全部删除,我们一点点写一个HelloWorld程序,并通过编写这个程序了解一下什么是React中的组件化编程。

(4.3.1) 编写入口文件

在src文件夹下,新建index.js入口文件。

(4.3.2) 导入相关包

index.js文件中,导入必要的基础包

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.js'

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

首先引入React两个必要的文件,然后引入一个App组件(目前这个组件还没有编写,需要一会创建),然后用React的语法把App模块渲染到Root ID上rootID是在 public\index.html 文件中的真实DOM

(4.3.3) 编写App组件

import React, {Component} from 'react'

class App extends Component {
  render(){
    return(
    	<div>
    		Hello Word!!!
    	</div>
    )
  }
}
export default App;

这里导入的包,导入形式有些不同,

//ES6的语法-解构赋值
import React, {Component} from 'react'

//分开写
import React from 'react'
const Component = React.Component

注意:

React的主要优势之一就是组件化编写,这也是现代前端开发的一种基本形式
创建React组件时,必须使用大写字母开头。一般文件名和组件名相同

(4.3.4) 启动项目

在终端,进入项目目录,使用npm start命令启动项目,我们可以在页面上看到Hello Word

(4.4) React中JSX语法

JSX语法,看起来跟html标签几乎一样

(4.4.1) JSX简介

JSX就是JavascriptXML结合的一种格式。React发明了JSX,可以方便的利用HTML语法来创建虚拟DOM,当遇到<JSX就当作HTML解析,遇到{就当JavaScript解析.

比如我们写一段JSX语法:

<ul className="my-list">
    <li>JAVA</li>
    <li>I love React</li>
</ul>

就等于之前我们写一段JS代码:

var child1 = React.createElement('li', null, 'JSPang.com');
var child2 = React.createElement('li', null, 'I love React');
var root = React.createElement('ul', { className: 'my-list' }, child1, child2);

从代码量上就可以看出JSX语法大量简化了我们的工作。

(4.4.2) 组件和普通JSX语法的区别

这个说起来也只有简单的一句话,就是你自定义的组件必须首写字母要进行大写,而JSX是小写字母开头的。

(4.4.3) JSX使用三元越算符

import React from 'react'
const Component = React.Component

class App extends Component{
  render(){
    return(
    	<ul className = 'my-list'>
    		<li>{Hello Word ? '你好':'欢迎你'}</li> //三目运算
    		<li> I  love  React </li>
    	</ul>
    )
  }
}
export default App;

(4.5) React 实例学习

(4.5.1) 新建组件【小姐姐组件】

现在src的目录下面,新建一个文件Xiaojiejie.js文件,然后写一个基本的HTML结构。代码如下:

import React,{Component} from 'react'

class Xiaojiejie exetends Component{
  render(){
    return(){
      <div>
      	 <div>
      		<input/>
      		<button>增加服务</button>
      	 </div>
      	 <ul>
      	 	<li>头部按摩</li>
      	 	<li>精油推背</li>
      	 </ul>
      </div>
    }
  }
}
export default Xiaojiejie

然后我们把入口文件的<App />组件换成Xiajiejie组件。

(4.5.1.1) 组件外层包裹原则

这是一个很重要的原则,比如上面的代码,我们去掉最外层的<Div>,就回报错,因为React要求必须在一个组件的最外层进行包裹。
所以我们在写一个组件的时候,组件的最外层都需要有一个包裹。

(4.5.1.2) Fragment标签讲解

加上最外层的DIV,组件就是完全正常的,但是你的布局就偏不需要这个最外层的标签怎么办?比如我们在作Flex布局的时候,外层还真的不能有包裹元素。这种矛盾其实React16已经有所考虑了,为我们准备了<Fragment>标签。

要想使用<Fragment>,需要先进行引入。

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

然后把最外层的<div>标签,换成<Fragment>标签,代码如下:

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

class Xiaojiejie extends Component{
    render(){
        return  (
            <Fragment>
               <div><input /> <button> 增加服务 </button></div>
               <ul>
                   <li>头部按摩</li>
                   <li>精油推背</li>
               </ul> 
            </Fragment>
        )
    }
}
export default Xiaojiejie 

这时候你再去浏览器的Elements中查看,就回发现已经没有外层的包裹了。

(4.5.2) React 响应式设计和数据绑定

(4.5.2.1) React 响应式设计

React不建议你直接操作DOM元素,而是要通过数据进行驱动,改变界面中的效果。React会根据数据的变化,自动的帮助你完成界面的改变。所以在写React代码时,你无需关注DOM相关的操作,只需要关注数据的操作就可以了(这也是React如此受欢迎的主要原因,大大加快了我们的开发速度)
现在的需求是增加小姐姐的服务项,就需要先定义数据。数据定义在Xiaojiejie组件中的构造函数里constructor

//js的构造函数,由于其他任何函数执行
constructor(props){
  super(props)//调用父类的构造函数,固定写法
  this.state = {
    inputValue:'',//input中的值
    list:[]//服务列表
  }
}

React中的数据绑定和Vue中几乎一样,也是采用字面量(我自己起的名字)的形式,就是使用{}来标注,其实这也算是js代码的一种声明。比如现在我们要把inputValue值绑定到input框中,只要写入下面的代码就可以了。其实说白了就是在JSX中使用js代码。

<input value={this.state.inputValue} /> 

现在需要看一下是不是可以实现绑定效果,所以把inputValue赋予一个'jspang',然后预览看一下效果。在这里我们并没有进行任何的DOM操作,但是界面已经发生了变化,这些都时React帮我们作的,它还会自动感知数据的变化

(4.5.2.2) React 绑定事件

这时候你到界面的文本框中去输入值,是没有任何变化的,这是因为我们强制绑定了inputValue的值。如果要想改变,需要绑定响应事件,改变inputValue的值。比如绑定一个改变事件,这个事件执行inputChange()(当然这个方法还没有)方法。

 <input value={this.state.inputValue} onChange={this.inputChange.bind(this)} />

现在还没有inputChange()这个方法,在render()方法的下面建立一个inputChange()方法,代码如下:

inputChange(e){
    // console.log(e.target.value);
    // this.state.inputValue=e.target.value;
    this.setState({
        inputValue:e.target.value
    })
}

注意点:
1.一个是this指向不对,你需要重新用bind设置一下指向(ES6的语法)。
2.另一个是React中改变值需要使用this.setState方法。

(4.5.3) React 列表渲染

(4.5.3.1) 遍历数组

现在的列表还是写死的两个<li>标签,那要变成动态显示的,就需要把这个列表先进行数据化,然后再用javascript代码,循环在页面上。
ist数组增加两个数组元素,代码如下:

constructor(props){
    super(props) //调用父类的构造函数,固定写法
    this.state={
        inputValue:'jspang' , // input中的值
        //----------主要 代码--------start
        list:['基础按摩','精油推背']   
        //----------主要 代码--------end
    }
}

有了数据后,可以在JSX部分进行循环输出,代码如下:

render(){
    return  (
        <Fragment>
            <div>
                <input value={this.state.inputValue} onChange={this.inputChange.bind(this)} />
                <button> 增加服务 </button>
            </div>
            <ul>
                {
                    this.state.list.map((item,index)=>{
                        return <li>{item}</li>
                    })
                }
            </ul>  
        </Fragment>
    )
}

完成上面的步骤,数据就不再是固定的了,而是动态管理的

解决 Key值报错:
打开浏览器的控制台F12,可以清楚的看到报错了。这个错误的大概意思就是缺少key值。就是在用map循环时,需要设置一个不同的值,这个时React的要求。

<ul>
    {
        this.state.list.map((item,index)=>{
            return <li key={index+item}>{item}</li>
        })
    }
</ul>  
(4.5.3.2) 增加数组元素

要增加服务选项,我们需要再增加按钮上先绑定一个方法this.addList(这个方法目前还没有,需要我们接下来建立).

<button onClick={this.addList.bind(this)}> 增加服务 </button>

接下来就是把this.addList方法,代码如下:

//增加服务的按钮响应方法
addList(){
    this.setState({
        list:[...this.state.list,this.state.inputValue]
    })

}

注意:
...这个是ES6的新语法,叫做扩展运算符。意思就是把list数组进行了分解,形成了新的数组,然后再进行组合

(4.5.3.3) 删除数组元素

如果要删除一个东西,就要得到数组里的一个编号,这里指下标。传递下标就要有事件产生,先来绑定一个双击事件.代码如下:

<ul>
    {
        this.state.list.map((item,index)=>{
            return (
                <li 
                    key={index+item}
                    onClick={this.deleteItem.bind(this,index)}
                >
                    {item}
                </li>
            )
        })
    }
</ul> 

为了看着更清晰,我们在return部分加了()这要就可以换行编写JSX代码了.在onClick我们绑定了deleteItem方法.

获得了数据下标后,删除数据就变的容易起来.先声明一个局部变量,然后利用传递过来的下标,删除数组中的值.删除后用setState更新数据就可以了.

//删除单项服务
deleteItem(index){
    this.state.list.splice(index,1)
    this.setState({
        list:this.state.list
    }) 
}

注意:

记住React是禁止直接操作state的,虽然上面的方法也管用,但是在后期的性能优化上会有很多麻烦,所以一定不要这样操作

(4.5.4) JSX语法注意事项

** JSX的注释,可以有下面两种写法:**

<Fragment>
    {/* 正确注释的写法 */}
    <div>
        <input value={this.state.inputValue} onChange={this.inputChange.bind(this)} />
        <button onClick={this.addList.bind(this)}> 增加服务 </button>
    </div>

如果你记不住,有个简单的方法,就是用VSCode的快捷键,直接按Ctrl+/,就会自动生成正确的注释了。

你可以把这个理解为,在jsx中写javascript代码。所以外出我们套入了{},然后里边就是一个多行的javascript注释。如果你要使用单行祝注释//,你需要把代码写成下面这样。

<Fragment>
    {
        //正确注释的写法 
    }
    <div>
        <input value={this.state.inputValue} onChange={this.inputChange.bind(this)} />
        <button onClick={this.addList.bind(this)}> 增加服务 </button>
    </div>
  1. class换成className,它是防止和js中的class类名 冲突,所以要求换掉
  2. JSX中Html解析问题
    如果想在文本框里输入一个<h1>标签,并进行渲染。默认是不会生效的,只会把<h1>标签打印到页面上,这并不是我想要的。如果工作中有这种需求,可以使用dangerouslySetInnerHTML属性解决。具体代码如下:
<ul>
    {
        this.state.list.map((item,index)=>{
            return (
                <li 
                    key={index+item}
                    onClick={this.deleteItem.bind(this,index)}
                    dangerouslySetInnerHTML={{__html:item}}
                >
                </li>
            )
        })
    }
</ul> 

上面的代码就可以实现html格式的输出。
4. JSX
label是html中的一个辅助标签,也是非常有用的一个标签。

先看下面的代码,我们在文本框前面加入一个<label>

<div>
    <label>加入服务:</label>
    <input className="input" value={this.state.inputValue} onChange={this.inputChange.bind(this)} />
    <button onClick={this.addList.bind(this)}> 增加服务 </button>
</div>

这时候想点击“加入服务”直接可以激活文本框,方便输入。按照html的原思想,是直接加ID就可以了。代码如下:

<div>
    <label for="jspang">加入服务:</label>
    <input id="jspang" className="input" value={this.state.inputValue} onChange={this.inputChange.bind(this)} />
    <button onClick={this.addList.bind(this)}> 增加服务 </button>
</div>

这时候你浏览效果虽然可以正常,但console里还是有红色警告提示的。大概意思是不能使用for它容易和javascript里的for循环混淆,会提示你使用htmlfor

<div>
    <label htmlFor="jspang">加入服务:</label>
    <input id="jspang" className="input" value={this.state.inputValue} onChange={this.inputChange.bind(this)} />
    <button onClick={this.addList.bind(this)}> 增加服务 </button>
</div>

这时候代码就正确了,可以实现点击<label>后,激活<input>标签了。

(4.5.5) Simple React Snippets插件

vscode中的Simple React Snippets

1. 安装
打开SCode的插件查单,然后在输入框中输入imple React Snippets然后点击进行安装就可以了

2. 快速进行引入import
直接在vscode中输入imrc,就会快速生成最常用的import代码。

import React, { Component } from 'react';

2. 快速生成class
在作组件的时候,都需要写一个固定的基本格式,这时候你就可以使用快捷键cc插件就会快速帮我们生成如下代码

class  extends Component {
    state = {  }
    render() { 
        return (  );
    }
}

export default ;

更多详细快捷键,打开插件的说明文件看一下就可以了

(4.5.6) 组件拆分

小姐姐服务菜单已经完美的制作好了,但是这从头到尾我们只用了一个组件,但是在实际工作中肯定是团队开发,我们会把一个大功能分成不同的组件。比如把文本框和按钮单独一个组件,把下面的服务列表单独一个组件,这涉及了一个组件拆分的能力和知识

1. 新建服务菜单组件
src目录下,新建一个文件,这里就叫做XiaojiejieItem.js

import React, { Component } from 'react'; //imrc
class XiaojiejieItem  extends Component { //cc

    render() { 
        return ( 
            <div>小姐姐</div>
         );
    }
}
export default XiaojiejieItem;

到以前写的Xiaojiejie.js文件中用import进行引入,代码如下:

import XiaojijieItem from './XiaojiejiItem'

2. 修改Xiaojiejie组件
已经引入了新写的组件,这时候原来的代码要如何修改才能把新组件加入?

把原来的代码注释掉,当然你也可以删除掉,我这里就注释掉了,注释方法如下:


{/*
<li 
    key={index+item}
    onClick={this.deleteItem.bind(this,index)}
    dangerouslySetInnerHTML={{__html:item}}
>

</li>
*/ }

然后在最外层加入包裹元素<div>,为的是防止两个以上的标签,产生错误信息。

最后直接写入Xiaojiejie标签就可以了.

<XiaojiejieItem />

完整代码:


import React,{Component,Fragment } from 'react'
import './style.css'
import XiaojiejieItem from './XiaojiejieItem'

class Xiaojiejie extends Component{
//js的构造函数,由于其他任何函数执行
constructor(props){
    super(props) //调用父类的构造函数,固定写法
    this.state={
        inputValue:'' , // input中的值
        list:['基础按摩','精油推背']    //服务列表
    }
}

render(){
    return  (
        <Fragment>
            {/* 正确注释的写法 */}
<div>
    <label htmlFor="jspang">加入服务:</label>
    <input id="jspang" className="input" value={this.state.inputValue} onChange={this.inputChange.bind(this)} />
    <button onClick={this.addList.bind(this)}> 增加服务 </button>
</div>
            <ul>
                {
                    this.state.list.map((item,index)=>{
                        return (
                            //----------------关键修改代码----start
                            <div>
                                <XiaojiejieItem />
                            </div>
                            //----------------关键修改代码----end

                        )
                    })
                }
            </ul>  
        </Fragment>
    )
}

    inputChange(e){
        // console.log(e.target.value);
        // this.state.inputValue=e.target.value;
        this.setState({
            inputValue:e.target.value
        })
    }
    //增加服务的按钮响应方法
    addList(){
        this.setState({
            list:[...this.state.list,this.state.inputValue],
            inputValue:''
        })

    }
//删除单项服务
deleteItem(index){
    let list = this.state.list
    list.splice(index,1)
    this.setState({
        list:list
    })

}

}
export default Xiaojiejie 

(4.5.7)React-父子组件的传值

上面已经把"小姐姐"组件做了一个基本的拆分,但是还不能实现随着输入,显示出输入的内容。这里涉及的是父组件向子组件传值。然后点击删除,就相当于子组件向父组件传值。

(4.5.7.1)父组件向子组件传参

这里只介绍最实用的,最快速的上手方法。就是使用组件属性的形式父组件给子组件传值。比如:我们在<XiaojiejieItem>组件中加入content属性,然后给属性传递{item},这样就完成了父组件向子组件传值。

<XiaojiejieItem content={item} />

现在值已经顺利的传递了过去,这时候可以通过this.props.xxx的形式进行接受,比如传递过来的值,可以用如下代码进行接收。

import React, { Component } from 'react'; //imrc
class XiaojiejieItem  extends Component { //cc

    render() { 
        return ( 
            <div>{this.props.content}</div>
         );
    }
}

export default XiaojiejieItem;

修改完小姐姐子项的组件后,可以打开浏览器进行预览了。试着添加几个新的选项试一下
父组件向子组件传递内容,靠属性的形式传递。

(4.5.7.2)子组件向父组件传参

现在要作这样一个功能:点击组件中的菜单项后,删除改菜单项。这就涉及了一个字组件向父组件传递数据的知识需要掌握。

先来绑定点击事件,这时候当然是要在XiaojiejieItem组件中绑定了,代码如下:

import React, { Component } from 'react'; //imrc
class XiaojiejieItem  extends Component { //cc

    render() { 
        return ( 
            <div onClick={this.handleClick}>{this.props.content}</div>
         );
    }

    handleClick(){
        console.log('撩拨了小姐姐')
    }

}

export default XiaojiejieItem;

这时候进行预览,打开F12,再点击服务菜单项,就会再console里显示出"撩拨了小姐姐"的字样。但是console里还有一个warning警告,这个警告我们见过,就是要求循环时必须设置key值。

修改XiaoJieJie组件的render代码如下:

<ul>
    {
        this.state.list.map((item,index)=>{
            return (
                <XiaojiejieItem 
                key={index+item}  
                content={item} />
            )
        })
    }
</ul> 

绑定成功后,现在就要通过操作子组件删除父组件里的数据了。但是React有明确规定,子组件时不能操作父组件里的数据的,所以需要借助一个父组件的方法,来修改父组件的内容。其实在以前已经写了一个删除方法deleteItem,现在要作的就是子组件调用这个方法。

//删除单项服务
deleteItem(index){
    let list = this.state.list
    list.splice(index,1)
    this.setState({
        list:list
    })

}

1. 获取数组索引下标
那现在问题来了,要删除就要知道索引值,还是需要通过父组件传递给子组件.这里还是通过props属性的形式进行传递。

<ul>
    {
        this.state.list.map((item,index)=>{
            return (
                <XiaojiejieItem 
                key={index+item}  
                content={item}
                index={index} />
            )
        })
    }
</ul>  

然后修改XiaojiejieItem组件,在handleClick方法里,写入下面代码:

return ( 
    <div onClick={this.handleClick.bind(this)}>
        {this.props.content}
    </div>
);

构造函数里绑定的。(有言曰:构造函数中绑定性能会高一些,特别是在高级组件开发中,会有很大的作用)
constructor绑定this方法。


import React, { Component } from 'react'; //imrc
class XiaojiejieItem  extends Component { //cc
   //--------------主要代码--------start
   constructor(props){
       super(props)
       this.handleClick=this.handleClick.bind(this)
   }
   //--------------主要代码--------end
    render() { 
        return ( 
            <div onClick={this.handleClick}>
                {this.props.content}
            </div>
        );
    }
    handleClick(){
        console.log(this.props.index)
    }
}

export default XiaojiejieItem;

2. 子组件调用父组件方法
如果子组件要调用父组件方法,其实和传递数据差不多,只要在组件调用时,把方法传递给子组件就可以了,记得这里也要进行this的绑定,如果不绑定子组件是没办法找到这个父组件的方法的。

<ul>
    {
        this.state.list.map((item,index)=>{
            return (
                <XiaojiejieItem 
                key={index+item}  
                content={item}
                index={index}
                //关键代码-------------------start
                deleteItem={this.deleteItem.bind(this)}
                //关键代码-------------------end
                />
            )
        })
    }
</ul>  

传递后,在XiaojiejieItem组件里直接hi用就可以了,代码如下:

handleClick(){
    this.props.deleteItem(this.props.index)
}

(4.5.8)React-单项数流

1. 单项数据流
React的特性中有一个概念叫做“单项数据流”,可能刚刚接触React的小伙伴不太明白这个概念,还是拿出《小姐姐服务菜单》的Demo,来给大家讲解。比如我们在父组件中可以直接把this.state.list传递过来。例如下面代码:

<ul>
    {
        this.state.list.map((item,index)=>{
            return (
                <XiaojiejieItem 
                key={index+item}  
                content={item}
                index={index}
                list={this.state.list}
                deleteItem={this.deleteItem.bind(this)}
                />
            )
        })
    }
</ul> 

其实这样传是没有问题的,问题是你只能使用这个值,而不能修改这个值,如果你修改了,比如我们把代码写成这样:

handleClick(){
    //关键代码——---------start
    this.props.list=[]
    //关键代码-----------end
    this.props.deleteItem(this.props.index)
}

就会报下面的错误;

TypeError: Cannot assign to read only property 'list' of object '#<Object>'

意思就是list是只读的,单项数据流。那如果要改变这里边的值怎么办?其实上节课已经讲过了,就是通过传递父组件的方法。

2. React和其他框架配合使用

是可以的,React其实可以模块化和组件化开发。

/public/index.html文件,代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />

    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <!--关键代码start-->
    <div id="root"></div>
     <!--关键代码end-->

  </body>
</html>

其实React只对这一个<div>,外边的其他DOM并不受任何影响,比如我们在它的下方再写一个<div>,然后查看效果。

<div id="root"></div>
<div style="color:red">今天过的好开心,服务很满意!</div>

你可以在其他的div里加入任何内容,但是这种情况很少,不建议这么使用。希望小伙伴们还是统一技术栈。

3. 函数式编程
在面试React时,经常会问道的一个问题是:函数式编程的好处是什么?

  • 函数式编程让我们的代码更清晰,每个功能都是一个函数。
  • 函数式编程为我们的代码测试代理了极大的方便,更容易实现前端自动化测试
    React框架也是函数式编程,所以说优势在大型多人开发的项目中会更加明显,让配合和交流都得心应手。

(4.5.9) React developer tools

React在浏览器端是有一个调试工具的,这就是React developer tools,这个是React人必下的一个调试工具

React developer tools有三种颜色,三种颜色代表三种状态:

  • 灰色: 这种就是不可以使用,说明页面不是又React编写的。
  • 黑色: 说明页面是用React编写的,并且处于生成环境当中。
  • 红色: 说明页面是用React编写的,并且处于调试环境当中。

打开浏览器,然后按F12,打开开发者工具,然后在面板的最后一个,你会返现一个React,这个就是安装的插件了。

(4.5.10) PropTypes 校验传值

在父组件向子组件传递数据时,使用了属性的方式,也就是props,但“小姐姐服务菜单”的案例并没有任何的限制。这在工作中时完全不允许的,因为大型项目,如果你不校验,后期会变的异常混乱,业务逻辑也没办法保证。

1. PropTypes的简单应用
我们在Xiaojiejie.js组件里传递了4个值,有字符串,有数字,有方法,这些都是可以使用PropTypes限制的。在使用需要先引入PropTypes

import PropTypes from 'prop-types'

引入后,就可以在组件的下方进行引用了,需要注意的是子组件的最下面(不是类里边),写入下面的代码:

XiaojiejieItem.propTypes={
    content:PropTypes.string,
    deleteItem:PropTypes.func,
    index:PropTypes.number
}

具体意思,我会在视频中进行讲解,请观看视频。为了防止你的为止写错,我这里给出这个XiaojiejieItem.JS文件的代码。

import React, { Component } from 'react'; //imrc
import PropTypes from 'prop-types'

class XiaojiejieItem  extends Component { //cc

   constructor(props){
       super(props)
       this.handleClick=this.handleClick.bind(this)
   }

    render() { 
        return ( 
            <div onClick={this.handleClick}>
                {this.props.content}
            </div>
        );
    }

    handleClick(){

        this.props.deleteItem(this.props.index)
    }

}
 //--------------主要代码--------start
XiaojiejieItem.propTypes={
    content:PropTypes.string,
    deleteItem:PropTypes.func,
    index:PropTypes.number
}
 //--------------主要代码--------end
export default XiaojiejieItem;

这时候你在浏览器中查看效果,是什么都看不出来的,你需要修改一个错误的校验。比如我们把index改为必须是字符串

index:PorpTypes.string

这时候浏览器的console里就会报错了,报错信息如下:

Warning: Failed prop type: Invalid prop `index` of type `number` supplied to `XiaojiejieItem`, expected `string`.
    in XiaojiejieItem (at Xiaojiejie.js:28)
    in Xiaojiejie (at src/index.js:5)

意思就是要求传递字符串,而我们却传递了数字过去,所以给了警告。

必传值的校验:
比如现在我们加入一个avname的属性,并放入JSX中,就算不传递这个值也不会报错的。代码如下:

render() { 
    return ( 
        <div onClick={this.handleClick}>
            {this.props.avname}为你做- {this.props.content}
        </div>
    );
}

这时候代码是不会报错的,我们传不传无所谓。比如我们现在传一个属性过来。

<ul>
    {
        this.state.list.map((item,index)=>{
            return (
                <XiaojiejieItem 
                key={index+item}  
                content={item}
                index={index}
                avname='你好'
                deleteItem={this.deleteItem.bind(this)}
                />
            )
        })
    }
</ul> 

这时候页面显示正常了,但是怎样避免必须传递avname这个属性值?如果不传递就报错,这就需要使用isRequired关键字了,它表示必须进行传递,如果不传递就报错。

avname:PropTypes.string.isRequired

使用默认值defaultProps:
defalutProps就可以实现默认值的功能,比如现在把avname的默认值设置成"欢迎光临" ,然后把avname的属性删除掉

XiaojiejieItem.defaultProps = {
    avname:'欢迎光临'
}

(4.5.11) ref的使用

在编写组件中的方法时,经常会遇到语义化很模糊的代码,这对于团队开发是一个很大的问题。因为review代码或者合作时都会影响开发效率。或者到这核心成员离开,项目倒闭的严重影响。所以我们必须重视react代码当中的语义化

1. 代替原来项目中e.target.value
以前的案例中,我们写了下面的代码,使用了e.target,这并不直观,也不好看。这种情况我们可以使用ref来进行解决

之前代码:

inputChange(e){

    this.setState({
        inputValue:e.target.value
    })
}

如果要使用ref来进行,需要现在JSX中进行绑定, 绑定时最好使用ES6语法中的箭头函数,这样可以简洁明了的绑定DOM元素。

<input 
    id="jspang" 
    className="input" 
    value={this.state.inputValue} 
    onChange={this.inputChange.bind(this)}
    //关键代码——----------start
    ref={(input)=>{this.input=input}}
    //关键代码------------end
    />

绑定后可以把上边的类改写成如下代码:

inputChange(){
    this.setState({
        inputValue:this.input.value
    })
}

这就使我们的代码变得语义化和优雅的多。但是就我个人的经验来讲,我是不建议用ref这样操作的,因为React的是数据驱动的,所以用ref会出现各种问题。

1. ref中的坑
比如现在我们要用ref绑定取得要服务的数量,可以先用ref进行绑定。

<ul ref={(ul)=>{this.ul=ul}}>
    {
        this.state.list.map((item,index)=>{
            return (
                <XiaojiejieItem 
                key={index+item}  
                content={item}
                index={index}
                deleteItem={this.deleteItem.bind(this)}
                />
            )
        })
    }
</ul> 

绑定后可以在addList()方法中,获取当前<div>的值.

 addList(){
    this.setState({
        list:[...this.state.list,this.state.inputValue],
        inputValue:''
    })
    //关键代码--------------start
    console.log(this.ul.querySelectorAll('div').length)
    //关键代码--------------end

}

这时候你打开控制台,点击添加服务按钮,你会返现数量怎么少一个?(就是这个坑),其实这个坑是因为React中更多setState是一个异步函数所造成的。也就是这个setState,代码执行是需要有一个时间的,如果你真的想了解清楚,你需要对什么是虚拟DOM有一个了解。简单的说,就是因为是异步,还没等虚拟Dom渲染,我们的console.log就已经执行了。

那这个代码怎么编写才会完全正常那,其实setState方法提供了一个回调函数,也就是它的第二个函数。下面这样写就可以实现我们想要的方法了。

addList(){
    this.setState({
        list:[...this.state.list,this.state.inputValue],
        inputValue:''
        //关键代码--------------start
    },()=>{
        console.log(this.ul.querySelectorAll('div').length)
    })
    //关键代码--------------end
}

(4.6) React 生命周期

React的生命周期从广义上分为三个阶段:挂载、渲染、卸载

因此可以把React的生命周期分为两类:挂载卸载过程和更新过程。

(4.6.1) React的生命周期图

(4.6.2) 废弃三个旧的生命周期函数

React V16.3 版本中,为下面三个生命周期函数加上了 UNSAFE

  • UNSAFE_componentWillMount
  • UNSAFE_componentWillReceiveProps
  • UNSAFE_componentWillUpdate
  • 标题中的废弃不是指真的废弃,只是不建议继续使用,并表示在 V17.0 版本中正式删除。先来说说React为什么要这么做。

主要是这些生命周期方法经常被_误用和滥用_。并且在 React V16.0 之前,React 是_同步渲染的_,而在 V16.0 之后 React 更新了其渲染机制,是通过_异步的方式进行渲染_的,在 render 函数之前的所有函数都有可能被执行多次。

长期以来,原有的生命周期函数总是会诱惑开发者在 render 之前的生命周期函数中做一些动作,现在这些动作还放在这些函数中的话,有可能会被调用多次,这肯定不是我们想要的结果。

(4.6.2.1)废弃 UNSAFE_componentWillMount 的原因

有一个常见的问题,有人问为什么不在 UNSAFE_componentWillMount 中写 AJAX 获取数据的功能,他们的观点是,UNSAFE_componentWillMountrender 之前执行,早一点执行早得到结果。但是要知道,在 UNSAFE_componentWillMount 中发起 AJAX 请求,不管多快得到结果也赶不上首次 render,数据都是要在 render 后才能到达。

而且 UNSAFE_componentWillMount 在服务器端渲染也会被调用到(此方法是服务端渲染唯一会调用的生命周期函数。),你肯定不希望AJAX 请求被执行多次,所以这样的IO 操作放在 componentDidMount 中更合适。

尤其是在 Fiber 启用了异步渲染之后,更没有理由在 UNSAFE_componentWillMount 中进行 AJAX 请求了,因为 UNSAFE_componentWillMount 可能会被调用多次,谁也不会希望无谓地多次调用 AJAX 吧。

还有人会将事件监听器(或订阅)添加到 UNSAFE_componentWillMount 中,但这可能导致服务器渲染(永远不会调用 componentWillUnmount)和异步渲染(在渲染完成之前可能被中断,导致不调用 componentWillUnmount)的内存泄漏。

人们通常认为 UNSAFE_componentWillMountcomponentWillUnmount 是成对出现的,但这并不能保证。只有调用了 componentDidMount 之后,React 才能保证稍后调用 componentWillUnmount 进行清理。因此,添加监听器/订阅的推荐方法是使用 componentDidMount 生命周期。

(4.6.2.2)废弃 UNSAFE_componentWillReceiveProps 的原因

有时候组件在 props 发生变化时会产生副作用。与 UNSAFE_componentWillUpdate 类似,UNSAFE_componentWillReceiveProps 可能在一次更新中被多次调用。因此,避免在此方法中产生副作用非常重要。相反,应该使用 componentDidUpdate,因为它保证每次更新只调用一次。

UNSAFE_componentWillReceiveProps 是考虑到因为父组件引发渲染可能要根据 props 更新 state 的需要而设立的。新的 getDerivedStateFromProps 实际上与 componentDidUpdate 一起取代了以前的 UNSAFE_componentWillReceiveProps 函数。

(4.6.2.3)废弃 UNSAFE_componentWillUpdate 的原因

有些人使用 UNSAFE_componentWillUpdate 是出于一种错误的担心,即当 componentDidUpdate 触发时,更新其他组件的 state 已经”太晚”了。事实并非如此。React 可确保在用户看到更新的UI之前,刷新在 componentDidMountcomponentDidUpdate 期间发生的任何 setState 调用。

通常,最好避免这样的级联更新。当然在某些情况下,这些更新也是必需的(例如:如果你需要在测量渲染的 DOM 元素后,定位工具的提示)。不管怎样,在异步模式下使用 UNSAFE_componentWillUpdate 都是不安全的,因为外部回调可能会在一次更新中被多次调用。相反,应该使用 componentDidUpdate 生命周期,因为它保证每次更新只调用一次。

大多数开发者使用 UNSAFE_componentWillUpdate 的场景是配合 componentDidUpdate,分别获取 rerender 前后的视图状态,进行必要的处理。但随着 React 新的 suspensetime slicing、异步渲染等机制的到来,render 过程可以被分割成多次完成,还可以被暂停甚至回溯,这导致 UNSAFE_componentWillUpdatecomponentDidUpdate 执行前后可能会间隔很长时间,足够使用户进行交互操作更改当前组件的状态,这样可能会导致难以追踪的 BUG

React 新增的 getSnapshotBeforeUpdate 方法就是为了解决上述问题,因为 getSnapshotBeforeUpdate 方法是在 UNSAFE_componentWillUpdate 后(如果存在的话),在 React 真正更改 DOM 前调用的,它获取到组件状态信息更加可靠。

除此之外,getSnapshotBeforeUpdate 还有一个十分明显的好处:它调用的结果会作为第三个参数传入 componentDidUpdate,避免了 UNSAFE_componentWillUpdatecomponentDidUpdate 配合使用时将组件临时的状态数据存在组件实例上浪费内存,getSnapshotBeforeUpdate 返回的数据在 componentDidUpdate 中用完即被销毁,效率更高。

(4.6.3) 新增两个生命周期函数

React V16.3 中在废弃(这里的废弃不是指真的废弃,只是不建议继续使用,并表示在 V17.0 版本中正式删除)三个旧的生命周期函数的同时,React 还新增了两个生命周期函数:

  • static getDerivedStateFromProps
  • getSnapshotBeforeUpdate
    React V16.3 版本中加入的 static getDerivedStateFromProps 生命周期函数存在一个问题,就是在生命周期的更新阶段只有在 props 发生变化的时候才会调用 static getDerivedStateFromProps,而在调用了 setStateforceUpdate 时则不会。

React 官方也发现了这个问题,并在 React V16.4 版本中进行了修复。也就是说在更新阶段中,接收到新的 props,调用了 setStateforceUpdate 时都会调用 static getDerivedStateFromProps。具体在下面讲到这个函数的时候有详细说明。

(4.6.4) React 生命周期梳理

React 生命周期主要分为三个阶段:

  • 挂载阶段
  • 更新阶段
  • 卸载阶段
(4.6.4.1) 挂载阶段

挂载阶段也可以理解为初始化阶段,也就是把我们的组件插入到 DOM 中。这个阶段的过程如下:

  • constructor
  • getDerivedStateFromProps
  • UNSAVE_componentWillMount
  • render
  • (React Updates DOM and refs)
  • componentDidMount

constructor
组件的构造函数,第一个被执行。如果在组件中没有显示定义它,则会拥有一个默认的构造函数。如果我们显示定义构造函数,则必须在构造函数第一行执行 super(props),否则我们无法在构造函数里拿到 this,这些都属于 ES6 的知识。

在构造函数中,我们一般会做两件事:

  • 初始化 state
  • 对自定义方法进行 this 的绑定
constructor(props) {
    super(props);

    this.state = {
      width,
      height: 'atuo',
    }

    this.handleChange1 = this.handleChange1.bind(this);
    this.handleChange2 = this.handleChange2.bind(this);
}

getDerivedStateFromProps
使用方式:

//static getDerivedStateFromProps(nextProps, prevState)

class Example extends React.Component {
  static getDerivedStateFromProps(props, state) {
    //根据 nextProps 和 prevState 计算出预期的状态改变,返回结果会被送给 setState
    // ...
  }
}

新的 getDerivedStateFromProps 是一个静态函数,所以不能在这函数里使用 this,简单来说就是一个纯函数。也表明了 React 团队想通过这种方式防止开发者滥用这个生命周期函数。每当父组件引发当前组件的渲染过程时,getDerivedStateFromProps 会被调用,这样我们有一个机会可以根据新的 props 和当前的state来调整新的 state

这个函数会返回一个对象用来更新当前的 state,如果不需要更新可以返回 null。这个生命周期函数用得比较少,主要用于在重新渲染期间手动对滚动位置进行设置等场景中。该函数会在挂载时,在更新时接收到新的 props,调用了 setStateforceUpdate 时被调用。

新的 getDerivedStateFromProps 实际上与 componentDidUpdate 一起取代了以前的 UNSAFE_componentWillReceiveProps 函数。UNSAFE_componentWillReceiveProps 也是考虑到因为父组件引发渲染可能要根据 props 更新 state 的需要而设立的。

UNSAVE_componentWillMount
UNSAFE_componentWillMount() 在挂载之前被调用。它在 render() 之前调用,因此在此方法中同步调用 setState() 不会触发额外渲染。通常,我们建议使用 constructor() 来初始化 state。避免在此方法中引入任何副作用或订阅。如遇此种情况,请改用 componentDidMount()

此方法是服务端渲染唯一会调用的生命周期函数。UNSAFE_componentWillMount() 常用于当支持服务器渲染时,需要同步获取数据的场景。

render
这是 React 中最核心的方法,class 组件中唯一必须实现的方法。

render 被调用时,它会检查 this.propsthis.state 的变化并返回以下类型之一:

  • 原生的 DOM,如 div
  • React 组件
  • 数组或 Fragment
  • Portals(插槽)
  • 字符串和数字,被渲染成文本节点
  • Booleannull,不会渲染任何东西
    render() 函数应该是一个纯函数,里面只做一件事,就是返回需要渲染的东西,不应该包含其它的业务逻辑,如数据请求,对于这些业务逻辑请移到 componentDidMountcomponentDidUpdate 中。

componentDidMount
componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。依赖于DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅

你可以在 componentDidMount() 里直接调用 setState()。它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在render() 两次调用的情况下,用户也不会看到中间状态。

请谨慎使用该模式,因为它会导致性能问题。通常,你应该在 constructor() 中初始化 state。如果你的渲染依赖于 DOM 节点的大小或位置,比如实现 modalstooltips 等情况下,你可以使用此方式处理

(4.6.4.2) 更新阶段

更新阶段是指当组件的 props 发生了改变,或组件内部调用了setState或者发生了 forceUpdate,则进行更新。

这个阶段的过程如下:

  • UNSAFE_componentWillReceiveProps
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • UNSAFE_componentWillUpdate
  • render
  • getSnapshotBeforeUpdate
  • (React Updates DOM and refs)
  • componentDidUpdate

UNSAFE_componentWillReceiveProps
UNSAFE_componentWillReceiveProps 是考虑到因为父组件引发渲染可能要根据 props 更新 state 的需要而设立的。UNSAFE_componentWillReceiveProps 会在已挂载的组件接收新的 props 之前被调用。如果你需要更新状态以响应 prop 更改(例如,重置它),你可以比较 this.propsnextProps 并在此方法中使用 this.setState() 执行 state 转换。

如果父组件导致组件重新渲染,即使 props 没有更改,也会调用此方法。如果只想处理更改,请确保进行当前值与变更值的比较。在挂载过程中,React 不会针对初始 props 调用UNSAFE_componentWillReceiveProps()。组件只会在组件的 props 更新时调用此方法。调用 this.setState() 通常不会触发 UNSAFE_componentWillReceiveProps()

getDerivedStateFromProps
这个方法在挂载阶段已经讲过了,这里不再赘述。记住该函数会在挂载时,在更新时接收到新的 props,调用了 setStateforceUpdate 时被调用。它与componentDidUpdate一起取代了以前的 UNSAFE_componentWillReceiveProps 函数。

shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState) {
  //...
}

它有两个参数,根据此函数的返回值来判断是否进行重新渲染,true 表示重新渲染,false 表示不重新渲染,默认返回 true。注意,首次渲染或者当我们调用 forceUpdate 时并不会触发此方法。此方法仅用于性能优化。

因为默认是返回true,也就是只要接收到新的属性和调用了 setState 都会触发重新的渲染,这会带来一定的性能问题,所以我们需要将this.propsnextProps 以及 this.statenextState 进行比较来决定是否返回 false,来减少重新渲染,以优化性能。请注意,返回false 并不会阻止子组件在 state 更改时重新渲染。

但是官方提倡我们使用内置的 PureComponent 来减少重新渲染的次数,而不是手动编写 shouldComponentUpdate 代码。PureComponent 内部实现了对 propsstate 进行浅层比较。

如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()render()componentDidUpdate()。官方说在后续版本,React 可能会将 shouldComponentUpdate 视为提示而不是严格的指令,并且,当返回 false 时,仍可能导致组件重新渲染。

UNSAFE_componentWillUpdate
当组件收到新的 propsstate 时,会在渲染之前调用 UNSAFE_componentWillUpdate()。使用此作为在更新发生之前执行准备更新的机会。初始渲染不会调用此方法。但是你不能此方法中调用 this.setState()。在 UNSAFE_componentWillUpdate() 返回之前,你也不应该执行任何其他操作(例如,dispatch Reduxaction)触发对 React 组件的更新。

通常,此方法可以替换为 componentDidUpdate()。如果你在此方法中读取 DOM 信息(例如,为了保存滚动位置),则可以将此逻辑移至 getSnapshotBeforeUpdate() 中。

render
这个方法在挂载阶段已经讲过了,这里不再赘述。

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate(prevProps, prevState) {
  //...
}

getSnapshotBeforeUpdate 生命周期方法在 render 之后,在更新之前(如:更新 DOM 之前)被调用。给了一个机会去获取 DOM 信息,计算得到并返回一个 snapshot,这个 snapshot 会作为 componentDidUpdate 的第三个参数传入。如果你不想要返回值,请返回 null,不写的话控制台会有警告。

并且,这个方法一定要和 componentDidUpdate 一起使用,否则控制台也会有警告。getSnapshotBeforeUpdatecomponentDidUpdate 一起,这个新的生命周期涵盖过时的 UNSAFE_componentWillUpdate 的所有用例。

getSnapshotBeforeUpdate(prevProps, prevState) {
  console.log('#enter getSnapshotBeforeUpdate');
  return 'foo';
}

componentDidUpdate(prevProps, prevState, snapshot) {
  console.log('#enter componentDidUpdate snapshot = ', snapshot);
}

上面这段代码可以看出来这个 snapshot 怎么个用法,snapshot 乍一看还以为是组件级别的某个“快照”,其实可以是任何值,到底怎么用完全看开发者自己,getSnapshotBeforeUpdatesnapshot 返回,然后 DOM 改变,然后 snapshot 传递给 componentDidUpdate

官方给了一个例子,用 getSnapshotBeforeUpdate 来处理 scroll,并且说明了通常不需要这个函数,只有在重新渲染过程中手动保留滚动位置等情况下非常有用,所以大部分开发者都用不上,也就不要乱用

componentDidUpdate

componentDidUpdate(prevProps, prevState, snapshot) {
  //...
}

componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行此方法。在这个函数里我们可以操作 DOM,和发起服务器请求,还可以 setState,但是注意一定要用if 语句控制,否则会导致无限循环。

componentDidUpdate(prevProps) {
  // 典型用法(不要忘记比较 props):
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}

如果组件实现了 getSnapshotBeforeUpdate() 生命周期,则它的返回值将作为 componentDidUpdate() 的第三个参数 snapshot 参数传递。否则此参数将为 undefined

(4.6.4.3) 卸载阶段

卸载阶段,这个阶段的生命周期函数只有一个:

componentWillUnmount
componentWillUnmount() 会在组件卸载及销毁之前直接调用。我们可以在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在componentDidMount()中创建的订阅等。注意不要在这个函数里调用setState(),因为组件不会重新渲染了。

static getDerivedStateFromError()

static getDerivedStateFromError(error) {
  //...
}

此生命周期会在后代组件抛出错误后被调用。它将抛出的错误作为参数,并返回一个值以更新 stategetDerivedStateFromError() 会在渲染阶段调用,因此不允许出现副作用。如遇此类情况,请改用 componentDidCatch()

componentDidCatch()

componentDidCatch(error, info) {
  //...
}

此生命周期在后代组件抛出错误后被调用。它接收两个参数:

  1. error —— 抛出的错误。
  2. info —— 带有componentStack key 的对象,其中包含有关组件引发错误的栈信息。
    componentDidCatch() 会在“提交”阶段被调用,因此允许执行副作用。它应该用于记录错误之类的情况:

如果发生错误,你可以通过调用 setState 使用 componentDidCatch() 渲染降级 UI,但在未来的版本中将不推荐这样做。可以使用静态getDerivedStateFromError() 来处理降级渲染。

(4.7) React-请求远程数据

ajax请求框架Axios请求远程数据

1. 安装
Axios的安装可以使用npm来进行安装,你可以直接在项目根目录下,输入下面的代码。

npm install -save axios

2. axios请求远程数据
安装好axiso之后,需要在使用ajax的地方先引入axios,比如现在想在Xiaojiejie.js中使用axios,写入下面的代码进行引入:

import axios from 'axios'

引入后,可以在componentDidMount生命周期函数里请求ajax,我也建议在componentDidMount函数里执行,因为在render里执行,会出现很多问题,比如一直循环渲染;在componentWillMount里执行,在使用RN时,又会有冲突。所以强烈建议在componentDidMount函数里作ajax请求。

componentDidMount(){
    axios.post('https://web-api.juejin.im/v3/web/wbbr/bgeda')
        .then((res)=>{console.log('axios 获取数据成功:'+JSON.stringify(res))  })
        .catch((error)=>{console.log('axios 获取数据失败'+error)})
}

上面的代码是以掘金的一个接口为例,做了一次ajax请求。并且请求到了数据,给我们返回了。

(4.8) React-Mock数据

mock数据网站:

https://www.fastmock.site/#/projects

创建项目,创建接口,添加数据

将远程获得的数据赋值给数组:


componentDidMount(){
    axios.get('xxxx')
        .then((res)=>{
            console.log('axios 获取数据成功:'+JSON.stringify(res))

            this.setState({
                list:res.data.data
            })
          })
        .catch((error)=>{console.log('axios 获取数据失败'+error)})
}

(4.9) 实现react动画

(4.9.1) 用css3实现react动画

1. 新建组件Boss
需要给“小姐姐服务菜单”增加一个Boss服务人物,点击一下按钮就会自动出现"Boss级人物-孙悟空",不要管什么恰当不恰当了,咱们是为了练习一下动画。在src文件夹下,新建一个Boss.js文件

import React, { Component } from 'react';
class Boss extends Component {
    constructor(props) {
        super(props);
        this.state = {  }
    }
    render() { 
        return ( 
            <div>
                <div>BOSS级人物-孙悟空</div>
                <div><button>召唤Boss</button></div>
            </div>
          );
    }
}

export default Boss;

2. 业务逻辑

目前组件没有任何业务逻辑,只有一个UI,这是没办法实现动画效果的。业务逻辑是点击按钮的时候可以改变字的''显示隐藏。

要实现这个业务逻辑,先在constructor里增加stateisShow,详情请看下面的代码。

this.state = { 
    isShow:true
}

然后把“字”的部分,增加className,并用isShow进行控制。

<div className={this.state.isShow ? 'show' : 'hide'}>BOSS级人物-孙悟空</div>

需要点击按钮时,有响应的事件,所以需要一个方法,我们编写一个toToggole()方法

toToggole(){
    this.setState({
        isShow:this.state.isShow ? false : true
    })
}

意思就是当isShowtrue时,我们赋值false;当isShowfalse时,我们赋值true

有了方法后,可以给<button>加上onClick响应事件了

<div><button onClick={this.toToggole}>召唤Boss</button></div>

写完这个事件,还是需要到constructor里绑定一下this

constructor(props) {
    super(props);
    this.state = { 
        isShow:true
    }
    this.toToggole = this.toToggole.bind(this);
}

这样我们的基本业务逻辑就算写完了,可以把代码加入到Xiaojiejie组件中,看一下效果了。

3. 加入CSS动画
在页面上看不出任何的效果,如果你打开浏览器控制台是可以看到每次点击按钮,class都会变化的。界面没变化,知识我们没有写CSS。现在可以在style.css里写样式,代码如下:

.show{ opacity: 1; transition:all 1.5s ease-in;}
.hide{opacity: 0; transition:all 1.5s ease-in;}

这样就用CSS3实现了React中动画,这知识最简单的实践动画

(4.9.2) css3的Keyframes动画

transition只能作一些最简单的动画,如果你想稍微复杂点,transition就做不出来了。这时候就可以用CSS3中的关键帧动画keyframes

** 1. keyframes介绍**
此属性与animation属性是密切相关的,keyframes译成中文就是关键帧,最早接触这个关键帧的概念是字flash中,现在Flash已经退出历史舞台了。他和transition比的优势是它可以更加细化的定义动画效果。比如我们之前的按钮隐藏动画,不仅可以设置透明度,还可以设置颜色。

@keyframes hide-item{
    0% {
        opacity:1;
        color:yellow;
    }
    50%{
        opacity: 0.5 ;
        color:red;
    }
    100%{
        opacity:0;
        color: green;
    }
}

2.使用动画
使用动画的关键词是animation,然后后边跟上你的制作的动画名称,如下面这段代码。

.hide{ animation:hide-item 2s ease-in ; }

这句的意思就是,使用hide-item动画,持续时间是2秒钟,然后缓动效果是由慢到快(开始的时候慢,之后快)。

但是你会发现,动画执行一遍后又恢复了原状,这个是因为没设置forwards属性,它是用来控制停止到最后一帧的。 我们把代码改写成下面的样子。

.hide{ animation:hide-item 2s ease-in forwards; }

完整代码css:

.show{ animation:show-item 2s ease-in forwards; }
.hide{ animation:hide-item 2s ease-in forwards; }

@keyframes hide-item{
    0% {
        opacity:1;
        color:yellow;
    }
    50%{
        opacity: 0.5 ;
        color:red;
    }
    100%{
        opacity:0;
        color: green;
    }
}

@keyframes show-item{
    0% {
        opacity:0;
        color:yellow;
    }
    50%{
        opacity: 0.5 ;
        color:red;
    }
    100%{
        opacity:1;
        color: green;
    }
}

keyframes也是只能实现很简单的动画效果,一些复杂的动画最好还是使用别人造好的轮子,下节课继续学习React中的动画吧。

(4.9.3) react-transition-group

React生态中有很多第三方的动画组件,你应该学习一下react-transition-group动画组件

1. 安装
使用它要先进行安装,这里使用npm的形式进行安装了,当然也可以使用yarn

先用VSCode打开项目根目录,然后打开终端,输入下面的命令,进行安装:

npm install react-transition-group --save

安装好后,你可以先去github上来看一下文档,他是有着三个核心库(或者叫组件)。

  • Transition

  • CSSTransition

  • TransitionGroup

2. 使用CSSTransition

其实这个库用起来根ng-animate差不多,先来看看如何使用CSSTransition

先用import进行引入,代码如下:

import { CSSTransition } from 'react-transition-group'

引入后便可以使用了,使用的方法就和使用自定义组件一样,直接写<CSSTransition>,而且不再需要管理className了,这部分由CSSTransition进行管理。修改上节课写的Boss.js文件里的render区域。

render() { 
    return ( 
        <div>
            <CSSTransition 
                in={this.state.isShow}   //用于判断是否出现的状态
                timeout={2000}           //动画持续时间
                classNames="boss-text"   //className值,防止重复
            >
                <div>BOSS级人物-孙悟空</div>
            </CSSTransition>
            <div><button onClick={this.toToggole}>召唤Boss</button></div>
        </div>
        );
}

需要注意的是classNames这个属性是由s的,如果你忘记写,会和原来的ClassName混淆出错,这个一定要注意。

然后你就可以到CSS中改写style了。在修改样式之前,有那些类名。

  • xxx-enter: 进入(入场)前的CSS样式;
  • xxx-enter-active:进入动画直到完成时之前的CSS样式;
  • xxx-enter-done:进入完成时的CSS样式;
  • xxx-exit:退出(出场)前的CSS样式;
  • xxx-exit-active:退出动画知道完成时之前的的CSS样式。
  • xxx-exit-done:退出完成时的CSS样式。

知道了这些要设置的CSS,就可以删除原来写的CSS了,把下面的代码写上:

.input {border:3px solid #ae7000}

.boss-text-enter{
    opacity: 0;
}
.boss-text-enter-active{
    opacity: 1;
    transition: opacity 2000ms;

}
.boss-text-enter-done{
    opacity: 1;
}
.boss-text-exit{
    opacity: 1;
}
.boss-text-exit-active{
    opacity: 0;
    transition: opacity 2000ms;

}
.boss-text-exit-done{
    opacity: 0;
}

这时候你的动画样式就正常了,你回发现我们再也不用自己管理className了,而是完全交给了react-transition-group来作。

3. unmountOnExit属性
我们给<CSSTransition>加上unmountOnExit,加上这个的意思是在元素退场时,自动把DOM也删除,这是以前用CSS动画没办法做到的。

render() { 
    return ( 
        <div>
            <CSSTransition 
                in={this.state.isShow}   //用于判断是否出现的状态
                timeout={2000}           //动画持续时间
                classNames="boss-text"   //className值,防止重复
                unmountOnExit
            >
                <div>BOSS级人物-孙悟空</div>
            </CSSTransition>
            <div><button onClick={this.toToggole}>召唤Boss</button></div>
        </div>
        );
}

(4.9.4) 多DOM动画制作和编写

控制多个动画react-transition-group这个动画库也是可以做到的。

1. 使用TransitionGrop

它就是负责多个DOM元素的动画的,我们还是拿小姐姐这个案例作例子,现在可以添加任何的服务项目,但是都是直接出现的,没有任何动画,现在就给它添加上动画。添加动画,先引入transitionGrop

直接打开/src/Xiaojiejie.js的文件,然后在最顶部同时

import {CSSTransition , TransitionGroup} from 'react-transition-group'

引入之后,就可以使用这个组件了,方法是在外层增加<TransitionGroup>标签。

<ul ref={(ul)=>{this.ul=ul}}>
    <TransitionGroup>
    {
        this.state.list.map((item,index)=>{
            return (
                <XiaojiejieItem 
                key={index+item}  
                content={item}
                index={index}
                deleteItem={this.deleteItem.bind(this)}
                />
            )
        })
    }
    </TransitionGroup>
</ul>

这个需要放在循环的外边,这样才能形成一个组动画,但是只有这个<TransitonGroup>是不够的,你还是需要加入<CSSTransition>,来定义动画。

2. 加入标签

可以完全仿照上节课的经验,为Xiaojiejie组件,加上具体的动画设置,就可以实现多DOM元素的动画效果了。代码如下:

<ul ref={(ul)=>{this.ul=ul}}>
    <TransitionGroup>
    {
        this.state.list.map((item,index)=>{
            return (
                <CSSTransition
                    timeout={1000}
                    classNames='boss-text'
                    unmountOnExit
                    appear={true}
                    key={index+item}  
                >
                    <XiaojiejieItem 
                    content={item}
                    index={index}
                    deleteItem={this.deleteItem.bind(this)}
                    />
                </CSSTransition>
            )
        })
    }
    </TransitionGroup>
</ul>  
<Boss />
</Fragment>

总结:React动画还有很多知识,能做出很多酷炫的效果,完全可以单独分出来一个岗位,我在工作中用的都是比较简单的动画,用react-transition-group动画已经完全可以满足我的日常开发需求了。如果你想学习更多的React动画知识,可以看看文档或者书。

posted @ 2021-03-17 16:52  Mr*宇晨  阅读(263)  评论(0编辑  收藏  举报