Compare commits

...
7 Commits
Author SHA1 Message Date
ascoders 33e9a77a1b 241 2022-05-23 09:20:56 +08:00
ascoders fb492c7c6b Merge branch 'master' of https://github.com/ascoders/weekly 2022-05-18 14:16:08 +08:00
ascoders dd0f3b3d8b fix: typo 2022-05-18 14:16:05 +08:00
黄子毅 b68d83f282 Merge pull request #417 from kongmoumou/patch-2
Update 240.精读《React useEvent RFC》.md
2022-05-17 20:44:37 +08:00
kongmoumou 663eb89301 Update 240.精读《React useEvent RFC》.md 2022-05-16 14:52:42 +08:00
ascoders 01d658e744 240 2022-05-16 09:14:17 +08:00
ascoders 36349d365d fix: 修复 nestjs 描述 2022-05-12 23:38:36 +08:00
5 changed files with 310 additions and 3 deletions
+3 -1
View File
@@ -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)