react
一、脚手架
cra(create-react-app)
- 安装脚手架(已安装了新版本的node.js)
npx create-react-app 项目名称
- 脚手架不要安装在本地,因为react脚手架升级很快,一旦官网升级,我们的脚手架必须升级,否则用不了,所以现在几乎使用线上脚手架
- 当前脚手架还是使用webpack库
- react16.8前为老版本,16.8后为新版本
- 与Vue脚手架Vite的区别
- cra脚手架安装后有
git
库- 再执行
npm
安装依赖(vite创建的项目初始是没有安装依赖的,需要单独安装)
- 再执行
- cra安装后自动安装了依赖
- cra脚手架安装后有
- 运行
npm start
项目目录和eject解析使用
-
public
- 放置的是项目静态文件,html页面加载的文件
manifest.json
可以做缓存使用
-
src 源码
-
index.js
- 入口文件,相当于
main.js
- 入口文件,相当于
-
setupTest.js
- 单元测试
-
App.js
- 项目入口
-
package-lock.json
- 锁定依赖安装版本和地址的文件
-
package.json
-
包含框架所有的依赖
"dependencies"
-
项目运行、打包、检查、释放等命令
"scripts"
"start"
运行"build"
打包"test"
检查"eject"
释放:把脚手架全部释放为原生配置。- 最好不要使用释放,一旦释放不可逆
- 如
"react-scripts start"
变为node script/start.js
-
eslint等配置
"eslintConfig"
-
path.js
入口配置
-
二、创建react组件
react开发特性
- 一切皆组件,最小化到一个元素
- react框架没有去解决开发中的渲染性能问题,需要自己去优化,否则性能不高
React.createElment()方法 -- 创建虚拟dom节点
(type: "input", props?: React.InputHTMLAttributes, ...children: React.ReactNode[])
-- 举例input
: React.DetailedReactHTMLElement
-
type
标签名 -
props
标签属性 -
...children
从第三个参数开始都为子节点- 虚拟dom实际上是什么?底层是如何渲染到页面上?
- 由
createElment()
方法创建的一个描述了真实dom结构的js对象 - 使用递归方法,把js对象中的元素使用
document.createElment()
方法创建出来并appendChild()
到root元素上
- 由
import React from 'react' // 在react中,每一个组件的第一行代码应该都是这个(引入react库) const vDom = React.createElement( // 对比h() 'h1', { style:{ fontSize:'14px', color:'red' } }, 'hello react!', React.createElement( 'span', { style:{ fontSize:'10px', color:'blue' } }, 'hahahahahaha' ) )
- 虚拟dom实际上是什么?底层是如何渲染到页面上?
ReactDom.render()方法 -- 绑定根节点(react17-版本)
(element: React.DOMElement,container: ReactDOM.Container,callback?: (() => void))
-
element
-
container
-
callback
import React from 'react' // 新版本报错:ReactDOM.render is no longer supported in React 18 import ReactDOM from 'react-dom' const vDom = React.createElement(...) // 创建虚拟dom节点 const root = document.getElementById(root) // 保存根节点 ReactDOM.render(vDom, root)
createRoot()方法 -- 创建根节点实例(react18+版本)
(container: Element, options?: RootOptions): Root
-
container
-
options
container.render(vDom)方法 -- 根节点 渲染 虚拟dom(react18+版本)
import React from 'react'
import {createRoot} from 'react-dom/client' // react18+版本的挂载根节点,需要引入client模块
const vDom = React.createElement(...) // 创建虚拟dom
const root = createRoot(document.getElementById(root)) // 创建根节点实例
root.render(vDom) // 根节点渲染虚拟dom
三、组件
jsx语法(JavaScript XML)-- 使用js语法编写html元素 -- 创建虚拟dom
麻烦:react没有vue的template语法,如果dom节点复杂,那么会编写很麻烦(createElment一层又一层)
解决:在react中,编写的html元素都是js对象
如何创建虚拟dom?
jsx语法编写的html在执行渲染时,会编译成
React.createElment()
方法创建的虚拟dom对象React定义组件的文件名:
js
、jsx
、tsx
js与jsx文件几乎没区别,js或jsx格式化语法补全时会有区别
-
格式
- 为了编写格式化和整洁,可以使用
()
把jsx对象包含起来 - js代码可以嵌套jsx,jsx也可以嵌套js代码
(jsx)
、{js}
- 为了编写格式化和整洁,可以使用
-
绑定变量
-
唯一语法:
{}
const title='hello worlllllld' const vDom = ( <h1> hello {title} </h1> )
-
-
优化
- 为了避免定义的title造成全局污染,采用闭包。如下编写,就称其为一个函数组件
类函数 -- 定义一个继承Component对象的类
16.8版本之前,没有函数组件,只有类组件,它有this
-
硬性规定
Component
这个类必须实现render方法render()
必须返回一个js对象- 类组件的方法必须定义在类里面,因为在类里面可以使用this对象
import React, { Component } from 'react' class VDom extends Component { render() { return ( // 返回一个jsx <h1> hello </h1> ) } }
**函数组件 **-- 把一个函数称为组件
16.8版本以后 重点发展方向 (函数式编程)
函数组件就是对类组件render()方法的抽取,所以函数组件没有this
-
react组件首字母必须大写
-
必须遵从函数式编程的规则,函数式组件没有this
function VDom() { // 闭包 const title = 'hello worlllllld' // console.log(this) // undefined return ( <h1> hello {title} </h1> ) } root.render(<VDom/>)
四、语法
条件渲染
- 元素条件渲染
- 三目运算符
{it.count === 5 ? <button>减少</button> : <button>添加</button>}
- 短目运算符(||、&&)
{(it.count===1)||<button>减少</button>}
{(it.count!==5)&&<button>添加</button>}
- 三目运算符
- 动态类名
- 动态样式
遍历渲染
key
<li key={it.id}>...</li>
- 添加原因:为了优化differ算法(对比当前虚拟dom节点和修改之前的虚拟节点的对比方法)
- 赋值和渲染是异步的(为了提高性能),所以赋值和渲染用的不是一个虚拟dom(进行了一次拷贝),
- 渲染完成以后,若修改了数据,会拿着赋值用的虚拟dom去对比渲染用的虚拟dom
- 所以需要添加一个唯一标志(key),当数据为数组的时候,需要添加key
合成事件
- react中的事件全部都是合成事件 -- react把原生事件做了一个封装,然后定义为jsx事件
- 格式
- 所有事件名字(名字和JS原生名字一样)都是驼峰书写
- eg .
<button onClick={this.btnEvt}>按钮</button>
- 合成事件传参
- 合成事件没有返回this(react定义事件无返回void),所以类组件需要对this进行重新指向
- 用箭头函数重新指向this 但是会没有arguments
btnEvt=()=>{}
- apply、call会立即执行,所以用bind(react中一个方法要传参,必须使用bind)所以在react中常常不会使用箭头函数来进行this重指向
<button onClick={this.btnEvt.bind(this,it)}>添加</button>
<button onClick={this.btnEvt(it)}>按钮</button>
错误写法,表示把btnEvt方法的执行结果绑定给onClick合成事件
- 用箭头函数重新指向this 但是会没有arguments
- 合成事件没有返回this(react定义事件无返回void),所以类组件需要对this进行重新指向
表单绑定
import React from "react"
class App extends React.Component {
btnEvt(){
console.log(arguments)
console.log(this) // undefined
}
render() {
let arr = Array(10).fill().map((_, index) => {
return {
id: 'cart-' + index,
name: 'HuaWei' + index,
price: 5000,
count:Math.ceil(Math.random()*5)
}
})
let items = arr.map(it => {
return (
<li key={it.id}>
产品名称:{it.name} 价格:{it.price} 数量:{it.count}
{/* {it.count === 5 ? <button>减少</button> : <button>添加</button>} */}
{
(it.count===1)||<button>减少</button>
}
{
(it.count!==5)&&<button}>添加</button>
}
</li>
)
})
return (
<ul>
{items}
</ul>
)
}
}
state 内部数据
react中有且只有两种数据:state内部数据、props外部数据
react中,如果数据不是state,则无法改变,改变后无法渲染
-
定义
constructor(){}
方法- 必须先实现
super()
方法
- 必须先实现
state={}
组件私有变量state
这个名称是固定的,其值几乎也固为对象
-
赋值
this.setState(obj,ob?)
-
react是单向数据流,没有办法双向绑定,所以当修改页面数据,需要改变state值,必须调用setState来进行页面赋值和重新渲染
-
如果要改变state中的一个对象,那么常用扩展运算符
this.setState({ obj:{...this.state.obj,title:evt.target.value} })
this.setState({ list:[...this.state.list,{...this.state.obj,id:'id'+Date.now(),state:'new'}] })
-
-
获取
this.state.xx
-
赋值和渲染是异步的,采用了防抖机制,多次非异步赋值,合并渲染,提高虚拟dom渲染的性能
如何解决异步(如改变页面盒子大小,去绘制chart图)
-
this.setState(obj,cb)
的回调函数- 相当于vue的
nextTick()
-- 指页面渲染完成 - callback是在防抖Promise内部执行的,所以此方法绝对是赋值后第一个执行的
- 相当于vue的
-
Promise.resolve().then()
- 必须放在赋值
this.setState()
之后,若放之前拿到的是赋值以前的数据 - 因为react里实现赋值和渲染防抖机制实现的原理就是使用Promise
- 必须放在赋值
-
setTimeout()
和setInterval()
- 可放赋值
this.setState()
之前
- 可放赋值
-
requetAnimationFrame()
获取下一帧动画(微任务)- 指本次页面渲染完成以后执行
- 可放赋值
this.setState()
之前,优先级低于Promsie - 前端有,后端没有(js文件node执行报错undefined,游览器打印可以拿到对象数据)
-
-
最新的react不允许ref是字符串,必须为一个关联对象,因为虚拟dom更新后要重新进行ref关联,所以必须为关联对象
- 定义:
xxx: createRef()
- current属性表示当前ref关联的虚拟dom对象
- 获取:
this.state.xxx.current.属性
- 定义:
import React from "react"
class App extends Components{
// constructor(){
// super()
// this.state={
// value:10
// }
// }
state={
value: 20,
node: createRef()
}
optEvt(type){
if(type=='add'){
this.setState({
this.state.value+=1
})
}else{
this.setState({
this.state.value-=1
})
console.log(this.state.node.current.innerText)
}
console.log(this.state.value)
}
render(){
console.log('========================= render function') // 每次调用this.state改版值,会调用render方法(render方法自己调用不会触发)
return (
<div>
<button onClick={this.optEvt.bind(this,'add')}>+</button>
<span ref={this.state.node}>{this.state.value}</sapn>
<button onClick={this.optEvt.bind(this)>-</button>
</div>
)
}
}
// Component组件底层模拟
class Component {
state; // state = undefined
setState(obj) {
Object.assign(this.state, obj)
Promise.resolve().then(()=>{
this.render() // 执行渲染
})
cb()
}
constructor() {
setTimeout(() => {
this.init()
})
}
init() {
this.render()
}
}
props外部数据
-
props详解
-
props数据只读,不可能更改
-
一个组件是一个类,那么这个类在这里进行实例化
-
这里绑定的数据,是为这个Item实例绑定数据,那么类中可以使用实例的数据
-
关于传值:不能直接传it(对象为引用数据类型,传参后变动会影响原数据),深拷贝后传参或单独属性传参
-
Object.freeze(a) // 冻结一个对象,则无法改变其值
-
父子组件通信
-
函数的引用
function a(){} var b = a // b是对a函数的引用(b为变量) b() // b相当于就是a函数 // => undefined
-
子组件通过
props
调用父组件函数- 子组件
- 可以修改
parentEvt
属性的this指向 - 但这里调用函数需要立即执行,所以不用bind
- 可以修改
class Child extends Component{ render(){ return ( <div> <p> count:{this.props.count}<button onClick={ ()=>{ // 1. parentEvt方法指向Child // 调用函数需要立即执行,所以不用bind this.props.parentEvt() } }>改变数量</button> </p> </div> ) } }
- 父组件
- 把
parentEvt
传递给子组件实例 parentEvt
属性的this指向可由子组件实例改变,如this.props.parentEvt().call(this,2)
this.changeCount
函数的this指向Parent
- 把
class Parent extends Component{ state={ count:1 } changeCount(step){ this.setState({ // this指向的是谁? count:this.state.count+step }) } render(){ return ( <div> <Child count={this.state.count} parentEvt={this.changeCount.bind(this,2)}/> </div> ) } }
- 子组件
-
-
props.children
- 类似vue的插槽
class Parent extends Component{ render(){ return ( <div> <Child> <p>父组件的元素</p> </Child> </div> ) } }
class Child extends Component{ render(){ console.log(this.props.children) return ( <div> {this.props.children} <button>>改变数量</button> </div> ) } }
-
数据校验 prop-type
react第三方库
prop-type
prop-types数据校验在公司开发中必须添加!
-
安装
cnpm i -S prop-types
-
使用
- 给组件添加静态属性
propTypes
(不能被继承)PropTypes
数据类型isRequired
表示必传
import React from 'react'; import PropTypes from 'prop-types' class MyComponent extends React.Component { /** 方法一: */ // static propTypes = { // xxx : PropTypes.isRequired // } render() { // ... do things with the props } } /** 方法二: */ MyComponent.propTypes = { xxx : PropTypes.isRequired }
// 父组件创建子组件实例 <MyComponent count={this.state.count}/>
- 给组件添加静态属性
-
-
状态提升
-
变量提升
- 只是提升了定义,赋值没有提升
function test1(){ console.log(a) var a = 10 } // 等同于 function test1(){ var a; console.log(a); // undefined a = 10; }
-
函数
- 函数整个提升
function test2(){ console.log(a) function a(){} } // 等同于 function test2(){ function a(){} console.log(a) // f a(){} }
-
d
- sdf
function test3(){ console.log(a) var a = function(){} } // 等同于 function tests(){ var a; console.log(a); // undefined a = function(){} }
-
变量提升(状态提升)
变量提升实质上就是 将受控组件转换为非受控组件 的过程
(如果一个组件没有自己的state数据,那么这个组件就叫做 受控组件)
-
把子组件A的state数据提升到父组件定义
-
当子组件A数据发生变化时,需要子组件B的数据相应变化,则子组件A通知父组件调用setState改变state数据,
-
父组件调用setState方法会调用render方法
-
父组件会重新渲染所有组件
-
子组件B需要的数据被重新渲染
-
父组件
import React, { Component,createRef } from 'react' import InputAmount from './InputAmount' import Label from './Label' export default class App extends Component { state = { node:createRef(), amount:0 // 子租金state变量提升到父组件定义 } render() { return ( <div> <p onClick={ ()=>{ // debugger console.log(this.state.node.current) } }>aaaa</p> <p>填写本金</p> <InputAmount ref={this.state.node} changeAmount={ (_amount)=>{ this.setState({ amount:_amount*1 }) } } count={this.state.amount}/> <Label amount={this.state.amount}/> </div> ) } }
-
子组件A
import React, { Component } from 'react' import Types from 'prop-types' export default class InputAmount extends Component { static propTypes={ amount:Types.number.isRequired } render() { return ( <div> <input placeholder='请输入本金' value={this.props.amount} onChange={ evt => { this.props.changeAmount(evt.target.value) } } /> </div> ) } }
-
子组件B
import React, { Component } from 'react' export default class Label extends Component { render() { return ( <div> <p>您输入的本金是:{this.props.amount}</p> </div> ) } }
-
五、生命周期
react中只有类组件有生命周期,函数组件没有生命周期
react生命周期比较乱,是按照js的执行逻辑来定义命名的
三个阶段
装载(挂载)阶段
-
constructor()
用户初始化state数据
-
不能调用setState()方法,因为setState()方法会执行组件的回流,因为挂载第一个生命周期都没有执行完成
-
这个生命周期必须返回一个对象(普通对象或者null)
constructor() { super() this.state = { name : 'yyy' } }
-
-
static getDerivedStateFromProps()
可以把props数据转为state数据的生命周期
(类似于beforemount)
-
不能执行setState方法,因为这个生命周期不是实例方法,没有this,无需实例化即可调用
- 静态方法没有this(App调用静态方法,非实例调用,类没有this)
-
因为页面执行渲染,可能是state数据发生变化,也可能是props数据发送变化
-
可以把props数据转为state数据
static getDerivedStateFromProps(props, state) { Object.assign(this.state,{test:10}) return { amount:props.amount } }
-
如果只是在初始化的时候把props映射过来,那么因该如下编写
static getDerivedStateFromProps(props, state) { return state.amount == undefined ? { amount: props.amount! } : null }
-
-
render()
dsa
- 也不能执行setState方法,因为setState会执行render方法,那么会无限递归
-
componentDidMount()
挂在完成生命周期,常用于赋异步初始值
类似于(mounted)
- 挂载阶段唯一可以调用setState的生命周期
- 也是唯一一个可以在初始化调用数据接口或者页面异步初始化数据赋值的地方
- 任何一个组件没有被销毁,那么挂载生命周期只执行一次
更新阶段
-
static getDerivedStateFromProps()
-
shouldComponentUpdate()
即将进行更新
类组件要进行性能优化,只能用
shouldComponentUpdate()
组件通过返回的true或false判断是否需要渲染render
-
参数是最新的props数据
-
this上的是更新前的props数据
shouldComponentUpdate(props){ return props.count!==this.props.count }
-
-
render()
-
getSnapshotBeforeUpdate()
-
componentDidUpdate()
更新完成以后
卸载阶段
componentWillUnmount()
两个时期
- render阶段
- commit阶段
React Fiber 架构
页面渲染:通过虚拟dom来执行createElement创建元素来实现页面初次展示
diff算法:
查找修改虚拟dom的过程
当页面有变化的时候,会对新虚拟dom和旧虚拟dom进行对比,查找更新的虚拟dom,然后批量修改和渲染(渲染防抖)
虚拟dom在内存中有两个,一个用于渲染,一个用于操作数据
执行渲染时是递归渲染,无法中断
eg. 页面有10000个节点,每个节点渲染需要1ms 那么渲染全部需要10s 如果用户输入了东西 那么在执行渲染的时候 就不会响应用户的输入
根据以上问题,react做了一个优化,实现了一个渲染和diff算法的切片,实现了渲染中断
组合
上下文Context
高阶组件
常见Context和HOC封装使用
hooks
- 为什么使用hooks
- 钩子函数 -- 以前把生命周期也成为钩子函数
- 需要用某个将函数传递
- 钩子函数 -- 以前把生命周期也成为钩子函数
class Test{
value;
created;
mounted
constructor({created,mounted}){
this.mounted-mounted
// 构造函数执行最后,执行渲染
Promise.resolve().then(()=>this.render())
}
render(){
console.log('------------- 渲染完成')
Promise.resolve.then(()=>this.mounted())
}
}
function getValue(){
}
new Test({
created:getValue, // 钩子函数
mounted:finishRender
})