前言
学习了前端的html,js,css 。还是得了解一下前端的框架有名的框架React,主要学习下react的思想和关键使用方法
JSX
定义规则
- 只能返回一个根元素
- 标签必须闭合
- 使用驼峰式命名法给 大部分属性命名
index=>tabIndexclass=>classNamefor=>htmlFor
有关{}的使用
在书写Html内容那边,想加入js相关的动态变量内容的引用或者计算表达式,用{}来标记
定义:const element = Hello, world!;
使用:<div>{element}</div>
<h1>{name}'s To Do List</h1> // 有效
<{tag}>gjc To Do List</{tag}> // 无效
有时候在JSX 中看到 {{ 和 }}时,其实是包在大括号里的一个对象
<div person={{ name: "Hedy Lamarr", inventions: 5 }}></div>
<ul style={{backgroundColor: 'black',color: 'pink'}}>
针对如下使用场景的{}的用法
- 表示
true或者false - 表达式书写花括号中
- 具体对象中的参数获取
- 使用三目运算代替if和else
- 内联样式
html标签调用方法js中表达整个标签对象js中表达整个标签数组
<div maskClosable={false}></div>
<div>{1+1}</div>
<div>{this.state.name}</div>
第一种:<div>{i == 1 ? 'True!' : 'False'}</div>
第二种:className={ this.state.curId === item.id?'active':'asideItem'}
第三种:style={this.state.curId === item.id ? {fontSize:'20px'}:{fontSize:'12px'}}
定义 var myStyle={fontSize: 100, color: '#FF0000' };
使用 <div style= {myStyle}/>
function formatName(user){return user.name}
function getGreeting(user) {
if (user) {return <h1>Hello, {formatName(user)}!</h1>;}
return <h1>Hello, Stranger.</h1>;
}
const element = (
<h1 className="greeting">Hello, world!</h1>
);
使用<div>{element}</div>
定义var arr = [ <div>11</div>,<div>22</div>]
使用<div>{arr}</div>
获取渲染列表内容
通过map,filter等函数对数组进行数据筛选,然后拼接成到html上,return返回
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
);
return (
<ul>
{listItems}
</ul>
);
注意渲染列表: 直接放在 map() 方法里的 JSX 元素一般都需要指定 key 值(可以是数据库的主键id,也可以是uuid等自增唯一值)
逻辑判断渲染内容
- 通过
if..else控制渲染内容的不同
let button;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}
2. 通过运算符&&
通过在js上的上对运算符的短路原则了解,只有左边true的时候,右边才会执行,所以在这边就是右边元素会渲染
<div>{isLoggedIn && <UserProfile />}</div>
3. 三目运算符 ?x:y
{isLoggedIn? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />}
基础使用
创建项目
我们可以通过create-react-app来实现创建react脚手架
npx create-react-app demo // 创建项目
yarn start // 启动项目,yarn可以通过npm安装
当然也可以通过vite来构建项目 ,名称为my-react-app的react项目 ,当然是--template 可以有很多其他的选择vue,vue-ts,react,react-ts等等这些
yarn create vite my-react-app --template react
函数组件
编写函数组件的形式的模板案例(vscode使用快捷键rfc)
注意: 函数组件无法使用this,点击事件绑定可以直接指定函数名或者箭头函数调用要指定的函数
export default function Text(props) {
// function 的方式形式
function text1(){}
// 箭头函数的形式
const text2 = () => {}
return (
<div>
<!-- 不能使用this -->
<Button type='primary' onClick={text2}>按钮</Button>
<Button type='primary' onClick={()=>test2()}>按钮</Button>
</div>
)
}
目前使用最多的还是函数组件,毕竟现在使用hooks多,且函数组件支持hooks写法。
类组件
编写类组件的形式模板案例(vscode使用快捷键rcc)
注意:类组件中无法使用function,点击事件绑定函数时可以通过构造指定,也可以点击事件上指定。{this.xxClick.bind(this)}
export default class Home extends Component {
// 箭头函数 ,调用: {() => this.button1Click("error")} / {this.button1Click}
button1Click = (type) => {
message[type](type)
}
// 无法使用function
// 1 .使用方法 , 且需要构造方法中this.buttonClick= this.buttonClick.bind(this); 调用:{this.buttonClick.bind(this)}
// 2, 获取调用时候绑定且能传值 {this.buttonClick.bind(this,1)}
buttonClick(){
alert("test")
}
render() {
return (
<div>
<Button type='primary'
onClick={() => this.button1Click("error")}>按钮</Button>
</div>
)
}
}
React的一些方法
React.createElement()// 创建标签React.createClass()// 创建类组件
import * as Icon from "@ant-design/icons";
const icon = React.createElement(Icon[item.icon])
const CreateClassCom = React.createClass({
render: function() {
return <div>这是React.createClass定义的组件</div>
}
});
空标签
<Fragment> 通常使用 <>...</> 代替,它们都允许你在不添加额外节点的情况下将子元素组合
<>
<OneChild />
</>
Props
React 组件使用 props 来互相通信。每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它
注意:不要尝试更改 props。 当你需要响应用户输入时,你可以去设置 state
父组件传递给子组件的值
传递两个参数:person(一个对象)和 size(一个数字)
export default function Profile() {
return (
<Avatar
person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
size={100}
/>
);
}
子组件读取父组件传的值
- 方式一:通过对象解构
- 方式二:
props对象接收然后读取值
// 方式一:通过对象解构
function Avatar({ name, imageId}) {
// 在这里 name 和 imageId是可访问的
}
// 方式二: props对象接收然后读取值
function Avatar(props) {
let name= props.name;
let imageId= props.imageId;
// ...
}
注意:接收的时候也可以指定参数的默认值
function Avatar({ name, imageId = 'xxxxx' }) {
// ...
}
组件的children
props中含有children属性,代表其包含的组件列表,所以可以通过拿到标签的children做各种修饰效果
// 定义组件Card
function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}
组件Card作用就是对其内部标签进行重新定义效果,有点类似装饰
// 使用,此时Card的children则为<Avatar>...</Avatar>
<Card>
<Avatar person={{ name: 'Katsuko Saruhashi',imageId: 'YfeOqp2'}}/>
</Card>
// 最后效果等同于
<div className="card">
<Avatar person={{ name: 'Katsuko Saruhashi',imageId: 'YfeOqp2'}}/>
</div>
State
组件通常需要根据交互更改屏幕上显示的内容,我们单纯去修改变量的赋值,达不到这样的效果,所以得用State来保证重新渲染效果
基本使用
在保证使用新数据更新组件,需要保留渲染间的数据,还有就是触发React 使用新数据渲染组件,此时我们可以使用hook提供的useState方法
- State变量用于保存渲染件的数据
- State setter 函数 更新变量并触发 React 再次渲染组件
具体使用方法:通过定义一个名称和一个set函数方法,和useState中设置初始值,具体使用案例如下所示中
const [index, setIndex] = useState(0);
function changeNumber() {
setIndex((index) => index + 1);
}
return (
<div>
<Button type="primary" onClick={changeNumber}>
按钮
</Button>
<div>{index}</div>
</div>
);
注意: setIndex(index+1) 这样使用会有问题,因为会合并所有更新状态且是异步执行,导致index可能会是旧的值
使用场景
针对控制不同情况的变量,可以分开定义State的值,对于那些时常一起变动的可以在一个State上值为对象,对于修改对象字段时,要保证赋值的是新对象,才能触发渲染
- 修改
State的值为对象内容的时候,单一修改个别里面字段
const [person,setPerson] = useState({})
setPerson({
...person, // 复制上一个 person 中的所有字段
firstName: e.target.value // 但是覆盖 firstName 字段
});
- 修改
State的值为对象内容的时候,修改嵌套对象属性
const [person,setPerson] = useState({})
setPerson({
...person, // 复制其它字段的数据
artwork: { // 替换 artwork 字段
...person.artwork, // 复制之前 person.artwork 中的数据
city: 'New Delhi' // 但是将 city 的值替换为 New Delhi!
}
});
3. 修改State的值为数组内容的时候
这个时候就不能使用常规的crud数组了,也是需要构建新的数组
使用State的规范
- 合并关联的 state。如果你总是同时更新两个或更多的 state 变量,请考虑将它们合并为一个单独的 state 变量。
- 避免互相矛盾的 state。当 state 结构中存在多个相互矛盾或“不一致”的 state 时,你就可能为此会留下隐患。应尽量避免这种情况。
- 避免冗余的 state。如果你能在渲染期间从组件的 props 或其现有的 state 变量中计算出一些信息,则不应将这些信息放入该组件的 state 中。
- 避免重复的 state。当同一数据在多个 state 变量之间或在多个嵌套对象中重复时,这会很难保持它们同步。应尽可能减少重复。
- 避免深度嵌套的 state。深度分层的 state 更新起来不是很方便。如果可能的话,最好以扁平化方式构建 state。
注意: 在使用State的时候,多个组件共同状态的State可以提取到公共父组件,保证由上游的单一变化的State联动控制下游组件。
// 父组件
export default function Parent(){
// 上游的check控制
const [check,setCheck] = useState(false)
return (<div>
<Son check={check}>
</div>
)
}
// 子组件
export default function son({check}){
return (<div>
{check&&<Button>按钮<Button>}
<div>)
}
不同位置state的保留和移除
当我们定义了某个组件,在不同场景下使用该组件,他们各自维持的state都由自身去控制,数据是隔离的互不影响。
当某个组件被移除的时候,组件中保存的state也同步消失。此时我们要注意,对于同一组件UI树的位置发生变化的时候,其保存的内容才会消失 。
// 同一个位置组件在UI 树没变化 ,因为同样是Son组件 ,切换时Son保存保存内容不消失
{checked?<Son person="mike">:<Son person="jake">}
// 同一个位置组件在UI 树有变化 , 因为一个是p组件,一个是Son组件 ,切换时Son保存内容有变化
{checked?<p>nihao</p> :<Son checked={checked}>}
// 定义两块地方位置,组件在UI 树有变化,切换时Counter内保存的内容会消失
{isPlayerA &&<Counter person="Taylor" />}
{!isPlayerA &&<Counter person="Sarah" />}
// 通过key来区分不同组件,切换时会触发Son内保存内容消失
{checked?<Son key="k1" checked={checked}>:<Son key="k2" checked={checked}>}
所以要想组件内的State的内容保留,前提就是保证在同一位置,如果不想保留,就需要实现不同位置或者可以定义组件的Key来保证组件不同
// 定义一个test组件
export default function test(){
const [count, setCount] = useState(1);
// f1组件写在了test组件内部
function f1(){
const [text, setText] = useState('');
return (<div>
{test}
</div>)
}
return (<div>
<f1/>
<Button type="primary"
onClick={()=>setCount(count=>count+1)}>按钮</Button>
<div>)
}
以上情况就是每次渲染test组件时候,都会创建不同f1函数组件,然后相当于你在相同位置渲染的是不同的组件,所以其下的state就都重置了
所以注意: 组件不应该写成函数嵌套在其他组件内部,应该永远要将组件定义在最上层并且不要把它们的定义嵌套其他组件内
Reducer
随着组件状态逻辑越来越多,我们把所有逻辑都可以存放在一个易于理解的地方。
定义增删改逻辑
定义的增删改查逻辑的方法内都通过给dispatch传递一个对象action可以由自己定义,可以命名type标记行为,其他自己额外增加参数dispatch({action对象})
function add(){
dispatch({
type: 'add',
text:'内容',
id:1
});
}
function deleted(){
dispatch({
type: 'deleted',
id:1
});
}
声明和核心逻辑
useReducer接收一个reducer函数核心处理逻辑 和一个初始的State
// 声明
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
const initialTasks = []
// 核心逻辑 reducer函数
function tasksReducer(tasks,action){
switch (action.type) {
case 'add': return [...task,{text:action.text,id:action.id}]
case 'deleted' : return tasks.filter((t) => t.id !== action.id);
default: {
throw Error('未知 action: ' + action.type);
}
}
}
可以在项目中同时使用 reducer和state自由搭配,对于简单逻辑使用state,使用了reducer在逻辑上更清晰和规整,方便调试和排查
Context
父子组件间传递值,通过props一层一层的传递显得非常的不方便和冗长。
使用步骤:
- 创建 一个
context,通过createContext函数创建且赋值初始值 - 在需要数据的组件内 使用 刚刚创建的
context,通过useContext使用 - 在指定数据的组件中 提供 这个
context,通过TestContext.Provider提供值
// 创建一个TestContext,赋值初始值
export const TestContext = createContext(1);
// 父组件,不同son组件上标记不同的值
function Father(){
return (<div>
<Son count={1}>
<GrandSon>
<GrandSon>
</Son>
<Son count={2}>
<GrandSon>
<GrandSon>
</Son>
</div>)
}
// 子组件获取标记上的值封装到TestContext上
function Son({ count, children }){
return (<TestContext.Provider value={count}>
{children}
</TestContext.Provider>)
}
// 孙子组件获取到UI树上最近的组件对应的的Context的值
function GrandSon(){
const count = useContext(TestContext);
}
上述案例的流程就是,将一个 count 参数传递给 <Son>, Son 把它的子元素包在 <TestContext.Provider value={count}> 里面, GrandSon 使用 useContext(TestContext) 访问上层最近的 TestContext 提供的值
注意: Context 会穿过中间层级的组件, 让你在提供组件的Context和使用它中间可以存在相隔很多个组件,且不同的 React context 不会覆盖彼此
使用场景: 虽然Context虽好,但是还是需要使用者合理的规划使用它,传统的Props虽然传递繁琐,但是更加的直观可见。对于那些不传递数据的样式组件层可以通过children包装传递,减少多余的组件层传递
//使用Layout布局,没有涉及到数据使用
<Layout data={data}>
function Layout({data}){
return (<div>
<Data data={data}>
<div>)
}
// 可以直接通过chidren包装传递,减少组件间传递
<Layout>
<Data data={data}>
</Layout>
function Layout({chidren}){
return (<div>
{chidren}
<div>)
}
Ref
用于组件记住信息,但不重新触发渲染。
基本使用
使用步骤如下:
- 声明
const count = useRef(0) - 获取到
ref的对象结果为{current:0} - 修改
ref的值count.current = 1
const count = useRef(0);
return (
<div>
<div>{count.current}</div>
<Button
type="primary"
onClick={() => {count.current = count.current + 1;alert(count.current);}}
>按钮</Button>
</div>
);
注意: 修改ref的值,不会触发重新渲染,alert中输出的值会一直递增+1, 但div中的值一直都是0
具体的使用场景:存储不影响渲染逻辑的内容 (timeOutId,Dom元素等)
ref操作DOM
const myRef = useRef(null);
<div ref={myRef}>
// 你可以使用任意浏览器 API,例如:
myRef.current.scrollIntoView();
获取标签dom节点,直接在标签上绑定属性ref,此时React 会把对该节点的引用放入 myRef.current
function Parent(){
const myRef = useRef(null);
return <Son ref={myRef}></Son>
}
function Son({ref}){
return <input ref={ref}></div>
}
// Parent组件的myRef控制内容input组件的dom元素
注意: 可以通过props传递ref来实现控制其他组件的dom,ref一般使用场景为管理焦点、滚动位置或调用 React 未暴露的浏览器 API,避免手动修改dom导致与React的更改进行冲突
Effect
基本使用
Effect的执行逻辑是在渲染结束后运行,就是先渲染页面更新,然后且得看依赖数组再判断是否在执行Effect代码
useEffect接收两个参数
- 执行逻辑的函数,当函数有返回时(每次
Effect重新运行之前调用,并在组件卸载/被移除时最后一次调用) - 依赖数组(只有当你指定的 所有 依赖项的值都与上一次渲染时完全相同,
React才会跳过重新运行该Effect)。
对应的执行场景
useEffect(() => {
// 这里的代码会在每次渲染后运行
});
useEffect(() => {
// 这里的代码只会在组件挂载(首次出现)时运行
}, []);
useEffect(() => {
// 这里的代码不但会在组件挂载时运行,而且当 a 或 b 的值自上次渲染后发生变化后也会运行
}, [a, b]);
useEffect(() => {
return ()=>{}; // Effect重新运行之前执行,和组件写在/被移出时调用
}, []);
以下是要避免的场景:state和Effect相互触发
// 如下代码造成死循环现象
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
上述代码会导致死循环是因为Effect 运行、更新 state、触发重新渲染、于是又触发 Effect 运行、再次更新 state,继而再次触发重新渲染。如此反复,从而陷入死循环
结论: 只有指定依赖数组后,才会避免死循环
Effect执行的次数2次?
在开发环境下,有严格模式的约束下,会通过重新挂载你的组件,React 验证了离开页面再返回不会导致代码出错,所以导致会执行两次逻辑。
在正式环境就不用担心有这个问题,只会执行一次。
但如何在开发模式下,保证严格执行校验2次的结果保证一致,类似幂等。通过定义一个ref记录初始化的值,第二次执行的时候判断是否可以执行即可
function Father() {
const initialized = useRef(false);
useEffect(() => {
// 只执行一次
if (!initialized.current) {
console.log("初始化副作用执行");
initialized.current = true;
}
return () => {
console.log("清理副作用");
};
}, []);
return <div>Father Component</div>;
}
生命周期
每个 React 组件都经历相同的生命周期:
- 当组件被添加到屏幕上时,它会进行组件的 挂载。
- 当组件接收到新的
props或state时,通常是作为对交互的响应,它会进行组件的 更新。 - 当组件从屏幕上移除时,它会进行组件的 卸载。
useEffect(() => {
console.log(`装载 ${roomId}`);
return () => console.log(`卸载 ${roomId}`);
}, [roomId]);
上述代码在执行声明周期所执行挂载,更新,卸载的过程是如下
// 组件的首次挂载执行更新逻辑
console.log(`装载 ${roomId}`);
//组件更新,执行清理函数,在重新执行更新逻辑
console.log(`卸载 ${roomId}`)
console.log(`装载 ${roomId}`);
// 组件卸载,执行清理函数
console.log(`卸载 ${roomId}`)
Hooks
注意: Hooks只能在组件或自定义 Hook 的最顶层调用。不能在循环语句、条件语句或 map() 函数中调用
除了上面提到的hooks,如 useState和useEffect,useReducer,useContext , useRef这些, 还有一些比较常用的hooks
useMemo
useMemo 是 React 中一个非常实用的 Hook,用于缓存计算结果 ,避免在每次渲染时都执行昂贵的计算操作。它非常适合用来优化性能
const memoizedValue = useMemo(() => expensiveFn(a, b), [a, b]);
() => expensiveFn(a, b): 用来计算值的函数;[a, b]: 依赖数组,只有其中任意一项变化时才会重新执行函数;- 返回的是函数的结果,而不是函数本身。
适用的场景: 适用于需要缓存昂贵计算结果的场景,例如排序、筛选或大量数据的处理
const Child = React.memo(({ items, onSelect }) => {
console.log('Child re-rendered');
return (
<ul>
{items.map(it => (
<li key={it.id} onClick={() => onSelect(it)}>
{it.name}
</li>
))}
</ul>
);
});
function Parent({ data }) {
const [activeId, setActiveId] = useState(null);
// 仅当 data 变化时重新计算 items
const items = useMemo(() => {
console.log('Recompute items');
return data.filter(d => d.visible);
}, [data]);
// 保证 onSelect 的引用稳定
const handleSelect = useCallback(it => {
setActiveId(it.id);
}, []);
return <Child items={items} onSelect={handleSelect} />;
}
当你用 React.memo 包裹子组件时,子组件依赖的每个 prop 的引用都应该稳定,否则即使值没变,浅比较也会判定为“新 prop”,从而触发子组件重渲染。
使用 useMemo 可确保传入的对象、数组等复杂类型 props 在依赖不变时保持同一引用,从而触发 React.memo 的优化逻辑。
useCallback
useCallback 用于缓存函数本身,确保在依赖未改变的情况下,函数 引用保持稳定,从而避免无谓的重新渲染或副作用重新执行
const memoizedFn = useCallback(fn, deps);
- 接收一个函数
fn和依赖数组deps; - 初次渲染会缓存
fn; - 当依赖未变化时,返回相同函数引用,否则返新的函数,引起重新生成;
- 这样可以保证在组件重渲染时,传递给子组件的
props或其他hooks(如 useEffect)中的回调不会意外变更
const Button = React.memo(({ onClick }) => {
console.log('Button 渲染');
return <button onClick={onClick}>Click me</button>;
});
function Parent() {
const [cnt, setCnt] = useState(0);
const handleClick = useCallback(() => setCnt(c => c + 1), []);
return (
<>
<Button onClick={handleClick} />
<div>{cnt}</div>
</>
);
}
如果不使用 useCallback,handleClick 会在每次渲染中产生新引用,导致 Button 重新渲染。使用缓存后,仅在依赖变化时才重新生成回调,避免不必要渲染
useCallback用于 缓存函数引用;- 场景:传给子组件、作为 hook 依赖、在订阅场景;
- 搭配 React.memo 更有效;
- 必须慎用,确保真实性能瓶颈才用。
总结
react 具体的使用没有过多的复杂的 api,感觉上还比较灵活多变。写的好的和差的都比较多。主要还是所涉及的哲学设计思想领悟才是最为关键的