一、简介(背景)
本文篇幅较多,建议耐心看完,我相信多少会对你有所帮助!
1.1、概念
它是一个将数据渲染为HTML视图 的js库
1.2、原生js痛点
- 用dom的API去操作dom,繁琐且效率低
- 用js直接操作dom,浏览器会进行大量的回流和重绘
- 原生js没有组件化的编程方案,代码复用性低,哪怕有模块话的概念,但模块化也只能拆解一个个js,对样式和结构也没办法拆解,组件化就相当于3剑客整体拆解,成为一个个的小功能
1.3、React特点
- 采用组件化模式,声明式编码,提高开发效率和组件复用性
- 在React Native中可以用react预发进行安卓、ios移动端开发
- 使用虚拟dom和有些的diffing算法,尽量减少与真实dom的交互,提高性能
二、React初体验
2.1、html中使用react
需求:往div中添加一个h1标签
代码注解:这里涉及到3个库文件,一个react(核心库),react-dom(用于支持react操作dom的)还有一个babel(将jsx语法转成js语法,因为浏览器只能识别js),还有就是script标签中的type需要写成text/babel,因为script标签中要写jsx的语法,这里再说一点,就是你引入了react-dom库,就可以使用ReactDOM变量了,跟以前学习jquery一样,引入jquery文件就可以使用$和jquery是同样的道理
2.1.1、 创建虚拟dom的方式
①使用原生js的方式去写(一般不用)
代码注解:使用原生js来创建虚拟dom时,此时script标签的type就不需要写成text/babel,写成原来的text/javascript就可以了,同时也可以删除babel库了,因为不需要它进行转换,浏览器本身就能识别
②使用jsx语法创建(就是最开始的那种方式)
这里可能会有小伙伴会问,为什么不用js来创建dom呢?我看写起来也还行啊,这是因为你还没见过结构嵌套的情况,当需求改为h1标签内再嵌套一个span标签,你怎么办?是不是后面还得再继续套React.createElement(‘span’,{},‘span内容’) ,那如果继续嵌套呢?是不是废了。。。,所以这里就能看出jsx的好处了,其实babel将jsx转换后的代码就相对于用原生js写的那样,只不过这些不用你写,babel帮你转换
2.1.2、 关于创建的虚拟dom
- 本质上其实就是一个object对象;
- 虚拟dom上的属性比较少,真实dom属性多,因为虚拟dom只在recat内部使用,用不到那么多的属性
- 虚拟dom最终会被react转换成真实dom,呈现再页面上
2.2、JSX语法
- 定义虚拟dom时不要用引号
- 标签中引入js表达式要用{}
- 如果在jsx要写行内样式需要使用style={{coler:red}}形式
- 样式的类名指定不能写class,要写className;
- 只有一个根标签
- 标签必须闭合
- 标签首字母 ①若小写字母开头,则会将该标签转为html同名标签,如果没找到,则会报错; ②若大写字母开头,则会认为是组件,它就会去找对应的组件,如果没找到,就会报组件未定义的错误;
三、React面向组件编程
3.1 安装开发者工具(React Developer Tools )
这里工具的具体安装方式就不多说了,由两种方式,一种是翻墙,直接在chrome浏览的商城去下载,还有一种方式是在浏览器扩展程序中打开开发者模式,然后导入已下载的插件文件即可
3.2 组件的分类
1. 函数式组件(适用于简单组件)
2. 类式组件(适用于复杂组件)
3.3 组件实例对象的三大属性
3.3.1、 state
这里我们用一个小需求来说说state和组件内绑定方法这两个问题,需求:点击文本,改变天气状态
上述图片中的例子这样的写法并不能实现我们的需求,为什么?因为changeWeather方法中打印的this是undefined,不是该weather组件的实例对象,自然也就无法拿到该实例对象上的state等属性,更加不用说去改变state中的状态了。。。那为什么会这样的?原因就写在图中,那我们要怎么处理才能让该方法拿到该组件的实例对象呢?来,看下面。。
在构造函数中加一句这个语句就可以了,那这行代码是什么意思呢?
构造函数中的this永远指向该组件的实例对象,所以=右侧意思就是该组件实例对象自身此时还没有该方法,他就会去原型对象上看有没有,显然这里是有的,然后调用bind方法,该方法做两件事,一、创建一个新的函数;二、函数内的this指向bing()括号中传入的,显然这里是组件实例对象;右侧执行完后,将该方法赋值给了实例对象本身的一个方法名(changeWeather),这样实例对象本身就有一个changeWeather方法了,并且内部this就是组件实例对象,所以此时render中点击调用的那个方法,实际上是this实例对象自身的一个changeWeather方法,而不是写在类中的那个原型对象上的changeWeather方法,我们为了不混淆两个重名的方法,我们区分看一下
好了,接下来我们要做改变状态这件事了,我们要通过react中的内置API(setState方法),不能直接更改state,就像下面这样。
好了,到现在需求已经满足了,但我们要对其简化。。。
上述将state和自定义方法直接写在了类中,这样写的意思就是说,给类组件的实例对象添加了一个state属性和自定义方法,而且这里的自定义方法必须写成箭头函数的形式,因为箭头函数内部是没有this指向的,他会去他外部找this,那此时外部的this就是组件实例对象
总结:
- state是组件实例对象最重要的属性,必须是对象的形式
- 组件被称为状态机,通过更改state的值来达到更新页面显示(重新渲染组件)
- 组件render中的this指的是组件实例对象
- 状态数据不能直接赋值,需要用setState()
- 组件自定义方法中的this为undefined,怎么解决?①将自定义函数改为表达式+箭头函数的形式(推荐) ②在构造器中用bind()强制绑定this
3.3.2、 props
props就是在调用组件的时候在组件中添加属性传到组件内部去使用
简单demo:
接下来这里我们想对传入的props属性进行一些必传、默认值、类型的校验,所以就需要用到一个prop-types库
下载:npm i prop-types --save
引入:import PropTypes from ‘prop-types’
构造器问题
构造器仅用于以下两种情况:
- 通过this.state赋值对象来初始化内部的state
- 为事件处理函数绑定实例(就是上面的this.changeWeather=this.changeWeather.bind(this))
但是上述的这两个点我们都有简单的方法来实现,state我们可以用state={name:11}这种直接赋值的方式来实现,自定义事件的话可以通过赋值语句+箭头函数的方式来实现,所以一般开发过程中都不写构造器,还有就是如果一定要写构造器,那么构造器是否接受props,是否传递给props,取决于是否要在构造器中通过this访问props
函数组件中的props
因为函数组件没有组件实例对象,所以其他两个state和refs是没有的,只有props属性可用,直接在()中接受props对象即可,函数内部就可以结构使用props中的值了
总结:
- 每个组件都会有props属性
- 组件标签的所有属性都保存在props
- 组件内部不能改变外部传进来的props属性值
3.3.3 refs属性
- 字符串形式的ref(这种方式已过时,不推荐使用,因为效率低)
refs是组件实例对象中的属性,它专门用来收集那些打了ref标签的dom元 素,比方说,组件中的input添加了一个ref=“input1”,那么组件实例中的refs就={input1:input(真实dom)},这样就可以通过this.refs.input1拿到input标签dom了,就不需要想原生js那样通过添加属性id,然后通过document.getElementById(“id”)的方式拿
- 回调函数形式
直接让ref属性=一个回调函数,为什么这里说是回调函数呢?因为这个函数是你定义的,但不是你调用的,是react在执行render的时候,看到ref属性后跟的是函数,他就会帮你调用了,然后把当前dom标签当成形参传入,所以上述例子这样写,就相当于把当前节点dom赋值给了this.input1,那这个this指的是谁呢?不难理解,这里是箭头函数,本身没有this指向,所以这个this得看外层的,该函数外层是render函数体内,所以this就是组件实例对象,所以ref={c=>this.input1=c}意思就是给组件实例对象添加一个input1,最后要取对应节点dom也直接从this(组件实例中取)
这里我们再来探讨一个小问题,就是这个ref的回调函数会被执行几次的问题?第一次在页面刚进来执行render渲染的时候,react会把当前节点当成参数赋值给组件实例,当组件更新的时候(状态改变时),它又会执行一次render,这个时候,react官方说明了这点,它会执行两次,第一次是将原先的实例属性清空,传入的是null,第二次再把当前节点传如赋值给组件实例的input1属性,这个在一般开发过程中无关紧要,所以大家知道下有这个情况就可以了,当然它也有解决办法:通过将ref的回调函数定义成类的绑定函数的方式,即ref={this.func},func是定义在类中的方法,func=©=>{this.input1=c} ,这种方式就可以解决上述执行两次的问题,一般开发中我们写成回调的形式就可以了
- createRef
代码解读:createRef()方法是React中的API,它会返回一个容器,存放被ref标记的节点,但该容器是专人专用的,就是一个容器只能存放一个节点;
当react执行到div中第一行时,发现input节点写了一个ref属性,又发线在上面创建了myRef容器,所以它就会把当前的节点存到组件实例的myRef容器中
注意:如果你只创建了一个ref容器,但多个节点使用了同一个ref容器,则最后的会覆盖掉前面的节点,所以,你通过this.ref容器.current拿到的那个节点是最后一个节点
四、 React中的事件处理
- 通过onXxxx属性指定事件处理函数(小驼峰形式)
- 通过event.target可以得到发生事件的Dom元素
- 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
4.1 高阶函数和函数柯里化
我们写个用到高阶函数和函数柯里化的例子:
...其余代码省略
saveFormData=(type)=>{
return (event)=>{
this.setState({
[type]:event.target.value
})
}
}
<form>
<input onChange={ this.saveFormData('username') } />
<input onChange={ this.saveFormData('password') } />
<button>登录</button>
</form>
上面代码的saveFormData函数其实就是高阶函数,因为它返回的值是一个函数,而且这个函数就是通过函数柯里化的方式在调用(先是saveFormData函数接收一个参数type,随后返回函数,然后再接收别的参数,然后最终来处理结果(设置state值))
- 提问1:那为什么要写成这样呢?我直接在函数saveFormData中同时接收两个参数不行吗? 答:不行,因为你拿不到event,因为这是react帮你处理的
- 提问2:那还有没有别的方式来实现,不用柯里化处理方式? 答:有
//将下面函数的调用方式改为这样就可以了,函数接收方式也改下
saveFormData=(type,value)=>{
this.setState({
[type]:value
})
}
<form>
<input onChange={ (event)=>{this.saveFormData('username',event.target.value)} } />
<input onChange={ (event)=>{this.saveFormData('password',event.target.value)} } />
<button>登录</button>
</form>
五、 生命周期函数
5.1 老版的生命周期过程:
挂载时:先执行构造器(constructor)=》组件将要挂载(componentWillMount)=》组件挂载渲染(render)=》组件挂载完成(componentDidMount)=》组件销毁(componentWillUnmount)
组件内部状态更新:组件是否应该更新(shouldComponentUpdate)=》组件将要更新(componentWillUpdate)=》组件更新渲染(render)=》组件更新完成(componentDidUpdate)
强制更新:调用this.forceUpdate(),这个api和setState一样都是react自带的,一般这个强制更新很少用,它的执行流程就是比上述的正常更新流程少一步询问是否更新(shouldComponentUpdate)
父组件重新render:调用组件将要接收新props(componentWillReceiveProps)=》组件是否应该更新(shouldComponentUpdate)=》组件将要更新(componentWillUpdate)=》组件更新渲染(render)=》组件更新完成(componentDidUpdate)
注意:上述加粗的函数,只有在父组件状态发生改变了,重新调用render时才会调用子组件的componentWillReceiveProps函数,父组件第一次引用子组件的时时不会调用的
5.2 新版生命周期函数
新版生命周期函数和旧版的差别:新版即将废弃老的3个钩子(componentWillMount、componentWillReceiveProps、componentWillUpdate),新增了2个钩子(getDerivedStateFromProps、getSnapshotBeforeUpdate)
六、 DOM的Diff算法
虚拟DOM中的key的作用:
当状态中的数据发生改变时,react会根据【新数据】生成【新虚拟DOM】,随后react会进行【新虚拟DOM】和【旧虚拟DOM】的diff算法比较,具体的比较规则如下:
- 若【旧DOM】中找到了与【新DOM】相同的key,则会进一步判断两者的内容是否相同,如果也一样,则直接使用之前的真实DOM,如果内容不一样,则会生成新的真实DOM,替换掉原先的真实DOM
- 若【旧DOM】中没找到与【新DOM】相同的key,则直接生成新的真实DOM,然后渲染到页面
用index作为key可能引发的问题
- 若对数据进行:逆序添加、逆序删除等破坏顺序的操作时会产生不必要的真实DOM更新,造成效率低下
- 如果结构中还包含输入类的dom,会产生错误dom更新,出现界面异常
开发中如何选择key
- 最好选中标签的唯一标识id、手机号等
- 如果只是简单的展示数据,用index也是可以的
七、 脚手架
使用create-react-app(脚手架工具)创建一个初始化项目
1、下载脚手架工具:npm i -g create-react-app
2、创建引用:create-react-app my-app
3、运行应用:cd my-app(进入应用文件夹),npm start(启动应用)
7.1 React脚手架配置代理
- 方法一
在package.json中追加如下配置
"proxy":"http://localhost:5000"
说明:
1、优点:配置简单,前端请求资源可以不加任何前缀
2、缺点:不能配置多个代理(如果请求的不同服务器就不行)
3、工作方式:当请求了自身3000端口不存在的资源时,那么会转发给5000端口(优先会匹配自身的资源,如果自己有就不会请求5000端口了)
- 方法二
1、第一步:创建代理配置文件
在src下创建配置文件:src/setupProxy.js
2、编写代理配置规则
说明:
1、优点:可以配置多个代理,可以灵活控制请求是否走代理
2、缺点:配置繁琐,前端请求资源时必须加前缀
八、 消息订阅-发布机制
背景:
原先react传递数据基本用的是props,而且只能父组件传给子组件,如果子组件要传数据给父组件,只能先父组件传一个函数给子组件,子组件再调用该方法,把数据作为形参传给父组件,那考虑一个事情,兄弟间组件要如何传递数据呢?这就要引出下面这个消息订阅-发布机制
工具库:PubSubJs
下载:
npm install pubsub-js --save
使用:
- 先引入:import PubSub from “pubsub-js”
- 要接收数据方订阅:
PubSub.subscribe('消息名',(data)=>{ console.log(data) })
- 传递数据方发布:
PubSub.publish('消息名',data)
九、React路由
9.1 相关理解
9.1.1.SPA理解
- 单页面应用(single page web application SPA)
- 整个页面只有一个完整的页面(html文件)
- 点击页面的链接不会刷新页面,只会做页面的局部更新
- 数据都需要通过ajax请求获取,并在前端异步展示
9.1.2.路由的理解
1、 什么是路由?
- 一个路由就是一个映射关系
- key永远为路径,value可能是function或者component
2、 路由分类
- 后端路由a )理解:后端路由的key还是路径,只不过value是上述说的function,这个在学习node和Express的时候应该看到过,如下图 b) 注册路由:router.get(path,function(req,res){…}) c) 工作过程:当node接收到一个请求时,会根据请求路径去匹配对应的路由,然后调用对应路由中的函数来处理请求,返回响应数据
- 前端路由a) 浏览器端路由,value是对应组件(component),用于展示页面内容 b) 注册路由:
<Route path='/test',component={Test}>
c) 工作过程:当浏览器path变为/test时,当前路由组件就会变成Test组件
9.2 react-router-dom
9.2.1 理解
- 它是react的一个插件库
- 专门用来实现一个SPA单页面应用
- 基于react的项目基本都用它
9.2.2 常用API
1、内置组件
<BrowserRouter>
<HashRouter>
<Route>
<Redirect>
<Link>
<NavLink>
<Switch>
2、知识点
- 路由的基本使用
- 路由组件和一般组件
- NavLink的使用
- Switch的使用
- 路由的模糊匹配和精准匹配
- Redirect的使用
- 嵌套路由
- 向路由组件传递参数
- 路由跳转的两种模式(push、replace)
默认开启的是push模式,push模式就是说每次的点击跳转改变路径,都是往浏览器历史记录的栈中不断追加一条记录,然后你点回退按钮时,它会指向当前栈顶记录的前一条,replcae模式就是说替换掉当前的那条记录,然后你点回退的时候,就不会显示上次被替换掉的那条记录了,只会显示上上条记录,那要怎么设置为replace模式呢?直接在
<Link replace to='XXX'>
标签上添加一个replace属性即可
- 编程式路由导航(不借助link或者navLink这种手动点触发路由跳转)
就是借用history对象的api来操作路由的跳转、前进、后退
- withRouter的使用
作用:它就是专门解决在一般组件中想要使用路由组件的那几个API的这个问题的,它接收一个一般组件,然后调用后,该一般组件身上也有了路由组件的history、match等属性
如何使用:
1、先引入import { withRouter} from "react-router-dom"
2、定义一般组件class XX extends ...
3、export default withRouter( XX )
- BrowserRouter和HashRouter的区别
10、redux
10.1 redux理解
10.1.1 redux是什么?
- 它是专门做状态管理的js库,不是react插件库
- 它可以用在angular、vue、react等项目中,但与react配合用到最多
- 作用:集中式管理react应用中多个组件共享的状态
10.1.2 什么情况下需要使用它
- 某个组件的状态需要让其他组件也能拿到
- 一个组件需要改变另一个组件的状态(通信)
- 总体原则:能不用就不用,如果不用比较吃力,就可以使用
redux的工作流程
11、react-redux
原先redux是独立公司创建的,后来react公司发现开发者都这么喜欢在react项目中使用redux,所以就自己开发了一个react的插件叫react-redux
11.1、react-redux模型图
11.2、react-redux基本使用
11.2、纯函数
11.3、react-redux开发者工具
- 打开chrome网上商店,下载开发者工具Redux DevTools
- 下载完后右上方的插件图标还是不会亮的,因为它还识别不了你写的redux,所以还需要下载一个库(redux-devtools-extension)
- 然后再你的store文件中引入该库文件
import {composeWithDevTools} from redux-devtools-extension
- 然后再createStore()的第二个参数位置调用composeWithDevTools(),将之前放在这个位置的中间件传到该方法中
export default createStore(allReducer,composeWithDevTools(applyMiddleware(thunk)))
12、项目打包运行
在react脚手架中通过npm run start 来运行启动项目并打开页面,打包生成静态文件就要用到另一个命令(npm run build),它会生成一个build文件夹,一般这个生成的静态文件都是放到服务器上去运行的,那么问题来了,服务器要怎么搭建呢?
方法一:用node+express可以搭建一个简单的服务器
方法二:需要用到一个库serve,使用前需要先下载npm i serve -S
,然后直接在对应文件夹中执行serve即可,比方在这里,当前文件路径是项目根目录,直接serve build即可,就可以开启一个5000端口的服务器了
13、Hooks
13.1、React Hook /Hooks是什么?
1、Hook是React 16.8新推出的新特性/新语法
2、可以让你在函数式组件中使用state或其他特性
13.2、三个常用Hook
1、React.useState()
2、React.useEffect()
3、React.useRef()
13.3、useState
Demo:
import React,{useState} from 'react';
export default ()=>{
//useState()中接收的是state的默认值,前面是数组解构,第一个是state变量,第二个是改变state的方法,类似于setState()
const [count,setCount] =useState( 0 )
render(
<div>{count}</div>
<button onClick={()=>{setCount(count+1)}}>点我+1</button>
)
}
这里思考个问题,当你点击按钮+1时,状态改变的时候,整个函数组件就会执行,所以第四行也自然会执行,那它的状态不就又变成0了吗?还能正常+1吗?
答案:是可以正常+1,为什么呢?因为这行代码底层做了处理,当状态改变重新调用整个函数组件时,这句话是会执行,但它不会去覆盖count值,所以count值还是会正常+1
13.4、useEffect
总结:这个钩子函数相当于类组件三个钩子函数的集合,当你想用做componentDIdMount时,可以在第二个参数中加上[],表示谁都不监听,只会在第一次挂载时调用,这就相当于didMount函数了,如果你想当componentDidUpdate函数用,那么你可以在第二个参数加上你要监听更新的state值,注意是数组,如果你要当componentWillUnmount函数,则在useEffect()接收的第一个函数中返回一个函数,这个返回的函数就相当于willUnMount
13.5、useRef
14、React扩展知识
14.1、setState的两种写法
Demo:
//这里我们先统一不考虑第二个参数回调
对象式写法:
const {count} =this.state;
this.setState({ count:count+1}) //setState()方法接收一个对象
函数式写法:
this.setState((state,props)=>{ //函数式写法可以接收老的state和props数据
return {count:1}
})
简写:this.setState(state=>({count+state}))
14.2、lazyload(懒加载)
这里我们要讲的懒加载主要是针对路由组件的懒加载,就是你点击路由导航菜单,加载对应组件的时候懒加载,具体来看下面code。。。
//原先的路由组件引入:
import Demo1 from "./Demo1"
import Demo2 from "./Demo2"
<Route path='/demo1',component={Demo1}/>
<Route path='/demo2',component={Demo2}/>
//现在使用lazyload:
import React,{component,lazy} from "react // 需要用到react中的lazy函数
// 引入路由组件方式通过调用lazy(),然后接收一个函数的方式,import 引入文件也可以通过函数调用的方式
const Demo1 = lazy(()=>{import('./Demo1')})
const Demo2 = lazy(()=>{import('./Demo2')})
//路由配置那边也要改动,用Suspense标签包裹下,再传一个fallback属性,接收一个组件,你可以自定义一个加载过程中的简单组件,再资源没回来之前会显示这个传入的简单加载组件,就是为了再网速慢得情况下,页面不至于白屏,提高用户体验
<Suspense fallback={组件}>
<Route path='/demo1',component={Demo1}/>
<Route path='/demo2',component={Demo2}/>
</Suspense>
**注意:**这里引入得加载组件必须通过直接引入的方式import Loading from "./Loading"`
14.3、Fragment标签
简单一句话概括:这个标签就是用在有时页面结构层级太多,而且有些都是语法要求,实际没意义的结构层级(return()中的根节点就是这个情况),这时你就可以用Fragment标签,当然<></>在一般情况下和Fragment标签作用相同,当时有一点不一样,就是Fragment标签能接收一个key属性,而<></>什么属性都不能接收
14.4、Context
理解:这里说的Context其实就是类组件中实例对象中的一个属性,它和state、props、ref是同一级的;
作用:它可以解决多层组件之间,祖先组件要往后代组件传递数据的情况,不用再一层一层的props传
使用原理: 举个例子,要把父组件中state的值传递给孙子组件,那么在父组件的全局位置创建一个容器对象,然后用这个容器对象的Provider标签包裹父组件,同时传value={state数据},注意,这里的value字段名不能改,只能是value,然后孙子组件可以用下面的两种方式去接收数据
14.5、PureComponent(纯组件)
具体的使用:就是原先extends Component=》extends PureComponent 即可
14.6、错误边界
所谓的错误边界就是说,在实际开发过程中,组件复用是很正常的,但你很难避免调用的子组件出现错误(语法错误,或者是因为数据格式不对导致的报错,你不可能兼容到各个位置),如果没有错误边界,当子组件出现问题的时候,整个页面就都会挂掉,所以为了用户体验,让错误不影响整个页面,所以要用这个错误边界,注意:这个错误边界时要在父组件中进行处理的
版权归原作者 Ronychen’s blog 所有, 如有侵权,请联系我们删除。