1.渲染原理
- jsx基于@babel/preset-react-app,将jsx文件编译成ast语法树,再解析ast对象拼接成React.createElement的函数
- 调用React.createElement生成virtualDom
- 调用render方法传入virtualDom和container,渲染真实dom;
- 16调用ReactDom.render(virtualDom,container)
- 18调用React.createRoot(container).render(virtualDom)
2.函数组件渲染/更新
函数组件的每一次渲染(或者是更新),都是把函数(重新)执行,产生一个全新的“私有上下文”!
- 重新执行函数时,内部的代码也需要重新执行
- 涉及的函数需要重新的构建{这些函数的作用域(函数执行的上级上下文),是每一次执行组件产生的闭包}
- 每一次执行函数组件,也会把useState重新执行,但是:
- 执行useState,只有第一次,设置的初始值会生效,其余以后再执行,获取的状态都是最新的状态值「而不是初始值」
- 返回的修改状态的方法,每一次都是返回一个新的
import React, { useState } from "react";
import { Button } from 'antd';
import './Demo.less';
import { flushSync } from 'react-dom';
const Demo = function Demo() {
console.log('RENDER渲染 产生新的闭包');
let [x, setX] = useState(10),
const handle = () => {
setX(x + 1);
};
return <div className="demo">
<span className="num">x:{x}</span>
<Button type="primary"
size="small"
onClick={handle}>
新增
</Button>
</div>;
};
export default Demo;
3.useState
- useState自带了性能优化的机制:
- 每一次修改状态值的时候,会拿最新要修改的值和之前的状态值做比较「基于Object.is作比较」
- 如果发现两次的值是一样的,则不会修改状态,也不会让视图更新「可以理解为:类似于PureComponent,在shouldComponentUpdate中做了浅比较和优化」
- useState的更新机制
- 18版本中:当前同步任务中的所有操作,都会先存放到一个数组中,始终在下一个js事件队列中批量更新,所以它始终是异步的;类组件的setState也是同样的原理!
- 16版本中:也会收集存放到数组中,只是它只在唯一的一次微任务中执行;如果你在同步任务中调用,它就是异步的;如果你在异步中调用,它就成了同步的;如果你在定时器中调用,由于定时器在微任务后执行,所以会跳过微任务,再执行时就成了同步!
import React, { useState } from "react";
import { Button } from 'antd';
import './Demo.less';
import { flushSync } from 'react-dom';
const Demo = function Demo() {
console.log('RENDER渲染 产生新的闭包');
let [num, setNum] = useState(0);
const handle1 = () => {
setNum(100);
setTimeout(() => {
console.log(num); // 0 始终是上一次函数执行产生的闭包中的数据
}, 1000);
setTimeout(() => {
// 18版本永远会在下一个任务队列中执行!
// 16版本只会在一个微任务中执行,再往后的都是同步!
setNum(200)
}, 2000);
};
const handle = () => {
for (let i = 0; i < 10; i++) {
setNum(num + 1); // 会被批处理合并成一次 最后永远是1
}
};
return <div className="demo">
<span className="num">{num}</span>
<Button type="primary"
size="small"
onClick={handle}>
新增
</Button>
</div>;
};
export default Demo;
- 如果传递的是函数,函数中可以接收上一次的state,和类组件中的setState一样
// 如何在每一次循环中都递增数据,而不是只递增一次?
const handle = () => {
for (let i = 0; i < 10; i++) {
setX((prev) => {
console.log(prev); // prev:存储上一次的状态值
return prev + 1; //返回的信息是我们要修改的状态值
});
}
};
- 使用flushSync可以立即清空一次setState队列
const handle = () => {
// 立即刷新任务队列
flushSync(() => {
setX(x + 1);
setY(y + 1);
});
setZ(z + 1);
};
- 对useState初始值的优化
每次更新状态都会重新执行函数组件中的所有内容,虽然useState对初始值做了保护,如果初始值依赖于某个props属性,也会导致其重复设置初始值;
// 每一次都会重复执行这一坨
let { x, y } = props, total = 0;
for (let i = x; i <= y; i++) {
total += +String(Math.random()).substring(2);
}
let [num, setNum] = useState(total)
// 改成下面这种形式
let [num1, setNum1] = useState(() => {
let { x, y } = props,
total = 0;
for (let i = x; i <= y; i++) {
total += +String(Math.random()).substring(2);
}
return total;
});
- 简单实现useState
var _state;
function useState(initialValue) {
if (typeof _state === "undefined") {
if(typeof initialValue==="function"){
// 初始值如果是函数,就调用该函数获取返回值
_state = initialValue();
}else{
_state = initialValue
}
};
var setState = function setState(value) {
if(Object.is(_state,value)) return;
if(typeof value==="function"){
// 如果传进来的是函数,就把上一次的状态传过去
_state = value(_state);
}else{
_state = value;
}
// 通知视图更新
};
return [_state, setState];
}
- 实现一个partialState
useState的setState没有类组件中的setState那样拥有partailState能力,每次修改状态都是全量替换,所以每次都需要拷贝一份,非常麻烦!
export function usePartailState<T>(
initState: T
): [T, (state: T extends object ? Partial<T> : T) => void] {
const [state, setState] = useState<T>(initState);
function setPartailState(newState: any) {
if (typeof state === "object" && state !== null) {
if (!isEqual(state, newState)) {
if (Array.isArray(newState) && Array.isArray(state)) {
newState = [...state, ...newState];
} else {
newState = { ...state, ...newState };
}
setState(newState);
}
} else {
setState(newState);
}
}
return [state, setPartailState];
}
4.useEffect
- 在函数组件中使用的每一个useEffect,都会被MountEffect函数将effect接收的回调函数,收集到一个effect链表中
- 每一次函数重新执行后(状态更新),都会调用updateEffect函数,取出并执行链表中的effect回调函数
- 4种形态
useEffect(() => {
// useEffect(callback):没设置依赖
// + 第一次渲染完毕后,执行callback,等价于 componentDidMount
// + 在组件每一次更新完毕后,也会执行callback,等价于 componentDidUpdate
console.log("@1", num);
});
useEffect(() => {
// useEffect(callback,[]):设置了,但是无依赖
// + 只有第一次渲染完毕后,才会执行callback
// + 等价于 componentDidMount
console.log("@2", num);
}, []);
useEffect(() => {
// useEffect(callback,[依赖的状态(多个状态)]):
// + 第一次渲染完毕会执行callback
// + 当依赖的状态值(或者多个依赖状态中的一个)发生改变,也会触发callback执行
// + 但是依赖的状态如果没有变化,在组件更新的时候,callback是不会执行的
console.log("@3", num);
}, [num]);
useEffect(() => {
return () => {
// 返回的小函数,会在组件释放的时候执行
// 如果组件更新,会把上一次返回的小函数执行「可以“理解为”上一次渲染的组件释放了」
console.log("@4", num);
};
});
- useEffect接收的函数不能是async
因为加了async后,其函数就成了一个promise实例!而React需要的是可以被调用的普通函数!
// 这样写是不行的!
useEffect(async () => {
let data = await queryData();
console.log('成功:', data);
}, []); */
// 需要改成这样
useEffect(() => {
queryData()
.then(data => {
console.log('成功:', data);
});
}, []);
// 或者这样
const getXXX = async () => {
let data = await queryData();
console.log('成功:', data);
};
useEffect(() => {
getXXX();
}, []);
- useEffect和useLayoutEffect的区别
- useLayoutEffect会阻塞浏览器渲染真实DOM,优先执行Effect链表中的callback;
- useEffect不会阻塞浏览器渲染真实DOM,在渲染真实DOM的同时,去执行Effect链表中的callback;
- useLayoutEffect设置的callback要优先于useEffect去执行!!
- 在两者设置的callback中,依然可以获取DOM元素「原因:真实DOM对象已经创建了,区别只是浏览器是否渲染」
- 如果在callback函数中又修改了状态值「视图又要更新」
- useEffect:浏览器肯定是把第一次的真实dom已经绘制了,再去渲染第二次真实DOM
- useLayoutEffect:浏览器是把两次真实DOM的渲染,合并在一起渲染的
useLayoutEffect(() => {
// 一个超大的循环,将阻塞渲染,多久渲染出来,取决于电脑性能多久能把这个循环执行完!
for (let i = 0; i < 1000000000000; i++) {}
console.log("useLayoutEffect"); //第一个输出
}, [num]);
useEffect(() => {
console.log("useEffect"); //第二个输出
}, [num]);
5.useRef
- 类组件获取ref的3种方式
class Demo extends React.Component {
ref1 = React.createRef();
ref2 = null;
logRef = () => {
console.log(ref1, ref2, this.refs.ref3);
};
render() {
return (
<div ref={ref1}>
<div ref={(ref) => (this.ref2 = ref)}>
<div ref="ref3"></div>
</div>
</div>
);
}
}
- 函数组件获取ref的3种方式
const Demo = function Demo() {
let ref1 = useRef(null);
let ref2 = React.createRef();
let ref3;
useEffect(() => {
console.log(ref1,ref2,ref3);
}, []);
return (
<div ref={ref1}>
<div ref={ref2}>
<div ref={(ref) => (ref3 = ref)}></div>
</div>
</div>
);
};
- useRef和createRef的区别
useRef只会在组件第一次渲染时创建,后面每一次更新都是取的缓存中的;只能在函数组件中使用!
而createRef在函数组件中使用时,由于函数组件每次更新都会全部执行一次,所以每次都会重新创建一个新的ref对象;但是在类组件中使用时,类组件只更新的是render中的,不会重新new,所以也只执行一次;
所以:在函数组件中虽然可以使用createRef,但是不推荐使用!还是用为函数组量身定制的useRef最佳!
let prev1,prev2;
const Demo = function Demo() {
let [num, setNum] = useState(0);
let box1 = useRef(null), box2 = React.createRef();
if (!prev1) {
// 第一次DEMO执行,把第一次创建的REF对象赋值给变量
prev1 = box1;
prev2 = box2;
} else {
console.log(prev1 === box1); //true
console.log(prev2 === box2); //false
}
return <div className="demo">
<span className="num" ref={box1}>{num}</span>
<span className="num" ref={box2}>哈哈哈</span>
<Button type="primary" size="small"
onClick={() => {
setNum(num + 1);
}}>
更新视图
</Button>
</div>;
};
6.useImperativeHandle
写在类组件上面的ref,能够获取到类组件的实例;而函数组件没有实例,无法直接通过ref获取函数组件的ref;
React.forwardRef能对函数组件做ref转发,将ref对象传递给函数子组件
useImperativeHandle函数能将指定数据回传给父组件,搭配React.forwardRef就能实现ref的传递
const Child = React.forwardRef(function Child(props, ref) {
let [text, setText] = useState("你好世界");
const submit = () => {};
useImperativeHandle(ref, () => {
// 在这里返回的内容,都可以被父组件的REF对象获取到
return {
text,
submit,
};
});
return (
<div className="child-box">
<span>哈哈哈</span>
</div>
);
});
const Parent = () => {
const childRef = useRef(null);
useEffect(() => {
console.log(childRef.current);
}, []);
return <Child ref={childRef} />;
};
7.useMemo
对数据做缓存!
具备“计算缓存”,在依赖的状态值没有发生改变,callback没有触发执行的时候,获取的是上一次计算出来的结果;和Vue中的计算属性非常的类似!!
有时候我们只改变了某一个状态,并不想让其它没变化的数据做不必要的更新!
const Demo = function Demo() {
let [supNum, setSupNum] = useState(10),
[oppNum, setOppNum] = useState(5),
[x, setX] = useState(0);
/*
let xxx = useMemo(callback,[dependencies])
+ 第一次渲染组件的时候,callback会执行
+ 后期只有依赖的状态值发生改变,callback才会再执行
+ 每一次会把callback执行的返回结果赋值给xxx
+ useMemo具备“计算缓存”,在依赖的状态值没有发生改变,
+ callback没有触发执行的时候,xxx获取的是上一次计算出来的结果
+ 和Vue中的计算属性非常的类似!!
*/
let ratio = useMemo(() => {
let total = supNum + oppNum,
ratio = '--';
if (total > 0) ratio = (supNum / total * 100).toFixed(2) + '%';
return ratio;
}, [supNum, oppNum]);
return <div className="vote-box">
<div className="main">
<p>支持人数:{supNum}人</p>
<p>反对人数:{oppNum}人</p>
<p>支持比率:{ratio}</p>
<p>x:{x}</p>
</div>
<div className="footer">
<Button type="primary" onClick={() => setSupNum(supNum + 1)}>支持</Button>
<Button type="primary" danger onClick={() => setOppNum(oppNum + 1)}>反对</Button>
<Button onClick={() => setX(x + 1)}>干点别的事</Button>
</div>
</div>;
};
export default Demo;
8.useCallback
对函数做缓存!
函数一般都是常量不会发生变化,但是由于函数组件每次更新都会全部执行一遍,所以每次都会重新创建一个函数,而我们并不想让它重复创建新的函数!
const Demo = function Demo() {
let [x, setX] = useState(0);
// const handle = () => { }; //第一次:0x001 第二次:0x101 第三次:0x201 ...
const handle = useCallback(() => { }, []); //第一次:0x001 第二次:0x001 第三次:0x001 ...
return <div className="vote-box">
<Child handle={handle} />
<div className="main">
<p>{x}</p>
</div>
<div className="footer">
<Button type="primary" onClick={() => setX(x + 1)}>累加</Button>
</div>
</div>;
};
9.React.memo
对函数组件添加类似PureComponent的能力!
假如我们对函数组件传递一个不会发生变化的参数,例如基于useCallback的函数,在父组件更新时,子组件并没有发生状态变化,此时我们并不希望子组件更新;但是由于父组件重新执行,就一定会重新调用子组件!子组件就会产生不必要的更新!
// 类组件的PureComponent会添加一个shouldComponentUpdate周期函数,并对新老状态和props做钱比较!
class Child extends React.PureComponent {
render() {
console.log('Child Render'); // 父组件更新不会触发这里
return <div>我是子组件</div>;
}
}
// 函数组件使用memo包裹,也类似PureComponent
const Child1 = React.memo(function Child1(props) {
console.log('Child Render1'); // 父组件更新不会触发这里
return <div>我是子组件1</div>;
});
// 普通函数组件,不加memo,每一次都会触发更新
const Child2 = function Child2(props) {
console.log('Child Render2'); // 每一次都有不必要的更新
return <div>我是子组件2</div>;
};
const Demo = function Demo() {
let [x, setX] = useState(0);
const handle = useCallback(() => { }, []);
return <div className="vote-box">
<Child handle={handle} />
<Child1 handle={handle} />
<Child2 handle={handle} />
<div className="main">
<p>{x}</p>
</div>
<div className="footer">
<Button type="primary" onClick={() => setX(x + 1)}>累加</Button>
</div>
</div>;
};
10.useMemo和useCallback慎用原因
如果你的数据每次必然会发生变化,那么实际上用了反而会增加多余的步骤!并且可能会出现一些其它问题!
11.自定义hook
export const useDidMount = function useDidMount(callback) {
useEffect(() => {
if (typeof callback === "function") {
callback();
}
}, []);
};