我发现了一个React、Vue等所有前端框架都存在的隐秘Bug?

时间:2022-12-11 08:03:41

什么 Bug?

昨天有个朋友请教了我一个问题,她在使用原生的 Details 元素封装一个手风琴组件。但是无论如何都不能按照预期工作。

起初我认为是她水平比较差,代码写的有问题。但是她一再向我保证绝对不是她的问题。所以我就抽出时间帮她看了一下。意外发现这一个框架的隐秘 Bug。

我把这个代码放到了码上掘金上,你可以看一下。

import React, { useState } from 'react';
import ReactDom from 'react-dom';

function App() {const [isOpen, setIsOpen] = useState(false)return (<><span>状态: {isOpen ? 'open' : 'closed'}</span><details open={isOpen}><summaryonClick={() => {setIsOpen(!isOpen)}}>Summary</summary>Details</details></>)
}

ReactDom.render(<App />, document.getElementById('app')); 

我们发现在组件的一开始并没有按照预期去渲染组件。之后的每一次点击,都不会按照预期去渲染。

为什么会这样?

原因在于 details 元素具有自身的状态,React 并不知道。

简单来说,这个问题在于 details 的 open 属性有两个数据来源:React 和浏览器。

更详细的讨论可以看这个 Github issue

首先我解释一下当第一次单击按钮时会发生什么:

1.summary 元素的 onClick 事件触发,状态 isOpen 从 false 变为 true。
2.React 重新渲染组件,将 details 元素的 open 属性设置为 true。
3.details 元素的默认行为会切换自身 open 状态,将 open 设置为 false,但 React 并不知道。

所以这就是 details 元素最终没有将 open 属性设置为 true 的原因,而我们的 isOpen 状态依然是 true。

第二次点击:

1.summary 元素的 onClick 处理程序 被触发,切换 isOpen 到 false.
2.React 重新渲染,发现 details 已经关闭,所以它不会去改变它。
3.details 元素的默认行为再次切换它的 open 状态。现在是 false,所以它会把 open 状态改变为 true,而 React 仍然不知道。

在此之后,一切都会打破。

怎么解决?

e.preventDefault

解决思路其实很简单,只要不让浏览器乱动状态就可以了。我们可以使用 e.preventDefault 来禁止浏览器的默认行为。这样就只有 React 能够控制它的状态了。

toggle

除了上面的方法外,还有一种方法是通过 details 的 toggle 事件来处理它。

function App() {const [isOpen, setIsOpen] = useState(false)return (<><span>状态: {isOpen ? 'open' : 'closed'}</span><detailsopen={isOpen} onToggle={() => {setIsOpen(!isOpen)}}><summary>Summary</summary>Details</details></>)
} 

这样似乎正常了。

但是很快我的朋友又遇到了新的麻烦,她在 details 中有一个按钮,这个按钮可以改变 isOpen 的状态。

function App() {const [isOpen, setIsOpen] = useState(false)return (<><span>状态: {isOpen ? 'open' : 'closed'}</span><detailsopen={isOpen}onToggle={() => {setIsOpen(!isOpen)}}><summary>Summary</summary>Details<button onClick={() => setIsOpen(!isOpen)}>切换状态</button></details></>)
} 

当点击这个按钮时,浏览器就抽风了,进入了死循环状态。

我又试着帮她解析了一下这个问题的原因:

1.按钮的 onClick 事件会切换 isOpen,同时会更改 details 的 open 属性。
2.open 属性的变化会触发 onToggle 事件。
3.onToggle 事件会再次切换 isOpen 的状态。同时改变了 details 的 open 属性,这时又回到了第 2 步,所以进入了无限循环状态。

这个 Bug 是 React 框架独有的吗?

虽然朋友解决了这个问题,但是她也向我吐槽 React 难用。

我很好奇这个问题是 React 独有的问题吗?其他类似的框架,比如 Soild、Svelte 和 Vue 它们会有这个问题吗?

于是我尝试了其他所有框架,发现它们都有这个问题。

Vue 的代码如下:

<template><span>状态: {{isOpen ? 'open' : 'closed'}}</span><details :open="isOpen"><summary @click="()=> {isOpen = !isOpen}">Summary</summary>Details</details>

</template>

<script> import { defineComponent, ref } from 'vue';

export default defineComponent({setup() {const isOpen = ref(false);return {isOpen};},
}); </script> 

我也放到了码上掘金上,你可以看一下。

这个 Bug 到底是谁的锅?

鉴于所有的框架都有这个问题,所以我认为它不应该是框架的问题。

于是我尝试用原生的 JavaScript 来编写这段程序。

<!DOCTYPE html>
<html><body><span>状态: closed</span><details><summary>Summary</summary>Details</details><script> const span = document.querySelector('span')const details = document.querySelector('details')const summary = document.querySelector('summary')let isOpen = falsesummary.addEventListener('click', () => {if (isOpen) {isOpen = falsespan.textContent = '状态: closed'details.removeAttribute('open')return}isOpen = truespan.textContent = '状态: open'details.setAttribute('open', '')}) </script></body>
</html> 

现在看来,这似乎是 details 这个元素的底层工作原理的问题,和框架无关。

能够完美解决的唯一办法就是通过 e.preventDefault 来禁止掉浏览器默认行为,让 JavaScript 中的变量成为唯一的数据源。

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。
我发现了一个React、Vue等所有前端框架都存在的隐秘Bug?
我发现了一个React、Vue等所有前端框架都存在的隐秘Bug?
我发现了一个React、Vue等所有前端框架都存在的隐秘Bug?
我发现了一个React、Vue等所有前端框架都存在的隐秘Bug?

有需要的小伙伴,可以点击下方卡片领取,无偿分享