0 概述
React 经验汇总
1 类型
1.1 VDOM
interface ReactElement<
P = any,
T extends string | JSXElementConstructor<any> =
| string
| JSXElementConstructor<any>
> {
type: T
props: P
key: Key | null
}
ReactElement,就是自动jsx自动生成出来的代码了
namespace JSX {
// ...
interface Element extends React.ReactElement<any, any> { }
// ...
}
JSXElement就是ReactElement本身(仅仅是泛型参数固定为any而已)
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
ReactNode是支持字符串,数字,布尔,空值,ReactElement数组,ReactElement等的组合,换句话说,任意能在Component的render函数返回的值都属于ReactNode
JSX.Element ≈ ReactElement ⊂ ReactNode
1.2 VDOM生成器
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P> | undefined;
contextTypes?: ValidationMap<any> | undefined;
defaultProps?: Partial<P> | undefined;
displayName?: string | undefined;
}
FunctionComponent组件,泛型参数为props的类型
2 render与portal
代码在这里
2.1 虚拟DOM与父子关系的不同
import React, { useEffect } from 'react';
import ReactDOM, { createPortal } from 'react-dom';
const HelloFromPortal: React.FC<any> = (props) => {
return (
<div
onClick={() => {
alert('我爸应该知道我被点击了');
}}
>
我是传送门里出来的Portal
</div>
);
};
const AmISameAsPortal: React.FC<any> = (props) => {
return (
<div
onClick={() => {
alert('是不是从传送门里出来呢? 我妈应该知道我被点击了');
}}
>
是不是从传送门里出来呢? not Portal
</div>
);
};
const HelloReact: React.FC<any> = (props) => {
useEffect(() => {
//render是没有返回值的
//只有AmISameAsPortal自身会响应,虚拟节点上没有父节点,不能响应
ReactDOM.render(
<AmISameAsPortal />,
document.getElementById('another-container')!,
);
}, []);
return (
<div>
<h1>父组件</h1>
<div
onClick={() => {
alert('YES Dispaly');
}}
>
{
//createPortal是有返回值的,它在指定DOM节点上渲染数据,但是挂载在虚拟的DOM节点下面。
//所以,能看到一个神奇的现象,点击指定DOM节点上的标记,
//不仅HelloFromPortal会响应,虚拟节点的父节点也会响应
createPortal(
<HelloFromPortal />,
document.getElementById('another-root')!,
)
}
</div>
XXXX XXXX
<div
onClick={() => {
alert('No display');
}}
></div>
</div>
);
};
export default HelloReact;
要点如下:
- ReactDOM.render是没有返回节点的,React.createPortal是返回节点的
- ReactDOM.render与React.createPortal都是挂载在一个指定的DOM节点上。但是React.createPortal竟然还能与原有的节点组成虚拟DOM的父子关系,而ReactDOM.render与原节点时没有虚拟DOM的父子关系
2.2 命令式与声明式的不同
import React, { useEffect, useRef, useState } from 'react';
import ReactDOM, { createPortal } from 'react-dom';
const HelloFromPortal: React.FC<any> = (props) => {
return <div>Protal对话框</div>;
};
const AmISameAsPortal: React.FC<any> = (props) => {
return <div>Not Portal对话框</div>;
};
const HelloReact: React.FC<any> = (props) => {
let [firstPortalShow, setFirstPortalShow] = useState(false);
let data = useRef({
isShow: false,
ref: document.getElementById('another-container')!,
});
const hiddenDialog = () => {
ReactDOM.unmountComponentAtNode(data.current.ref);
data.current.isShow = false;
};
const showDialog = () => {
ReactDOM.render(<AmISameAsPortal />, data.current.ref);
data.current.isShow = true;
};
return (
<div>
<h1>父组件</h1>
<button
onClick={() => {
if (firstPortalShow == true) {
setFirstPortalShow(false);
} else {
setFirstPortalShow(true);
}
}}
>
是否显示Protal
</button>
<div>
{
//createPortal的另外一个好处是,可以套用state的方式来控制是否显示该节点。
firstPortalShow
? createPortal(
<HelloFromPortal />,
document.getElementById('another-root')!,
)
: null
}
</div>
<button
onClick={() => {
//ReactDOM.render的方式就是只能为命令式的
if (data.current.isShow == false) {
showDialog();
} else {
hiddenDialog();
}
}}
>
是否显示NotProtal
</button>
</div>
);
};
export default HelloReact;
要点如下:
- ReactDOM.render使用命令式的编程方式,使用ReactDOM.render来触发首次渲染,以及触发后续的重render。最后使用ReactDOM.unmountComponentAtNode来卸载节点。
- React.createPortal使用声明式的编程方式,数据驱动的编程方式。因为createPortal自身返回的就是个Element,我们在render的时候简单地返回null来卸载这个节点。
2.3 构造命令式对话框
import React, { ReactElement } from 'react';
import ReactDOM from 'react-dom';
type ProtalRender<T> = (portal: MyPortal<T>) => ReactElement;
const MyPortalWrapper: React.FC<{ render: ProtalRender<any>; portal: MyPortal<any> }> = (
props,
) => {
const dom = props.render(props.portal);
return <>{dom}</>;
};
class MyPortal<T = any> {
private ref: HTMLElement | null = null;
private resultNotify: ((data: T) => void) | null = null;
constructor(private render: ProtalRender<T>) { }
public open() {
if (this.ref) {
throw new Error('对话框已经打开了');
}
this.ref = document.createElement('div');
document.body.appendChild(this.ref);
const node = <MyPortalWrapper render={this.render} portal={this} />;
ReactDOM.render(node, this.ref);
}
//仅仅调用了setResult的时候才会返回
//如果portal没有触发setResult的话不会返回。
public awaitOpen(): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.open();
this.resultNotify = resolve;
});
}
public rerender() {
if (!this.ref) {
throw new Error('对话框未打开');
}
const node = <MyPortalWrapper render={this.render} portal={this} />;
ReactDOM.render(node, this.ref);
}
public setResult(data: T) {
if (this.resultNotify != null) {
this.resultNotify(data);
this.resultNotify = null;
}
}
public close() {
if (!this.ref) {
throw new Error('对话框未打开');
}
ReactDOM.unmountComponentAtNode(this.ref);
this.ref.parentElement?.removeChild(this.ref);
this.ref = null;
}
}
export default MyPortal;
先创建一个MyPortal类
import React, {
ReactElement,
ReactNode,
useEffect,
useRef,
useState,
} from 'react';
import ReactDOM, { createPortal } from 'react-dom';
import MyPortal from './MyPortal';
type SamplePortal = {
onClick: () => void;
counter: number;
};
const SamplePortal: React.FC<SamplePortal> = (props) => {
return (
<div>
Sample Protal对话框
<span style={{ color: 'red' }}>{props.counter}</span>
<button onClick={props.onClick}>关闭</button>
</div>
);
};
const HelloReact: React.FC<any> = (props) => {
const [state, setState] = useState(0);
const data = useRef<MyPortal<string>>();
const counter = useRef<number>(0);
return (
<div>
<h1>父组件</h1>
<button
onClick={async () => {
if (data.current) {
return;
}
data.current = new MyPortal((protal) => {
const onClick = () => {
protal.setResult("cc");
protal.close();
data.current = undefined;
};
return (
<SamplePortal
onClick={onClick}
counter={counter.current}
/>
);
});
let result = await data.current.awaitOpen();
console.log(result);
}}
>
显示Protal
</button>
<button
onClick={() => {
if (data.current) {
data.current.close();
data.current = undefined;
}
}}
>
隐藏Protal
</button>
<div>计算器:{counter.current}</div>
<button
onClick={() => {
counter.current++;
setState(state + 1);
//因为对话框是用命令的方式,而不是state的方式生成出来的。
//所以对话框依赖的数据变更了以后,需要手动触发rerender
if (data.current) {
data.current.rerender();
}
}}
>
递增计数器
</button>
</div>
);
};
export default HelloReact;
然后使用这个MyPortal来创建对话框
大部分的React库都是使用声明方式来构造对话框的,我们试试用命令方式来构造对话框。使用时的特点如下:
- 优点,命令式对话框,使用起来更简单直观,不需要创建一个额外的节点。
- 优点,命令式对话框,避免声明式需要一个独立的visible的state
- 优点,命令式对话框的事件不会冒泡到原有节点上。
- 缺点,原有的父节点不太容易同步状态到命令式对话框。
3 html
有时候,我们需要直接赋值html到元素里面。代码看这里
3.1 div的Html声明式赋值
import { useState } from 'react';
import { Button } from 'antd';
const divTest: React.FC<any> = (props) => {
const [state, setState] = useState('<p>欢迎<span style="color:red;">fish</p>');
return (
<div>
<h1>{'Div的dangerHtml测试'}</h1>
<Button onClick={() => {
setState('<p>欢迎<span style="color:blue;">cat</p>');
}}>切换</Button>
<div dangerouslySetInnerHTML={{ __html: state }}></div>
</div>
);
}
export default divTest;
简单,没啥好说的,性能稍差一点
3.2 div的Html命令式赋值
import { useEffect, useRef, useState } from 'react';
import { Button } from 'antd';
const divTest: React.FC<any> = (props) => {
const divRef = useRef(null as unknown as HTMLDivElement);
useEffect(() => {
divRef.current.innerHTML = '<p>欢迎<span style="color:red;">fish</p>';
}, []);
return (
<div>
<h1>{'Div的dangerHtml测试'}</h1>
<Button onClick={() => {
divRef.current.innerHTML = '<p>欢迎<span style="color:blue;">cat</p>';
}}>切换</Button>
<div ref={divRef}></div>
</div>
);
}
export default divTest;
性能最好,命令式赋值
3.3 iframe的Html声明式赋值
import { useState } from 'react';
import { Button } from 'antd';
const divTest: React.FC<any> = (props) => {
const [state, setState] = useState('<p>欢迎<span style="color:red;">fish</p>');
return (
<div>
<h1>{'Div的dangerHtml测试'}</h1>
<Button onClick={() => {
let result = '<div>';
for (var i = 0; i != 100; i++) {
result += `<p>欢迎<span style="color:blue;">cat${i}</p>`
}
result += "</div>";
setState(result);
}}>切换</Button>
<div style={{ width: '100%', height: '500px', border: '1px solid black' }}>
<iframe style={{ width: '100%', height: '100%', border: 0 }} srcDoc={state} />
</div>
</div>
);
}
export default divTest;
使用srcDoc就能做iframe的声明式赋值
3.4 iframe的Html命令式赋值
import { useState, useRef, useEffect } from 'react';
import { Button } from 'antd';
const divTest: React.FC<any> = (props) => {
const frameRef = useRef(null as unknown as HTMLIFrameElement);
useEffect(() => {
frameRef.current.contentWindow?.document.write('<p>欢迎<span style="color:red;">fish</p>');
}, []);
return (
<div>
<h1>{'Div的dangerHtml测试'}</h1>
<Button onClick={() => {
let result = '<div>';
for (var i = 0; i != 100; i++) {
result += `<p>欢迎<span style="color:blue;">cat${i}</p>`
}
result += "</div>";
//write是续写,要先调用close清除数据
frameRef.current.contentWindow?.document.close();
frameRef.current.contentWindow?.document.write(result);
}}>切换</Button>
<div style={{ width: '100%', height: '500px', border: '1px solid black' }}>
<iframe ref={frameRef} style={{ width: '100%', height: '100%', border: 0 }} />
</div>
</div>
);
}
export default divTest;
使用iframe的ref下面的contentWindow?.document来赋值,要注意write是后续写操作,重新写需要先用close清空。
4 ReactElement与JSXElementConstructor
代码在这里
4.1 cloneElement
import React from 'react';
const Item:React.FC<{value:string}> = (props)=>{
return (<div>{props.value}</div>);
}
const Container:React.FC<{}> = (props)=>{
const data = ["1","2","3"];
let result = [];
for( var i in data ){
var single = data[i];
//提供通用修改属性的能力
let singleElem = React.cloneElement(<Item value=""/>,{
key:i,
value:"++++"+single+"++++",
});
result.push(singleElem);
}
return (
<div>{result}</div>
);
}
export default Container;
cloneElement这点也没啥好说的,就是将JSX生成的结果,取出来做一个merge操作。
4.2 JSXElementConstructor
type JSXElementConstructor<P> = ((props: P) => ReactElement<any, any> | null) | (new (props: P) => Component<any, any, any>)
JSXElementConstructor的定义也比较简单,相当于就是React.Func与React.ClassComponent两种了。既然JSXElement只是一个函数,或者一个new方法,为什么我们不能用高阶函数来生成一个动态的Component类型。
import { ReactElement ,useState,JSXElementConstructor } from "react"
import {Input} from 'antd';
type MyComponentType<T> = (props:{data:T,dataIndex:keyof T,manualRefresh:()=>void})=>ReactElement;
function ComponentFactory<T>(data:T):MyComponentType<T>{
const result:MyComponentType<T> = (props)=>{
return (<div>
<span>Input</span><Input value={props.data[props.dataIndex] as any} onChange={(e)=>{
props.data[props.dataIndex] = e.target.value as any;
props.manualRefresh();
}}/></div>);
}
return result;
}
const data = {
name:'fish',
age:123,
}
const MyComponent = ComponentFactory(data);
const MyComponentConstructor:JSXElementConstructor<{data:typeof data,dataIndex:keyof typeof data,manualRefresh:()=>void}> = MyComponent;
const Page:React.FC<any> = (props)=>{
const [state,setState] = useState(0);
const manualRefresh = ()=>{
setState((v)=>v+1);
}
return (
<div>
<MyComponent data={data} dataIndex={'name'} manualRefresh={manualRefresh}/>
<MyComponent data={data} dataIndex={'age'} manualRefresh={manualRefresh}/>
</div>
);
}
export default Page;
我们用ComponentFactory来生成Component,然后用这个动态的Component来创建JSX.Element,一切都是正常好用的。这种做法的好处生成的MyComponent有更好的编译提示,dataIndex只能为name或者age,否则会报错。
4.3 过度动态的JSXElementConstructor
import { ReactElement ,useState } from "react"
import {Input} from 'antd';
type MyComponentType<T> = (props:{data:T,dataIndex:keyof T,manualRefresh:()=>void})=>ReactElement;
function ComponentFactory<T>(data:T):MyComponentType<T>{
const result:MyComponentType<T> = (props)=>{
return (<div>
<span>Input</span><Input value={props.data[props.dataIndex] as any} onChange={(e)=>{
props.data[props.dataIndex] = e.target.value as any;
props.manualRefresh();
}}/></div>);
}
return result;
}
const data = {
name:'fish',
age:123,
}
const Page:React.FC<any> = (props)=>{
const [state,setState] = useState(0);
const MyComponent = ComponentFactory(data);
const manualRefresh = ()=>{
setState((v)=>v+1);
}
return (
<div>
<MyComponent data={data} dataIndex={'name'} manualRefresh={manualRefresh}/>
<MyComponent data={data} dataIndex={'age'} manualRefresh={manualRefresh}/>
</div>
);
}
export default Page;
但是这种方法也有局限的地方,如果MyComponent不是初始化一次生成,而是在每次render的时候重新生成。使用React的diff操作就会出问题,因为两个VDOM的比较,不仅比较key,而且比较JSXElementConstructor的引用是否一致。
所有,以上的代码产生的问题在于,当在Input输入框修改文字的时候,就会触发render,产生一个不同引用的JSXElementConstructor,最终导致每次的Input都要卸载重新挂载,Input上的焦点丢失了。
5 再谈类型
代码看这里
import React, { JSXElementConstructor, ReactElement, ReactNode } from 'react';
//函数与类组件
const ComponentA:React.FC<{title:string}> = (props)=>{
return (<h1>{props.title}</h1>);
}
class ComponentB extends React.Component<{size:number}>{
render(){
return (<div>{this.props.size}</div>);
}
}
//使用Typescript获取组件的props,这个很好用
type componentAProps = React.ComponentProps<typeof ComponentA>;
type componentBProps = React.ComponentProps<typeof ComponentB>;
//函数与类组件都属于JSXElementConstructor
function componentConstructor(data: JSXElementConstructor<any>){
}
componentConstructor(ComponentA);
componentConstructor(ComponentB);
//凡是组件都能返回JSXElement(等价于ReactElement),而字符串和数字等等都不属于ReactElement
const element1:ReactElement = <ComponentA title={"13"}/>
//const element2:ReactElement = "23"
//const element3:ReactElement = 123
//而ReactNode是更为基础的渲染元素了,唯一的缺点在于ReactNode不能使用cloneElement等的方法
const node1:ReactNode = <ComponentA title={"13"}/>
const node2:ReactNode = "string"
const node3:ReactNode = 123
const node4:ReactNode = null
const node5:ReactNode = undefined
没啥好说的,如上面代码的注释所说的
6 数据源与两步编辑
代码在这里
6.1 外部数据源
import styles from './index.less';
import {Input,Button} from 'antd';
import { useState } from 'react';
const MyInput:React.FC<any> = (props)=>{
return (<Input value={props.value} onChange={(e)=>{
props.onChange(e.target.value);
}}/>);
}
const ShowTip:React.FC<any> = (props)=>{
return <h2>状态为:{props.value}</h2>
}
export default function IndexPage() {
const [state,setState] = useState('');
const [state2,setState2] = useState('');
console.log('render');
return (
<div>
<Button onClick={()=>{
setState('Fish');
}} type="primary">{'外部设置数据为:Fish'}</Button>
<Button onClick={()=>{
console.log('state',state);
console.log('state2',state2);
}}>{'获取全部数据'}</Button>
<MyInput value={state} onChange={setState}/>
<MyInput value={state2} onChange={setState2}/>
<ShowTip value={state}/>
</div>
);
}
我们有两个Input,其中一个Input是独立的,无需更新其他部分。另外一个Input需要与ShowTip的同步数据。以上代码比较简单,将数据抽取为外部数据源就可以了,缺点是:
- MyInput2,无需更新其他部分,但每次都触发全局Render
- MyInput1,需要同步更新ShowTip,但是更新频率太高,Render次数太多。
6.2 内部数据源
import styles from './index.less';
import {Input,Button} from 'antd';
import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
const MyInput:React.FC<any> = forwardRef((props,ref)=>{
const [state,setState] = useState('');
useImperativeHandle(ref,()=>({
getValue(){
return state;
},
setValue(value:string){
setState(value);
}
}));
return (<Input value={state} onChange={(e)=>{
setState(e.target.value);
setTimeout(props.onChange,0);
}}/>);
});
const ShowTip:React.FC<any> = (props)=>{
return <h2>状态为:{props.value}</h2>
};
export default function IndexPage() {
const [tip,setTip] = useState('');
const ref = useRef<any>();
const ref2 = useRef<any>();
console.log('render');
return (
<div>
<Button onClick={()=>{
ref.current.setValue('Fish');
setTip('Fish');
}} type="primary">{'外部设置数据为:Fish'}</Button>
<Button onClick={()=>{
console.log('state',ref.current.getValue());
console.log('state2',ref2.current.getValue());
}}>{'获取全部数据'}</Button>
<MyInput ref={ref} onChange={()=>{
setTip(ref.current.getValue());
}}/>
<MyInput ref={ref2}/>
{<ShowTip value={tip}/>}
</div>
);
}
将Input的数据源移入到Input内部,我们得到了以下优化:
- MyInput2,无需更新其他部分,这次不再需要触发全局Render了
- MyInput1,需要同步更新ShowTip,但是更新频率太高,Render次数太多。
缺点是,取数据的时候不太方便,需要用ref来取
6.3 内部数据源2
import styles from './index.less';
import {Input,Button} from 'antd';
import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
const useManualRefresh = ()=>{
const [state,setState] = useState(0);
return {
manualRefresh:()=>{
setState(v=>v+1);
}
}
}
const MyInput:React.FC<any> = forwardRef((props,ref)=>{
const {manualRefresh} = useManualRefresh();
return (<Input value={props.value[props.name]} onChange={(e)=>{
props.value[props.name] = e.target.value;
if( props.onChange ){
props.onChange();
}
manualRefresh();
}}/>);
});
const ShowTip:React.FC<any> = (props)=>{
return <h2>状态为:{props.value}</h2>
};
export default function IndexPage() {
const {manualRefresh} = useManualRefresh();
const refData = useRef({name:''});
console.log('render');
return (
<div>
<Button onClick={()=>{
refData.current.name = "Fish";
manualRefresh();
}} type="primary">{'外部设置数据为:Fish'}</Button>
<Button onClick={()=>{
console.log('state',refData.current);
}}>{'获取全部数据'}</Button>
<MyInput value={refData.current} name="name" onChange={()=>{
manualRefresh();
}}/>
<MyInput value={refData.current} name="name2"/>
{<ShowTip value={refData.current.name}/>}
</div>
);
}
我们进一步优化一下,内部数据源更新数据的时候,做原地更新就可以了。保留了6.2的优点,同时改进了6.2的缺点
6.4 两步编辑
import styles from './index.less';
import {Input,Button} from 'antd';
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
const useManualRefresh = ()=>{
const [state,setState] = useState(0);
return {
manualRefresh:()=>{
setState(v=>v+1);
}
}
}
const MyInput:React.FC<any> = forwardRef((props,ref)=>{
const [state,setState] = useState(props.value);
const ref2 = useRef<any>();
useImperativeHandle(ref,()=>({
getValue(){
return state;
}
}));
useEffect(()=>{
ref2.current.focus();
ref2.current.select();
},[]);
return (<Input ref={ref2} value={state} onChange={(e)=>{
setState(e.target.value);
}}/>);
});
const WrapInput:React.FC<any> = (props)=>{
const [isEdit,setIsEdit] = useState(false);
const ref = useRef<any>();
if( isEdit == false ){
return (<div style={{border:'1px solid black',height:'30px'}} onClick={()=>{
setIsEdit(true);
}}>{props.value[props.name]}</div>);
}else{
return (
<div onBlur={()=>{
setIsEdit(false);
props.value[props.name] = ref.current.getValue();
if(props.onChange){
props.onChange();
}
}} style={{height:'30px'}}>
<MyInput ref={ref} value={props.value[props.name]}/>
</div>
);
}
}
const ShowTip:React.FC<any> = (props)=>{
return <h2>状态为:{props.value}</h2>
};
export default function IndexPage() {
const {manualRefresh} = useManualRefresh();
const refData = useRef({name:''});
console.log('render');
return (
<div>
<Button onClick={()=>{
refData.current.name = "Fish";
manualRefresh();
}} type="primary">{'外部设置数据为:Fish'}</Button>
<Button onClick={()=>{
console.log('state',refData.current);
}}>{'获取全部数据'}</Button>
<WrapInput value={refData.current} name="name" onChange={()=>{
manualRefresh();
}}/>
<WrapInput value={refData.current} name="name2"/>
{<ShowTip value={refData.current.name}/>}
</div>
);
}
我们使用WrapInput来实现两步编辑法,这个时候同时改进了两个问题:
- MyInput2,无需更新其他部分,这次不再需要触发全局Render了
- MyInput1,需要同步更新ShowTip,而且仅在onBlur的时候进行更新,Render次数大大减少。
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!