前提知识:函数式组件在每次props、state变动时,都会重新执行整个函数,重新渲染页面。
在使用React的class组件时,我们可以使用state,this.xxx,以及生命周期(componentDidMount、componentDidUpdate、componentWillUnmount)等钩子,但函数式组件却无法使用这些,为解决这个问题,React在函数式组件中引入了hooks(class组件无法使用hooks)。Hook是指是一些可以在函数组件中“钩入”React state及生命周期等特性的函数,其实现原理在此不细述,关键就是闭包+单链表。
hook函数在一个函数组件中可以调用多次,不需要统一在一起:
export default (props: any) => {
const [num, setNum] = useState(0)
const [name, setName] = useState("")
useEffect(()=>{
// 启动时才执行的 *** 作,相当于componentDidMount
},[])
useEffect(()=>{
// 渲染后执行的 *** 作,相当于componentDidMount+componentDidUpdate
})
}
只能在函数组件最外层调用 Hook,切记不要在循环、条件判断或者子函数中调用,因为会导致单链表错乱只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook
useState
在class组件中可以使用this.state存储页面数据:
class Hooks1Class extends Component {
constructor(props: any) {
super(props)
this.state = {
num: 0
}
}
render(): React.ReactNode {
return <SafeAreaView style={styles.root}>
<Text>{this.state.num}</Text>
<TouchableOpacity style={styles.button} onPress={() => {
this.setState({
num: this.state.num + 1
})
}}>
<Text style={{ fontSize: 16, color: 'white' }} >Add</Text>
</TouchableOpacity>
</SafeAreaView>
}
}
在函数组件中,等加写法就是:
export default (props: any) => {
const [num, setNum] = useState(0)
return (
<SafeAreaView style={styles.root}>
<Text>{num}</Text>
<TouchableOpacity style={styles.button} onPress={() => { setNum(num + 1) }}>
<Text style={{ fontSize: 16, color: 'white' }} >Add</Text>
</TouchableOpacity>
</SafeAreaView>
);
};
这里的setNum与class组件的setState区别就是,函数组件更新state变量总是替换整个state,而不是class组件的合并state。
每个函数组件可以定义多个state,而在不同函数中引用相同组件,组件间的state是完全独立的:
const Component1 = (props: any) => {
const [num, setNum] = useState(0)
return (
<SafeAreaView style={styles.root}>
<Text>{num}</Text>
<TouchableOpacity style={styles.button} onPress={() => { setNum(num + 1) }}>
<Text style={{ fontSize: 16, color: 'white' }} >Add</Text>
</TouchableOpacity>
</SafeAreaView>
);
}
export default (props: any) => {
return (
<SafeAreaView style={{ ...styles.root, flexDirection: 'row' }}>
<Component1 />
<Component1 />
</SafeAreaView>
);
};
可以看到,左右两个Component1组件的state是相互独立的。
以上设置state的方法setNum(num + 1)还可以修改为函数式设置:
...
<TouchableOpacity style={styles.button} onPress={() => { setNum(n => n + 1) }}>
...
此方式可以使设置时不会依赖于num的state,导致在useEffect等hooks里产生副作用(下文细述)。
在实际使用useState时,需要适度拆分state,避免冗余 *** 作:
// 摘自官方例子
function Box() {
// 位置、大小信息
const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
useEffect(() => {
// 模拟鼠标移动时,改变Box坐标信息
function handleWindowMouseMove(e) {
// 展开 「...state」 以确保我们没有 「丢失」 width 和 height
setState(state => ({ ...state, left: e.pageX, top: e.pageY }));
}
// 注意:这是个简化版的实现
window.addEventListener('mousemove', handleWindowMouseMove);
return () => window.removeEventListener('mousemove', handleWindowMouseMove);
}, []);
// ...
可以看到在setState时,需要使用…state以确保不会丢失Box大小, *** 作冗余,所以可以改成
function Box() {
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 100, height: 100 });
useEffect(() => {
function handleWindowMouseMove(e) {
setPosition({ left: e.pageX, top: e.pageY });
}
// ...
而对于较复杂的state,如state逻辑较复杂且包含多个子值,或者下一个state依赖于之前的state等,可以使用useReducer替代。
Hook简单原理先看个小例子:
const f = () => {
for (var i = 5; i >= 1; i--) {
setNum(num + 1)
// 输出啥?for之后num是啥?改成setNum(num=>num + 1)呢?
console.log(num)
}
}
以useState为例,此处需要简单说一下hook的实现思想:
// const [num, setNum] = useState(0)
// 闭包,决定了state只在当前组件内有效,且可以像class组件的state一样“记住”上次的值
// useState可以在组件内多次使用以构建多个state,同时根据函数组件的useState等hook顺序,可以区分出不同的hook,是因为使用了单链表,此处使用数组和index模拟
let state = [], index = 0;
// 利用Promise微任务,把刷新放到js的事件队列后面
const defer = (fn) => Promise.resolve().then(fn);
function useState(initialValue) {
// 保存当前的索引
let currentIndex = index;
if (typeof initialValue === "function") {
// 函数式初始化
initialValue = initialValue();
}
// render时更新state(初始化)
state[currentIndex] = state[currentIndex] === undefined ? initialValue : state[currentIndex];
const setState = newValue => {
if (typeof newValue === "function") {
// 函数式更新
newValue = newValue(state[currentIndex]);
}
state[currentIndex] = newValue;
// 同时setState的话,index = 0会阻断多次renderComponent
// 因此for循环setState会只执行一次,且会出现设置不上去的情况(除非用函数式更新)
if (index!==0) {
defer(renderComponent);
}
// 同时setState的话,index会阻断多次renderComponent
index = 0;
};
// 每个useState递增当前下标
index += 1;
return [state[currentIndex], setState];
}
useReducer
没错,这个就是简化版的组件级Redux,实现复杂state管理:
const [state, dispatch] = useReducer(reducer, initialArg, init);
state:当前state值,当使用Object.is(浅层比较)比较两次渲染的state相同时,React将跳过子组件的渲染及副作用的执行,优化性能dispatch:函数组件中可使用形如dispatch({type[, newStateValue]})函数替换state。dispatch在useEffect中无需依赖,React保证其不变性,同时,dispatch也可结合useContext实现避免向下传递回调(下文细述)。reducer:实际处理dispatch的函数,根据 *** 作类型type返回新的stateinitialArg:state的初始值init:当init被设置时,state的初始值会惰性初始化,在被调用时会执行init(initialArg)得到初始的state值以下实现简单的选项卡:
// 惰性初始化函数
// initialArg为页面传递的初始化参数
function init(initialArg) {
return {
...initialArg,
list: [{ id: 0, txt: "第0项", checked: false },
{ id: 1, txt: "第1项", checked: false },
{ id: 2, txt: "第2项", checked: false }],
allFlag: { id: -1, txt: "全选", checked: false }
}
}
// 根据type执行 *** 作,返回替换的state
function reducer(state, action) {
switch (action.type) {
case 'clickItem': {
let index = action.item.id
state.list[index].checked = !state.list[index].checked
let all = state.list.every(item => item && (item.checked == true))
let newState = { ...state, allFlag: { ...state.allFlag, checked: all } }
return newState
}
case 'clickAll': {
let all = !(state.allFlag.checked)
state.list.forEach((item, index) => item.checked = all)
let newState = { ...state, allFlag: { ...state.allFlag, checked: all } }
return newState
}
default:
return state;
}
}
// 控件、函数等建议可以的话尽量定义到外部,避免不小心引用了外部组件属性导致依赖
const ListItem = ({ item, setSelection }) => {
return (
<View style={styles.checkboxContainer}>
<CheckBox
value={item.checked}
onValueChange={setSelection}
style={styles.checkbox}
/>
<Text style={styles.label}>{item.txt}</Text>
</View>
)
}
export default (props) => {
// 使用useReducer,暴露state、dispatch供使用
const [state, dispatch] = useReducer(reducer, props.initData, init)
const renderItem = ({ item }) => {
return (
<ListItem
item={item}
setSelection={(newValue) => {
// 触发事件
dispatch({ type: 'clickItem', item })
}}
/>
);
};
return (
<SafeAreaView style={{ ...styles.root }}>
<ListItem
// 使用数据
item={state.allFlag}
setSelection={(newValue) => dispatch({ type: 'clickAll'})}
/>
<FlatList
data={state.list}
renderItem={renderItem}
keyExtractor={item => item.id} />
</SafeAreaView>
);
};
useContext
useContext主要用于省略一步步向下传递属性、依赖等,比如控制底层子组件主题、回调方法等,调用了useContext的组件总会在context值变化时重新渲染,因此需注意若组件渲染耗时,需要使用useMemo等技术提升性能(下文细述)。
使用useContext+useReducer就可以实现轻量级的Redux,可用于避免向下传递回调。假设上个例子的FlatList外层还有一堆包浆层,则需要将state与dispatch一层层传递下去,而结合useContext的话,则可以改造成:
...
// 使用createContext,参数为context的默认值
// createStore像不像?
const ReduxContext = React.createContext({})
const ListView = () => {
// 引入定义的context,由上层组件中距离当前组件最近的的value属性决定
const redux = useContext(ReduxContext)
const renderItem = ({ item }) => {
return (
<ListItem
item={item}
setSelection={(newValue) => {
// 使用context
redux.dispatch({ type: 'clickItem', item })
}}
/>
);
};
return (
<FlatList
data={redux.state.list}
renderItem={renderItem}
keyExtractor={item => item.id} />
)
}
export default (props) => {
const [state, dispatch] = useReducer(reducer, props.initData, init)
return (
<SafeAreaView style={styles.root}>
<ListItem
// 使用数据
item={state.allFlag}
setSelection={(newValue) => dispatch({ type: 'clickAll' })}
/>
{/* 是不是很眼熟?对,就是类似redux最外层的Provider */}
<ReduxContext.Provider value={{ state, dispatch }}>
{/* 模拟多层包浆,此处不需要一层层把state和dispatch传递给ListView */}
<View style={styles.root}>
<View>
<View>
<View>
<ListView />
</View>
</View>
</View>
</View>
</ReduxContext.Provider>
</SafeAreaView>
);
};
参考资料
Hook简介
React Hooks原理探究
React setState、useState核心实现原理–模拟实现
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)