React 框架分析
1. 基础语法
根标签
根标签必须为单节点,如有多个节点可用<React. Fragment></React. Fragment>或者<></>
插值表达式
在类型:
boolean / null / undefined / symbol / bigint渲染结果为空
Object普通对象不支持直接渲染
数组对象分别把每项进行渲染, 并不是直接toString(), [1, 2] => 12
循环五次的方法
{
new Array(5).fill(null).map((_, index) => < div > {
{
index
}
} < /div>);
}
2.jsx 处理机制
第一次渲染时候将虚拟 dom 直接转化为真实 dom, 后面根据 vdom 的 diff 算法计算出差异的部分 Patch, 再渲染 patch
2.1 将 jsx 转 AST 语法树
使用babel-preset-react-app将 jsx 语法转化为 AST 语法树,
例如以下 jsx 语法
< div >
<
h1 className = "main-title" > 我的标题 < /h1> <
div style = {
{
color: "red"
}
} > content < /div> <
h2 > message < /h2> < /
div >
React17 以及以前转化为 React.createElement
/* classic转化方法
React.createElement(element,props,children)
element:元素或者是组件
props:属性,中间包括id
children:子节点
*/
import React from "react";
React.createElement(
"div",
null,
React.createElement(
"h1", {
className: "main-title",
},
"\u6211\u7684\u6807\u9898"
),
React.createElement(
"div", {
style: {
color: "red",
},
},
"content"
),
React.createElement("h2", null, "message")
);
React18 转化 jsx 函数
import {
jsx as _jsx
} from "react/jsx-runtime";
import {
jsxs as _jsxs
} from "react/jsx-runtime";
_jsxs("div", {
children: [
_jsx("h1", {
className: "main-title",
children: "\u6211\u7684\u6807\u9898",
}),
_jsx("div", {
style: {
color: "red",
},
children: "content",
}),
_jsx("h2", {
children: "message",
}),
],
});
2.2 生成虚拟 dom
使用 React.createElement(react 17)或者 jsx(react 18)函数生成虚拟 dom
virtualDom = {
$$typeof: Symbol(React.element),
ref: null,
key: null
props: {
children: []
},
type: 'div'
}
简单实现 createElement
// mini-react/min-react-dom.js
export function createElement(element, props, ...children) {
let vitualDom = {
$$typeof: Symbol("React.element"),
ref: null,
key: null,
props: {
children: [],
},
type: "div",
};
let len = children.length;
vitualDom.type = element;
if (props !== null) {
vitualDom.props = {
...props
};
}
if (len === 1) {
vitualDom.props.children = children[0];
}
if (len >= 1) {
vitualDom.props.children = children;
}
return vitualDom;
}
2.3 虚拟 dom 转化为真实 dom
简单实现 render, 将虚拟 dom 转化为真实 dom, render 方法
// mini-react/min-react-dom.js
// 遍历所有的属性
function each(obj, callback) {
if (obj === null || obj === undefined || typeof obj !== "object") {
throw new TypeError("obj is not a object");
}
Reflect.ownKeys(obj).forEach((key) => callback(key, obj[key]));
}
export function render(vitualDom, container) {
let {
type,
props
} = vitualDom;
// 如果是函数组件的话,直接执行,将参数传入
if (typeof type === "function") {
const componentFn = type;
const componentVm = componentFn(props);
render(componentVm, container);
return;
}
if (typeof type === "string") {
let ele = document.createElement(type);
each(props, (key, value) => {
//className直接设置 ele.className='box'
if (key === "className") {
ele.className = value;
return;
}
//style属性需要深层遍历,ele.style.color='red'
if (key === "style") {
each(value, (attr, val) => {
ele.style[attr] = val;
});
return;
}
// 子节点,children
if (key === "children") {
let children = value;
if (!Array.isArray(children)) {
children = [children];
}
children.forEach((child) => {
// 子节点是文本节点,就是字符串
if (/^(string|number)$/.test(typeof child)) {
ele.appendChild(document.createTextNode(child));
return;
}
// 子节点是个vitualDom
render(child, ele);
});
return;
}
// 普通属性直接设置 如id等
ele.setAttribute(key, value);
});
container.appendChild(ele);
}
}
2.4 函数组件渲染机制
- 基于babel-preset-react-app将 jsx 语法转化为 AST 语法树
babel-preset-react-app将 jsx 语法转化为 AST 语法树
2.4.1 基于React.createElement(TestComponent, {
title: "TestComponent",
data: [1, 2],
className: "test-box",
style: {
color: "red",
},
});
- AST 语法树转化为虚拟 dom
2.4.2 AST 语法树转化为虚拟 dom
virtualDom = {
$$typeof: Symbol(React.element),
ref: null,
key: null
props: {
children: [],
title: "TestComponent",
data: [1, 2],
className: "test-box",
style: {
color: "red"
}
},
type: TestComponent
}
2.4.3 render 过程
render 过程:
执行函数组件 TestComponent
把 props 传给函数 TestComponent(props)
import {
createElement,
render
} from "../mini-react/min-react-dom";
/**
*
function TestComponent (props){
return (
<div className={props.className} style={props.style}>
<div>{props.title}</div>
<div>{props.data}</div>
</div>
);
}
*/
function TestComponent(props) {
return createElement(
"div", {
className: props.className,
style: props.style,
},
createElement("div", null, props.title),
createElement("div", null, props.data)
);
}
/**
<TestComponent
title="TestComponent"
data={1}
className="test-box"
style={{
color: "red"
}}
/>
*/
const jsxObj = createElement(TestComponent, {
title: "TestComponent",
data: 1,
className: "test-box",
style: {
color: "red",
},
});
render(jsxObj, document.getElementById("root"));
render 函数(/mini-react/min-react-dom.js)中增加对组件(函数)的判断
// 如果是函数组件的话,直接执行,将参数传入
if (typeof type === "function") {
const componentFn = type;
const componentVm = componentFn(props);
render(componentVm, container);
return;
}
2.4.4 关于函数 props 属性
props 属性只读
- 调用组件,传进来的属性为只读属性,已经被冻结了 Object.freeze(props);
- 父组件调用子组件时候,可以把属性传给子组件,不同的组件收到不同的属性,展示出组件的复用性
4. hooks 函数组件
函数组件每次渲染或者更新,都是函数重新执行一次,产生一个全新的私有上下文
内部的代码都需要重新执行一次
// 示例
import React, {
useState
} from "react";
export default function TestHookUseState() {
const [count, setCount] = useState(0);
function addCount() {
setCount(count + 1);
}
return (
<div>
<div> count: {count} </div>
<div>
<button onClick = {addCount}> 数字加1 </button>
</div>
</div>
);
}
4.1 执行过程
1. 第一次渲染组件, 把函数执行【 传递属性】
产生私有上下文EC1
私有变量:
count = 0;
setCount = 修改状态的函数
addCountClick = 函数
开始编译jsx函数, 创建virtualDom, 最后渲染为真实的dom
点击addCountClick函数, 执行addCountClick
产生私有上下文EC1 - addCountClick
setCount(count + 1) 变为setCount(10);
修改状态
控制视图更新
4.2 useState 实现示例
4.2.1 示例实现代码
let _state;
export function useState(initVal) {
if (typeof _state === "undefined") {
_state = initVal;
}
const setState = (newValue) => {
_state = newValue;
console.log("render");
};
return [_state, setState];
}
4.2.2 示例实现代码
在 react18 中,基于 useState 创建出来的修改状态的方法,执行过程也是异步的
基于异步操作与更新队列,实现更新状态的批处理
更新队列:
[setState1, setState2, , setState3, .....];
4.2.3 惰性化初始值
将初始值放在 useState 的初始化的值里面,最开始只是执行一次,
以后组件更新不会执行
import React, {
useState
} from "react";
export default function TestHookUseState() {
const [timeStamp, setTimeStamp] = useState(() => {
return Date.now();
});
return (
<div>
<div> timeStamp: {timeStamp} </div>
</div>
);
}
4.3 flushSync
flushSync 会立即执行一次更新
import React, {
useState
} from "react";
// import {useState} from './useState';
import {
flushSync
} from "react-dom";
export default function TestHookUseState() {
console.log("render TestHookUseState");
const [count, setCount] = useState(0);
const [count2, setCount2] = useState(0);
const [count3, setCount3] = useState(0);
function add() {
setCount(count + 1);
flushSync(() => {
setCount2(count2 + 1);
});
// count已放队列,count2放队列之后执行一次更新,之后再count3放入更新队列执行一次更新
setCount3(count3 + 1);
}
return (
<div>
<div> count: {count} </div>
<div> count2: {count2} </div>
<div>
<button onClick = {add}> 数字加1 </button>
</div>
</div>
);
}
4.4 useEffect 实现示例
4.4.1
useEffect(callback):
第一次渲染完毕之后执行。callback(等于 componentDidMount)
组件每次更新完毕之后执行 callback(等于 componentDidUpdate)
useEffect(callback, [dep, dep2]):
第一次渲染完毕之后执行
当依赖的状态值发生改变的时候,也会执行callback
当依赖的状态值未发生改变的时候,就算组件更新,也不会执行callback
useEffect(callback, []):
callback返回的函数在组件释放的时候执行
import React, {
useEffect,
useState
} from "react";
export default function TestHookUseEffect() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("useEffect callback TestHookUseEffect");
});
useEffect(() => {
console.log("useEffect count callback TestHookUseEffect");
}, [count]);
function add() {
setCount(count + 1);
}
return (
<div>
<div>count:{count}</div>
<div>
<button onClick = {add}> 数字加1 </button>
</div >
</div>
);
}
4.4.2
useEffect执行过程
- 第一次渲染产生executeContext(执行上下文)
- 其中的变量count初始化
- MountEffect方法把callback的依赖项加入effect链表
// 示例
@1 useEffect callback 依赖项
@2 useEffect callback 依赖项
@3 useEffect callback 依赖项 返回函数
基于UpdateEffect方法通知effect链表中的callback按照要求执行
- 基于依赖项
4.4.3 useLayoutEffect与useEffect区别
useEffect
和 useLayoutEffect
都是React的hooks函数,都可以用来处理副作用。但是它们执行的时间是不一样的。
useEffect
会在渲染完毕后异步执行,而 useLayoutEffect
会在渲染前同步执行。这就是它们的主要区别。
因此,如果你有需要在渲染前执行的事件, useLayoutEffect
是更好的选择。
另一个区别是在服务端渲染(SSR)时的表现。 useEffect
不会阻塞渲染,而 useLayoutEffect
会在渲染前执行,可能会阻塞渲染。
以下是一些使用建议:
- 如果你的事件不需要操作DOM,可以使用
useEffect
。 - 如果你的事件需要操作DOM,并且希望在渲染前执行,可以使用
useLayoutEffect
。 - 如果你使用了服务端渲染(SSR),并且你的事件需要在渲染前执行,并且需要操作DOM,可以使用
useLayoutEffect
,但要注意可能会阻塞渲染。
4.5 useRef与React.createRef
- 在类组件与函数组件都可以使用createRef
- 在函数组件中只能使用useRef
- useRef在每次组件更新时候使用的是一个ref,不会创建新的ref对象
- React.createRef在每一次组件更新时候,会创建一个全新的ref对象,比较浪费性能,在类组件中不会出现每次都创建ref对象
- 所以React.createRef适合在类组件中使用
- useRef适合在函数组件中使用
const form = useRef();
// dom对象
form.current
const form1 = React.createRef();
// dom对象
form1.current
4.6 React.forwardRef与useImperativeHandle
- 实现元素转发
const Child =React.forwardRef(function Child(props,ref){
const [text,setText] = useState('');
const onOk = ()=>{
console.log('onOk')
}
useImperativeHandle(ref,()=>{
return {
text,
onOk
}
})
return (
<div>
<div ref={ref}>childText</div>
</div>
)
})
const Parent = function (){
let childEleRef = useRef(null);
useEffect(()=>{
// childEleRef.current拿到内嵌的对象 {text,onOk}
console.log(childEleRef.current)
},[])
return <div>
<Child ref={childEleRef} />
</div>
}
4.7 useMemo
- useMemo比较类型与Vue的computed
const result = useMemo(callback,[dep]);
function Calculate(){
const [a,setA]=useState(0);
const [b,setB]=useState(0);
const sum = useMemo(()=>{
const num = a+b;
return num;
},[a,b])
return <div>
<div>a:{a}</div>
<div>b:{a}</div>
<div>sum:{sum}</div>
<button onClick={()=>setA(a+1)}> add a</button>
<button onClick={()=>setB(b+1)}> add b</button>
</div>
}