第一次接触React是2016年7月份,记得刚开始学习的时候,从JQ 思想好难转变过来,成了困难户似的,哈哈。后来慢慢的尝试让自己脱离手动操作 DOM,忘记JQ,慢慢的就适应过来了,初学者一般都有这个经历吧?记得当时还是 15的版本吧,感觉已经是很久以前的事了,哎呀,老了。React 最近几年也是更新挺快,现在已经17 版本了,本文主要重学了 16.3之后的版本,写了一些笔记,帮助学习消化,希望也能帮助到大家。
1、为什么使用React
React 是一个用于构建用户界面的JS 库,具有组件化、声明式、DOM性能优化等优点。是一个渐进式的框架。
说到渐进式,这里做一个说明:就是主张最少,也就是可以只用它其中的一部分,比如,开始搭建项目时,开发者只需要用到React,就可以只引入React,当项目有了新的需求后,可以引入其他的类库,如路由库,状态管理库等。
1.1、专注于视图层
React 专注于 View 层的解决方案,就是在使用React 的时候,只要告诉React 需要的视图长什么样,或者告诉React 在什么时间点,把视图更新成什么样就可以,剩下的视图渲染、性能优化等一系列问题都交给React 搞定即可。
1.2、 组件化开发和声明式编程
在React 中,通常会把视图抽象成一个个组件,然后通过组件的自由结合来拼成完整的视图。提升开发效率,便于后期维护,测试也便捷。
在传统的项目开发中,通常采用命令式的编程方式。一步一步下命令的方式就是命令式编程。
而React 遵从的是声明式编程,逻辑清晰,易读易维护。
声明式和命令式的区别:
1、命令式编程注重过程,开发者需要告诉程序每步要怎么做
2、声明式编程注重结果,直接告诉程序要什么
1.3、Virtual DOM
在React 中,每一个组件都会生成一个虚拟DOM 树。这个DOM树会以纯对象的方式来对视图(真实DOM)进行描述。
React 会根据组件生成的虚拟DOM 来生成真实的DOM。组件中的数据变化后,组件又会生成一个新的虚拟DOM,React 会对新旧虚拟DOM 进行对比,找出发生变化的节点,然后只针对发生变化的节点进行重新渲染,这样就极大的提升了重新渲染的性能。
2、ReactDOM
// React 核心文件,如组件、Hooks、虚拟DOM...
<script src="js/react.js"></script>
// 对真实DOM 的相关操作,如将虚拟DOM 渲染到真实DOM,或者从真实DOM中获取节点
<script src="js/react-dom.js"></script>
// 以上有依赖顺序,必须先引入react.js, 再引入react-dom.js
2.1、render
ReactDOM.render(element, container[, callback])
// render 方法用于将React生成的虚拟DOM 生成到真实的 DOM 中去。
// element 是React 生成的虚拟DOM,也叫作ReactElement或 ReactNode。除此之外也可以使用字符串去实现。
// container 要放置 element的容器,它必须是一个已经存在的真实DOM节点。
// callback 是将ReactNode 渲染到 container 之后的回调函数。
2.2、hydrate
ReactDOM.hydrate(element, container[, callback])
一般配合React SSR时使用,hydrate作用于 ReactDOM 复用 ReactDOMServer 服务端渲染的内容时,能够尽可能保留其结构,并补充事件绑定等特性。
2.3、findDOMNode
ReactDOM.findDOMNode(Component)
Component 指的是 React 组件,如果该组件已经渲染到DOM 中,可以通过findDOMNode 获取真实的DOM。
2.4、unmountComponentAtNode
ReactDOM.unmountComponentAtNode(container)
用于 container 中删除已安装的 React 组件并清理其事件处理程序和状态。如果在容器中没有安装组件,调用这个函数则无任何反应。如果组件已经卸载,则返回true;如果组件未卸载,则返回false。
2.5、createPortal
ReactDOM.createPortal(reactNode, newContainer)
用于将节点添加到一个新的容器中,而非父组件归属的容器。必须是一个真实的DOM节点。
注意:除了render 方法之外,不建议直接在项目中使用这些API,并且在实际项目中一般也没有使用render 以外的API 的需求。
3、React 视图渲染
构建视图一直是React 的重点,从 createElement 到 JSX。
3.1、ReactElement
当需要用React 创建虚拟DOM 时,React 专门提供了一个方法 createElement()。注意该方法并非是原生DOM 中的 createElement。
React.createElement(type, congfig, children)
// type 要创建的标签类型,字符串
// congfig 设置生成的节点的相关属性,纯对象
// children 代表该元素的内容或者子元素
利用createElement方法,就可以来创建 ReactElement,也就是React中的虚拟DOM。
在使用congfig 的时候,有两个问题需要注意:
1、没有属性需要定义,但又需要传递children 参数时,congfig 可以给null
2、congfig 中有两个固定的参数key 和 ref,最好不要乱用。
在使用children的时候,有三种不同的写法:
1、children 是字符串时,则代表在元素里添加文本内容。
2、children 是数组时,则会把数组中的内容展开放入元素中。
3、children 是ReactElement时,会直接当作元素的子节点进行添加。需要添加多个子节点时,可以一直跟在后边写。
以上这种方式编写视图是极其不清晰的,不推荐使用。所以React中提供了一个编写视图的神器JSX。
3.2、JSX
JSX = JS + XML, 是一个看起来很像XML 的JS语法扩展。
JSX是JS 语法扩展,但是浏览器并不识别这些扩展,所以需要借助babel.js 来对JSX进行编译,使其成为浏览器识别的语法,也就是React.createElement。
注意:
1、使用JSX 时必须引用bable对代码进行编译
2、该script标签内的代码需要使用bable编译时,必须设置type="text/bable"
JSX 本身是一个值,这个值是一个 ReactElement,而非字符串。
3.2.1、插值表达式
在使用插值表达式时,注意一下几点:
1、{} 中,接收一个JS表达式,可以是运算式,变量或函数调用等。表达式的意思就是一定会有一个值返回,而插值的意思就是把表达式计算得到的值插入到视图中去。
2、{} 中,接收的是函数调用时,该函数需要有返回值。
3、字符串、数字 会原样输出。
4、布尔值、空、未定义 会输出空值,也不会有错误。
5、数组 支持直接输出,默认情况下把数组的连接符 “,” 替换成空,然后直接输出。
6、对象 不能直接输出,但是可以通过其他方式,如:Object.value、Object.keys 等方法去解析对象,转换成数组之后进行输出。
以下几种特殊的渲染情况:
1、列表渲染
2、条件渲染 (&&(且)、||(或)、三目运算、函数)
3.2.2、JSX属性书写
JSX属性书写注意事项:
1、所有的属性名都使用驼峰命名法
2、如果属性值是字符串并且是固定不变的,则可以直接写。
3、如果属性值是非字符串类型,或者是动态的,则必须用插值表达式
4、有一些特殊的属性名并不能直接用:
1)、class 属性改为 className
2)、for属性改为 htmlFor
3)、colspan 属性改为 colSpan
5、style 接收的值是一个对象
3.2.3、JSX注意事项
JSX注意事项汇总:
1、浏览器并不支持JSX,在使用时需要babel 编译
2、JSX 不要写成字符串,否则标签会被当作文本直接输出
3、JSX 是一个值,在输出时只能有一个顶层标签
1)、如果JSX 的顶层标签不希望在DOM 中被解析出来,则可以使用<React.Fragment></React.Fragment>作为顶层标签。
2)、Fragment 是React 提供的一个容器组件,它本身并不会在真实的DOM中渲染出来。
3)、注意在 React 中, <></> 是 <React.Fragment/> 的语法糖。
4、所有的标签名字都必须小写
5、无论单标签还是双标签都必须闭合
6、JSX 并不是HTML,在书写时很多属性的写法不一样
1)、属性名都必须遵循驼峰命名法,从第二个单词开始首字母大写
2)、个别属性名有变化。
3)、style的值接收的是一个对象
7、在JSX 中,插入数据需要用插值表达式 {数据}
4、create-react-app
为了降低开发的复杂程度,涌现了很多好的解决方法:
1、模块化开发
2、Less、Saas 等CSS 语法扩展
3、TS 等 JS 语法扩展
4.1、React.StrictMode
StrictMode 是用来检查项目中是否有潜在风险的检测工具,类似于JS中的严格模式。跟Fragment 类似,不会渲染任何真实的DOM。只是为后代元素触发额外的检查和警告。
StrictMode 可以在代码中的任意地方使用,当然也可以直接用在index.js 中,开启全局检测。
StrictMode 只在开发模式下运行,不会与生产模式 冲突。
1、识别具有不安全生命周期的组件
2、有关旧式字符串ref 用法的警告
3、关于已弃用的findDOMNode 用法的警告
4、检测意外的副作用
5、检测遗留的context API
都是在控制台输出警告⚠️!
5、定义 React 组件
在React 中提倡的是组件化开发。在使用React 编写项目时,会把视图抽象成一个个组件,最终使用这些组件拼成开发者想要的视图。
React编写组件的两种方式:
1、类组件
2、函数式组件
5.1、类组件
React.Component
render
6、组件间通信
6.1、props使用
这块真没啥想说的...,补充一句:建议使用ES6的解构。
6.2、state 使用
哎,不能太懒了,还是加个代码吧:
class App extends Component{
constructor(props){
super(props)
this.state = {
name: 'zhangsan'
}
}
render(){
let {name} = this.state
return (<div>姓名:{name}</div> )
}
}
class/extends 类继承 React.Component, 所以要加 super,super关键字用于访问和调用一个对象的父对象上的函数。在构造函数中使用时,super关键字将单独出现,并且必须在使用this关键字之前使用。super关键字也可以用来调用父对象上的函数。
通过setState() 来修改数据。其接受两种参数,一种对象,一种有返回值的函数,返回对象。该方法时异步的。多个setState()会被合并,但是会引起一次试图渲染render
6.3、组件间的通信
1、父级向子级进行通信:props
2、子级向父级进行通信:回调方法
3、同级组件之间传递:回调父级组件方法,传给兄弟组件
6.4、跨组件通信
const context = createContext()
const {Provider, Consumer} = context;
调用createContent 方法会返回两个组件,Provider 和 Consumer。Provider 组件用于向其后代组件传递数据,把需要传递的子集的数据加给Provider的value属性即可向下传递。Consumer 组件用于接收父祖级传递下来的数据,在Consumer中可以编写一个函数,父祖级传递下来的数据会以参数的形式传递给该函数。
示例如下:
1、新建context.js 文件,在context.js 中新建context 并导出 context、Provider、Consumer
import {createContext} from 'react'
const context = createContext()
const {Provider, Consumer} = context;
export {Provider, Consumer}
export default context
2、修改App.js 文件
import React, {Component} from 'react'
import Child from './child'
import {Provider} from './context'
calss App extends Component{
render() {
return (
<Provider value={{
info: '传递的数据'
}}>
<Child />
</Provider>
)
}
}
export default App
3、新建child.js 文件
import React, {Component} from 'react'
import {Consumer} from './context'
class Child extends Component{
render() {
return (
<Consumer>
{(val)=>{
return <p>{val.info}</p>
}}
</Consumer>
)
}
}
export default Child
其实还可以通过类的 contextType 来接收,然后在组件的context 属性中就可以获取到Provider 传递过来的数据。
import React, {Component} from 'react'
import context from './content'
class Child extends Component{
static contextType = context
render(){
return <p>{this.context.info}</p>
}
}
Child.contextType = context
export default Child
7、组件的生命周期
React 组件的生命周期分为三个阶段,分别是:
1、挂载阶段(Mounting),这个阶段会从组件的初始化开始,一直到组件创建完成并渲染到真实的DOM中。
2、更新阶段(Updating),顾名思义组件发生了更新,这个阶段从组件开始更新,一直监测到组件更新完成并重新渲染完DOM。
3、卸载阶段(Unmounting),这个阶段监听组件从DOM中卸载。
7.1、挂载阶段生命周期函数
1、constructor(props) 初始化该组件,记得加上 super
2、static getDerivedStateFromProps(props,state) 从props 中获取state,根据props 来对state 进行修改。需注意:
1、是一个静态方法,使用时内部不能使用this。
2、是16.3版本之后新增的,使用需要注意版本号。
3、必须有返回值,其返回值是对state的修改。相当于this.setData()
4、作用是根据props 来修改state 的,所以组件初始时一定要定义state属性
3、componentWillMount 将要挂载,需注意:
1、16.3版本之后不建议使用了。
2、不能和 getDerivedStateFromProps 同时使用。
4、render return 的值生成虚拟DOM,然后提交给ReactDOM,渲染真实的DOM
5、componentDidMount 组件已经挂载完毕,虚拟DOM 已经添加到真实的DOM中。
class App extends Component{
constructor(props){
super(props)
console.log("1--组件初始化")
this.state = {}
}
static getDerivedStateFromProps(props,state){
console.log("2--将props映射到state")
return {
state:1
}
}
componentWillMount(){
console.log("3---组件即将进行挂载")
}
render(){
console.log("4---根据return 生成虚拟DOM")
return <div>hello react</div>
}
componentDidMount(){
console.log("5---组件已经挂载,虚拟DOM已经添加到真实的DOM")
}
}
7.2、更新阶段生命周期函数
React 组件更新的生命周期有三种不同的过程:
1、父组件更新引起的当前组件更新
2、当前组件自己更新
3、forceUpdate
16.3版本是有一个大的变动,在此就不说16.3版本之前的使用方法了,毕竟尽量都用最新的。
7.2.1、父组件更新引起的当前组件更新
1、static getDerivedStateFromProps(newProps,newState) 获取新的props 和 state,同样返回值是要对state做修改
2、shouldComponetUpdate 判断组件是否更新
3、render 生成新的虚拟DOM
4、getSnapshotBeforeUpdate(prevProps,prevState) 用于获取渲染前的DOM快照
1、中this.state 和 this.props 已经是最新的,需要之前的数据可以通过参数获取。
2、必须有返回值,其返回值会传递给componentDidUpdate
5、componentDidUpdate(prevProps,prevState,snapshot) snapshot接收getSnapshotBeforeUpdate通过返回值传递过来的信息。
class App extends Component{
constructor(props){
super(props)
console.log("1--组件初始化")
this.state = {}
}
static getDerivedStateFromProps(props,state){
console.log("2--将props映射到state")
console.log("6--获取新的props和state")
return {
state:1
}
}
componentWillMount(){
console.log("3---组件即将进行挂载")
}
render(){
console.log("4---根据return 生成虚拟DOM")
console.log("8---组件正在更新")
return <div>hello react</div>
}
componentDidMount(){
console.log("5---组件已经挂载,虚拟DOM已经添加到真实的DOM")
}
shouldComponetUpdate(newProps,newState){
console.log("7---判断组件是否需要更新")
return true
}
getSnapshotBeforeUpdate(prevProps,prevState){
console.log("9--用于获取更新的DOM")
return {
info: '要传递给componentDidUpdate的信息'
}
}
componentDidUpdate(prevProps,prevState,snapshot){
console.log("10--组件更新完成",snapshot)
}
}
7.2.2、当前组件自己更新
组件自己更新即在组件内部调用了setState,引起当前组件更新。已经经历了三个版本:16.3版本之前,16.3版本,16.4版本及以后。在此还是以最新的为主做说明。
顺序:static getDerivedStateFromProps(props,state) ---> shouldComponetUpdate(newProps,newState) ----> render ----> getSnapshotBeforeUpdate ----> componentDidUpdate
7.2.3、forceUpdate
forceUpdate 就是强制更新,就是当组件依赖的数据不是state 时,数据改变了,此时希望视图进行改变就可以使用forceUpdate。会强制试图进行更新,所以生命周期跟其他更新略有不同,不会在调用shouldComponetUpdate。
let name = 'zhangsan'
class App extends Componet{
render(){
<div>
<div>{name}</div>
<button onClick={()=>{
name = "lisi"
this.forceUpdate()
}}>更新名字</button>
</div>
}
}
7.3、卸载阶段生命周期函数
组件卸载就是把组件从DOM中删除。
1、componentWillUnmount 通常用于在组件卸载时,删掉一些组件加在全局中的内容,比如定时器...
8、ref
在开发项目时,会遇到一些特殊情况,需要使用原生DOM节点,一般我们可以使用getElementById 等js 原生方法。React 也给我们提供了一个API:ref。
8.1、string ref
class App extends Component{
componentDidMount(){
console.log(this.refs.parent, this.refs.child)
}
render(){
return <div>
<p ref="parent">父组件</p>
<Child ref="child"/>
</div>
}
}
需要注意的事项:
1、ref可以自定义,但是需要遵循驼峰命名法
2、单个组件内,多个ref时不能重名
3、获取ref 时需要 在 componentDidMount 和 componentDidUpdate 中进行,否则ref是还没有赋值或还没有更新的。
8.2、createRef
163.版本新增了createRef 方法
class App extends Component{
parent = createRef()
child = createRef()
componentDidMount(){
console.log(this.parent.current, this.child.current)
}
render(){
return <div>
<p ref={this.parent}>父组件</p>
<Child ref={this.child} />
</div>
}
}
9、key
key 的作用就是给元素分别加上一个唯一的标识,组件更新之后根据这个标识进行对比。具体规则就是在旧的虚拟DOM中找到key为1的元素,和新的虚拟DOM中key为1的元素进行差异对比。需注意:
1、同一元素更显前后要保持key统一,也就是说元素A更新前key为1,更新后key也要为1
2、一组元素中的key不能重复。不加key时,React会默认数组索引为key。一般顺序不变的,可以采用数组索引做key。当顺序有变化时,建议以id(数据唯一标识)为key。
10、添加事件
React 使用的是一种合成事件而非原生的DOM 事件。React元素添加事件有点像行间事件,但又稍微不同。行间事件名称是纯小写,React 则是遵循驼峰命名法。行间事件接受的是字符串,React 则是通过JSX 则是通过JSX 的插值放入一个函数。需注意:
1、事件处理函数的this 默认为 undefined。如果希望this为组件实例的话,可以绑定函数的this 或者使用箭头函数
2、在React 中阻止默认事件不能使用return false,必须使用event.preventDefault
11、表单
11.1、受控组件
11.2、非受控组件
12、其他特性
12.1、children
children 属性是React 中一个特殊的API,主要用于传递组件内部要渲染的内容。
class Popup extends Component{
render(){
let {title, children} = this.props
return <div>
<h2>{title}</h2>
<div>{children}</div>
</div>
}
}
class App extends Componet{
render(){
return <Popup title="自定义弹窗">
<div>弹窗内容</div>
</Popup>
}
}
12.2、dangerouslySetInnerHTML
dangerouslySetInnerHTML 可以帮助开发者在 React 元素中直接添加InnerHTML。
class App extends Component{
render(){
return <div dangerouslyInnerHTML={{
__html: `<div>hello react<div>`
}}></div>
}
}
12.3、函数式组件
除了类组件之外,在React 中存在一种简易的组件:函数式组件。函数式组件即一个函数就是一个组件。函数的第一个参数就是父级传递进来的props,返回值是该组件要输出的视图。
function App(props){
return <div>hello react</div>
}
在16.7版本之前,函数式组件中没有办法定义state,也没有生命周期,一般作为纯展示组件使用,所以又被称为无状态组件。
13、React Hooks
终于说到hooks 这部分了,个人是对该部分情有独钟的,自己用了一年吧,感觉很不错。下边就大概说说。
Hooks 是 16.7版本内测新增的,16.8版本正式新增的一个特性。帮助React 解决了很多繁琐的问题。
使用类组件时,有大量的业务逻辑如各类的接口请求需要放在componentDidMount 和 componentDidUpdate 等生命周期函数中,这样就会使组件变得复杂并且难以维护。并且Class中的this问题也导致很多初学者在使用时吃了不少的苦头。另外很多逻辑难以通过组件进行复用,比如只想要一个可以去获取滚动条位置的方法,或者只是对单一接口的请求方法封装。函数组件倒是可以避免this问题,但是函数组件没有生命周期和state等特性。
13.1、常用的hooks
13.1.1、usesState
const [state, setState] = useState(initState)
示例代码:
import React,{useState} from 'react'
function App(){
const [Name, setName] = useState('zhangsan')
return <div>
<div>{Name}</div>
<button onClick={()=>{
setName('lisi')
}}>修改名字</button>
</div>
}
export default App
注意事项:
1、useStaet 返回的 setState 方法同类组件的setState 一样,也是一个异步的方法,需要组件更新之后 state 的值才会变成新值。
2、useState 返回的setState 并不具有类组件的setState 合并多个 state 的作用,如果 state 中有多个state,在更新时,其他值一同更新。
3、同一个组件中可以使用 useState 创建多个 state。
13.1.2、useRef
useRef 可以看成createRef 的hook 版。示例代码:
import React,{useRef} from 'react'
function App(){
let myName = useRef()
return <div>
<div ref={myName}>hello hooks</div>
<button onClick={()=>{
console.log(myName.current)
}}>我的标题</button>
</div>
}
export default App
13.1.3、useEffect
Effect 翻译成专业术语称之为副作用。什么是副作用呢? 网络请求、DOM操作都是副作用的一种,useEffect 就是专门用来处理副作用的。useEffect 相当于 componentDidMount、componentDidUpdate 和 componentWillUnmount 的集合体。
useEffect 包括两个参数:执行时的回调函数和依赖参数(数组),并且回调函数可以有一个返回函数。代码示例:
useEffect(()=>{
console.log("组件挂在或更新")
return ()=>{
console.log("清理更新前的一些全局内容,或检测组件即将卸载")
}
},[num, num2, num3]) // 只有 num 或 num1 或 num2 更新时才会执行回调函数
整个组件的生命周期过程:
1、组件挂载
2、执行副作用(回调函数)
3、组件更新
4、执行清理函数(返还函数)
5、执行副作用(回调函数)
6、组件准备卸载
7、执行清理函数(返还函数)
8、组件卸载
特殊情况注意:
1、如果只想挂载后执行(componentDidMount),可以把依赖参数置为空(空数组),这样就是第一次挂载后执行,更新时不执行该副作用了。
2、如果只想在卸载前执行(componentWillUnmount),同样是把依赖参数置为空(空数组),该副作用的返还函数就会在卸载前执行。
3、只检测更新相对比较麻烦(componentDidUpdate),其实就是判断依赖的数据和初始值一致不。一致就是挂载,不一致就是更新。
// useRef 除了用来绑定DOM 节点外,
// 还用来保存跨渲染周期的数据,也就是获取组件渲染之前的数据,
// 搭配 useEffect 使用
let prevNum = useRef(num)
let prevNum2 = useRef(num2)
useEffect(()=>{
if(num!==prevNum.current || num2 !== prevNum2.current){
// 比较前后两次的值,不相同就是更新
console.log("组件更新")
// ref 不会自动更新,需要手动更新
prevNum.current = num
precNum2.current = num2
}
},[])
常用的Hooks 就是这三个,其他的用的少,不够后续也可以扩展介绍一下。
13.2、hooks 使用规则
使用注意事项:
1、只能在函数式组件和自定义Hooks 中调用 hooks,普通的函数或者类组件不能使用。
2、只能在函数的第一层调用hooks。hooks 的设计极度依赖其定义时候的顺序,如果组件更新时hooks 的调用顺序变了,就会出现不可预知的问题。保证其调用顺序的稳定性。eslint-plugin-react-hooks 检测其规范使用,在开发环境会有错误警告。
13.3、自定义hooks
自定义hooks 可以完美的解决一些单纯的逻辑难以通过组件去复用的问题。所以可以把一些重复使用的逻辑自定义成hook。要求use 开头,以区别其他函数。
示例代码:
function useScrollY(){
let [ScrollY, setScrollY] = useState(0)
function scroll(){
setScrollY(window.scrollY)
}
useEffect(()=>{
window.addEventListener("scroll", scroll);
return ()=>{
window.removeEventListener("scroll", scroll)
}
}, [])
return scrollY
}
小结
React 发展变化挺快的,应该随时关注React官网,了解新动向。提升自己技术。多加练习。本文写的稍微简略一点,希望没看懂的或者不够深入的地方,大家自己多去查阅资料学习,以便自己掌握的更好!