useEffect的渲染
当监听一个Object或Function时,会在每次渲染时都触发。
所以应该用useMemo或useCallback等类似工具包裹后才能监听。
router
单页面应用的原理是改变url地址,会监听url变化,并切换组件。
history
使用popstate监听路由变化
window.addEventListener('popstate',function(e){ /* 监听改变 */ })
hash
使用onhashchange监听路由
window.addEventListener('hashchange',function(e){
/* 监听改变 */
})
流程分析
当地址栏改变url,组件的更新渲染都经历了什么? 拿history模式做参考。当url改变,首先触发histoy,调用事件监听popstate事件, 触发回调函数handlePopState,触发history下面的setstate方法,产生新的location对象,然后通知Router组件更新location并通过``context上下文传递,switch通过传递的更新流,匹配出符合的Route组件渲染,最后有Route组件取出context内容,传递给渲染页面,渲染更新。
参考:「源码解析 」这一次彻底弄懂react-router路由原理 个人理解,单页面应用是使用一个html下,一次性加载js, - 掘金
状态存储
由于在每个组件的虚拟dom上存储状态时,是使用了数组的形式,并且以下标的方式去索引,所以不允许在判断中使用useState,否则可能造成索引不正确的问题
useRef
不会重新渲染
所以不适合存储希望显示在屏幕上的信息。
跨渲染周期
useState创建的对象会在每次渲染时都重置,无法获取上次渲染时的值。
而useRef创建的值可以跨渲染周期,所以适用于制作倒计时、计时器等。
本地信息
对于每个函数组件而言,创建在组件外部的变量是全局共享的。但是用 useref创建可以保证每个组件之间数据的隔离。
创建可以操作dom的变量
例如使用useref创建一个绑定子组件ref的变量,子组件通过forwardref暴露出ref方法
性能优化hooks useMemo useCallBack
memo
当父组件的参数变化时,父组件重新渲染,子组件也会随之渲染,但是子组件没有任何变化,这是无效的重复渲染,此时使用React.memo包裹子组件可以避免重复渲染。
const ChildComponent = memo(() => {
console.log('子组件执行了');
return (
<p>我是子组件的内容</p>
)
});
const ParentComponent = () => {
console.log('父组件执行了');
const [count, setCount] = useState<Number>(1);
const changeCount = () => {
setCount(count + 1);
};
return (
<>
<button onClick={changeCount}>点击</button>
<p>count is: </p>
<p>{count}</p>
<ChildComponent />
</>
)
};
useCallBack
useCallback 是一个用于优化性能的 React Hook,它的主要作用是避免在每次渲染时都重新创建函数。通过记住上一次创建的函数,只有在依赖项发生变化时才重新创建新的函数,从而提高性能。
useCallback 接受两个参数:
- 一个函数,这个函数是我们需要记住的函数。
- 一个依赖项数组,当数组中的依赖项发生变化时,才会重新创建新的函数。
父组件的state中有数据变化导致重新渲染,即使子组件没有任何变化也会重新渲染,这是因为如果只使用memo,memo检测到的是props数据中栈地址是否发生变化。而父组件重新构建时会重新构建父组件中的所有函数(旧函数销毁,新函数创建,等于更新了函数地址)。
此时就可以用useCallBack包裹传给子组件的函数。
const parent = props => {
const [num, setNum] = useState(0);
const getValue = useCallback(value => {
console.log(value);
}, []);
const changeState = () => {
setNum(val => val + 1);
};
return (
<div>
我是父组件
<Button onClick={changeState}>点我改变state</Button>
{num}
<child getValue={getValue} />
</div>
);
};
useMemo
用于优化渲染性能。useMemo 会接收一个箭头函数包裹的回调函数和依赖项数组,然后返回回调函数的计算结果。当依赖项数组中的某个值发生变化时,useMemo 会重新计算回调函数。如果依赖项没有发生变化,useMemo 会返回上一次计算的结果,这样可以避免不必要的计算。如下,只有在a或者b发生改变的时候,value的值才会重新计算。
会缓存计算出来的值,只有当依赖值发生变化(即使依赖值被重新赋值,但是由于判断出来new值和old值是相等的,那么依旧判定为无变化)时,才会重新计算
import React, { useState , useMemo } from "react";
function App() {
const [introduction, setIntroduction] = useState("店长很懒什么都没写...")
// 桃子的单价和价格
const [peach, setPeach] = useState({
num: 10,
unitPrice: 5
});
// 香蕉的单价和价格
const [banana, setBanana] = useState({
num: 10,
unitPrice: 10
});
const change = () => { setIntroduction("这是一家水果店") }
const price = useMemo(()=>{
console.log("价格重新计算了--");
return (peach.num * peach.unitPrice) + (banana.num * banana.unitPrice)
},[peach,banana])
const addLike = () => {
setLikeCount(likeCount + 1)
}
const change = () => {
setPeach({
...peach,
unitPrice: 20
})
}
return (
<div>
<h3>店铺点赞数:{likeCount}</h3>
<button onClick={addLike}>点赞</button>
<button onClick={change}>修改桃子的单价</button>
<h4> 购买的水果清单: </h4>
<h4> 桃子数量: {peach.num} 单价:{peach.unitPrice}元 </h4>
<h4> 香蕉数量: {banana.num} 单价:{banana.unitPrice}元 </h4>
价格:{price}元
</div>
);
}
Route
完整route
import { NavLink, Routes, Route } from "react-router-dom";
import Product from "./Product";
import About from "./About";
import Home from "./Home";
import Error from "./Error";
import "./styles.css";
export default function App() {
return (
<div className="App">
<header>
<h1>Hello World</h1>
</header>
<nav>
<NavLink to="">首页</NavLink>
<NavLink to="product">产品</NavLink>
<NavLink to="about">关于</NavLink>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/product" element={<Product />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<Error />} />
</Routes>
</div>
);
}
useState原理
陷阱
调用set函数不会改变已经执行的代码中当前的state。它只影响下一次渲染中useState返回的内容。
function handleClick() {
setName('Robin');
console.log(name); // Still "Taylor"!
}
利用闭包
function useState<T>(initialState: T): [T, (newState: T) => void]{
state = state || initialState;
function setState(newState: T) {
state = newState;
render();
}
return [state, setState];
}
这样来看,vue的数据注册也是使用闭包?
Redux和context
Redux:利用context和provider构造出来的
倒计时问题
如果仅仅使用useEffect和useState会导致每次刷新页面时,且useState是异步,监听不准确。
此时使用useRef解决问题。
import React, { useEffect, useRef } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
const timerRef = useRef()
useEffect(() => {
timerRef.current = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(timerRef.current)
}, [])
return <div>{count}</div>
}
useEffect
模拟生命周期
接收两个参数,第一个参数必传,是个函数,触发后执行。第二个参数非必传,是个数组。
1.不传数组
将在页面第一次刷新以及每次更新渲染后触发,即mount和update
2.传空数组
将在页面第一次刷新后触发,即mount
3.传有数据的数组
将在监听的数据发生变化时触发,即watch
4.第一个参数(函数)return一个函数,且第二个参数为空数组
将在页面销毁前触发,即componentWillUnmount
父组件调用子组件的函数
函数式组件: 简单来说,父组件向子组件的props传递一个用useRef()创建出来的变量,然后在子组件中使用useImperativeHandle()(可选操作),并且用forwardRef包裹子组件
父组件
const childRef = useRef()
<Child childRef={childRef} />
子组件
const { childRef } = props
useImperativeHandle(childRef, () =>({
console.log('调用子组件组件')
}))
包裹子组件
let ForwardChild = forwardRef(Child);
此时在父组件中使用ref.current去调用
childRef.current.get()
如不需要useImperativeHandle(最简单) 首先用useRef创建变量,然后绑定到子组件的ref上,然后用forwardRef包裹子组件,父组件就可以用变量.current调用,此时子组件接收两个参数,一个是props一个是传进来的ref,且此时父组件需要引入的是包裹后的子组件
父组件
const childRef = useRef()
<Child ref={childRef} />
包裹子组件
let ForwardChild = forwardRef(Child);
此时在父组件中使用ref.current去调用
childRef.current.get()
路由传参及跳转的四个hooks
1.useNavigate 用于前一个页面传递参数
const navigate = useNavigate()
navigate('url',{state:value})
2.useLocation usePatams useSearchParams 用于后一个页面接受参数
3.useHistory 老hooks 类似$router
4.传参的两种方式:state和search search类似于query state刷新后会消失