Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33e9a77a1b | ||
|
|
fb492c7c6b | ||
|
|
dd0f3b3d8b | ||
|
|
b68d83f282 | ||
|
|
663eb89301 | ||
|
|
01d658e744 | ||
|
|
36349d365d |
@@ -6,7 +6,7 @@
|
||||
|
||||
前端界的好文精读,每周更新!
|
||||
|
||||
最新精读:<a href="./前沿技术/239.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20%E6%95%B0%E7%BB%84%E7%9A%84%E5%86%85%E9%83%A8%E5%AE%9E%E7%8E%B0%E3%80%8B.md">239.精读《JS 数组的内部实现》</a>
|
||||
最新精读:<a href="./源码解读/241.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-snippets%20-%20Router%20%E6%BA%90%E7%A0%81%E3%80%8B.md">241.精读《react-snippets - Router 源码》</a>
|
||||
|
||||
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
|
||||
|
||||
@@ -189,6 +189,7 @@
|
||||
- <a href="./前沿技术/237.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204.5-4.6%20%E6%96%B0%E7%89%B9%E6%80%A7%E3%80%8B.md">237.精读《Typescript 4.5-4.6 新特性》</a>
|
||||
- <a href="./前沿技术/238.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%8D%E5%86%8D%E9%9C%80%E8%A6%81%20JS%20%E5%81%9A%E7%9A%84%205%20%E4%BB%B6%E4%BA%8B%E3%80%8B.md">238.精读《不再需要 JS 做的 5 件事》</a>
|
||||
- <a href="./前沿技术/239.%E7%B2%BE%E8%AF%BB%E3%80%8AJS%20%E6%95%B0%E7%BB%84%E7%9A%84%E5%86%85%E9%83%A8%E5%AE%9E%E7%8E%B0%E3%80%8B.md">239.精读《JS 数组的内部实现》</a>
|
||||
- <a href="./前沿技术/240.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20useEvent%20RFC%E3%80%8B.md">240.精读《React useEvent RFC》</a>
|
||||
|
||||
### 设计模式
|
||||
|
||||
@@ -245,6 +246,7 @@
|
||||
- <a href="./源码解读/156.%20%E7%B2%BE%E8%AF%BB%E3%80%8Areact-intersection-observer%20%E6%BA%90%E7%A0%81%E3%80%8B.md">156. 精读《react-intersection-observer 源码》</a>
|
||||
- <a href="./源码解读/227.%20%E7%B2%BE%E8%AF%BB%E3%80%8Azustand%20%E6%BA%90%E7%A0%81%E3%80%8B.md">227. 精读《zustand 源码》</a>
|
||||
- <a href="./源码解读/229.%E7%B2%BE%E8%AF%BB%E3%80%8Avue-lit%20%E6%BA%90%E7%A0%81%E3%80%8B.md">229.精读《vue-lit 源码》</a>
|
||||
- <a href="./源码解读/241.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-snippets%20-%20Router%20%E6%BA%90%E7%A0%81%E3%80%8B.md">241.精读《react-snippets - Router 源码》</a>
|
||||
|
||||
### 商业思考
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
第一名 [next.js](https://github.com/vercel/next.js) 在整体榜单里了,在 Node 框架一骑绝尘。
|
||||
|
||||
第二名 [nest](https://github.com/nestjs/nest) 和 next.js 很像,据我当时的了解,是因为 next.js 起步较慢,源码还不支持 ts,所以就有了这个更时髦的新框架。但实际上 next.js 早就全部改为 ts 了,而且正如整体榜单所说,现在已经开始引领潮流了,所以不怪 nest 定位重合,只能怪 next.js 后续发力太猛了。nest 的唯一特点就是没有绑定 UI 库。
|
||||
第二名 [nest](https://github.com/nestjs/nest) 是一个 node 版 server 框架,支持传统的 Controller、Module、Service,支持用装饰器申明路由、控制器等,语法上比较时髦。
|
||||
|
||||
第三名 [Strapi](https://github.com/strapi/strapi) 专门为 API 场景服务,提供了一个 API 管理后台,解决了只需要一个便捷 API 管理,而不希望了解一个大而全的后端框架的痛点。
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
## 概述
|
||||
|
||||
JS 数组的内部类型有很多模式,如:
|
||||
|
||||
0
|
||||
- PACKED_SMI_ELEMENTS
|
||||
- PACKED_DOUBLE_ELEMENTS
|
||||
- PACKED_ELEMENTS
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
useEvent 要解决一个问题:如何同时保持函数引用不变与访问到最新状态。
|
||||
|
||||
本周我们结合 [RFC](https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md) 原文与解读文章 [What the useEvent React hook is (and isn't)](https://typeofnan.dev/what-the-useevent-react-hook-is-and-isnt/) 一起了解下这个提案。
|
||||
|
||||
借用提案里的代码,一下就能说清楚 `useEvent` 是个什么东西:
|
||||
|
||||
```ts
|
||||
function Chat() {
|
||||
const [text, setText] = useState('');
|
||||
|
||||
// ✅ Always the same function (even if `text` changes)
|
||||
const onClick = useEvent(() => {
|
||||
sendMessage(text);
|
||||
});
|
||||
|
||||
return <SendButton onClick={onClick} />;
|
||||
}
|
||||
```
|
||||
|
||||
`onClick` 既保持引用不变,又能在每次触发时访问到最新的 `text` 值。
|
||||
|
||||
为什么要提供这个函数,它解决了什么问题,在概述里慢慢道来。
|
||||
|
||||
## 概述
|
||||
|
||||
定义一个访问到最新 state 的函数不是什么难事:
|
||||
|
||||
```ts
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
const sayCount = () => {
|
||||
console.log(count)
|
||||
}
|
||||
|
||||
return <Child onClick={sayCount} />
|
||||
}
|
||||
```
|
||||
|
||||
但 `sayCount` 函数引用每次都会变化,这会直接破坏 `Child` 组件 memo 效果,甚至会引发其更严重的连锁反应(`Child` 组件将 `onClick` 回调用在 `useEffect` 里时)。
|
||||
|
||||
想要保证 `sayCount` 引用不变,我们就需要用 `useCallback` 包裹:
|
||||
|
||||
```ts
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
const sayCount = useCallback(() => {
|
||||
console.log(count)
|
||||
}, [count])
|
||||
|
||||
return <Child onClick={sayCount} />
|
||||
}
|
||||
```
|
||||
|
||||
但即便如此,我们仅能保证在 `count` 不变时,`sayCount` 引用不变。如果想保持 `sayCount` 引用稳定,就要把依赖 `[count]` 移除,这会导致访问到的 `count` 总是初始值,逻辑上引发了更大问题。
|
||||
|
||||
一种无奈的办法是,维护一个 countRef,使其值与 count 保持同步,在 `sayCount` 中访问 `countRef`:
|
||||
|
||||
```ts
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
const countRef = React.useRef()
|
||||
countRef.current = count
|
||||
|
||||
const sayCount = useCallback(() => {
|
||||
console.log(countRef.current)
|
||||
}, [])
|
||||
|
||||
return <Child onClick={sayCount} />
|
||||
}
|
||||
```
|
||||
|
||||
这种代码能解决问题,但绝对不推荐,原因有二:
|
||||
|
||||
1. 每个值都要加一个配套 Ref,非常冗余。
|
||||
2. 在函数内直接同步更新 ref 不是一个好主意,但写在 `useEffect` 里又太麻烦。
|
||||
|
||||
另一种办法就是自创 hook,如 `useStableCallback`,这本质上就是这次提案的主角 - `useEvent`:
|
||||
|
||||
```ts
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
const sayCount = useEvent(() => {
|
||||
console.log(count)
|
||||
})
|
||||
|
||||
return <Child onClick={sayCount} />
|
||||
}
|
||||
```
|
||||
|
||||
所以 `useEvent` 的内部实现很可能类似于自定义 hook `useStableCallback`。在提案内也给出了可能的实现思路:
|
||||
|
||||
```ts
|
||||
// (!) Approximate behavior
|
||||
function useEvent(handler) {
|
||||
const handlerRef = useRef(null);
|
||||
|
||||
// In a real implementation, this would run before layout effects
|
||||
useLayoutEffect(() => {
|
||||
handlerRef.current = handler;
|
||||
});
|
||||
|
||||
return useCallback((...args) => {
|
||||
// In a real implementation, this would throw if called during render
|
||||
const fn = handlerRef.current;
|
||||
return fn(...args);
|
||||
}, []);
|
||||
}
|
||||
```
|
||||
|
||||
其实很好理解,我们将需求一分为二看:
|
||||
|
||||
1. 既然要返回一个稳定引用,那最后返回的函数一定使用 `useCallback` 并将依赖数组置为 `[]`。
|
||||
2. 又要在函数执行时访问到最新值,那么每次都要拿最新函数来执行,所以在 Hook 里使用 Ref 存储每次接收到的最新函数引用,在执行函数时,实际上执行的是最新的函数引用。
|
||||
|
||||
注意两段注释,第一个是 `useLayoutEffect` 部分实际上要比 `layoutEffect` 执行时机更提前,这是为了保证函数在一个事件循环中被直接消费时,不可能访问到旧的 Ref 值;第二个是在渲染时被调用时要抛出异常,这是为了避免 `useEvent` 函数被渲染时使用,因为这样就无法数据驱动了。
|
||||
|
||||
## 精读
|
||||
|
||||
其实 `useEvent` 概念和实现都很简单,下面我们聊聊提案里一些有意思的细节吧。
|
||||
|
||||
### 为什么命名为 useEvent
|
||||
|
||||
提案里提到,如果不考虑名称长短,完全用功能来命名的话,`useStableCallback` 或 `useCommittedCallback` 会更加合适,都表示拿到一个稳定的回调函数。但 `useEvent` 是从使用者角度来命名的,即其生成的函数一般都被用于组件的回调函数,而这些回调函数一般都有 “事件特性”,比如 `onClick`、`onScroll`,所以当开发者看到 `useEvent` 时,可以下意识提醒自己在写一个事件回调,还算比较直观。(当然我觉得主要原因还是为了缩短名称,好记)
|
||||
|
||||
### 值并不是真正意义上的实时
|
||||
|
||||
虽然 `useEvent` 可以拿到最新值,但和 `useCallback` 拿 `ref` 还是有区别的,这个差异体现在:
|
||||
|
||||
```ts
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
const sayCount = useEvent(async () => {
|
||||
console.log(count)
|
||||
await wait(1000)
|
||||
console.log(count)
|
||||
})
|
||||
|
||||
return <Child onClick={sayCount} />
|
||||
}
|
||||
```
|
||||
|
||||
`await` 前后输出值一定是一样的,在实现上,`count` 值仅是调用时的快照,所以函数内异步等待时,即便外部又把 `count` 改了,当前这次函数调用还是拿不到最新的 `count`,而 `ref` 方法是可以的。在理解上,为了避免夜长梦多,回调函数尽量不要写成异步的。
|
||||
|
||||
### useEvent 也救不了手残
|
||||
|
||||
如果你坚持写出 `onSomething={cond ? handler1 : handler2}` 这样的代码,那么 `cond` 变化后,传下去的函数引用也一定会变化,这是 `useEvent` 无论如何也避免不了的,也许解救方案是 Lint and throw error。
|
||||
|
||||
其实将 `cond ? handler1 : handler2` 作为一个整体包裹在 `useEvent` 就能解决引用变化的问题,但除了 Lint,没有人能防止你绕过它。
|
||||
|
||||
### 可以用自定义 hook 代替 useEvent 实现吗?
|
||||
|
||||
不能。虽然提案里给了一个近似解决方案,但实际上存在两个问题:
|
||||
|
||||
1. 在赋值 ref 时,`useLayoutEffect` 时机依然不够提前,如果值变化后立即访问函数,拿到的会是旧值。
|
||||
2. 子组件 layout effect 在父组件之前执行,拿到的也是旧值。
|
||||
3. 生成的函数被用在渲染并不会给出错误提示。
|
||||
|
||||
## 总结
|
||||
|
||||
`useEvent` 显然又给 React 增加了一个官方概念,在结结实实增加了理解成本的同时,也补齐了 React Hooks 在实践中缺失的重要一环,无论你喜不喜欢,问题就在那,解法也给了,挺好。
|
||||
|
||||
> 讨论地址是:[精读《React useEvent RFC》· Issue #415 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/415)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
造轮子就是应用核心原理 + 周边功能的堆砌,所以学习成熟库的源码往往会受到非核心代码干扰,[Router](https://github.com/ashok-khanna/react-snippets/blob/main/Router.js) 这个 repo 用不到 100 行源码实现了 React Router 核心机制,很适合用来学习。
|
||||
|
||||
## 精读
|
||||
|
||||
[Router](https://github.com/ashok-khanna/react-snippets/blob/main/Router.js) 快速实现了 React Router 3 个核心 API:`Router`、`navigate`、`Link`,下面列出基本用法,配合理解源码实现会更方便:
|
||||
|
||||
```tsx
|
||||
const App = () => (
|
||||
<Router
|
||||
routes={[
|
||||
{ path: '/home', component: <Home /> },
|
||||
{ path: '/articles', component: <Articles /> }
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
const Home = () => (
|
||||
<div>
|
||||
home, <Link href="/articles">go articles</Link>,
|
||||
<span onClick={() => navigate('/details')}>or jump to details</span>
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
首先看 `Router` 的实现,在看代码之前,思考下 `Router` 要做哪些事情?
|
||||
|
||||
- 接收 routes 参数,根据当前 url 地址判断渲染哪个组件。
|
||||
- 当 url 地址变化时(无论是用户触发还是自己的 `navigate` `Link` 触发),渲染新 url 对应的组件。
|
||||
|
||||
所以 `Router` 是一个路由渲染分配器与 url 监听器:
|
||||
|
||||
```tsx
|
||||
export default function Router ({ routes }) {
|
||||
// 存储当前 url path,方便其变化时引发自身重渲染,以返回新的 url 对应的组件
|
||||
const [currentPath, setCurrentPath] = useState(window.location.pathname);
|
||||
|
||||
useEffect(() => {
|
||||
const onLocationChange = () => {
|
||||
// 将 url path 更新到当前数据流中,触发自身重渲染
|
||||
setCurrentPath(window.location.pathname);
|
||||
}
|
||||
|
||||
// 监听 popstate 事件,该事件由用户点击浏览器前进/后退时触发
|
||||
window.addEventListener('popstate', onLocationChange);
|
||||
|
||||
return () => window.removeEventListener('popstate', onLocationChange)
|
||||
}, [])
|
||||
|
||||
// 找到匹配当前 url 路径的组件并渲染
|
||||
return routes.find(({ path, component }) => path === currentPath)?.component
|
||||
}
|
||||
```
|
||||
|
||||
最后一段代码看似每次都执行 `find` 有一定性能损耗,但其实根据 `Router` 一般在最根节点的特性,该函数很少因父组件重渲染而触发渲染,所以性能不用太担心。
|
||||
|
||||
但如果考虑做一个完整的 React Router 组件库,考虑了更复杂的嵌套 API,即 `Router` 套 `Router` 后,不仅监听方式要变化,还需要将命中的组件缓存下来,需要考虑的点会逐渐变多。
|
||||
|
||||
下面该实现 `navigate` `Link` 了,他俩做的事情都是跳转,有如下区别:
|
||||
|
||||
1. API 调用方式不同,`navigate` 是调用式函数,而 `Link` 是一个内置 `navigate` 能力的 `a` 标签。
|
||||
2. `Link` 其实还有一种按住 `ctrl` 后打开新 tab 的跳转模式,该模式由浏览器对 `a` 标签默认行为完成。
|
||||
|
||||
所以 `Link` 更复杂一些,我们先实现 `navigate`,再实现 `Link` 时就可以复用它了。
|
||||
|
||||
既然 `Router` 已经监听 `popstate` 事件,我们显然想到的是触发 url 变化后,让 `popstate` 捕获,自动触发后续跳转逻辑。但可惜的是,我们要做的 React Router 需要实现单页跳转逻辑,而单页跳转的 API `history.pushState` 并不会触发 `popstate`,为了让实现更优雅,我们可以在 `pushState` 后手动触发 `popstate` 事件,如源码所示:
|
||||
|
||||
```tsx
|
||||
export function navigate (href) {
|
||||
// 用 pushState 直接刷新 url,而不触发真正的浏览器跳转
|
||||
window.history.pushState({}, "", href);
|
||||
|
||||
// 手动触发一次 popstate,让 Route 组件监听并触发 onLocationChange
|
||||
const navEvent = new PopStateEvent('popstate');
|
||||
window.dispatchEvent(navEvent);
|
||||
}
|
||||
```
|
||||
|
||||
接下来实现 `Link` 就很简单了,有几个考虑点:
|
||||
|
||||
1. 返回一个正常的 `<a>` 标签。
|
||||
2. 因为正常 `<a>` 点击后就发生网页刷新而不是单页跳转,所以点击时要阻止默认行为,换成我们的 `navigate`(源码里没做这个抽象,笔者稍微优化了下)。
|
||||
3. 但按住 `ctrl` 时又要打开新 tab,此时用默认 `<a>` 标签行为就行,所以此时不要阻止默认行为,也不要继续执行 `navigate`,因为这个 url 变化不会作用于当前 tab。
|
||||
|
||||
```tsx
|
||||
export function Link ({ className, href, children }) {
|
||||
const onClick = (event) => {
|
||||
// mac 的 meta or windows 的 ctrl 都会打开新 tab
|
||||
// 所以此时不做定制处理,直接 return 用原生行为即可
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 否则禁用原生跳转
|
||||
event.preventDefault();
|
||||
|
||||
// 做一次单页跳转
|
||||
navigate(href)
|
||||
};
|
||||
|
||||
return (
|
||||
<a className={className} href={href} onClick={onClick}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
这样的设计,既能兼顾 `<a>` 标签默认行为,又能在点击时优化为单页跳转,里面对 `preventDefault` 与 `metaKey` 的判断值得学习。
|
||||
|
||||
## 总结
|
||||
|
||||
从这个小轮子中可以学习到一下几个经验:
|
||||
|
||||
- 造轮子之前先想好使用 API,根据使用 API 反推实现,会让你的设计更有全局观。
|
||||
- 实现 API 时,先思考 API 之间的关系,能复用的就提前设计好复用关系,这样巧妙的关联设计能为以后维护减少很多麻烦。
|
||||
- 即便代码无法复用的地方,也要尽量做到逻辑复用。比如 `pushState` 无法触发 `popstate` 那段,直接把 `popstate` 代码复用过来,或者自己造一个状态沟通就太 low 了,用浏览器 API 模拟事件触发,既轻量,又符合逻辑,因为你要做的就是触发 `popstate` 行为,而非只是更新渲染组件这个动作,万一以后再有监听 `popstate` 的地方,你的触发逻辑就能很自然的应用到那儿。
|
||||
- 尽量在原生能力上拓展,而不是用自定义方法补齐原生能力。比如 `Link` 的实现是基于 `<a>` 标签拓展的,如果采用自定义 `<span>` 标签,不仅要补齐样式上的差异,还要自己实现 `ctrl` 后打开新 tab 的行为,甚至 `<a>` 默认访问记录行为你也得花高成本补上,所以错误的设计方向会导致事半功倍,甚至无法实现。
|
||||
|
||||
> 讨论地址是:[精读《react-snippets - Router 源码》· Issue #418 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/418)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user