设计思路
React hooks给我们提供了useState等hook,那么我们想要设计一个useKeyboardShortcut的hook,可以这样来思考:
- 需求:提供快捷键、快捷键组合和回调函数,当按下的按键为目标时,执行回调函数。
- 传入的参数:useKeyboardShortcut(shortcutString, callback)
- 解析传入的按键,将字符串进行处理,匹配到对应按键
- 编写按下和抬起的监听
- 将监听事件注册到window
实现过程
按键的解析
在这里我们需要对传入的字符串进行解析,传入的可能是单个按键:"f",或者是组合按键:"ctrl+f"
const usekeyboardShortcut = (command: string, callback: Function) => {
const shortcutKeys = command.split("+") // 将传入的符号转变为数组
const initKeysMapping = shortcutKeys.reduce((pre, cur) => {
pre[cur.toLowerCase()] = false;
return pre
},{});
}
经过这些预处理后,我们将传入的按键变为了一个map,{key: false},将其转化为了按键-是否按下的键值对形式。
创建reducer
这里我们需要reducer来进行按键状态的管理
const keysReducer = (state: any, action: any) => {
switch(action.type) {
case "key-down":
const keyDownState = { ...state, [action.key]: true };
return keyDownState
case "key-up":
const keyUpState = { ...state, [action.key]: false };
return keyUpState
case "reset-keys":
const resetState = { ...action.data };
return resetState;
default:
return state
}
}
const [keys, setKeys] = useReducer(keysReducer, initKeysMapping);
按下把对应的key置为true,松开置为false,后文会解释为什么使用reducer来管理而不用state
创建相应按键的函数
const keydownListener = assignedKey => (keydownEvent: any) => {
const loweredKey = assignedKey.toLowerCase();
keydownEvent.stopPropagation();
keydownEvent.cancelBubble = true;
if (keydownEvent.repeat) return
if (blacklistedTargets.includes(keydownEvent.target.tagName)) return;
if (loweredKey !== keydownEvent.key.toLowerCase()) return;
if (keys[loweredKey] === undefined) return;
setKeys({ type: "key-down", key: loweredKey });
return false;
},
把传入key,和event进行对比,这里注意要判定repeate的情况,因为一直按下去会持续触发keydown,所以可以用repeat来判断只执行一次
适当的情况下进行触发
useEffect(() => {
if (!Object.values(keys).filter(value => !value).length) {
callback(keys)
} else {
setKeys({ type: null });
}
}, [callback, keys, initKeysMapping]);
使用useeffect,在keys改变的时候,也就是reducer起作用的时候,判断是否所有的键都按下去了,如果都按下去,那就直接进行一个函数的触发,依赖列表也很清晰,就是keys,回调函数。这里也要说明一下为什么不能用state,如果用state,state改变,useeffect触发,就会造成死循环
react 的文档中,明确提出了 useReducer 是 useState 的替代方案,这里是文档原话
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
这里使用useReducer可以解决这个问题,这里是有个setKeys,但其实如果删掉的话,不在useEffect里面调用setKeys,也可以做,都可以实现的
添加到事件监听器
useEffect(() => {
shortcutKeys.forEach(key => window.addEventListener("keydown", keydownListener(key)));
return () => shortcutKeys.forEach(key => window.removeEventListener("keydown", keydownListener(key)))
}, [])
useEffect(() => {
shortcutKeys.forEach(key => window.addEventListener("keyup", keyupListener(key)));
return () => shortcutKeys.forEach(key => window.removeEventListener("keyup", keyupListener(key)))
}, [])
添加keydown和keyup listener,在停止挂载的时候进行一个删除回调