day-077-seventy-seven-20230524-useEffect()与useLayoutEffect()-TaskOA任务管理系统
useEffect()与useLayoutEffect()
useEffect()与useLayoutEffect()的作用:在函数组件中使用生命周期函数。
Hook函数useEffect()细节
-
useEffect必须处于函数组件的顶层,不能嵌入到判断/循环等操作中。
// useEffect必须处于函数组件的顶层「不能嵌入到判断/循环等操作中」 if (x > 12) { useEffect(() => { console.log('AAA') }) } //修改为 useEffect(() => { if (x > 12) { console.log('AAA') } }, [x])
import { useState, useEffect, useLayoutEffect } from 'react' import { Button } from 'antd' export default function Demo() { let [x, setX] = useState(10), [y, setY] = useState(20), [z, setZ] = useState(30) // useEffect必须处于函数组件的顶层,不能嵌入到判断/循环等操作中。 if(x>12){ useEffect(()=>{ console.log(`AAA`); }) } return <div className="demo" style={{ padding: '50px' }}> <p>{x} - {y} - {z}</p> <Button type='primary' size='small' onClick={() => setX(x + 1)}>修改X</Button> <Button type='primary' size='small' onClick={() => setY(y + 1)}>修改Y</Button> <Button type='primary' size='small' onClick={() => setZ(z + 1)}>修改Z</Button> </div> }
-
应修改为:
import { useState, useEffect, useLayoutEffect } from 'react' import { Button } from 'antd' export default function Demo() { let [x, setX] = useState(10), [y, setY] = useState(20), [z, setZ] = useState(30) // 会报错; // useEffect必须处于函数组件的顶层,不能嵌入到判断/循环等操作中。 // if (x > 12) { // useEffect(() => { // console.log(`AAA`); // }) // } useEffect(() => { if (!(x > 12)) { return } console.log(`AAA`); }) return <div className="demo" style={{ padding: '50px' }}> <p>{x} - {y} - {z}</p> <Button type='primary' size='small' onClick={() => setX(x + 1)}>修改X</Button> <Button type='primary' size='small' onClick={() => setY(y + 1)}>修改Y</Button> <Button type='primary' size='small' onClick={() => setZ(z + 1)}>修改Z</Button> </div> }
-
-
在useEffect传递的callback函数中,不写返回值或者只能返回一个函数。
// 在useEffect传递的 callback 函数中,不写返回值或者只能返回一个函数,所以导致 callback 不能基于 async 修饰「因为一但修饰,则直接返回 promise 实例」 const query = async () => { await api.query() } //应修改为: useEffect(() => { // (async () => { // await api.query() // })() query() }, [])
- 所以导致,callback函数不能基于async修改。
-
因为一旦修饰,则直接返回promise实例对象。
// 会报错; // 在useEffect传递的callback函数中,不写返回值或者只能返回一个函数。所以导致,callback函数不能基于async修改。 useEffect(async () => { // await api.query() },[])
-
修改为:
useEffect(async () => { // (async () => { await api.query() })() }, [])
const query = async () => { await api.query() } useEffect(async () => { query() }, [])
-
-
- 所以导致,callback函数不能基于async修改。
useEffect()基本语法回顾
-
useEffect(callback) 在第一次渲染完毕和每一次组件更新完毕后,触发callback执行。
- 等价于 componentDidMount 和 componentDidUpdate。
-
useEffect(callback,[]) 只在第一次渲染完毕后触发执行。
- 等价于 componentDidMount。
-
useEffect(callback,[依赖的状态]) 在第一次渲染完毕后触发执行,以及依赖的状态发生改变后触发执行。
- 类似于Vue2中watch监听器。
-
useEffect(() => callback, []) 组件销毁之前,触发callback执行。
useEffect(() => { // 第一次渲染完毕做的事情 return () => { // 组件销毁之前做的事情 } }, [])
-
useEffect(() => callback) 当用户第一次修改了状态,本次闭包触发了
()=>callback
第一次状态更新返回了第一个callback
。之后下次闭包形成。- 之后等待用户第二次修改了状态。直到下次闭包触发了第二次状态更新,就会执行
第一个callback
,之后才是触发了()=>callback
第二次状态更新返回了第二个callback
。 - 之后等待用户第三次修改了状态。
- 之后等待用户第二次修改了状态。直到下次闭包触发了第二次状态更新,就会执行
-
useEffect(() => callback,[依赖的状态]) 当用户第一次修改了
依赖的状态
,本次闭包触发了()=>callback
第一次状态更新返回了第一个callback
。之后下次闭包形成。- 之后等待用户第二次修改了
依赖的状态
。直到下次闭包触发了第二次状态更新,就会执行第一个callback
,之后才是触发了()=>callback
第二次状态更新返回了第二个callback
。 - 之后等待用户第三次修改了
依赖的状态
。
- 之后等待用户第二次修改了
-
给useEffect传递的callback函数,其内部可以不写返回值,如果设置返回值,则"必须"返回一个函数!
useEffect(() => { return () => { // 但凡有状态更新(或者依赖的状态发生改变),在组件更新之前,先把此函数执行 -> 通俗理解:产生新的闭包之前,处理的事情 } }, 不写数组或者设置依赖项)
useLayoutEffect()
- useEffect()与useLayoutEffect():
-
useEffect()
:基于useEffect()
放在effect链表中的callback函数
,会在组件渲染与更新完毕后触发执行。- 实际上已经
GUI线程的绘制
与effect链表中的执行
是两个线程同步进行的。 - 完毕是指:已经把VirtualDOM渲染为真实DOM,并且浏览器已经把真实DOM绘制到页面中。
- 也就是说,已经能在浏览器页面中看到该新DOM了。
- 实际上已经
-
useLayoutEffect:基于useLayoutEffect放在effect链表中的callback函数,会阻碍GUI线程的渲染,也就是在通知effect链表中的callback函数执行的时候,让浏览器停止对真实DOM的绘制,需要先把callback执行完,再去绘制。
- 实际上是本来
GUI线程的绘制
与effect链表中的执行
是两个线程要同步进行的。用了useLayoutEffect,就先执行effect链表中的执行
,等effect链表中的执行
完毕,之后再进行GUI线程的绘制
。
- 实际上是本来
-
VirtualDOM的生命周期:
- 第一次渲染:
- render返回jsx。
- 对jsx变成createElement()格式。
- createElement()格式变成虚拟DOM。
- 虚拟DOM变成真实DOM。
- 浏览器把真实DOM渲染到页面上,让用户可以看到。
- 这一步就是useEffect与useLayoutEffect的区别。
- 第一次渲染:
-
ref的处理
-
ref的处理
-
在类组件中:
- ref的设置方式:
- 字符串:
--> this.refs.AAA
- 函数:<div ref={refObject=>this.box=refObject}> --> this.box
- 基于React.createRef处理:
- 字符串:
- ref的作用:
- 给元素设置ref,其目的是获取这个真实DOM,操作非受控数组
- 给子组件设置ref
- 子组件是类组件:则是为了获取子组件的实例
- 拿到实例后,就可以直接调用其实例上的属性和方法了!
- 子组件是函数组件:默认会报错,说明函数组件不能直接设置ref。
- 子组件是类组件:则是为了获取子组件的实例
- ref的设置方式:
-
在函数组件中:
-
ref的设置方式:
-
字符串方式已经被淘汰了。
<span ref="myRef">01</span>
-
这样使用字符串方式声明 ref 并直接访问它的方式在函数组件中是不可行的!
-
间接的做法:
import React, { useRef } from "react"; export default function Demo() { const refs = useRef({}); // 创建一个字符串 ref const myRef = useRef(null); // 将字符串 ref 存储到对象中 refs.current["myRef"] = myRef; // 使用字符串方式获取 ref const getRef = (refName) => { return refs.current[refName]; }; const handleClick = () => { // 通过字符串方式获取 ref const ref = getRef("myRef"); console.log(`getRef("myRef")`,ref.current); // 输出 ref 对应的值 }; return ( <div> <span ref={myRef}>....</span> <button onClick={handleClick}>获取 ref 值</button> </div> ); }
-
-
设置为函数。
let spanBox1 = null <span ref={refObject => spanBox1 = refObject}>01</span> console.log('spanBox1', spanBox1);//第一个span。
-
设置为ref对象。
let spanBox2 = createRef() <span ref={spanBox2}>02</span> console.log('spanBox2.current', spanBox2.current);//第二个span。
let spanBox3 = useRef() <span ref={spanBox3}>03</span> console.log('spanBox3.current', spanBox3.current);//第三个span。
import { useState, useEffect, createRef,useRef } from "react"; export default function Demo() { let spanBox1 = null let spanBox2 = createRef() let spanBox3 = useRef() useEffect(() => { console.log('spanBox1', spanBox1);//第一个span。 console.log('spanBox2.current', spanBox2.current);//第二个span。 console.log('spanBox3.current', spanBox3.current);//第三个span。 }, []) return ( <div> <span ref={refObject => spanBox1 = refObject}>01</span> <span ref={spanBox2}>02</span> <span ref={spanBox3}>03</span> </div> ); }
-
-
createRef()
与useRef()
:- createRef可以用在任何组件中,useRef只能作用在函数组件中!
- 在函数组件中,使用createRef和useRef是有区别的:
-
使用createRef,则在每一次组件渲染和更新的时候
即函数重新执行的时候
,遇到createRef都会重新创建一个新的ref对象!这样比较浪费性能。import { useState, useEffect, createRef, useRef } from "react"; let prev export default function Demo() { let [, setRandom] = useState(0) let spanBox = createRef() if (!prev) { prev = spanBox//把第一次渲染创建的ref对象赋值给prev。 } else { // 在组件更新后,函数会重新执行,遇到createRef,又创建了一个ref对象。此时我们对比两次创建的是否是相同的对象!! console.log(`prev===spanBox`, prev === spanBox);//false; } useEffect(() => { console.log('spanBox.current', spanBox.current); }) return ( <div> <span ref={spanBox}>...</span> <button onClick={() => setRandom(+new Date())}>更新</button> </div> ); }
-
使用useRef,在每一次组件渲染和更新的时候,并不会创建新的ref对象,而是把
之前第一次创建的ref对象
直接拿过来使用,节约性能。-
所以在函数组件中,我们使用
useRef()
来代替createRef()
。import { useState, useEffect, createRef, useRef } from "react"; let prev export default function Demo() { let [, setRandom] = useState(0) let spanBox = useRef() if (!prev) { prev = spanBox//把第一次渲染创建的ref对象赋值给prev。 } else { // 在组件更新后,函数会重新执行,遇到useRef,依旧返回了之前的旧ref对象。此时我们对比两次创建的是否是相同的对象!! console.log(`prev===spanBox`, prev === spanBox);//true; } useEffect(() => { console.log('spanBox.current', spanBox.current); }) return ( <div> <span ref={spanBox}>...</span> <button onClick={() => setRandom(+new Date())}>更新</button> </div> ); }
-
-
-
设置REF的作用。
- 给元素设置,其目的是获取真实DOM。
- 给子组件设置。
- 类组件:获取子组件的实例。
- 函数组件:不能直接设置,需要经过处理。
- 好像也可以用给prop传一个对象类型的ref值,不过prop好像不能改值;
- 让函数组件更新,只能通过useState()拿到一个修改状态值的函数,之后调用那个函数,才让视图更新。
-
-
-
拿到组件实例:
-
给类组件类型的子组件设置ref,其目的是获取子组件实例。
import { useState, useEffect, createRef, useRef,Component } from "react"; // 创建子组件-类组件 class Child extends Component{ render(){ return <div className="child-box"> <span>子组件-类组件</span> </div> } } // 父组件 export default function Demo() { let children = useRef() useEffect(()=>{ console.log(`children.current`,children.current); },[]) return ( <div> {/* //给类组件类型的子组件设置ref,其目的是获取子组件实例。这样想操作子组件中的那些东西,在子组件内部,就把这些东西挂载到实例上就可以了。 */} <Child ref={children}></Child> </div> ); }
-
这样想操作子组件中的那些东西,在子组件内部,就把这些东西挂载到实例上就可以了。
import { useState, useEffect, createRef, useRef, Component } from "react"; //给类组件类型的子组件设置ref,其目的是获取子组件实例。 // 创建子组件-类组件 class Child extends Component { spanBox = createRef() state = { x: 100, } submit = () => { } render() { return <div className="child-box"> <span ref={this.spanBox}>子组件-类组件</span> </div> } } // 父组件 export default function Demo() { let children = useRef() useEffect(() => { console.log(`children.current`, children.current); }, []) return ( <div> <Child ref={children}></Child> </div> ); }
-
-
给函数类型的子组件设置ref,默认是报错的。
<Child x="10" ref={children}>Child是函数类型组件</Child>
-
报错:
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
。import { useState, useEffect, createRef, useRef, Component } from "react"; // 创建子组件-函数 const Child=function Child(props) { console.log(props); return <div className="child-box"> <span>子组件-类组件</span> </div> } // 父组件 export default function Demo() { let children = useRef() useEffect(() => { console.log(`children.current`, children.current); }, []) return ( <div> <Child x="10" ref={children}></Child> </div> ); }
-
我们需要把函数子组件,基于React.forwardRef进行处理,实现
ref的转发
。-
就是把调用子组件的时候,设置的属性(包含ref属性),都作为实参传递给子组件。
const Child = forwardRef( function Child(props, ref) { // props是传递的属性;ref是调用子组件的时候,设置的ref属性值。 //... } )
-
-
此时在子组件内部,如果我们把转发的ref值,赋值给子组件的某个元素的ref属性,这样在父组件中就可以直接获取到子组件这个元素的真实DOM了。
-
在子组件内部,还可以基于useImperativeHandle()这个API,把子组件内部的某些内容直接返回,这样在父组件中也就可以调用这些信息了!
-
-
模拟forwardRef
-
forwardRef()源码模拟
// 模拟forwardRef function forwardRef1(FunctionalComponent) { // FunctionalComponent是传递进来的函数组件。 // 返回的是一个做了处理的函数组件---真实源码中返回并不是函数组件,也不是类组件。而是内部专门做处理的一个专门类型。 return function Proxy() { // 在源码中,返回的Proxy会接收到传递的属性以及为其设置的ref。 return <FunctionalComponent></FunctionalComponent> // 在渲染这个组件的时候,把拿到的属性和ref,传递给这个组件! } }
```jsx import { useState, useEffect, createRef, useRef, Component, forwardRef } from "react"; // 创建子组件-函数 const Child = forwardRef( function Child(props, ref) { // props是传递的属性;ref是调用子组件的时候,设置的ref属性值。 console.log(`props`, props, `;ref`, ref); return <div className="child-box"> <span>子组件-类组件</span> </div> } ) // 父组件 export default function Demo() { let children = useRef() useEffect(() => { console.log(`children.current`, children.current); }, []) return ( <div> <Child x="10" ref={children}></Child> </div> ); } ```
-
forwardRef()的运行机制
import { useState, useEffect, createRef, useRef, Component, forwardRef } from "react"; // 创建子组件-函数 const Child = forwardRef( function Child(props, ref) { // props是传递的属性;ref是调用子组件的时候,设置的ref属性值。 console.log(`props`, props, `;ref`, ref); return <div className="child-box"> <span>子组件-类组件</span> </div> } ) // 父组件 export default function Demo() { let children = useRef() useEffect(() => { console.log(`children.current`, children.current); }, []) return ( <div> <Child x="10" ref={children}></Child> </div> ); }
-
forwardRef()的ref转发流程
import { useState, useEffect, createRef, useRef, Component, forwardRef } from "react"; // 创建子组件-函数 const Child = forwardRef( function Child(props, ref) { // props是传递的属性;ref是调用子组件的时候,设置的ref属性值。 // console.log(`props`, props, `;ref`, ref); return <div className="child-box"> <span ref={ref}>子组件-类组件</span> </div> } ) // 父组件 export default function Demo() { let children = useRef() useEffect(() => { console.log(`children.current`, children.current); }, []) return ( <div> <Child x="10" ref={children}></Child> </div> ); }
-
forwardRef()配合useImperativeHandle()后的ref转发加强-可转发对象
-
在函数类型的子组件中,我们想把那些东西传给父组件,就把那些东西放在
作为forwardRef()入参的函数组件内部中useImperativeHandle()第二个回调函数入参中的return返回的对象
中;import { useState, useEffect, createRef, useRef, Component, forwardRef, useImperativeHandle } from "react"; // 创建子组件-函数 const Child = forwardRef( function Child(props, ref) { // 基于useImperativeHandle()这个Hook函数,可以把函数类型子组件内部的某些东西返回,赋值给转发过来的ref对象中的current属性! useImperativeHandle(ref, () => { // 在函数类型的子组件中,我们想把那些东西传给父组件,就把那些东西放在forwardRef()第一个入参的函数组件内部中useImperativeHandle()第二个回调函数中的这个return返回的对象中; return { n: 100, m: 200, } }) return <div className="child-box"> <span>子组件-类组件</span> </div> } ) // 父组件 export default function Demo() { let children = useRef() useEffect(() => { console.log(`children.current`, children.current);//{n: 100,m: 200,} }, []) return ( <div> <Child x="10" ref={children}></Child> </div> ); }
-
TaskOA任务管理系统
-
做管理系统,要用UI框架,一般React得用antd;
- 用antd得考虑汉化,汉化分成组件库本体及日期插件如dayjs。
-
PC端UI组件库:
- 比较核心的组件,操作也复杂一些,可以当成一个技能来学的。
- 表格组件
- 后台管理系统上基本上最主要的了。
- 表单及表单校验
- 基本上最重要的了。
- 弹出层
- 文件上传
- 日历
- 这个主要是在它上面集成其它东西的。
- …
- 表格组件
- 如何看一个UI组件。
- 如果有案例,先看案例。
- 结合案例,查看该组件的核心API。
- 查看一个UI组件库的API列表,思考着能用这些API达到什么自定义的效果。
- 在控制台上查询页面上已修改得差不多的UI组件,之后查看UI组件的DOM元素,看它的类名,根据类名大概猜出那个类是重要的,根据需要自定义修改样式。
- 比较核心的组件,操作也复杂一些,可以当成一个技能来学的。
-
对着一份设计图,前后端都可以同步开始自己的任务。
- 如果可以,前端先问后端大概能给什么接口,接口的作用具体是什么。
- 前端可以先对着设计图搭页面。
- 如果页面已经搭完,那么前端可以先做一些假数据,根据这些假数据进行交互的设计。
- 后端写完接口,进行联调。