语雀前端知识沉淀
Hook是React16.8的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(如生命周期)。
顾名思义,也就是通过使用ES6类的编写形式去编写组件,
该类必须继承React.Component
如果想要访问父组件传递过来的参数,可通过this.props的方式去访问
在组件中必须实现render方法,在return中返回React对象,如下:
函数组件,顾名思义,就是通过函数编写的形式去实现一个React组件,是React中定义组件最简单的方式
function Welcome(props) { return Hello, {props.name}
; }
函数第一个参数为props用于接收父组件传递过来的参数
针对两种React组件,其区别主要分成以下几大方向:
但是函数组件使用useEffect也能够完成替代生命周期的作用
useState 是 React 中一个用于管理组件状态的 Hook,它可以让你在函数式组件中使用状态(state)。
useState 接收一个初始状态值,返回一个包含当前状态和状态更新函数的数组,你可以使用数组解构来获取这两个值。
使用 useState 可以让你在函数式组件中使用状态,这使得函数式组件可以管理自己的状态,避免了使用类组件的繁琐,同时也使得代码更加简洁易读。
使用规则
const [couter,setCouter] = useState(0)
当 useState 中的 state 是对象时,调用相应的setState ,需要注意,useState 不会自动合并更新对象,可以使用函数式的 setState 结合展开运算符来达到合并更新对象的效果。
export default function App() {
const initValue = { n: 0, m: 0 };
const [state, setState] = useState(initValue);
const addN = () => {
setState((state) => {
+ return { ...state, n: state.n + 1 };
});
};
return (
<div className="App">
<button onClick={addN}>+1,此时n:{state.n}</button>
</div>
);
}
useState 和useReduce 作为能够触发组件重新渲染的hooks,
我们在使用useState的时候要特别注意的是,useState派发更新函数的执行,
就会让整个function组件从头到尾执行一次。
setCount((prevCount) => prevCount + 1);
这里用到了 setCount 的另外一种写法,即使用一个回调函数来更新状态。
在 React 中,状态的更新是异步的。
当我们调用 setCount(count + 1) 时,并不能保证count 立即更新,而是会在后续某个时间点更新。
如果我们想根据当前 count 的值更新状态,我们就不能直接使用 setCount,而是应该使用回调函数。
回调函数的第一个参数则是当前状态的值,为了避免出现异步更新所带来的问题,我们需要在回调函数中显式地使用当前状态值,并返回一个新的状态。
这样我们就能够保证状态的更新是可靠的,而且顺序也正确。
因此,setCount((prevCount) => prevCount + 1) 相当于是将当前的 count 值作为第一个参数传入一个回调函数,这个回调函数会将当前值加 1,返回一个新的状态,
然后将这个新状态设置为 count 的新值。这样我们就能够正确地更新状态,而不用担心有什么异步更新的问题。
useState 的原理其实很简单:它是通过闭包和对象的引用来实现的。
因为 useState 在不断地记录着下一次组件渲染所需要用到的状态,并在该组件再次渲染时,重新取出之前
保存的状态值。这是通过闭包实现的。
实际上,每一次在 useState 中使用的状态值都是通过一个类似于对象的数组进行保存的。这个对象的引用在每一次函数执行结束后,被保存到了闭包中,以便在下一次函数执行时使用它,这就是通过闭包实现的。
当我们调用 useState 函数时,它会返回一个数组,其中第一个元素是组件中保存的状态值,第二个元素是更新状态值的函数。
在使用这个方法时,我们需要注意到的是,每次更新状态值时,都必须传入新的状态值,而不能改变已有的状态值。
因此,为了能够正常使用 useState,我们需要通过这个对象的引用来保存和更新状态值。
当调用更新函数时,React 将会重新渲染组件,并且使用新的状态值替换原来的状态值。
在这个过程中,React 会比较新的状态和之前的状态,以确定是否需要更新组件。
如果新状态和之前状态相同,则不会执行渲染操作。
需要注意的是,useState 创建的状态变量是可变的,即其值的状态是可以修改的。
但是,我们不应该直接修改状态变量的值,而是应该通过调用更新函数来修改值。
这是因为 React 在更新状态变量时可能会对多个调用进行批处理,提高性能和效率。
源码角度分析
事实上,React 中的 useState Hook 只是 useReducer 的一个简化版本。
useState 函数可以接受一个初始状态值,并返回一个数组,其中包含了状态值以及更新状态值的函数。
这个流程其实就是 useReducer 的包装过程,只是 useState 的更新函数内部已经实现好了对状态的更新行为,这样可以减少开发者的代码量,并且提高代码的可读性和可维护性。
FiberNode 是 React 内部实现协调的基本数据结构,可以理解为描述组件生成、更新、卸载过程的对象。
FiberNode 是 React Fiber 架构中的一个关键概念,在实现 React 的 Hooks 功能中也扮演着重要的角色
在组件内部通过 useState 函数声明一个变量,React 内部会根据这个变量创建并返回一个 Hook 对象,其中包含状态值以及更新状态值的函数。
useState 函数的返回值和保存当前状态值的变量的赋值操作都在该组件中执行,因此组件自身就存储了这个状态变量。
export function useState<S>(
initialState: (() => S) | S
): [S, Dispatch<SetStateAction<S>>] {
return useReducer(
basicStateReducer,
// useReducer has a special case to support lazy useState initialization
(initialState as any) as S,
(initialState: any) => {
return initialState
}
)
}
组件渲染时,React 首先会创建对应的 FiberNode,并将其加入 Fiber 树中。
接着,在 FiberNode 中创建一个 HookNode,
HookNode 中包含 index 和 memoizedState 两个字段,
分别表示 Hook 对象在 Hook 链表中的索引位置和当前状态的值。
React 在组件渲染时会扫描 FiberNode 上已经使用的 Hooks 链表,并根据每个 Hook 对象来获取和更新组件的状态。
React 通过调用 Hooks 根据 index 获取当前组件的 Hooks 链表中的 Hook 对象,并将其保存在变量 currentState 中。
function readContext(context, observedBits) {
const dispatcher = resolveDispatcher()
return dispatcher.readContext(context, observedBits)
}
function resolveCurrentlyRenderingFiber() {
const fiber = workInProgress ?? currentlyRenderingFiber
return fiber
}
function resolveCurrentlyRenderingFiberWithHooks() {
const currentlyRenderingFiber = resolveCurrentlyRenderingFiber()
invariant(
currentlyRenderingFiber !== null && currentlyRenderingFiber.memoizedState !== null,
'Hooks can only be called inside the body of a function component.'
)
return currentlyRenderingFiber
}
function getCurrentHookValue() {
const hook = resolveCurrentlyRenderingFiberWithHooks().memoizedState as Hook
return hook.memoizedState
}
在组件渲染完成后,React 代码便可执行 Hook 中定义的函数或代码片段,以根据 Hook 返回的状态值动态更新视图。
function useState<S>(
initialState: (() => S) | S
): [S, Dispatch<SetStateAction<S>>] {
const dispatcher = resolveDispatcher()
return dispatcher.useState(initialState)
}
useEffect 是 React 提供的一个 Hook,用于在函数组件中执行副作用操作(side effects),
如访问 DOM、调用 API、设置定时器等。它可以看作是类组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 的组合体。
useEffect 接收两个参数,第一个参数是副作用函数,第二个参数是依赖项数组(可选),用于指定副作用函数所依赖的状态变量,只有当这些状态变量发生变化时,副作用函数才会被调用。
如果没有指定依赖项数组,则副作用函数在每次组件渲染时都会被调用。
副作用函数中可以返回一个清除函数,用于清除副作用,例如清除定时器、取消订阅等。
useEffect 有以下几个特点:
React何时清除effect?
useEffect(()=>{
console.log("监听redux中的数据")
return ()=>{
console.log("取消监听redux中的数据变化")
}
})
const [count,setCount] = useState(0);
const [message,setMessage] = useState("Hello World ")
useEffect(()=>{
console.log("被count与message同时影响")
})
useEffect(()=>{
console.log("只受count影响")
},[count])// 控制回调函数只受count影响
如果一个函数我们不希望依赖任何的内容时,也可以传入一个新的空数组[]
const [count,setCount] = useState(0);
const [message,setMessage] = useState("Hello World ")
useEffect(()=>{
console.log("发送网络请求,从服务器获取数据")
return ()=>{
console.log("只有在组件被卸载的时候,才会执行一次")
}
},[])// 只在函数初始化的时候执行一次
使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题
但使用Effect Hook,可以很好的解决这一问题,我们可以将他们分离到不同的useEffect中。
useEffect(()=>{
console.log("修改title")
})
useEffect(()=>{
console.log("监听redux中的数据")
})
useEffect(()=>{
console.log("发送网络请求,从服务器获取数据")
})
不能直接用 async await 语法糖
/* 错误用法 ,effect不支持直接 async await 装饰的 */
useEffect(async ()=>{
/* 请求数据 */
const res = await getUserInfo(payload)
},[ a ,number ])
如果想要用 async 可以对 async 函数进行进行一层包装
useEffect(()=>{
async ()=>{
/* 请求数据 */
const res = await getUserInfo(payload)
}
},[ a ,number ])
useEffect 执行顺序 组件更新挂载完成 -> 浏览器dom 绘制完成 -> 执行useEffect回调 。
在 useEffect 中,闭包陷阱是指在 useEffect 中使引用了该 组件 中声明的某些状态,因为useEffect 只有
在第一次渲染的时候才会触发, 状态 渲染更新时, useEffect里面的回调函数并没有触发。
因此useEffect里面的 状态变量 还是初始化时的值,并没有获取到最新的. 这就是闭包陷阱
function MyComponent() {
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
const result = await fetch('https://example.com/data');
setData(result);
};
fetchData();
}, []);
// ...
}
function MyComponent() {
const [data, setData] = useState([]);
const dataRef = useRef([]);
useEffect(() => {
const fetchData = async () => {
const result = await fetch('https://example.com/data');
dataRef.current = result;
setData(result);
};
fetchData();
}, []);
// ...
}
useEffect 应用场景
useEffect(() => {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
setData(data);
})
.catch(error => {
setError(error);
});
}, []);
import React, { useEffect } from 'react';
function ScrollComponent() {
useEffect(() => {
function handleScroll() {
// 执行滚动条变化时的操作
console.log('scrolling');
}
// 在组件挂载时添加事件监听
window.addEventListener('scroll', handleScroll);
// 在组件卸载时移除事件监听
return () => window.removeEventListener('scroll', handleScroll);
}, []); // 空数组表示只在组件挂载和卸载时执行一次
return <div>ScrollComponent</div>;
}
useEffect(() => {
const interval = setInterval(() => {
setCount(count => count + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
const dom = useRef(0);
console.log(`r.current:${dom.current}`);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。
返回的 ref 对象在组件的整个生命周期内保持不变。
这个ref对象只有一个current属性,将一个值保存在内,它的地址一直不会变。
import React, { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
const handleClick = () => {
console.log(inputRef.current.value);
};
return (
<>
<input type="text" ref={inputRef} />
<button onClick={handleClick}>Get Input Value</button>
</>
);
}
在上面的示例中,使用 useRef 钩子创建了一个名为 inputRef 的变量,该变量被传递给 input 元素的 ref 属性中。
在 handleClick 函数中,可以使用 inputRef.current.value 获取 input 元素中当前的文本值,并将其输出到控制台中。
import { useRef, useEffect } from 'react';
function Example() {
const intervalIdRef = useRef(null);
useEffect(() => {
intervalIdRef.current = setInterval(() => {
console.log('Tick');
}, 1000);
return () => clearInterval(intervalIdRef.current);
}, []);
// ...
}
小结一下:
应用
react-redux 在react-hooks发布后,用react-hooks重新重构了其中的Provide,connectAdvanced)核心模块。
react-hooks在限制数据更新,高阶组件上有这一定的优势,其源码大量运用useMemo来做数据判定
/* 这里用到的useRef没有一个是绑定在dom元素上的,都是做数据缓存用的 */
/* react-redux 用userRef 来缓存 merge之后的 props */
const lastChildProps = useRef()
// lastWrapperProps 用 useRef 来存放组件真正的 props信息
const lastWrapperProps = useRef(wrapperProps)
//是否储存props是否处于正在更新状态
const renderIsScheduled = useRef(false)
这是react-redux中用useRef 对数据做的缓存,那么怎么做更新的呢 ,我们接下来看
//获取包装的props
function captureWrapperProps(
lastWrapperProps,
lastChildProps,
renderIsScheduled,
wrapperProps,
actualChildProps,
childPropsFromStoreUpdate,
notifyNestedSubs
) {
//我们要捕获包装props和子props,以便稍后进行比较
lastWrapperProps.current = wrapperProps //子props
lastChildProps.current = actualChildProps //经过 merge props 之后形成的 prop
renderIsScheduled.current = false
}
react-redux 用重新赋值的方法,改变缓存的数据源,避免不必要的数据更新。
如果选用useState储存数据,必然促使组件重新渲染,所以采用了useRef解决了这个问题。
不能直接在子组件上使用 ref
<Child ref={textInput} /> // 错误写法
但是这样还拿不到子组件的DOM,我们需要使用 forwardRef 配合使用,上面的例子可以写成这样
function CustomTextInput(props) {
// 这里必须声明 textInput,这样 ref 才可以引用它
const textInput = useRef(null);
// 打印 绑定到子组件的 ref
console.log(textInput);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<Child ref={textInput} /> //**依然使用ref传递**
<input type="button" value="Focus the text input" onClick={handleClick} />
</div>
);
}
const Child = forwardRef((props, ref) => { //** 看我 **
return <input type="text" ref={ref} />;//** 看我挂到对应的dom上 **
});
上面是通过forwardRef 把 Child 函数包起来,然后传入第二个参数 ref
最后挂载 ref={ref} 这样就可以拿到对应的DOM了,控制台打印一下看看
current: <input type="text"></input>
function useRef(initialValue) {
return createRef().current;
}
该函数接收一个初始值initialValue,然后调用createRef()函数返回一个对象,最后返回该对象的current属性。
之所以经过两次函数调用,而不是直接返回一个对象的current属性,是由于createRef()返回的对象只有在组件渲染时才会被创建,在每次重新渲染时都会创建一个新的对象。
因此,需要在函数内部使用useRef时,每次都要调用createRef()函数来创建一个新的ref对象,从而实现正确的数据引用。
在React 源码中,useRef函数的内部实现基于useMemo函数和函数组件的执行顺序。
useMemo 函数的作用是在渲染中缓存值,避免每次渲染都重新计算,从而提高性能。
useRef 函数的内部实现就是创建一个useMemo函数,该函数利用了函数组件的执行顺序,在每次组件重
新渲染时,始终返回同一个ref对象,从而实现了对相同的引用的保留。
总的来说,useRef的实现原理是基于函数式编程思想和React的内部机制,通过createRef()函数创建一个
ref对象,利用useMemo函数缓存引用,从而实现对DOM元素引用、临时状态、数据缓存等的管理。
useMemo 是 React 中的一个 Hook,用于在函数组件中进行性能优化。
当一个函数在不同的场景下可能被多次调用时,如果该函数的计算开销比较大,会导致不必要的性能开
销,这时候可以通过useMemo来对该函数进行封装,避免重复计算,提高性能。
它的作用就是将计算结果缓存起来,可以让组件只在必要的情况下进行重新计算,从而提高应用的性能,
避免在组件多次渲染的过程中进行重复计算。
useMemo 接收两个参数:
第一个参数是一个函数,这个函数的返回值是需要缓存的变量;
第二个参数是一个数组,用于指定依赖项,当依赖项中的任意一个发生变化时,就会重新计算缓存的值。如果依赖项未发生变化,则直接返回上一次缓存的值。
1. 缓存 useEffect 的引用类型依赖
import { useEffect } from 'react'
export default () => {
const msg = {
info: 'hello world',
}
useEffect(() => {
console.log('msg:', msg.info)
}, [msg])
}
此时 msg 是一个对象该对象作为了 useEffect 的依赖,这里本意是 msg 变化的时候打印 msg 的信息。
但是实际上每次组件在render 的时候 msg 都会被重新创建,
因为msg是引用类型,所以 msg 的引用在每次 render 时都是不一样的,就会出现Effect依赖无限循环。
即 useEffect 在每次render 的时候都会重新执行,和我们预期的不一样。
此时 useMemo 就可以派上用场了:
import { useEffect, useMemo } from "react";
const App = () => {
const msg = useMemo(() => {
return {
info: "hello world",
};
}, []);
useEffect(() => {
console.log("msg:", msg.info);
}, [msg]);
};
export default App;
同理对于函数作为依赖的情况,我们可以使用 useCallback:
import { useEffect, useCallback } from "react";
const App = (props) => {
const print = useCallback(() => {
console.log("msg", props.msg);
}, [props.msg]);
useEffect(() => {
print();
}, [print]);
};
export default App;
2. 缓存子组件 props 中的引用类型。
做这一步的目的是为了防止组件非必要的重新渲染造成的性能消耗,所以首先要明确组件在什么情况下会重新渲染。
这一步优化的目的是:
在父组件中跟子组件没有关系的状态变更导致的重新渲染可以不渲染子组件,造成不必要的浪费。
import { useCallback, useState, memo, useMemo } from "react";
const Child = memo((props) => {});
const App = () => {
const [count, setCount] = useState(0);
const handleChange = useCallback(() => {}, []);
const list = useMemo(() => {
return [];
}, []);
return (
<>
<div
onPress={() => {
setCount(count + 1);
}}
>
increase
</div>
<Child handleChange={handleChange} list={list} />
</>
);
};
export default App;
对于复杂的组件项目中会使用 memo 进行包裹,目的是为了对组件接受的 props 属性进行浅比较来判断组件要不要进行重新渲染,但是如果 props 属性里面有引用类型的情况,比如 handleChange 在 App 组件每次重新渲染的时候都会重新创建生成,引用当然也是不一样的,那么势必会造成 Child 组件重新渲染。
引用会改变,所以我们需要缓存这些值保证引用不变,避免不必要的重复渲染。
function useMemo(factory, deps) {
const patch = useContext(Context);
if (patch) patch.deps(deps);
const inputs = Array.isArray(deps) ? deps : [factory];
return patch ? patch.memo(inputs, factory) : factory();
}
该函数接收两个参数:factory和deps。
其中factory是一个函数,用于创建要缓存的值;
deps是一个数组,包含了所有要监测的值,当deps中的值发生变化时,useMemo才重新计算缓存的值。
在React源码中,useMemo内部实现主要基于Context和Fiber节点。
当调用useMemo时,会通过useContext函数获取到当前Context对象,并将deps作为一个参数传递给
Context的deps方法,该方法负责向Fiber节点添加监控依赖。
当组件重新渲染时,React会检查deps数组中的所有依赖项,如果发现有变化,就重新计算缓存的值;
如果没有变化,就用之前的缓存值,从而避免了不必要的计算。
如果存在相同的deps和factory,useMemo会返回之前缓存的值,否则会重新计算缓存的值并存储起来。
为了实现数据的缓存和共享,依赖项内部存储的值将被封装成一个Memo对象,并绑定到Fiber节点中。
当组件重新渲染时,可以通过Memo对象直接获取上一次缓存的结果。
简单来说就是返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)。
useCallback 的作用是缓存函数,避免重复地创建函数实例,减小子组件的没必要要重复渲染,从而提高组件的性能。
useCallback 和 useMemo 区别
useEffect和 useMemo useCallback的区别
有了useEffect,我们可以在函数组件中实现 像类组件中的生命周期那样某个阶段做某件事情,具有:
类似于类组件中的 shouldComponentUpdate,在子组件中使用 shouldComponentUpdate,
判定该组件的 props 和 state 是否有变化,从而避免每次父组件render时都去重新渲染子组件
当在父组件中传递一个回调函数给子组件时,使用useCallback可以避免子组件由于父组件的重新渲染而重
新渲染自身,从而提高性能。
但是如果在useCallback中依赖的props或state发生变化,则会触发useCallback中的函数引用发生变化,
从而引起父组件的重渲染,进而又触发子组件自身的重新渲染,这就导致了无限循环。
import { useState, useCallback } from "react";
function ParentComponent() {
const [count, setCount] = useState(0);
// useCallback函数依赖了count
const memoizedCallback = useCallback(() => {
console.log('memoizedCallback');
setCount((prevCount) => prevCount + 1);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<ChildComponent callback={memoizedCallback} />
</div>
);
}
function ChildComponent({ callback }) {
const [childCount, setChildCount] = useState(0);
const handleClick = () => {
console.log('handleClick');
callback();
setChildCount((prevCount) => prevCount + 1);
};
return (
<div>
<p>Child count: {childCount}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
在上面的例子中,当父组件的count发生变化时,会导致memoizedCallback函数发生变化,从而引发了父
组件的重新渲染,进而也会导致子组件重新渲染。
但是子组件的重新渲染又会导致父组件中的memoizedCallback再次发生变化,这就引发了无限循环。
useContext函数是React Hooks 中的一个hook 函数,它可以让你方便地使用 React Context 来在组件之间共享数据。
使用 useContext 函数,可以摆脱繁琐的 context.Consumer 和 context.provider 包裹,使代码更加简洁易读。
使用 useContext 函数,可以很方便的从 Provider 组件中取出对应的值,而且只需要一行代码就可以实现。
import React, {createContext} from "react";
export const UserContext = createContext
function Chat()
return (
<UserContext.Provider value={{currentChat,currentUser,socket,handleChatChange,handleWelcome}}>
</UserContext.Provider>
)
}
import React , { useContext }from 'react'
import { UserContext } from '../page/Chat';
const { currentUser,currentChat,socket } = useContext(UserContext)
它主要是通过React.createContext()函数创建一个上下文对象,用于在组件之间传递数据。
这个上下文对象提供了两个组件:Provider和Consumer,可以通过 Provider 组件来在整个应用中共享数
据,并且Consumer组件或useContext可以在应用中的任何地方访问到这些数据。
封装一个 Hook 的时候需要注意以下几点: