react
jsx
{}中写js表达式:执行有结果的
js表达式:
-
变量
-
数学运算
-
值
-
判断:三目运算符
-
循环中:所有命令编程的循环都不是(for,for/in for/of ),借助于数组的迭代方法处理:map
-
- Nmber/string 写的啥显示啥
- Boolean/null/undefiend/symbol/bigInt: 渲染出来就是空
- 普通对象无法渲染
- 数组对象:把数组中的每一项都拿出来渲染
- 函数对象不支持渲染,但是可以用组件的方式渲染
- ...
ReactDOM.createRoot();不能直接用HTML或者是body节点直接作为根节点,并且只能有一个根节点。可以用fragment<></>,每一个构建的视图只能有一个根节点,否则会报错。
<>
<App />
</>
-
给元素设置样式
function App() { return ( <div className="App" style={{width: '100px',height:'200px',border: '1px solid red'}}> react </div> ); }行内样式:样式属性要基于驼峰命名;设置class用className
控制元素的渲染
{ flg&& <button>按钮</button>}
jsx处理的底层机制
-
把jsx语法编译成虚拟DOM对象。
虚拟DOM对象:框架自己内部构建的一套对象系统
-
基于babel-preset-react-app 把jsx编译成React.createElement(...)这种格式
-
再把createElement 方法执行,创建出虚拟DOM
babeljs.io/ 可以进行转化
语法报编译的结果
<div className="App" style={{width: '100px',height:'200px',border: '1px solid red'}}> react <div> { flg&& <button>按钮</button>} </div> </div>import { jsx as _jsx } from "react/jsx-runtime"; import { jsxs as _jsxs } from "react/jsx-runtime"; _jsxs("div", { className: "App", style: { width: '100px', height: '200px', border: '1px solid red' }, children: ["react",_jsx("div", { children: flg && _jsx("button", { children: "\u6309\u94AE" }) })] });React.createElement(ele,props,...children)
- ele:元素标签或者是组件
- props: 元素属性的集合,如果没有属性就是null
- Children: 第三个元素及以后得参数都是当前元素的子节点
只要是元素节点必然会基于createElement处理。
-
-
把虚拟DOM构建成真实的DOM
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<>
<App />
</>
</React.StrictMode>
);
对象遍历
封装一个对象迭代的方法
基于传统的for-in循环,会存在一些性能上的缺陷, 【性能较差,既可以迭代私有的,也可以迭代公有的,只能迭代可枚举的,非Symbol属性。。。】
Object.getOwnPropertyNames() 获取对象非Symbol类型的私有属性
Object.getOwnPropertySymbols() 可以获取Symbol类型的私有属性
let keys = Object.getOwnPropertyNames(arr).concat(Object.getOwnPropertySymbols(arr))
也可以基于es6中的Reflect.ownKeys代替上述操作【弊端不兼容IE】
封装迭代函数
const each = function each(obj, callback) {
if (obj === null || typeof obj !== "object") {
throw new Error(obj + "is not object");
}
if (typeof callback !== "function") {
throw new Error(callback + "is not a function");
}
let keys = Reflect.ownKeys(obj);
keys.forEach(item => {
let value = obj[item];
// 每次迭代都把回调函数执行。
callback(value, item);
});
};
each(arr,(value,key)=>{
console.log(value);
})
函数组件
rfc
在src目录下创建一个xxx.jsx文件,就是创建一个组件,创建一个函数,让函数返回一个jsx视图,或者返回虚拟对象。可以用快捷方式创建函数组件 rfc
组件的名字一般都用大驼峰的写法。
import React from 'react'
export default function DemoOne() {
return (
<div>DemoOne</div>
)
}
调用的时候再入口导入,然后用标签的形式导入。
import React from 'react'; // react 语法核心
import ReactDOM from 'react-dom/client'; // 构建html的核心(webAPP)
import DemoOne from './views/DemoOne'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<>
<DemoOne />
</>
);
我们在调用组件的时候,可以设置组件各种各样的属性,可以用胡子语法传递。
<DemoOne title="我是子组件" x={10} y={[10,20]} style={{width:'100px'}}/>
渲染机制
基于babel-preset-react-app 把调用的组件转换为createElement格式
import { jsx as _jsx } from "react/jsx-runtime";
/*#__PURE__*/_jsx(DemoOne, {
title: "\u6211\u662F\u5B50\u7EC4\u4EF6",
x: 10,
y: [10, 20],
style: {
width: '100px'
}
});
把createElement方法执行,创建一个虚拟DOM。
基于root.render把虚拟DOM变成真实的DOM。
基于双闭合调用,可以传递子节点,在传递函数的props中,有一个children属性来存储这些节点。
<DemoOne title="我是子组件" x={10} y={[10,20]} style={{width:'100px'}}>
<h1>dddd</h1>
</DemoOne>
-
props传递过来的属性
-
是只读的,只能获取不能修改。props.xxx 因为这个props对象是被冻结的。
-
作用:可以让组件的复用性更强。基于props可以把不同的信息传递给子组件,子组件接受不同的属性,呈现出不同的效果。
-
import React from 'react' export default function DemoOne(props) {// 用props接收 console.log(props); // return ( <div>DemoOne</div> ) }-
虽然不能修改属性,但是可以做规则校验。
#设置默 认值 DemoOne.defaultProps = { x:0 }通过propTypes来进行格式校验
安装 prop-type: yarn add prop-typeimport React from 'react' import PropTyope from 'prop-types' // 导入prop-types export default function DemoOne(props) { console.log(props); return ( <div>DemoOne</div> ) } DemoOne.defaultProps = { x:0 } DemoOne.propTypes = { title:PropTyope.string.isRequired //PropTyope是导入的类型 string类型,而且是必输 } // 其余的类型校验还有 title: PropTypes.array, title: PropTypes.bigint, title: PropTypes.bool, title: PropTypes.func, title: PropTypes.number, title: PropTypes.object, title: PropTypes.string, title: PropTypes.symbol // 校验其中类型的一种 optionalUnion: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, PropTypes.instanceOf(Message) ]),传递过来的属性会先做规则校验,不管检验成功还是失败,最后都会把属性传递给props,只不过如果不符合设定的规则,控制台会报错。
-
-
对象的规则设置
冻结对象
Object.freeze(obj) // 冻结对象,不可以删除属性
Object.isFrozen(obj) // 检测对象是否被冻结
被冻结的对象不可以删除成员值,不能修改成员值,新增成员,不能做劫持Object.definePropotype()
密封对象
密封对象:Object.seal(obj)
检测密封:Object.isSealed(obj)
密封特点:可以修改成员值,不能新增,删除成员,不能劫持。
扩展
不扩展对象: Object.preventExtensions(obj) // 把对象设置成不可扩展
不可扩展检测:Object.isExtensible(obj)
特性:除了不能新增以外,其余都可以。
插槽处理机制
子组件通过双闭合调用的方式,通过子标签传递一些内容,在子组件中用props.children来接收插槽。
- 传递数据用属性
- 传递html结构用插槽
import Children from './Children.jsx'
function App() {
return <div className="App">
<Children>
<div>
我是插槽传递的内容
</div>
</Children>
</div>;
}
export default App;
import React from 'react'
export default function Children(props) {
return (
<div>
{props.children}
</div>
)
}
当没有传递插槽的时候是undified,传递一个插槽是对象,传递多个插槽children是一个数组。
可以给予React.Children 的方法对props.children进行处理。内部的方法对children做了处理。
import React from 'react'
export default function Children(props) {
console.log(React);
let children = React.Children.toArray(props.children)
return (
<div>
{children.map(item=> item)}
</div>
)
}
具名插槽
具名插槽就是在插槽上添加一个slot属性
<div slot="three"> 我是具名插槽</div>
在调用组件的时候,通过名字来显示组件,不用理会组件的属性,通过slot的值来自己决定显示在什么地方。
静态组件和动态组件
函数组件:最初的函数组件就是静态组件,函数组件第一次渲染完成之后,组件内容不会根据一些操作二更新组件,除非在父组件中重新调用组件。
类组件合hooks组件:第一次渲染完毕之后,基于内部的一些操作,可以更新视图,可以呈现出不同的效果。
类组件
组件状态
创建一个类组件 rcc
创建一个类,继承React.Component
必须给当前类设置一个render方法,放在原型上,在render方法中返回渲染的视图。
import React, { Component } from 'react'
export default class ClassCom extends Component {
render() {
return (
<div>ClassCom</div>
)
}
}
一些类的基本知识
class Person {
constructor(name,age){
// this====>是绑定到实例上的
this.name = name
this.age = age
// 设置私有方法
this.add = function(parms) {
}
}
// 在这里也是私有属性,类似与this.school = school
school = 'xiaoxue'
// 变量=xxx的都是私有属性
add1 = function(){}
// 这样写就是在原型上的方法
sum(){}
}
let p = new Person("zhangsan",12)
console.log(p);
从调用组件发生的事情:
- 初始化属性&& 规则校验
校验属性的方法也有两种
-
//第一种还是用组件名校验,跟函数组件一样 // 属性校验 第一种 ClassCom.propTypes = { title: PropTypes.number } // 第二中用static关键字修饰,用静态属性来校验 import React, { Component } from 'react' import PropTypes from 'prop-types' export default class ClassCom extends Component { // 属性校验第二种 static propTypes = { title: PropTypes.number } render() { return ( <div>{this.props.title}</div> ) } } -
方案一
// 添加constrcutor一定要添加super() constructor(props){ super(props) } -
方案二
即使我们不用constrcuror处理,在constrcuror执行完毕之后React 内部也会吧props挂载到实例上,所以在内部使用时,只要保证this是react的实例,就可以用过this.props来得到属性。同样this.props也是冻结的对象。
import React, { Component } from 'react' export default class ClassCom extends Component { render() { return ( <div>{this.props.title}</div> ) } }
- 初始化状态
状态:后期修改状态,组件会刷新重新渲染。
状态需要手动处理,如果我们不处理react会自动挂载一个state属性,this.state = null
初始化状态只需添加一个私有的state属性即可。
state = {
num:10,
}
在页面上用this.state.xxx来获取值。通过this.setState({})来修改属性值,可以只添加我们修改的值。支持部分修改。
还可以通过ths.forceUpdate()来进行强制刷新。
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class ClassCom extends Component {
// 属性校验第二种
static propTypes = {
title: PropTypes.string
}
// 状态初始化
state = {
num:10,
}
add = function(){
// 用setState来修改状态
this.setState({
num: this.state.num+1
})
}
render() {
return (
<>
<div>{this.props.title}</div>
<div>{this.state.num}</div>
<button onClick={()=>{this.add()}}>add</button>
</>
)
}
}
生命周期函数
生命周期函数也叫钩子函数:程序运行到某一阶段,提供给我们一个方法,让开发者可以在这个阶段做一些事情。
常用的生命周期有这些,由于hooks的出现,class组件用的比较少了,只介绍常用的生命周期。
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class ClassCom extends Component {
// 属性校验第二种
static propTypes = {
title: PropTypes.string
}
state = {
num:10,
}
add = function(){
this.setState({
num: this.state.num+1
})
}
render() {
console.log('reander');
return (
<>
<div>{this.props.title}</div>
<div>{this.state.num}</div>
<button onClick={()=>{this.add()}}>add</button>
</>
)
}
componentDidMount(){
console.log("componentDidMount");
}
componentDidUpdate(){
console.log("componentDidUpdate");
}
componentWillUnmount(){
console.log("componentWillUnmount");
}
组件第一次渲染的时候,会执行render函数和componentDidMount组件。
- render执行完毕之后就完成了渲染
- componentDidMoun是挂载之后,这个时候已经将虚拟dom变成了真实DOM
更新组件的时候会执行render函数和componentDidUpdate。
父子组件渲染顺序:深度优先原则 父willMount > 子render > 子willMount > 子render > 子didMounted > 父didMounted
PureComponent和Component
PureComponent 会默认给组件添加一个shouldComponentsUpdate周期函数,在这个函数中会对属性做一个浅比较,如果发现属性没有更新,则会返回false,就不会更新。
**浅比较:**对象的比较内存地址,而不是具体的值。
Ref的相关操作
受控组件:基于修改数据/状态,让视图更新。
非受控组件:基于ref获取dom元素,操作DOM元素实现需求。
**方式一:**不推荐使用
<div ref="titleName"></div>
this.refs.titleName
**方式二:**函数的方式
将ref设置成一个函数,函数的第一个参数就是DOM元素。
<div ref={x=this.titleName = x}></div>
this.titleName 来获取DOM
x是形参,存储的是当前的DOM元素,然后将这个DOM赋值给titleName属性
方式三:React.createRef()对象
refName = React.createRef()
<div ref={refName}></div>
this.refNamel来获取DOM
给元素设置ref可以获得DOM元素,给组件设置ref可以获取组件的实例。(函数组件不能设置ref,但是可以用React.fowardRef实现ref转发,获取组价内部的元素)
import React from 'react'
const Children = React.forwardRef(function(props,ref) {
let children = React.Children.toArray(props.children)
return (
<div ref={ref}>
{children.map(item=> item)}
</div>
)
})
export default Children
<Children ref={x=>this.childRef = x}>
</Children>
// this.childRef得到的就是Children组件中的div中的DOM元素
setState()进阶
this.setState([partialState],[callback])
partialState: 支持部分状态修改,无论多少个状态,我们只修改要修改的。
callback:在状态更改,视图更新完成之后触发执行,发生在componentDidupdate之后,只会在自己这个状态修改之后才会执行。
视图更新一次
handle = ()=>{
this.setState({
x:x+1,
y:y+1,
z:z+1
})
视图更新一次,就会发现每个setState是异步执行, 而不是同步执行的。React18中有一套更新机制,异步操作,实现批处理。可以减少视图更新的次数,降低渲染消耗的性能。让更新的逻辑和流程更加的清晰。
this.setState({x:x+1})// 不会立即更新状态和视图,而是加入到更新队列中。
this.setState({y:y+1})
this.setState({z:z+1})
- 在产生的私有上下文中,代码自上而下执行,先添加到更新队列中。
- 当上下文代码都处理完毕之后,会扔更新队列的任务,统一渲染/更新一次【批处理】
在react18中,this.setState是异步的,无论是在哪里执行,例如合成事件中,周期函数中,定时器中,setState都是异步的。
目的:实现状态的批处理,有效减少更新的次数,降低性能消耗。有效管理代码执行的逻辑顺序。
原理:利用更新队列【updater】机制来处理,在当前相同的时间段内,遇到setState会立即放入到更新队列当中,此时状态/视图还没更新。当前全部代码执行结束之后才回去更新视图,只触发一次视图的更新。
合成事件
onXxxxx = ()=>{}
点击事件
<div onClick={()=>{执行}}></div>
this丢失的问题
用箭头函数可以解决this丢失的问题。
基于React内部处理,如果我们给合成事件绑定一个普通函数,当事件触发行为,绑定行为函数,方法中的this会是undefined。
我们可以基于js中的bind方法进行解决,预处理函数中的this【用call和apply函数会立即执行,不符合要求,我们需要点击的时候才执行。】
import React, { Component } from 'react'
export default class ClassCom extends Component {
handle(){
console.log(this);// undefined
}
render() {
return (
<div>
<button onClick={this.handle}>点击</button>
</div>
)
}
}
也可以把绑定的函数设置成箭头函数,让其使用上下文中的this.要么在定义函数的时候定义成箭头函数,或者是在调用函数的时候用箭头函数来调用,其实道理是一样的。
import React, { Component } from 'react'
export default class ClassCom extends Component {
handle(){
console.log(this);// undefined
}
// 将函数定义成箭头函数
handle1 = ()=>{
console.log(this);
}
render() {
return (
<div>
<button onClick={this.handle1}>点击</button>
<button onClick={()=>{this.handle()}}>点击</button> // 用箭头函数的方式调用
</div>
)
}
}
合成事件对象
handle2(ev){
console.log(ev);
}
合成事件对象(SyntheticBaseEvent ):我们在React合成事件触发的时候,也可以获取到事件对象,这个对象就是合成事件对象,也就是ev,
也包含了浏览器内置事件对象的一些属性和方法。
可以箭头函数来传递额外的参数
handle2=(x,y,ev)=>{
console.log(x,y);
console.log(ev);
}
<button onClick={(ev)=>{this.handle2(10,20,ev)}}>点击</button>
也可以用过bind来实现。用bind来得到合成事件对象值得注意的是,最后一个参数才是合成对象,但是箭头函数的调用,可以任意位置,就像上面的代码一样,(ev)=>{this.handle(ev,10,10)} ev在第几个参数,拿在handle中就是第几个参数是合成对象。
handle2=(x,y,ev)=>{
console.log(x,y);
console.log(ev);
}
<button onClick={this.handle2.bind(this,10,20)}>点击</button>
事件和事件委托
事件具备的传播机制:点击最里层的
第一步:从最外层向里逐一查找(捕获阶段)
第二步:把事件源(被点击的元素)的点击行为触发(目标阶段)
第三步:按照捕获的阶段分析出来的路径,从里到外把每一个元素的点击事件触发(冒泡阶段)。
事件和事件绑定:
- 事件是浏览器赋予元素的默认行为
- 事件绑定是给这个行为绑定一个方法。
事件委托:
利用事件传播机制,实现一套事件绑定方案。只需要给容器做一个事件绑定,点击容器内部任何元素,根据事件传播机制,都会让容器点击的事件也触发。
React合成事件机制
React合成事件:
- 绝对不是给当前元素基于addEventListener单独做的事件绑定,react中的合成事件都是基于“事件委托”处理的。
- 在React17后都是委托给#root这个容器,冒泡和捕获都做了委托;在17版本之前都是委托给document,只做了冒泡阶段的委托。
- 对于没有实现事件传播机制的事件,才单独做的绑定[onMounseEnter......]
- 在组件渲染的时候发现jsx元素有onXxxxx/onXxxCaputer这样的属性,不会给当前元素直接做事件绑定,只是把绑定的方法赋值给元素的相关属性。然后对#root容器做了事件绑定[捕获和冒泡都做了]。
- 因为组件中渲染的内容,最后都会插入到#root容器中,这样点击页面中的任何一个元素,最后都会把#root的点击行为触发。而在给#root绑定的方法当中,把之前给元素设置的onXxxxx/onXxxCaputer属性执行。
(react有内置的事件,当事件触发的时候,会将这个事件赋值给onXxxxx/onXxxCaputer,这个就是合成事件绑定。将react内置的事件机制同onXxxxx/onXxxCaputer属性绑定)
hooks组件
hooks中useXxxx的就是hooks函数。hooks函数只能写在函数组件的最外层,不能嵌入到代码中去执行。
useState
静态函数组件是没办法修改状态让视图更新的。hooks组件引入了useState
useState返回的是一个数组,传入的是状态的初始值initialState。
- 第一个元素就是状态值xxx
- 第二个属性是一个修改状态的函数,为了语义化我们一般命名为setXxx
函数组件不是类组件,没有实例的概念,就没有this,只是把函数执行,创建一个函数执行上下文而已。
import React from 'react'
import {useState} from 'react'
import {Button} from 'antd'
export default function FuncCom() {
// useState
let [num, setNum] = useState(0);
return (
<div>
<div>{num}</div>
// 修改状态
<Button type="primary" onClick={()=>{setNum(num+1)}}>按钮</Button>
</div>
)
}
函数组件的每一次渲染或者是更新都是把函数的重新执行,每一次执行都会产生一个全新的私有上下文,内部的代码都需要重新执行一次。用setXxx执行的时候传递的不是初始值,而是最新的状态值。如果是普通变量,将没办法保持变量的最新状态。
每一次执行useState,只有第一次设置的初始值会生效,其余以后再执行,获取的状态都是最新的状态值,返回修改的方法返回的也是每次返回一个新的方法。而没有用useState的普通变量,每次更新状态之后都会变成初始值,因为函数成功执行了。
每次函数组件的执行都会产生一个上下文,当函数内部有方法执行的时候会产生一个私有上下文,这个私有上下文每次寻找的是上一次的状态;每次执行方法的时候就会重新执行函数组件,产生一个新的上下文。这个跟vue的双向绑定有很大的区别,不要用双向绑定的细想带入。你大爷已经不是你以前的大爷了,但是你现在依靠的还是之前的大爷,因为以后的大爷是属于以后的你。
let [num, setNum] = useState(0);
const changeNum = ()=>{
setNum(num+1) // num 1
console.log(num);// num 0
setTimeout(() => {
console.log(num); // num 0
}, 2000);
}
// 这里打印num是0的原因是因为,setNum之后会,虽然函数组件会重新执行,会形成一个新的上下文,但是打印中的num依赖的还是上次执行形成的上下文,num的值是上一个上下文的值。
执行一次setState ,把需要的状态信息放在对象中统一管理。
执行setState方法,传递什么值,就会把状态的整体修改成这个值,不支持部分状态的修改。
怎么修改对象呢?修改对象的时候发现值修改其中的某一个值,会覆盖之前的值,导致结果变成了只有这一个值了,修改的时候可以通过解构把旧值先附上去,再修改。
let stu = {
name:'张三',
age:'13',
sch:{
name:'小学'
}
}
const [student, setStudent] = useState(stu);
const changeStudent = ()=>{
setStudent({
...student,// 先将之前的值附上去。
name:'李四', // 修改新值
sch:{
name:'中心'
}
})
}
useState的同步异步:
setXxx更新的时候是异步去更新的,不是同步,也是利用更新队列,等同于类组件的this.setState,在任何地方都是异步的。
**flushSync()**可以立即刷新队列,让视图立即渲染。
flushSync(()=>{
setX(x+1)
setY(y+1)
})
setZ(z+1)
// 这就相当于刷新两次,x和y一起刷新,z单独更新
useState自带优化机制
- 每次修改的状态值的时候,会拿最新要修改的值跟之前的状态值作比较,基于Object.is比较
- 如果两次值是一样的,则不修改状态,也不会让视图更新
再循环中达到每次循环都可以加。
for(let i = 0;i<10;i++){
// pre 存储上一次的状态值。 返回信息是我们修改的状态值
setNum(pre=>pre+1)
}
如果我们的初始值需要经过一系列的计算才能得到
let [total, setTotal] = useState(()=>{
let x = 20,y = 10
total = 0
total = x+y
return total
});
useEffect
相当于在函数组件中使用生命周期函数。视图渲染之后才执行这个useEffect。
只有一个callback参数的时候,会在第一次渲染和更新的时候执行。在useEffect中可以获取最新的状态值,类似与didMount和didUpdate的结合。
useEffect(() => {
console.log("dddddddddddddddd");
});
添加第二个参数,[],只有第一次渲染完毕之后才会执行callback,更新状态的时候不会再去执行,相当于didiMount。
useEffect(() => {
console.log("dddddddddddddddd");
},[];
依赖多个状态,第一次渲染的执行,当依赖中的一个发生改变的时候,就不触发callback执行。如果依赖的状态没有变化,callback就不会执行。
useEffect(() => {
console.log("num2",num);
},[num]);
也是类似于vue中watch,监听,监听的数据发生改变的时候执行。
怎么在useEffect中获取到修改之前的状态呢?
在callback中返回一个函数就好了。
useEffect(() => {
return ()=>{
console.log("num4",num);// 这里的num就是上一次状态值
}
}
一些细节
-
useEffect只能在函数组件的最外层,不能嵌入到代码中去执行。
-
如果设置返回值,则必须返回一个函数。返回一个实例也不行,必须是一个函数。可以用promise的.then不能将回调函数加上async,可以在callback的内部包一层小函数来执行就可以了。
// 错误写法,也会有报错提示 useEffect(async() => { const res = await Promise() },[num]); // 报错提示 Effect callbacks are synchronous to prevent race conditions. Put the async function inside: useEffect(() => { async function fetchData() { // You can await here const response = await MyAPI.getData(someId); // ... } fetchData(); }, [someId]); // Or [] if effect doesn't need props or state // 我们修改成报错的提示方式写法 useEffect(() => { const fun = async()=>{ const res = await Promise() console.log(res,num); } fun() },[num]);
useLayoutEffect
useEffect实在虚拟DOM渲染成真是DOM之后才执行,而useLayoutEffect在创建虚拟DOM之后就执行了。如果在callback中又修改了状态值,对于useEffect来讲,第一次真是DOM已经渲染,组件更新会重新渲染真是的DOM,所以频繁切换的时候会出现内容闪烁。
对于useLatouyEffect来讲,第一次真实的DOM还没渲染,遇到callback中更新状态,视图立即更新,创建出来的虚拟DOM和上一次的虚拟DOM合并在一起渲染成真实DOM,也就是真实DOM就渲染了一次,不会出现内容的闪烁。
- useLayoutEffect设置的callback要优先于useEffect去执行
- 在两者设置的callback中,依然可以获取DOM元素,真是的DOM已经创建了,区别在于浏览器是否渲染
- 如果在callback中又修改了状态,useEffect之前已经渲染了真是的DOM,会第二次去渲染;useLayoutEffect会合并两次虚拟DOM,合成一次去渲染真是的DOM。
视图更新步骤
- 基于babel-preset-react-app把jsx编译成createElement
- 把createElement执行,创建出虚拟DOM
- 基于root.render把虚拟DOM渲染成真是DOM。useLayoutEffect会阻塞第四步操作,先去执行cllback然后再去执行第四步(和第四不步是同步),而useEffect会和第四步同事执行(和第四步是异步的)
- 浏览器绘制成真是的额DOM对象
useRef
在函数组件中使用ref,基于ref = {(x)=>{refs1 = x}} 先定义一个变量来接受dom,但是这种方式不推荐用在函数组件中。
let refs1 // 定义一个变量
useEffect(() => {
console.log(refs1);
});
<div ref={(x)=>{refs1 = x}}> // 第一个参数是DOM元素
{student.name}---{student.age}--{student.sch.name}
</div>
通过**React.createRef()**方法也可以
let box = React.createRef()
useEffect(() => {
console.log(box);
});
<div ref={box}>
{student.name}---{student.age}--{student.sch.name}
</div>
通过useRef()
useRef()创建的就是一个ref对象。
let useref = useRef(null)
useEffect(() => {
console.log(useref);
});
<div ref={useref}>
{student.name}---{student.age}--{student.sch.name}
</div>
在函数组件中尽量用useRef,因为性能比React.createRef好。
useImperativeHandle
useImperativeHandle可以配合React.forwardRef可以获取函数组件内部的状态和方法。
用React.forwardRef包裹组件 组件有两个参数是 props和ref,这个ref就是ref对象。
import React,{useImperativeHandle, useState} from 'react'
// 用React.forwardRef包裹组件 组件有两个参数是props和ref
const FunChild = React.forwardRef(function(props,ref){
const [text, setText] = useState("张三");
const submit = ()=>{
console.log("我是submit");
}
// 用这个hooks将需要状态和方法返回
useImperativeHandle(ref,()=>{
return{
text,
submit
}
})
return (
<div>
我的函数子组件
</div>
)
})
export default FunChild
在父组件中可以通过ref对象得到
const childRef = useRef(null)
useEffect(() => {
console.log(childRef); // 这里就可以得到子组件的状态和方法。
});
<FunChild ref={childRef}></FunChild>
useMemo
函数组件的每次更新,都是把函数重新执行一次,内部代码也要重新执行一次。
第一次渲染组件的时候,callback会执行,后期只有依赖值发生改变的时候,callback才执行,每一次执行都会把callback返回的结果赋值给变量xxx。
const xxx = useMemo(()=>{return a+b })
useMemo有一个缓存效果,在依赖的状态值没有发生改变时,callback没有触发执行的时候,xxx得到的就是上一次计算出来的结果,和vue中的计算属性相似。相当于缓存结果值
const total = useMemo(()=>{
return num+num2
},[num,num2])
useCallback
函数组件的每一次更新,都是把函数重新执行一遍,产生一个新的闭包,在闭包中所创建的函数操作,都会创建新的堆内存,也就是内部的函数都会重新创建。有些函数我们只需要执行一次就行了,没必要每次更新都去执行,怎么解决这个问题呢,可以用useCallback。
const xxx = useCallback(()=>{
},[])
-
组件第一次更新,useCallback执行,创建一个callback函数赋值给xxx
-
组件后续更新,判断依赖的状态是否改变,如果改变则会重新创建函数,重新赋值给xxx,但是如果,依赖的状态值没有改变或者没有设置依赖值,则xxx获取的一直是第一次创建的函数,不会创建新的函数进来。
简单来说useCallback就是缓存函数的,当依赖发生变化的时候才重新创建函数。
用途
诉求:当父组件更新的时候,传递给子组件的函数不随着父组件更新而更新。
可以将这个传递给子组件的函数用useCallback()
- 传递给子组件的属性,每一次需要相同的内存地址,基于callback处理
- 在子组件内部也要做一个处理,验证父组件的属性是否改变,如果没有改变,则子组件不能更新,有变化才需要更新
自定义hooks
将某些组件的逻辑可以提取到可重用的函数中。
创建一个函数,名字需要是useXxx,后去可以在组件中调用这个方法。
const usePart = function (initValue) {
const [state, setState] = useState(initValue);
// 支持状态的改变
const setPart = (initValue)=>{
return setState(initValue)
}
return [state,setPart]
}
组件之间的通信
父子通信
父组件通过props中的属性来传递属性,子组件通过callback函数将子组件的信息传递给父组件。
父组件把信息传递给子组件--基于信息及即可
子组件想修改父组件的值,父组件提供一个方法,这个方法通过属性传递给子组件,子组件执行即可。
类组件
// 父组件
import React, { Component } from 'react'
import ClassChildren from './ClassChildren';
export default class ClassComMes extends Component {
state = {
name:'张三',
age:12
}
childTop = (value)=>{
console.log(value);
}
render() {
return (
<div>
ClassComMes
{// 通过属性来传递信息,父组件定义 handle方法,子组件触发handle方法}
<ClassChildren message={this.state} handle={(value)=>{this.childTop(value)}}/>
</div>
)
}
}
// 子组件
import React, { Component } from 'react'
import {Button} from 'antd'
export default class ClassChildren extends Component {
render() {
// 将this.props解构出来,
let {message,handle} = this.props
console.log(this.props);
return (
<div>
{// 父组件传递的信息}
{message.name}-{message.age}
<div>
{// 子组件通过父组件传递来的方法,执行这个方法传递参数,可以修改父组件的状态}
<Button onClick={()=>{handle("我是子组件传递给父组件的消息")}}>子传父</Button>
</div>
</div>
)
}
}
如果想传递标签可以用前面讲的插槽。
函数组件的父子通信一样
// 父组件
import React,{useState,useCallback} from 'react'
import FuncChildren from './FuncChildren';
export default function FuncComMsg() {
const [message, setMessage] = useState({
name:'张三',
age:11
});
{// 用useCallback 当值不发生变化的时候,子组件不会再渲染}
const change =useCallback( (value)=>{
setMessage({
...message,
name:value
})
},[message])
return (
<div>
<FuncChildren message={message} change={(value)=>{change(value)}}></FuncChildren>
</div>
)
}
// 子组件
import React from 'react'
import PropTypes from 'prop-types'
export default function FuncChildren(props) {
let {message,change} = props
console.log("子组件渲染次数");
return (
<div>
{message.name}-----{message.age}
<button onClick={()=>{change("李四")}}>富川字</button>
</div>
)
}
FuncChildren.propTypes = {
message: PropTypes.object,
change: PropTypes.func
}
用了useCallback子组件除了第一次渲染,后面都没有渲染了,useCallback一定不要乱用,如果没有设置任何依赖,函数只会在第一次渲染。
跨代通信(爷孙通信)
第一种:父传子,在子传子的子。
第二种:上下文
祖先组件需要把信息放在上下文中,后代组件直接去上下文中获取。
类组件
-
创建上下文对象。
// ContextEvent.js中 import React from React; // 创建上下文对象 const Context = React.createContext(); export default Context; -
让祖先组件具备状态和修改状态的方法,同时把这些信息放在上下文中。
基于上下文对象中提供了一个Provider组件,组件中通过value来存储要传递的信息。
import React, { Component } from 'react' import ClassChildren from './ClassChildren'; import ContextEvent from './ContextEvent.js' export default class ClassComMes extends Component { state = { name:'张三', age:12 } childTop = (value)=>{ console.log(value); } render() { return ( <div> {/* ContextEvent,这个value就是存放要传递的信息 */} <ContextEvent.Provider value={{...this.state,handle:this.childTop}}> <ClassChildren /> </ContextEvent.Provider> </div> ) } } -
在后代组件中获取上线文信息。
方法一:导入ContextEvent,ContextEvent中有一个Consumer组件,这个组件中有一个函数插槽,这个函数的第一个参数context就是祖先组件传递过来的内容,然后这个函数返回这个视图 <ContextEvent.Consumer >{(context)=>{rerturn (
)}}</ContextEvent.Consumer >import React, { Component } from 'react' import ContextEvent from './ContextEvent' // 导入这个context export default class ClassChildren extends Component { render() { return ( <ContextEvent.Consumer > { (context)=>{ // 这个context就是祖先组件传递过来的值 let {name,age,changeHandle} = context // 将这个值解构出来 return( <div> {name}--{age} <div> <button onClick={()=>{changeHandle("万物")}}>子传父</button> </div> </div> ) } } </ContextEvent.Consumer> ) } } // 方法二 import React, { Component } from 'react' import ContextEvent from './ContextEvent' // 导入上下文对象 export default class ClassChildrenChild extends Component { static contextType = ContextEvent // 给定一个静态属性,将ContextEvent赋值给contextType,contextType是固定的写法 render() { console.log(ContextEvent); let {name,age,changeHandle} = this.context // 传递的内容在context中,context也是固定的 return ( <div> {name}--{age} </div> ) } }方法二:需要给定一个固定命名的contextType静态属性,将上下文对象赋值给contextType,然后获取this.context,传递的内容就在context中。
函数组件
第一步跟class组件一样的创建context
第二步也是一样的,通过ContextEvent.Provider的value来传递值
import React,{useCallback, useState} from 'react' import FuncChildren from './FuncChildren'; import ContextEvent from './ContextEvent'; export default function FuncComMsg() { const [message, setMessage] = useState({ name:'张三', age:11 }); const change =useCallback( (value)=>{ setMessage({ ...message, name:value }) },[message.name]) return ( <div> <ContextEvent.Provider value={{...message,change:change}}> <FuncChildren message={message} change={(value)=>{change(value)}}></FuncChildren> </ContextEvent.Provider> </div> ) }第三步获取上下文中的数据的时候第一种方式,通过ContextEvent.Consumer
第二种方式通过useContext来得到context
import React from 'react' import ContextEvent from './ContextEvent' export default function FuncChildrenChild(props,context) { return ( <div> <ContextEvent.Consumer> {(context)=>{ return ( <div> {context.name} </div> ) }} </ContextEvent.Consumer> </div> ) } // 方式二,通过hooks函数useContext import React,{useContext} from 'react' import ContextEvent from './ContextEvent' export default function FuncChildrenChild(props,context) { const {name,age,change} = useContext(ContextEvent); // 获取上下文信息 return ( <div> {name} </div> ) }
React样式私有化处理
在组件化中,各个组件之间的样式可能会冲突。
内联样式
这样确实可以保证组件与组件之间的样式不会冲突。但是不利于样式的复用;不能用伪类;不利于优化。
<div style={{color:'red'}}>
ssss
</div>
样式命名技巧
保证最外层类名不冲突,例如:路径-组件名-类名
后期组件内部的元素,其样式都基于less/scss嵌入到最外层容器的样式类名之下去编写。
通过xxx.module.css
可以基于css Modules实现样式私有化管理,把样式都写在xxx.module.css文件中,这样的文件不能写less/sass等预编译语言;
在组件中基于ES6Module导入样式。
高阶组件
利用js中的闭包【函数柯力化】实现组件的代理。
我们可以在代理组件中,经过业务逻辑处理,获取一些信息,最后基于属性等方案,传递给我们最终要渲染的组件。
// 父组件
<FuncChildrenChild a={4} b={5}></FuncChildrenChild>
// 子组件
import React,{useContext} from 'react'
import ContextEvent from './ContextEvent'
function FuncChildrenChild(props,context) {
const {name,age,change} = useContext(ContextEvent);
return (
<div>
{name}
</div>
)
}
// 执行这个方法,传递一个组件进来。
const proxyText = function proxyText(Component){
return function HOC(props){
console.log(props);
return (
<div>
{/* 真实渲染的是FuncChildrenChild组件 */}
<Component {...props}></Component>
</div>
export default proxyText(FuncChildrenChild) // 把函数执行的返回结果,基于es6module规范导出,
Redux
redux流程
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。提供公共状态管理。
父子组件一般用props/ref/redux等,其他组件一般都是用redux。
-
创建全局公共容器,用来存储各组件需要的公共信息。
const store = createStore(reducer) 在创建的容器中分为两部分:公共状态和事件池(存放让状态更新的方法) 公共状态一旦改变,会默认立即通过事件中方法执行,这些方法的执行只要就是通知组件的更新,就可以获取最新的状态信息。 修改公共状态不能直接修改,必须基于dispatch派发,在reducer中进行更新。 -
在组件内部获取公共状态信息,然后渲染。
store.getState() -
把让组件可以更新的方法放在公共容器中。后期公共状态改变了,事件池的方法按照顺序依次执行,也就是对应的组件也更新,也就可以从store容器中获取最的状态进行渲染。
store.subscribe(fun) -
创建容器的时候需要一个reducer
let init = {...} const reducer = function reducer(state,action){ //state 容器的状态 //action 派发的行为对象,必须具备type属 switch (action,type){ /根据传递的type值来修改不同的状态属性 } return state // 返回的信息会替换store中的状态 } -
p派发任务,通知reducer进行修改
store.dispathch({type:xxxx, })
redux示例
新建一个store/index.js文件
store/index.js
import { createStore } from "redux";
// 管理员:为了修改store容器中的公共状态,初始化状态值
let initial = {
num: 10,
age: 20,
};
const reducer = function (state = initial, action) {
// state 就是存储store的公共状态,最开始没有状态就赋值为初始状态
// action 每一次基于dispatch派发的时候,传递过来的行为对象,必须具备type属性,具备派发的属性
// 基于派发的行为标识,修改容器中的公共状态信息
newState = { ...state };
switch (action.type) {
case "add":
state.num++; // 不能直接修改状态,等到return,故我们需要克隆一份
newState.num++;
break;
default: // 第一次派发不会跟任何type匹配,就返回默认的状态
return newState;
}
// 返回这个状态将原来的状态替换掉
return newState;
};
// // 每次派发都会把reducer执行
// store.dispatch({
// type: "add",
// step: 10,
// });
const store = createStore(reducer);
export default store;
在组件中通过导入store/index.js 根据store.getState()来得到状态
函数组件
import React,{useState,useEffect} from 'react'
import store from './store/index'// 导入状态
export default function ReduxStydy() {
let {num,age} = store.getState() // 通过store.getState()来获取状态
console.log(store);
// 函数组件只有状态改变的时候组件才会渲染、组件第一次渲染完毕之后,把组件更新的方法放在store事件池中
const [num1, setNum1] = useState(0); // 让这个状态去触发函数组件的更新
// 组件更新
const updater = ()=>{
setNum1(num1+1)
}
useEffect(() => {
// let unsubcirbe = store.subscribe(让组件更新的方法)
// 把组件更新的方法放在事件池中,返回的是一个方法,移除事件池方法的函数
let unsubscirbe = store.subscribe(updater)
return ()=>{
unsubscirbe() // 把上一次创建的updater移除掉
}
}, [num1]);
// 改变公共状态的方法 点击按钮的时候改变
const changeNum=()=>{
store.dispatch({type:'add'})
}
return (
<div>
num:{num}
<br />
age:{age}
<br />
<button onClick={()=>{changeNum()}}>change</button>
</div>
)
}
类组件
import React, { Component } from 'react'
import store from './store/index'
export default class ReduxStydy extends Component {
// 第一次渲染完毕之后绑定store事件池中更新组件的方法,this.forceUpdate()就可以更新组件
componentDidMount(){
store.subscribe(()=>{
this.forceUpdate()
})
}
changeNum = ()=>{
store.dispatch({type:'add'})
}
render() {
// 获取公共状态
let states = store.getState()
let {num,age} = states
return (
<div>
num:{num}
<br />
age:{age} <br />
<button onClick={()=>{this.changeNum()}}>change</button>
</div>
)
}
}
总结一下
-
创建一个公共的初始状态
let initlal = { num:10 } -
创建reducer,创建reducer方法,有两个参数,第一个参数是state,默认值就是初始的公共状态,第二个状态是action,action 中有一个type属性,这个属性我理解的就是修改的状态的方法名,到时候页面修改的dispatch方法会传递一个type属性归来,在reducer中判断这个type属性值来决定怎么操作这个公共状态,是增加值?还是什么样的操作。值得注意的是,修改state不能直接修改,是在reducer方法最后返回新state替换之前的state,不能直接修改我们可以克隆一份state。
const reducer = function (state = initial, action) { // state 就是存储store的公共状态,最开始没有状态就赋值为初始状态 // action 每一次基于dispatch派发的时候,传递过来的行为对象,必须具备type属性,具备派发的属性 // 基于派发的行为标识,修改容器中的公共状态信息 newState = { ...state }; switch (action.type) { case "add": state.num++; // 不能直接修改状态,等到return,故我们需要克隆一份 newState.num++; break; default: // 第一次派发不会跟任何type匹配,就返回默认的状态 return newState; } // 返回这个状态将原来的状态替换掉 return newState; }; -
创建公共状态仓库,将reducer函数穿进去。
const store = createStore(reducer) export default store -
通过 store.getState() 来得到公共的状态
-
将更新组件的的方法添加到store的事件池中,在函数组件中只有状态改变了才会重新渲染组件,所以只需要让状态更新的方法放入到组件中就可以了.然后通过副作用函数useEffect来添加方法,将事件更新到事件池的方法时store.subscribe(updateFunc),而且还需要再状态更新的时候执行,就给useEffect添加依赖,把上一次事件池中的更新事件清楚。
// 函数组件只有状态改变的时候组件才会渲染、组件第一次渲染完毕之后,把组件更新的方法放在store事件池中 const [num1, setNum1] = useState(0); // 让这个状态去触发函数组件的更新 // 组件更新 const updater = ()=>{ setNum1(num1+1) } useEffect(() => { // let unsubcirbe = store.subscribe(让组件更新的方法) // 把组件更新的方法放在事件池中,返回的是一个方法,移除事件池方法的函数 let unsubscirbe = store.subscribe(updater) return ()=>{ unsubscirbe() // 把上一次创建的updater移除掉 } }, [num1]);- 然后时更新公共状态的方法:用store.dispatch({type:'xxxxx'})来更新,一般点击时候的执行这个方法。
- 给事件池添加更新组件方法的原因:当公共状态修改了的时候,她会自动去调用更新组件的方法,组件更新了,得到的就是最新的公共状态,如果组件不更新,得到的还是原来的旧状态。
redux工程化
按照模块化把reducer进行单独管理,每个模块儿都有自己的reducer,最后我们还需要把所有的reducer进行合并,合并为一个。赋值给我们创建的store。
创建NumReducer
const inital = {
num: 10,
};
export default function NumReducer(state, action) {
const newState = { ...inital };
if (action.type === "add") {
newState.num++;
} else {
return newState;
}
return newState;
}
创建AgeReducer
const inital = {
age: 20,
};
export default function AgeReducer(state = inital, action) {
const newState = { ...state };
if (action.type === "addAge") {
newState.age++;
} else {
return newState;
}
return newState;
}
合并每个reducer
每个模块儿的reducer创建出一个总的reducer,,此时容器中的公共状态会按照我们设置的成员名字来管理。
c// 合并reducer
import { combineReducers } from "redux";
import NumReducer from "./NumReducer";
import AgeReducer from "./AgeReducer";
const reducer = combineReducers({
NumReducer,
AgeReducer,
});
export default reducer;
// 会像这样去管理,我们通过store.getState()获取的就是这个整个state,
//state = {
// NumReducer: {
// num: 10,
// },
// AgeRedcer: {
// age: 20,
// },
// };
创建容器
import { createStore } from "redux";
import reducer from "./reducer"; // 引入总的reducer
const store = createStore(reducer);
export default store;
在组件中
store.getState()的得到的是一个对象,对象中有NumReducer和AgeReducer属性,然后每个属性是一个对像,就是真正的状态
import React,{useState,useEffect} from 'react'
import store from './store/index'// 导入状态
export default function ReduxStydy() {
let {AgeReducer,NumReducer} = store.getState() // 通过store.getState()来获取状态
// 函数组件只有状态改变的时候组件才会渲染、组件第一次渲染完毕之后,把组件更新的方法放在store事件池中
const [num1, setNum1] = useState(0); // 让这个状态去触发函数组件的更新
// 组件更新
const updater = ()=>{
setNum1(num1+1)
}
useEffect(() => {
// let unsubcirbe = store.subscribe(让组件更新的方法)
// 把组件更新的方法放在事件池中,返回的是一个方法,移除事件池方法的函数
let unsubscirbe = store.subscribe(updater)
return ()=>{
unsubscirbe() // 把上一次创建的updater移除掉
}
}, [num1]);
// 改变公共状态的方法 点击按钮的时候改变
const changeNum=()=>{
store.dispatch({type:'addNum',list:[2,1]})
}
return (
<div>
num:{NumReducer.num}
<br />
age:{AgeReducer.age}
<br />
<button onClick={()=>{changeNum()}}>change</button>
</div>
)
}
派发行为标识宏管理
每一次dispatch派发的时候,都会去每个reducer中找一遍,把所有和派发行为标识匹配的逻辑进行执行。如果团队人很多,这样派发的type可能会冲突。怎么保证派发标识的唯一性呢?
新建一个文件 action-types.js
// 统一管理行为标识
// 为了不冲突,将整个项目的type值写在这里
export const addNum = "addNum";
在reducer页面,将写死的type替换掉我们统一管理的就好了
import { addNum } from "./action-types"; // 引入统一管理的
const inital = {
num: 10,
};
export default function NumReducer(state = inital, action) {
const newState = { ...state };
if (action.type === addNum) { // 直接使用统一管理的变量
newState.num++;
} else {
return newState;
}
return newState;
}
在组件中也是直接导入使用
import { addNum } from './store/action-types';
...
// 改变公共状态的方法 点击按钮的时候改变
const changeNum=()=>{
store.dispatch({type:addNum,list:[2,1]})
}
...
派发行为对象统一管理
在store下新建一个action/numAction.js
给每个模块创建xxxAction.js,对每个模块的派发行为进行管理。
import { addNum } from "../action-types";
// 每个版块儿都有自己派发的行为管理
const numAction = {
addNum() {
return {
type: addNum,
};
},
// otherFunc
};
export default numAction;
将各个模块儿的action进行统一整合
在action文件夹下创建index.js
把每个版块的action进行合并成一个action即可
import numAction from "./numAction";
import ageAction from "./ageAction";
const action = {
num: numAction,
age: ageAction,
};
export default action;
在组件中直接导入action就可以了
import action from './store/actions/index';
...
// 改变公共状态的方法 点击按钮的时候改变
const changeNum=()=>{
store.dispatch(action.num.addNum())
}
...
目前来看这个步骤太多余了,反而让书写的步骤变得更加复杂了,之前直接写就可以了,还要写在每个模块儿下的action中。这个操作有什么意义呢?为react-redux做铺垫。
redux模块化总结
将标识统一管理
actions-types.js
// 统一管理行为标识
// 为了不冲突,将整个项目的type值写在这里
export const addNum = "addNum";
创建每个模块的reducer
numReducer.js
import { addNum } from "./action-types"; // 引入宏管理标识
const inital = {
num: 10,
};
export default function NumReducer(state = inital, action) {
const newState = { ...state };
if (action.type === addNum) {
newState.num++;
} else {
return newState;
}
return newState;
}
将每个模块的reducer合并
reducer.js
// 合并reducer
import { combineReducers } from "redux";
import NumReducer from "./NumReducer";
import AgeReducer from "./AgeReducer"; // 其他模块的reducer
const reducer = combineReducers({
NumReducer,
AgeReducer,
});
export default reducer;
创建store容器
store.js
import { createStore } from "redux";
import reducer from "./reducer";
const store = createStore(reducer);
export default store;
创建派发行为统一管理
action/numAction.js
import { addNum } from "../action-types";
// 每个版块儿都有自己派发的行为管理
const numAction = {
addNum() {
return {
type: addNum,
};
},
// otherAction
};
export default numAction;
合并派发行为
action/index.js
import numAction from "./numAction";
import ageAction from "./ageAction";
const action = {
num: numAction,
age: ageAction,
};
export default action;
在组件中使用
import React,{useState,useEffect} from 'react'
import store from './store/index'// 导入状态
import action from './store/actions/index'; // 导入派发行为
export default function ReduxStydy() {
let {AgeReducer,NumReducer} = store.getState() // 通过store.getState()来获取状态
// 函数组件只有状态改变的时候组件才会渲染、组件第一次渲染完毕之后,把组件更新的方法放在store事件池中
const [num1, setNum1] = useState(0); // 让这个状态去触发函数组件的更新
// 组件更新
const updater = ()=>{
setNum1(num1+1)
}
useEffect(() => {
// let unsubcirbe = store.subscribe(让组件更新的方法)
// 把组件更新的方法放在事件池中,返回的是一个方法,移除事件池方法的函数
let unsubscirbe = store.subscribe(updater)
return ()=>{
unsubscirbe() // 把上一次创建的updater移除掉
}
}, [num1]);
// 改变公共状态的方法 点击按钮的时候改变
const changeNum=()=>{
store.dispatch(action.num.addNum()) // 用统一管理的派发行为
}
return (
<div>
num:{NumReducer.num}
<br />
age:{AgeReducer.age}
<br />
<button onClick={()=>{changeNum()}}>change</button>
</div>
)
}
react-redux
react-redux 可以让redux使用起来更加方便。
主要是在组件运用中使用方便一些。
1.内部自己创建了上下文对象,可以把store放在了上下文对象,在组件中可以直接使用。
2.不用自己去创建一个状态来手动刷新函数组件了,会自动刷新。
import ReactReux from "./ReactReux"
import store from './store/index'
import { Provider } from 'react-redux'
export default function App() {
return (
<div>
<Provider store={store}>
<ReactReux />
</Provider>
</div>
)
}
2.在组件中获取公共的状态信息绑定,无需自己基于上下文对象获取store,也无需自己getState获取,直接基于react-redux提供的connect函数处理即可,而且不需要我们手动让组件更新的方法放在事件池中,react-redux给我们处理了。
connect(mapStateToProps,mapDispatchToProps)(Component)
第一个参数:将公共状态映射给属性,第二个参数:将派发方法映射给属性。柯力化的参数是将这些属性赋值给Component组件
mapStateToProps是一个函数,函数的第一个参数state就是redux中的公共状态,只需要把公共状态作为属性返回回去即可
connect((state)=>{
num:state.Numreducer.num // 将公共状态绑定给num属性,
},mapDispatchToProps)(Component)
mapDispatchToProps也是一个回调函数,第一个参数就是dispatch,就是执行需要派发的任务。返回的也是一个对象,在对象中需要绑定我们的函数属性。
import action from "./store/actions/index.js"
connect(null,(dispatch)=>{
return{
addNum(){ // 将派发的任务绑定到addNum函数
dispatch(action.num.addNum()) // 需要派发的任务
}
}
})(Component)
上面的这种写法有些过时了。可以简化成这样去写
import action from "./store/actions/index.js"
connect(null,action.num)(Component)
可以直接写成action.num就是因为我们在redux模块儿化的的时候,将每个模块抽取成单个的action之后,我们还将全部模块的action合并成了一个action
在组件中的完整使用
import React from 'react'
import { connect } from 'react-redux'
import action from './store/actions/index'
function ReactReux(props) {
console.log(props);
return (
<div>{props.num}
<button onClick={
()=>{
props.addNum()
}
}>加加</button>
</div>
)
}
export default connect(state=>{
return {
num:state.NumReducer.num
}
},action.num)(ReactReux)
Redux中间件及处理机制
redux中间件:
- redux-logger 每次派发在控制台输出开发日志,方便对redux进行调试
- redux-thunk/redux-promise 实现异步派发。
import { createStore, applyMiddleware } from "redux";
import reduxLogger from "redux-logger";
import reducer from "./reducer";
const store = createStore(
reducer,
applyMiddleware(reduxLogger)
); // 使用中间件);
export default store;
Object.defineProperty()
对象成员规则的限制:
- Object.getOwnPropertyDescriptor(对象,成员):获取某个成员的规则
- Object.getOwnPropertyDescriptor(对象):获取对象的全部规则
规则:
- configgurable:是否可以删除
- writable: 是否可以更改
- enumerable:是否可枚举
- value:成员值
Object.defineProperty(obj, x, {});设置规则
- 对某个成员设置规则
-
如果成员已存在,则需改其规则
-
如果成员不存在,则新增这个成员,并设置规则
Object.defineProperty(obj, 'name', { enumerable: false, writabletrue: true, configurable: true, value: 100, });
数据劫持
Object.defineProperty(obj, "name", {
get() {
// 获取object.x成员信息的时候,就会触发get函数执行
// 返回内容的是成员的值
console.log("get被触发了");
// return obj['name'];
},
set(val) {
console.log("set触发");
},
});
mobx
安装 yarn add mobx-react-lite mobx
import { makeAutoObservable } from "mobx";
class Store {
// 公共状态
num = 10;
constructor() {
// 将参数对象的属性设置成 observer state
// 将参数对象中的方法设置成action
makeAutoObservable(this);
}
// 修改公共状态的方法
change() {
this.num++;
}
}
const store = new Store();
export default store;
在组件中
import React from 'react'
// 引入observer ,监听当前组件,当oberable state更新的时候,通知组件刷新
import { observer } from "mobx-react-lite"
import store from './mobx/index'
function Mobx() {
return (
<div>
{store.num}
<div>
<button onClick={()=>{store.change()}}>+</button>
</div>
</div>
)
}
// 用observer将组件包裹
export default observer(Mobx)
react-router-dom
路由案例
路由页面
我们可以把BrowserRouter加在index.js或者是app.jsx中,然全局都被路由笼罩。
所有的路由规则放在中,每一条匹配规则还是基于
- 路由匹配成功,不在基于Indexr控制渲染,而是基于element
- 不在需要switch,默认一项匹配成功就不在往下匹配
- 不再需要exact,每一项都是精准匹配
<BrowserRouter>
<Routes>
<Route path="/Home" element={<Home />} />
</Routes>
</BrowserRouter
标识history路由,还有一种是<HashRouter ?>哈希路由
Routes组件,将全部的标签都放在闭合标签中。基于单个路由匹配还是基于Route
<Route path="/Home" element={}/> path是访问的路径,element中的值是这个路径相匹配的组件
是路由出口,就相当于vue中的
重定向:<Route path="/" element={<Navigate to={{pathname:'/Home'}}>}> 输入/的时候会重定向到"/Home"中。
二级路由的话是直接在中使用闭合标签,在闭合标签中继续添加
如果是首页的话可以直接用index标记,就是首页 <Route index element={}>
root.render(<BrowserRouter ><App></App></BrowserRouter>);
import React from 'react'
import { BrowserRouter,Routes,Route,Navigate,Link, Outlet } from 'react-router-dom'
import My from './views/My'
import Center from './views/Center'
import Home from './views/Home'
import Home1 from './views/Home1'
export default function App() {
// 在v6版本中移除了Switch\Redirect\withRoute
return (
<div>
<BrowserRouter>
<Link to="/">Home</Link> |
<Link to="/Center">Center</Link> |
<Link to="/My">my</Link> |
<Outlet></Outlet>
<Routes>
{/* 重定向 */}
<Route path="/" element={<Navigate to={{pathname:'/Home'}}></Navigate>}></Route>
<Route path="/Home" element={<Home></Home>}>
{/* 二级路由 */}
<Route path="/Home/Home1" element={<Home1></Home1>}></Route>
</Route>
<Route path="/Center" element={<Center></Center>}></Route>
<Route path="/My" element={<My></My>}></Route>
</Routes>
<div>
</div>
</BrowserRouter>
</div>
)
}
路由传参
在v6版本中,即便组件是基于Route渲染的,也不会基于属性传递参数。想获取相关的参数就可以基于Hook函数处理。
- 确保使用的Hook的组件是在Router中的。
- 只要是基于Route内部包裹的组件,无论是否基于匹配渲染的,默认不在使用props传参,只能基于路由的hook获取
命令式导航
通过Link标签或者是Navigate标签来实现跳转
<Link to="/Center/4">Center</Link> // 通过useParams来得到参数
<Link to="/My?name='张三'">my</Link> // 通过useSearchParams来得到参数useSearchParams()[0].get('name')
<Link to={{pathname:'/Home/Home1'}}>Home1</Link> // 通过useLocation来得到参数
编程式导航
通过useNavigate来实现跳转
import { useNavigate } from 'react-router-dom'
const navigate = useNavigate()
navigate('/c')
navigate({
pathname:'/c',
search:'?id=10'
}) 或者 navigate('/c?name=“zhangsan”') // 通过useSearchParams来获取值 useSearchParams()[0].get('name')
navigate('/c/id') // 通过useParams
navigate('/c',{state:{a:10,b:20}}) // // 通过useLocation来得到参数
路由表
创建router/routers.js文件夹
import { Navigate } from "react-router-dom";
import { lazy } from "react"; // 路由懒加载
import Home from "../views/Home";
import Home1 from "../views/Home1";
import Center from "../views/Center";
import My from "../views/My";
// 懒加载
const Home = lazy(() =>
import("../views/Home")
);
const routes = [
{
path: "/",
name: "/",
element: () => {
// 用函数的原因是,命中的时候才加载
return <Navigate to="/Home"></Navigate>;
},
},
{
path: "/Home",
name: "Home",
element: <Home />,
meta: {},
// 二级路由
children: [
{
path: "/Home/Home1",
element: <Home1 />,
},
],
},
{
path: "/Center/:id",
name: "Center",
element: <Center></Center>,
meta: {},
},
{
path: "/My",
name: "My",
element: <My />,
meta: {},
},
];
export default routes;
创建一个router/index.js
import React, { memo } from "react";
import routes from "./routes";
import { Routes, useRoutes } from "react-router-dom";
const DefauleRoutes = memo(() => {
return useRoutes(routes);
});
export default DefauleRoutes;
在App.js中
import React from 'react'
import {Link, Outlet,useNavigate } from 'react-router-dom'
import DefauleRoutes from './router/index'
export default function App() {
return (
<div>
<Link to="/">Home</Link> |
<Link to="/Center/4">Center</Link> |
<Link to="/My?name='张三'">my</Link> |
<Outlet></Outlet>
<DefauleRoutes></DefauleRoutes>
</div>
)
}
解决跨域
yarn add http-proxy-middleware
在src下面创建src/setupProxy.js
const proxy = require("http-proxy-middleware");
module.exports = function (app) {
app.use(
proxy.createProxyMiddleware("/api", {
//api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
target: "http://app.cbbgs.com:8089", //配置转发目标地址(能返回数据的服务器地址)
changeOrigin: true, //控制服务器接收到的请求头中host字段的值
/*
changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
changeOrigin默认值为false,但我们一般将changeOrigin值设为true
*/
pathRewrite: { "^/api": "" }, //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
}),
// 配置多个
proxy.createProxyMiddleware("/api2", {
target: "http://localhost:5001",
changeOrigin: true,
pathRewrite: { "^/api2": "" },
})
);
};