我想开始讨论创建回调的推荐方法,该方法从循环中创建的组件中获取参数。
例如,如果我正在填充一个将具有"删除"按钮的项目列表,我希望"onDeleteItem"回调知道要删除的项目的索引。 所以像这样:
const onDeleteItem = useCallback(index => () => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<div>
<span>{item}</span>
<button type="button" onClick={onDeleteItem(index)}>Delete</button>
</div>
)}
</div>
);
但这样做的问题是,onDeleteItem 将始终向 onClick 处理程序返回一个新函数,从而导致按钮重新呈现,即使列表没有更改也是如此。 所以它违背了useCallback
的目的.
我想出了我自己的钩子,我称之为useLoopCallback,它通过记住主回调以及循环参数映射到他们自己的回调来解决问题:
import React, {useCallback, useMemo} from "react";
export function useLoopCallback(code, dependencies) {
const callback = useCallback(code, dependencies);
const loopCallbacks = useMemo(() => ({map: new Map(), callback}), [callback]);
return useCallback(loopParam => {
let loopCallback = loopCallbacks.map.get(loopParam);
if (!loopCallback) {
loopCallback = (...otherParams) => loopCallbacks.callback(loopParam, ...otherParams);
loopCallbacks.map.set(loopParam, loopCallback);
}
return loopCallback;
}, [callback]);
}
所以现在上面的处理程序看起来像这样:
const onDeleteItem = useLoopCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
这工作正常,但现在我想知道这个额外的逻辑是否真的让事情变得更快,或者只是增加了不必要的开销。任何人都可以提供一些见解吗?
编辑:上述方法的替代方法是将列表项包装在自己的组件中。 所以像这样:
function ListItem({key, item, onDeleteItem}) {
const onDelete = useCallback(() => {
onDeleteItem(key);
}, [onDeleteItem, key]);
return (
<div>
<span>{item}</span>
<button type="button" onClick={onDelete}>Delete</button>
</div>
);
}
export default function List(...) {
...
const onDeleteItem = useCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<ListItem key={index} item={item} onDeleteItem={onDeleteItem} />
)}
</div>
);
}
性能优化总是有代价的。有时这种成本低于要优化的操作,有时更高。useCallback
它是一个与useMemo
非常相似的钩子,实际上你可以把它看作是只能在函数中使用的useMemo
的专业化。例如,波纹管语句是等效的
const callback = value => value * 2
const memoizedCb = useCallback(callback, [])
const memoizedWithUseMemo = useMemo(() => callback, [])
所以现在关于useCallback
的每一个断言都可以应用于useMemo
.
memoization
的要点是保留旧值的副本,以便在我们获得相同的依赖项时返回,当您有expensive
要计算的内容时,这可能很棒。看看下面的代码
const Component = ({ items }) =>{
const array = items.map(x => x*2)
}
Uppon 每render
一次常量array
都将由于在items
中执行map
而创建。因此,您可能会想执行以下操作
const Component = ({ items }) =>{
const array = useMemo(() => items.map(x => x*2), [items])
}
现在items.map(x => x*2)
只有在items
发生变化时才会被执行,但它值得吗?简短的回答是否定的。这样做获得的性能是微不足道的,有时使用memoization
比仅仅执行每次渲染的函数更昂贵。两个钩子(useCallback
和useMemo
(在两个不同的用例中都很有用:
- 引用相等
当您需要确保引用类型不会仅仅因为shallow comparison
失败而触发重新渲染时
- 计算成本高的操作(仅
useMemo
(
像这样的东西
const serializedValue = {item: props.item.map(x => ({...x, override: x ? y : z}))}
现在,您有理由memoized
操作并在每次更改props.item
时懒惰地检索serializedValue
:
const serializedValue = useMemo(() => ({item: props.item.map(x => ({...x, override: x ? y : z}))}), [props.item])
任何其他用例几乎总是值得再次重新计算所有值,React
它非常有效,并且附加渲染几乎不会导致性能问题。请记住,有时您优化代码的努力可能会反其道而行之,并生成大量额外/不必要的代码,这不会产生太多好处(有时只会导致更多问题(。
List 组件管理它自己的状态(list( 删除函数取决于此列表在其闭包中可用。因此,当列表更改时,删除功能必须更改。
使用 redux 这不是问题,因为删除项目将通过调度操作来完成,并且将由始终具有相同功能的化简器更改。
React 碰巧有一个 useReducer 钩子,你可以使用它:
import React, { useMemo, useReducer, memo } from 'react';
const Item = props => {
//calling remove will dispatch {type:'REMOVE', payload:{id}}
//no arguments are needed
const { remove } = props;
console.log('component render', props);
return (
<div>
<div>{JSON.stringify(props)}</div>
<div>
<button onClick={remove}>REMOVE</button>
</div>
</div>
);
};
//wrap in React.memo so when props don't change
// the ItemContainer will not re render (pure component)
const ItemContainer = memo(props => {
console.log('in the item container');
//dispatch passed by parent use it to dispatch an action
const { dispatch, id } = props;
const remove = () =>
dispatch({
type: 'REMOVE',
payload: { id },
});
return <Item {...props} remove={remove} />;
});
const initialState = [{ id: 1 }, { id: 2 }, { id: 3 }];
//Reducer is static it doesn't need list to be in it's
// scope through closure
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
//remove the id from the list
return state.filter(
item => item.id !== action.payload.id
);
}
return state;
};
export default () => {
//initialize state and reducer
const [list, dispatch] = useReducer(
reducer,
initialState
);
console.log('parent render', list);
return (
<div>
{list.map(({ id }) => (
<ItemContainer
key={id}
id={id}
dispatch={dispatch}
/>
))}
</div>
);
};