最近公司的前端项目从 react 16 升级到了 React 17,导致 ahooks 的 useClickAway 不能按预期工作。
下面西瓜哥我就来说说到底发生了什么事。
ahooks 是阿里巴巴维护的第三方 React Hook 库,里面封装了很多好用的 hook。
比如经常用到的组件挂载以及卸载的 useMount、useUnmount,还有支持自动请求、手动请求、防抖等各种功能请求 useRequest,以及可以将状态同步存取到 localStorage 的 useLocalStorageState。
当你想要写一个与业务无关的第三方 ahooks,你可以去 ahooks 里面找找,大概率能够找到,是比较优秀的 hook 库。
其中,useClickAway 的作用是 监听目标元素外的点击事件。
useClickAway 接受的第一个参数是一个事件回调函数。
第二个参数是被排除的目标元素,可以是 ref 或 dom 元素,或者是它们组成的数组,
第三个是需要监听的事件类型字符串或事件字符串数组。第三个参数是可选的,不使用的话默认用点击事件 'click' 。
下面是一个常用的写法:
useClickAway(() => {
console.log('点击到元素外的地方');
}, ref);
核心底层原理是,是在 document 上绑定了一个冒泡事件。当事件冒泡到 document 时,会判断事件目标元素是否为传入的 ref 下的子元素。
如果是,什么都不做。如果不是,执行回调函数。
这里给出 useClickAway 的源码地址,感兴趣的话可以研究一下:
https://github.com/alibaba/hooks/blob/v3.5.0/packages/hooks/src/useClickAway/index.ts。
如果你在 React 16 中使用 useClickAway,一切都表现良好。
但如果是 React 17 及以上版本使用,在一些情况下会有问题。
我们有这么一个场景。
点击一个搜索按钮,会出现一个输入框,此时用户需要在这个输入框内输入文字来搜索。如果点击到搜索按钮外的地方,输入框会消失。
核心实现如下:
function App() {
const [visible, setVisible] = useState(false);
const inputRef = useRef();
useClickAway(() => {
setVisible(false);
}, inputRef);
return (
<div>
<button onClick={() => setVisible(true)}>搜索</button>
{visible && <input ref={inputRef} autoFocus />}
</div>
);
}
这里提供一个线上 demo(用的是 React 17 版本):https://codesandbox.io/s/f54siy。
在 React 16 的时候,上面的写法是正常的。但升级到 17 后,你会发现点击 button 后什么事情都没有发生。
原因在于 React 17 对事件系统进行了改造。
16 升级到 17 后,React 将事件委托到 ReactDOM 挂载的根节点上,比如 div#app,而不再是原来 document。
首先,我们要知道的是,当调用 setVisible(true) 改变组件状态时,组件就立即被重新渲染了,然后调用了 useClickAway。状态更新后的组件重渲染是同步的,此时我们的事件流其实还没有结束。
需要注意的是,更新状态后的组件重新渲染,可能是同步,也可能是异步的。
在 React 16 中,事件都委托到了 document 上。
我们点击 button 元素,产生了一个事件流,当点击事件流动到 document 时,我们将 visible 设置为 true,组件进行了一次同步的重新渲染,并调用 useClickAway,做了个 document 上的冒泡事件绑定。
就像下面这样:
document.addEventListener('click', () => {
console.log('显示输入框')
// React 16 中 useClickAway 绑定事件的时机
document.addEventListener('click', () => {
console.log('隐藏输入框');
});
});
// 点击后的输出内容为:
// 显示输入框
在一个元素的事件触发过程中,往这个元素上注册新的相同类型的事件响应函数,这个新的响应函数不会在此次事件流上立即触发。
所以,前面的 useClickAway 写法在 React 16 是正常的。
但是,在 React 17 中就不同了,事件委托下放到了 div#app 中。
点击按钮,事件流冒泡到 div#app 元素,执行事件回调函数将 visible 设置为了 true,并重新渲染组件,执行 useClickAway 再给 document 绑定了新的事件响应函数。
此时事件流没有结束,继续冒泡到 document,将 visible 又设置回了 false。
所以,visible 在短暂地变成 true 后,又变回了 false,无事发生。
document.querySelector('#app').addEventListener('click', () => {
console.log('显示输入框')
// React 17 中 useClickAway 绑定事件的时机
document.addEventListener('click', () => {
console.log('隐藏输入框');
});
});
// 点击后的输出内容为:
// 显示输入框
// 隐藏输入框
<button
onClick={(e) => {
e.stopPropagation();
setVisible(true);
}}
>
我们给按钮加上阻止事件冒泡,提前结束事件流,使其不流到 document 上,就不会触发 document 的点击事件。
但这样也是有隐患的,e.stopPropagation 是破坏性的。
如果我们在其他的地方要写一些特殊的判断失焦逻辑,也要用到类似 useClickAway 的做法,我们点到这个 button 上就会让其他地方的逻辑走不通。
css 中的 overflow: hidden; 也具有破坏性,如果设置了该属性的容器内部的元素超出了容器范围,会被截断。
useClickAway(
() => setVisible(false),
inputRef,
['mousedown', 'touchstart']
);
mousedown 在 click 事件之前就结束了,所以在 click 事件流过程中不会触发它。
touchstart 是为了兼容移动设备的情况。因为触屏时,touchstart 一定会触发,mousedown 不一定,顺带一提,click 也不一定。
其他的比较优秀的第三方 React Hooks 库,比如 react-use 的 useClickAway,其实就是用 mousedown 和 touchstart 作为默认事件类型。
还有百度的 react-hooks 库,其下的 useClickOutside 不支持自定义事件类型,但也是用的 mousedown 和 touchstart。
useClickAway(
() => setVisible(false),
[inputRef, buttonRef]
);
这样就可以把 button 也排除在触发条件外。
但这样写很繁琐。如果输入框要封装成一个组件,你还得把 buttonRef 传入到这个组件中。
<button
onClick={() => {
setTimeout(() => {
setVisible(true);
});
}}
>
通过 setTimeout 的方式,确保输入框的出现在同步的事件流之后才出现,然后才触发 useClickAway 绑定逻辑。
React 16 升级为 17 后,React 中混合事件托管绑定到了 React 组件树挂载的 div#app 上,不再是之前的 document。
这让默认注册为 click 事件类型的 useClickAway 在一些场景下,表现上和 React 16 有一些不同。
对于上面的场景以及解决方案,我认为最好的是第二种:给 useClickAway 的事件类型设置为 mousedown 和 touchstart。这种方法更有普适性。
来源:https://www.toutiao.com/article/7110470685612392992
如今的 Web 前端已被 React、Vue 和 Angular 三分天下,尽管现在的 jQuery 已不再那么流行,但 jQuery 的设计思想还是非常值得致敬和学习的,特别是 jQuery 的插件化。
受控组件与非受控组件在官网与国内网上的资料都不多,有些人觉得它可有可不有,也不在意。这恰恰显示React的威力,满足不同规模大小的工程需求。
一般在传统模式下,我们构建前端项目很简单。就是下载各种js文件,如JQuery、Echart等,直接放置在html静态文件。Webpack则是JavaScript中比较知名的打包工具。这两个构建工具构成了React应用快速搭建的基础。
Gatsby能快速的使用 React 生态系统来生成静态网站,可以结合React Component、Markdown 和服务端渲染来完成静态网站生成让他更强大。
React推出后,出于不同的原因先后出现三种定义react组件的方式,殊途同归;具体的三种方式:函数式定义的无状态组件、es5原生方式React.createClass定义的组件、es6形式的extends React.Component定义的组件
React主要思想是通过构建可复用组件来构建用户界面,每个组件都有自己的生命周期,它规定了组件的状态和方法需要在哪个阶段改变和执行。所谓组件就是有限状态机,,表示有限个状态以及在这些状态之间的转移和动作行为的模型。
React 相关的优化:使用 babel-react-optimize 对 React 代码进行优化,检查没有使用的库,去除 import 引用,按需打包所用的类库,比如 lodash 、echarts 等.Webpack 构建打包存在的问题两个方面:构建速度慢,打包后的文件体积过大
这篇文章主要介绍React Router定义路由之后如何传值,有关React和React Router 。react router中页面传值的三种方法:props.params、query、state
react 高阶组件简单的理解是:一个包装了另一个基础组件的组件。高阶组件的两种形式:属性代理(Props Proxy)、反向继承 (Inheritance Inversion)
React 支持一种非常特殊的属性 Ref ,你可以用来绑定到 render() 输出的任何组件上。这个特殊的属性允许你引用 render() 返回的相应的支撑实例( backing instance )。这样就可以确保在任何时间总是拿到正确的实例
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!