Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c44507443 | ||
|
|
ffb736eb6d | ||
|
|
abc677a5f0 | ||
|
|
3c78a1a659 | ||
|
|
507df796b0 | ||
|
|
0f58ce27dc | ||
|
|
c4da7262cb |
+1
-1
@@ -92,7 +92,7 @@
|
||||
|
||||
**中南半岛由 5 个国家组成,从西到东分别是:缅甸、泰国、柬埔寨、老挝、越南**,其中缅、老、 越与中国接壤,除了老挝外都有足够的海岸线。这些国家大部分是殖民时代的遗产,英法分别在缅甸、越南发力,将泰国定位缓冲国。法国人曾将柬埔寨、老挝、越南合并成 “印支联邦” 与英国对抗,虽然现在又分裂成三个国家,因此却为越南埋下了大国梦。
|
||||
|
||||
缅甸在位置上,可以在陆地及海洋延伸中国的地缘影响力,而且也曾成为支持中国抗战的重要援助物资运输线。在缅甸西边是 “金三角地区”:
|
||||
缅甸在位置上,可以在陆地及海洋延伸中国的地缘影响力,而且也曾成为支持中国抗战的重要援助物资运输线。在缅甸东边是 “金三角地区”:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1iwnRchD1gK0jSZFKXXcJrVXa-1396-1266.png">
|
||||
|
||||
|
||||
@@ -198,9 +198,9 @@ function MyComponent(props) {
|
||||
正如这个 Demo 所示,正常情况的 `class1` 与 `class2` 存在许多重复定义的属性,但换成 css-in-js 的方案,编译后的效果等价于将 `class` 复用并拆解了:
|
||||
|
||||
```tsx
|
||||
<Component1 classNames=".classA .classC .classD">
|
||||
<Component1 classNames=".classA .classB .classD">
|
||||
|
||||
<Component2 classNames=".classA .classN .classD .classE">
|
||||
<Component2 classNames=".classA .classC .classD .classE">
|
||||
```
|
||||
|
||||
```scss
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
## 1 引言
|
||||
|
||||
取数是前端业务的重要部分,也经历过几次演化:
|
||||
|
||||
- [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) 的兼容性已经足够好,足以替换包括 `$.post` 在内的各种取数封装。
|
||||
- 原生用得久了,发现拓展性更好、支持 ssr 的同构取数方案也挺好,比如 [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch)、[axios](https://github.com/axios/axios)。
|
||||
- 对于数据驱动场景还是不够,数据流逐渐将取数封装起来,同时针对数据驱动状态变化管理进行了 `data` `isLoading` `error` 封装。
|
||||
- Hooks 的出现让组件更 Reactive,我们发现取数还是优雅回到了组件里,[swr](https://github.com/zeit/swr) 就是一个教科书般的例子。
|
||||
|
||||
[swr](https://github.com/zeit/swr) 在 2019.10.29 号提交,仅仅 12 天就攒了 4000+ star,平均一天收获 300+ star!本周精读就来剖析这个库的功能与源码,了解这个 React Hooks 的取数库的 Why How 与 What。
|
||||
|
||||
## 2 概述
|
||||
|
||||
首先介绍 swr 的功能。
|
||||
|
||||
为了和官方文档有所区别,笔者以探索式思路介绍这个它,但例子都取自官方文档。
|
||||
|
||||
### 2.1 为什么用 Hooks 取数
|
||||
|
||||
首先回答一个根本问题:为什么用 Hooks 替代 fetch 或数据流取数?
|
||||
|
||||
因为 **Hooks 可以触达 UI 生命周期,取数本质上是 UI 展示或交互的一个环节。** 用 Hooks 取数的形式如下:
|
||||
|
||||
```typescript
|
||||
import useSWR from "swr";
|
||||
|
||||
function Profile() {
|
||||
const { data, error } = useSWR("/api/user", fetcher);
|
||||
|
||||
if (error) return <div>failed to load</div>;
|
||||
if (!data) return <div>loading...</div>;
|
||||
return <div>hello {data.name}!</div>;
|
||||
}
|
||||
```
|
||||
|
||||
首先看到的是,以同步写法描述了异步逻辑,这是因为渲染被执行了两次。
|
||||
|
||||
`useSWR` 接收三个参数,第一个参数是取数 `key`,这个 `key` 会作为第二个参数 `fetcher` 的第一个参数传入,普通场景下为 URL,第三个参数是配置项。
|
||||
|
||||
Hooks 的威力还不仅如此,上面短短几行代码还自带如下特性:
|
||||
|
||||
1. 可自动刷新。
|
||||
2. 组件被销毁再渲染时优先启用本地缓存。
|
||||
3. 在列表页中浏览器回退可以自动记忆滚动条位置。
|
||||
4. tabs 切换时,被 focus 的 tab 会重新取数。
|
||||
|
||||
当然,自动刷新或重新取数也不一定是我们想要的,[swr](https://github.com/zeit/swr) 允许自定义配置。
|
||||
|
||||
### 2.2 配置
|
||||
|
||||
上面提到,`useSWR` 还有第三个参数作为配置项。
|
||||
|
||||
**独立配置**
|
||||
|
||||
通过第三个参数为每个 `useSWR` 独立配置:
|
||||
|
||||
```tsx
|
||||
useSWR("/api/user", fetcher, { revalidateOnFocus: false });
|
||||
```
|
||||
|
||||
配置项可以参考 [文档](https://github.com/zeit/swr#options)。
|
||||
|
||||
> 可以配置的有:suspense 模式、focus 重新取数、重新取数间隔/是否开启、失败是否重新取数、timeout、取数成功/失败/重试时的回调函数等等。
|
||||
|
||||
> 第二个参数如果是 object 类型,则效果为配置项,第二个 fetcher 只是为了方便才提供的,在 object 配置项里也可以配置 fetcher。
|
||||
|
||||
**全局配置**
|
||||
|
||||
`SWRConfig` 可以批量修改配置:
|
||||
|
||||
```tsx
|
||||
import useSWR, { SWRConfig } from "swr";
|
||||
|
||||
function Dashboard() {
|
||||
const { data: events } = useSWR("/api/events");
|
||||
// ...
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<SWRConfig value={{ refreshInterval: 3000 }}>
|
||||
<Dashboard />
|
||||
</SWRConfig>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
独立配置优先级高于全局配置,在精读部分会介绍实现方式。
|
||||
|
||||
最重量级的配置项是 `fetcher`,它决定了取数方式。
|
||||
|
||||
### 2.3 自定义取数方式
|
||||
|
||||
自定义取数逻辑其实分几种抽象粒度,比如自定义取数 url,或自定义整个取数函数,而 [swr](https://github.com/zeit/swr) 采取了相对中间粒度的自定义 `fetcher`:
|
||||
|
||||
```tsx
|
||||
import fetch from "unfetch";
|
||||
|
||||
const fetcher = url => fetch(url).then(r => r.json());
|
||||
|
||||
function App() {
|
||||
const { data } = useSWR("/api/data", fetcher);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
所以 `fetcher` 本身就是一个拓展点,我们不仅能自定义取数函数,自定义业务处理逻辑,甚至可以自定义取数协议:
|
||||
|
||||
```tsx
|
||||
import { request } from "graphql-request";
|
||||
|
||||
const API = "https://api.graph.cool/simple/v1/movies";
|
||||
const fetcher = query => request(API, query);
|
||||
|
||||
function App() {
|
||||
const { data, error } = useSWR(
|
||||
`{
|
||||
Movie(title: "Inception") {
|
||||
releaseDate
|
||||
actors {
|
||||
name
|
||||
}
|
||||
}
|
||||
}`,
|
||||
fetcher
|
||||
);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
这里回应了第一个参数称为取数 Key 的原因,在 graphql 下它则是一段语法描述。
|
||||
|
||||
到这里,我们可以自定义取数函数,但却无法控制何时取数,因为 Hooks 写法使取数时机与渲染时机结合在一起。[swr](https://github.com/zeit/swr) 的条件取数机制可以解决这个问题。
|
||||
|
||||
### 2.4 条件取数
|
||||
|
||||
所谓条件取数,即 `useSWR` 第一个参数为 null 时则会终止取数,我们可以用三元运算符或函数作为第一个参数,使这个条件动态化:
|
||||
|
||||
```tsx
|
||||
// conditionally fetch
|
||||
const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher);
|
||||
|
||||
// ...or return a falsy value
|
||||
const { data } = useSWR(() => (shouldFetch ? "/api/data" : null), fetcher);
|
||||
```
|
||||
|
||||
上例中,当 `shouldFetch` 为 false 时则不会取数。
|
||||
|
||||
第一个取数参数推荐为回调函数,这样 [swr](https://github.com/zeit/swr) 会 catch 住内部异常,比如:
|
||||
|
||||
```tsx
|
||||
// ... or throw an error when user.id is not defined
|
||||
const { data, error } = useSWR(() => "/api/data?uid=" + user.id, fetcher);
|
||||
```
|
||||
|
||||
如果 `user` 对象不存在,`user.id` 的调用会失败,此时错误会被 catch 住并抛到 `error` 对象。
|
||||
|
||||
实际上,`user.id` 还是一种依赖取数场景,当 `user.id` 发生变化时需要重新取数。
|
||||
|
||||
### 2.5 依赖取数
|
||||
|
||||
如果一个取数依赖另一个取数的结果,那么当第一个数据结束时才会触发新的取数,这在 [swr](https://github.com/zeit/swr) 中不需要特别关心,只需按照依赖顺序书写 `useSWR` 即可:
|
||||
|
||||
```tsx
|
||||
function MyProjects() {
|
||||
const { data: user } = useSWR("/api/user");
|
||||
const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);
|
||||
|
||||
if (!projects) return "loading...";
|
||||
return "You have " + projects.length + " projects";
|
||||
}
|
||||
```
|
||||
|
||||
[swr](https://github.com/zeit/swr) 会尽可能并行没有依赖的请求,并按依赖顺序一次发送有依赖关系的取数。
|
||||
|
||||
可以想象,如果手动管理取数,当依赖关系复杂时,为了确保取数的最大可并行,往往需要精心调整取数递归嵌套结构,而在 [swr](https://github.com/zeit/swr) 的环境下只需顺序书写即可,这是很大的效率提升。优化方式在下面源码解读章节详细说明。
|
||||
|
||||
依赖取数是自动重新触发取数的一种场景,其实 [swr](https://github.com/zeit/swr) 还支持手动触发重新取数。
|
||||
|
||||
### 2.6 手动触发取数
|
||||
|
||||
`trigger` 可以通过 Key 手动触发取数:
|
||||
|
||||
```tsx
|
||||
import useSWR, { trigger } from "swr";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<Profile />
|
||||
<button
|
||||
onClick={() => {
|
||||
// set the cookie as expired
|
||||
document.cookie =
|
||||
"token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
|
||||
|
||||
// tell all SWRs with this key to revalidate
|
||||
trigger("/api/user");
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
大部分场景不必如此,**因为请求的重新触发由数据和依赖决定,但遇到取数的必要性不由取数参数决定,而是时机时,就需要用手动取数能力了。**
|
||||
|
||||
### 2.7 乐观取数
|
||||
|
||||
特别在表单场景时,数据的改动是可预期的,此时数据驱动方案只能等待后端返回结果,其实可以优化为本地先修改数据,等后端结果返回后再刷新一次:
|
||||
|
||||
```tsx
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
function Profile() {
|
||||
const { data } = useSWR("/api/user", fetcher);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>My name is {data.name}.</h1>
|
||||
<button
|
||||
onClick={async () => {
|
||||
const newName = data.name.toUpperCase();
|
||||
// send a request to the API to update the data
|
||||
await requestUpdateUsername(newName);
|
||||
// update the local data immediately and revalidate (refetch)
|
||||
mutate("/api/user", { ...data, name: newName });
|
||||
}}
|
||||
>
|
||||
Uppercase my name!
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
通过 `mutate` 可以在本地临时修改某个 Key 下返回结果,特别在网络环境差的情况下加快响应速度。乐观取数,表示对取数结果是乐观的、可预期的,所以才能在结果返回之前就预测并修改了结果。
|
||||
|
||||
### 2.8 Suspense 模式
|
||||
|
||||
在 React Suspense 模式下,所有子模块都可以被懒加载,包括代码和请求都可以被等待,只要开启 `suspense` 属性即可:
|
||||
|
||||
```tsx
|
||||
import { Suspense } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
function Profile() {
|
||||
const { data } = useSWR("/api/user", fetcher, { suspense: true });
|
||||
return <div>hello, {data.name}</div>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<div>loading...</div>}>
|
||||
<Profile />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2.9 错误处理
|
||||
|
||||
`onErrorRetry` 可以统一处理错误,包括在错误发生后重新取数等:
|
||||
|
||||
```tsx
|
||||
useSWR(key, fetcher, {
|
||||
onErrorRetry: (error, key, option, revalidate, { retryCount }) => {
|
||||
if (retryCount >= 10) return;
|
||||
if (error.status === 404) return;
|
||||
|
||||
// retry after 5 seconds
|
||||
setTimeout(() => revalidate({ retryCount: retryCount + 1 }), 5000);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 3 精读
|
||||
|
||||
### 3.1 全局配置
|
||||
|
||||
在 Hooks 场景下,包装一层自定义 `Context` 即可实现全局配置。
|
||||
|
||||
首先 `SWRConfig` 本质是一个定制 `Context Provider`:
|
||||
|
||||
```tsx
|
||||
const SWRConfig = SWRConfigContext.Provider;
|
||||
```
|
||||
|
||||
在 `useSWR` 中将当前配置与全局配置 Merge 即可,通过 `useContext` 拿到全局配置:
|
||||
|
||||
```tsx
|
||||
config = Object.assign({}, defaultConfig, useContext(SWRConfigContext), config);
|
||||
```
|
||||
|
||||
### 3.2 useSWR 的一些细节
|
||||
|
||||
从源码可以看到更多细节用心,`useSWR` 真的比手动调用 `fetch` 好很多。
|
||||
|
||||
**兼容性**
|
||||
|
||||
`useSWR` 主体代码在 `useEffect` 中,但是为了将请求时机提前,放在了 UI 渲染前(`useLayoutEffect`),并兼容了服务端场景:
|
||||
|
||||
```tsx
|
||||
const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect;
|
||||
```
|
||||
|
||||
**非阻塞**
|
||||
|
||||
请求时机在浏览器空闲时,因此请求函数被 `requestIdleCallback` 包裹:
|
||||
|
||||
```tsx
|
||||
window["requestIdleCallback"](softRevalidate);
|
||||
```
|
||||
|
||||
`softRevalidate` 是开启了去重的 `revalidate`:
|
||||
|
||||
```tsx
|
||||
const softRevalidate = () => revalidate({ dedupe: true });
|
||||
```
|
||||
|
||||
即默认 2s 内参数相同的重复取数会被取消。
|
||||
|
||||
**性能优化**
|
||||
|
||||
由于 [swr](https://github.com/zeit/swr) 的 `data`、`isValidating` 等数据状态是利用 `useState` 分开管理的:
|
||||
|
||||
```tsx
|
||||
let [data, setData] = useState(
|
||||
(shouldReadCache ? cacheGet(key) : undefined) || config.initialData
|
||||
);
|
||||
// ...
|
||||
let [isValidating, setIsValidating] = useState(false);
|
||||
```
|
||||
|
||||
而取数状态变化时往往 `data` 与 `isValidating` 要一起更新,为了仅触发一次更新,使用了 `unstable_batchedUpdates` 将更新合并为一次:
|
||||
|
||||
```tsx
|
||||
unstable_batchedUpdates(() => {
|
||||
setIsValidating(false);
|
||||
// ...
|
||||
setData(newData);
|
||||
});
|
||||
```
|
||||
|
||||
其实还有别的解法,比如使用 `useReducer` 管理数据也能达到相同性能效果。
|
||||
|
||||
### 3.3 初始缓存
|
||||
|
||||
当页面切换时,可以暂时以上一次数据替换取数结果,即初始化数据从缓存中拿:
|
||||
|
||||
```tsx
|
||||
const shouldReadCache = config.suspense || !useHydration();
|
||||
|
||||
// stale: get from cache
|
||||
let [data, setData] = useState(
|
||||
(shouldReadCache ? cacheGet(key) : undefined) || config.initialData
|
||||
);
|
||||
```
|
||||
|
||||
上面一段代码在 `useSWR` 的初始化期间,`useHydration` 表示是否为初次加载:
|
||||
|
||||
```tsx
|
||||
let isHydration = true;
|
||||
|
||||
export default function useHydration(): boolean {
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
isHydration = false;
|
||||
}, 1);
|
||||
}, []);
|
||||
|
||||
return isHydration;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 支持 suspense
|
||||
|
||||
Suspense 分为两块功能:异步加载代码与异步加载数据,现在提到的是异步加载数据相关的能力。
|
||||
|
||||
Suspense 要求代码 suspended,即抛出一个可以被捕获的 Promise 异常,在这个 Promise 结束后再渲染组件。
|
||||
|
||||
核心代码就这一段,抛出取数的 Promise:
|
||||
|
||||
```tsx
|
||||
throw CONCURRENT_PROMISES[key];
|
||||
```
|
||||
|
||||
等取数完毕后再返回 `useSWR` API 定义的结构:
|
||||
|
||||
```tsx
|
||||
return {
|
||||
error: latestError,
|
||||
data: latestData,
|
||||
revalidate,
|
||||
isValidating
|
||||
};
|
||||
```
|
||||
|
||||
如果没有上面 `throw` 的一步,在取数完毕前组件就会被渲染出来,所以 `throw` 了请求的 Promise 使得这个请求函数支持了 Suspense。
|
||||
|
||||
### 3.5 依赖的请求
|
||||
|
||||
翻了一下代码,没有找到对循环依赖特别处理的逻辑,**后来看了官方文档才恍然大悟,原来是通过 `try/catch` + `onErrorRetry` 机制实现依赖取数的。**
|
||||
|
||||
看下面这段代码:
|
||||
|
||||
```tsx
|
||||
const { data: user } = useSWR("/api/user");
|
||||
const { data: projects } = useSWR(() => "/api/projects?uid=" + user.id);
|
||||
```
|
||||
|
||||
怎么做到智能按依赖顺序请求呢?我们看 `useSWR` 取数函数的主体逻辑:
|
||||
|
||||
```tsx
|
||||
try {
|
||||
// 设置 isValidation 为 true
|
||||
// 取数、onSuccess 回调
|
||||
// 设置 isValidation 为 false
|
||||
// 设置缓存
|
||||
// unstable_batchedUpdates
|
||||
} catch (err) {
|
||||
// 撤销取数、缓存等对象
|
||||
// 调用 onErrorRetry
|
||||
}
|
||||
```
|
||||
|
||||
可见取数逻辑被 `try` 住了,那么 `user.id` 在 `useSWR("/api/user")` 没有 Ready 的情况一定会抛出异常,则自动进入 `onErrorRetry` 逻辑,看看下次取数时 `user.id` 有没有 Ready。
|
||||
|
||||
那么什么时候才轮到下次取数呢?这个时机是:
|
||||
|
||||
```tsx
|
||||
const count = Math.min(opts.retryCount || 0, 8);
|
||||
const timeout =
|
||||
~~((Math.random() + 0.5) * (1 << count)) * config.errorRetryInterval;
|
||||
```
|
||||
|
||||
重试时间基本按 2 的指数速度增长。
|
||||
|
||||
所以 [swr](https://github.com/zeit/swr) 会优先按照并行方式取数,存在依赖的取数会重试,直到上游 Ready。这种简单的模式稍稍损失了一些性能(没有在上游 Ready 后及时重试下游),但不失为一种巧妙的解法,而且最大化并行也使得大部分场景性能反而比手写的好。
|
||||
|
||||
## 4 总结
|
||||
|
||||
笔者给仔细阅读本文的同学留下两道思考题:
|
||||
|
||||
- 关于 Hooks 取数还是在数据流中取数,你怎么看呢?
|
||||
- swr 解决依赖取数的方法还有更好的改进办法吗?
|
||||
|
||||
> 讨论地址是:[精读《Hooks 取数 - swr 源码》 · Issue #216 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/216)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,630 @@
|
||||
## 1 引言
|
||||
|
||||
这是继 [精读《React Conf 2019 - Day1》](https://github.com/dt-fe/weekly/blob/v2/127.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Conf%202019%20-%20Day1%E3%80%8B.md) 之后的第二篇,补充了 React Conf 2019 第二天的内容。
|
||||
|
||||
## 2 概述 & 精读
|
||||
|
||||
第二天的内容更为精彩,笔者会重点介绍比较干货的部分。
|
||||
|
||||
### Fast refresh
|
||||
|
||||
[Fast refresh](https://facebook.github.io/react-native/docs/fast-refresh) 是更好的 react-hot-loader 替代方案,目前仅支持 react-native 平台,很快就会支持 react-dom 平台。
|
||||
|
||||
相比不支持 Function component、无法错误恢复、更新经常失灵的 hot reloading 来说,fast refresh 还拥有以下几个优点:
|
||||
|
||||
- 状态保持。
|
||||
- 支持 Function Component Hooks。
|
||||
- 更快的更新速度。
|
||||
|
||||
Fast refresh 更新速度更快,是基于 Function Component 生成了 “签名”,从而最大成都避免销毁重渲染,尽可能保持对组件的 rerender 刷新。下面介绍签名机制的工作原理。
|
||||
|
||||
Fast refresh 对每个 Function component 都生成了一份专属签名,用以描述这个组件核心状态,当这个核心状态改变时,就只能销毁重渲染了,但对于不触及核心的修改就能进行代价非常小的 rerender。
|
||||
|
||||
这个签名包含了 hooks 和参数名:
|
||||
|
||||
```js
|
||||
// signature: "useState{isLoggedIn}"
|
||||
|
||||
function ExampleComponent() {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(true);
|
||||
}
|
||||
```
|
||||
|
||||
比如当参数名变更时,这个组件的逻辑已发生改动,此时只能销毁并重渲染了。因此实际上通过对签名的对比来判断是否要销毁并重刷新组件:
|
||||
|
||||
```js
|
||||
// signature: "useState{isLoggedOut}"
|
||||
|
||||
function ExampleComponent() {
|
||||
const [isLoggedOut, setIsLoggedOut] = useState(true);
|
||||
}
|
||||
```
|
||||
|
||||
同理,当 hooks 从 `useState` 改成了 `useReducer`,签名也会发生变化从而导致彻底的重渲染。
|
||||
|
||||
但除此之外,**比如对样式的修改、Dom 结构的修改都不会触发签名的变化**,从而保证了 “对不触及逻辑的改动进行高效的轻量 renreder”。
|
||||
|
||||
然而 Fast refresh 也有如下局限性:
|
||||
|
||||
- 还不能友好支持 Class component。
|
||||
- 混合导出 React 和非 React 组件时无法精确的 hot reload。
|
||||
- 更高的内存要求。
|
||||
|
||||
可以看到,Fast Refresh 随着功能推广与内置,现在已经覆盖了 Facebook 95% 以上 hot reload 场景了:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1bjm1mYr1gK0jSZR0XXbP8XXa-1598-1018.png">
|
||||
|
||||
这部分内容不仅揭开了 hot reload 技术内幕,还对其功能进行了进一步优化,2019 年的 React 开发体系已经进入精细化阶段。
|
||||
|
||||
### 重写 React devtools
|
||||
|
||||
React devtools 的更新终于被正式介绍了,本来笔者以为新的 devtools 只是支持了 hooks,但听完分享后发现还有更多有用的改进,包括:
|
||||
|
||||
- 更高的性能。
|
||||
- 更多特性支持。
|
||||
- 更好用户体验。
|
||||
|
||||
**找到节点渲染链路**
|
||||
|
||||
并不是每个 React 节点都参与渲染,新版 React devtools 可以展示出 rendered by:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1OSWomV67gK0jSZPfXXahhFXa-2354-668.png">
|
||||
|
||||
**调试 Suspense**
|
||||
|
||||
在 Day1 中讲到的 Suspense 特性可以在 React devtools 调试了:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1W790m4D1gK0jSZFsXXbldVXa-1816-660.png">
|
||||
|
||||
通过点击时钟 icon,可以模拟 Suspense 处于 pendding 或 ready 状态。
|
||||
|
||||
**增强调试能力**
|
||||
|
||||
可以通过点击直接跳转到组件源码:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1IBK1m4n1gK0jSZKPXXXvUXXa-1806-772.png">
|
||||
|
||||
最新版已增强至点击按钮后直接通过 Source 打开源码位置,**这样可以快速通过 UI 寻找到代码**。同时还可以看到,通过点击 debugger 按钮将当前组件信息打到控制台调试。
|
||||
|
||||
除此之外还可以动态修改组件的 props 与 hook state,大大增强了调试能力。
|
||||
|
||||
**profiler**
|
||||
|
||||
分析工具也得到了增强,现在可以看到每个组件被渲染了几次以及重新渲染的原因:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1cla1m7P2gK0jSZPxXXacQpXa-1824-690.png">
|
||||
|
||||
比如上图组件被渲染了 4 次,主要有两个原因:Hooks 改变与 Props 改变。
|
||||
|
||||
除此之外,还优化了更多细节体验,比如高亮搜索、HOC 的展示优化、嵌套层级过多时不会占用过多的横向宽度等等。
|
||||
|
||||
### react codemod
|
||||
|
||||
codemod 是一个代码重构的方式,通过 AST 方式精准触达代码,我们可以认为 codemod 是一个更聪明的“查找/替换”。
|
||||
|
||||
codemod 主要有以下三种使用方式:
|
||||
|
||||
- 重命名。
|
||||
- 代码排序。
|
||||
- 一定程度的代码替换。
|
||||
|
||||
接下来就讲到 [react codemod](https://github.com/reactjs/react-codemod) 了,它是 react 场景的 codemod 解决方案,facebook 是这么使用 react codemod 的:
|
||||
|
||||
- 迁移 facebook 代码。
|
||||
- 涉及几万个组件。
|
||||
- 修复了 3500 个文件的 React.PropTypes。
|
||||
- 修复了 8500 个文件的生命周期 unsafe。
|
||||
- 修复了 20000 个文件的 createClass 转 JSX。
|
||||
|
||||
使用方式:
|
||||
|
||||
```bash
|
||||
npx react-codemod React-PropTypes-to-prop-types
|
||||
```
|
||||
|
||||
可以看到,通过 cli 对文件进行一次性重构处理。除此之外,再列举几种使用场景:
|
||||
|
||||
- create-element-to-jsx 将 `React.createElement` 转换为 JSX。
|
||||
- error-boundaries 将 `unstable_handleError` 改为 `componentDidCatch`。
|
||||
- findDOMNode 将 `React.createClass` 中 `this.getDOMNode()` 改为 `React.findDOMNode`。
|
||||
- sort-comp 将 Class Component 生命周期按照规范排序,[eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/sort-comp.md) 插件也有相同能力。
|
||||
|
||||
理论上来讲,所有 codemode 做的事情都可以替换为 eslint 的 autofix 来完成,比如 sort-comp 就同时被 codemode 和 eslint 支持。
|
||||
|
||||
### Suspense
|
||||
|
||||
要理解 Suspense,就要理解 Suspense 与普通 loading 有什么区别。
|
||||
|
||||
从代码角度来说,Suspense 可以类比为 `try/catch` 的体验。为了简化代码复杂度,我们可以用 `try/catch` 包裹代码,从而简化 try 区块代码复杂度,并将兜底代码放在 catch 区块:
|
||||
|
||||
```js
|
||||
try {
|
||||
// 只要考虑正确情况
|
||||
} catch {
|
||||
// 错误时 fallback
|
||||
}
|
||||
```
|
||||
|
||||
Suspense 也一样,它在渲染 React 组件时如果遇到了 Promise 抛出的 Error,就会进入 `fallback`,所以 `fallback` 含义是 Loading 中状态:
|
||||
|
||||
```jsx
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<ProfilePage />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
与此同时,实际业务组件中的取数也不需要担心取数是否正在进行中,只要直接处理拿到数据的情况就好了:
|
||||
|
||||
```jsx
|
||||
function ProfileDetails() {
|
||||
// 直接使用 user,不用担心失败。
|
||||
const user = resource.user.read();
|
||||
return <h1>{user.name}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
进一步的,如果要处理组件渲染的异常,再使用 `ErrorBoundary` 包裹即可,此时的 `fallback` 含义是组件加载异常的错误状态:
|
||||
|
||||
```jsx
|
||||
function Home(props) {
|
||||
return (
|
||||
<ErrorBoundary fallback={<ErrorMessage />}>
|
||||
<Suspense fallback={<Placeholder />}>
|
||||
<Composer />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Suspense 模式的取数好处是 “fetch on render”,即渲染与取数同时进行,而普通模式的取数是 “fetch after render”,即渲染完成后再通过 `useEffect` 取数,此时取数时机已晚。
|
||||
|
||||
**队列加载**
|
||||
|
||||
假设 `Composer` 与 `NewsFeed` 组件内部都通过 `useQuery` 取数,那么并行取数时加载机制如下:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1AonZm7L0gK0jSZFtXXXQCXXa-1770-778.png">
|
||||
|
||||
这可能有两个问题:组件内部加载顺序不统一与组件间加载顺序不统一。
|
||||
|
||||
如果组件内部有图片,可能图片与组件渲染实际不一致,此时可以利用 Suspense 统一 hold 所有子组件的特性,将图片加载改为 Suspense 模式:
|
||||
|
||||
```jsx
|
||||
<div>
|
||||
<YourImage src={uri} alt={...} />
|
||||
<MoreComposer />
|
||||
</div>
|
||||
```
|
||||
|
||||
同一个 Suspense 可以等待所有子元素都 Ready 后才会一把渲染出 UI,因此可以看到网页被一次性刷新而不是分部刷新。
|
||||
|
||||
第二个问题是组件间加载顺序不统一,可能导致先渲染了文章内容,再渲染出文章头部,此时如果区块高度不固定,文章头部可能会撑开,导致文章内容下移,用户的阅读体验会遭到打断。可以通过 `suspense ordering` 解决这个问题:
|
||||
|
||||
```jsx
|
||||
function Home(props) {
|
||||
return (
|
||||
<SuspenseList revealOrder="forwards">
|
||||
<Suspense fallback={<ComposerFallback />}>
|
||||
<Composer />
|
||||
</Suspense>
|
||||
<Suspense fallback={<FeedFallback />}>
|
||||
<NewsFeed />
|
||||
</Suspense>
|
||||
</SuspenseList>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
比如 `forwards` 表示从上到下,那么一定会先渲染头部再渲染文章内容,这样文章内容就不会都抖动了。
|
||||
|
||||
### Render as you fetch
|
||||
|
||||
相比 “fetch on render”,更高级别的优化是 “Render as you fetch”,即取数在渲染时机之前。
|
||||
|
||||
比如页面路由的跳转、Hover 到一个区块,此时如果取数由这个动作触发,就可以再次将取数时机提前,Facebook 为此创造了一个新的 Hook:`usePreloadedQuery`。
|
||||
|
||||
用法是,在某个事件中取数,比如点击页面跳转按钮时,通过 `preloadQuery` 预取数,得到的结果并不是取数结果,而是一个标识,在渲染组件中,把这个标识传给 `usePreloadedQuery` 可以拿到真实取数结果:
|
||||
|
||||
```js
|
||||
// 组件 A 的 onClick
|
||||
const reference = preloadQuery(query, variables);
|
||||
// 组件 B 的 render
|
||||
const data = usePreloadedQuery(query, reference);
|
||||
```
|
||||
|
||||
可以看到,取数真正触发的时机在渲染函数执行之前,所以在 `usePreloadedQuery` 调用时取数肯定已经在路上,甚至已经完成。相比之下,普通的 `useQuery` 函数存在下面几个问题:
|
||||
|
||||
- 由于取数过程存在状态变化,可能导致组件在 “取数无意义” 状态下重新渲染多次。
|
||||
- 可能取数还未完成就触发重渲染。
|
||||
- 没有取消的机制,没有清除结果的机制。
|
||||
- 没有办法唯一标识组件。
|
||||
|
||||
preloadQuery 的好处就是将取数时机与 UI 分离,这样可以更细粒度的控制逻辑:
|
||||
|
||||
- 调用 preloadQuery 时:
|
||||
- 在组件销毁时取消取数。
|
||||
- 有新取数触发时取消取数。
|
||||
- 销毁一些轮询机制。
|
||||
- 渲染组件调用 usePreloadedQuery 时:
|
||||
- 不会再触发取数,不会触发意外的 re-render。
|
||||
- 不需要清空,因为取数不在这里发起。
|
||||
- 不需要清理轮询。
|
||||
|
||||
可见 preloadQuery 相比 useQuery 的确有了一些体验提升,然而这个优化比较追求极致,对大部分国内项目来说可能还走不到 facebook 这么极致的性能优化,所以投入产出比显得不是那么高,而且这个开发方式对开发者不是太友好,因为它让请求的时机割裂到两个模块中。
|
||||
|
||||
但毕竟用户体验是大于开发者体验的,React 尽量通过提高开发者体验来间接提高用户体验,使双方都满意,但像 preloadQuery 就无法两者兼顾了,为了用户体验可以适当的降低一些开发者体验。
|
||||
|
||||
### 如何维护代码
|
||||
|
||||
这个分享讲述了如何提升代码维护效率,毕竟一个月后可能连自己写的代码都看不懂了。[hydrosquall](http://github.com/hydrosquall) 通过类比地图的方式解释了程序员是如何维护代码的。
|
||||
|
||||
首先看我们是如何认路的。认路分为三个层次:
|
||||
|
||||
- 随意走走。
|
||||
- 通过一些地标判断方向。
|
||||
- 有方向的寻路。
|
||||
- 通过跟随同伴或者了解更多本地信息找到目的地。
|
||||
- 地图。
|
||||
- 通过 GPS 定位。
|
||||
- 通过模拟地图方式指出路线。
|
||||
|
||||
可以看到这三种方式是逐层递进的,那么类比到代码就有意思了:
|
||||
|
||||
- 随意走走(滚动查看源代码 + ctrl/f 查找代码 + grep 搜索)。
|
||||
- 入口(找到入口节点,查看数据结构)。
|
||||
- 标记(查看代码注释、查看 README)。
|
||||
- 发信号弹(断点、console.log 等调试行为)
|
||||
- 找到方向。
|
||||
- git blame 查看 owner,或直接根据文档找到 codeowners。
|
||||
- 地图。
|
||||
- 幸运的话你可以找到一份架构流程图。
|
||||
|
||||
可以看到,地图有几种抽象层次,比如忽略了细节的纽约地铁线路图:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB14k7pmYr1gK0jSZR0XXbP8XXa-1014-702.png">
|
||||
|
||||
或者是包含丰富地面信息的地铁线路图:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1Sbwpm.T1gK0jSZFrXXcNCXXa-692-750.png">
|
||||
|
||||
抽象到什么层次取决于用户使用的场景,那么代码抽象也是如此。[hydrosquall](http://github.com/hydrosquall) 做了一个工具自动分析出代码调用关系:[js-callgraph](https://github.com/persper/js-callgraph)
|
||||
|
||||

|
||||
|
||||
这就像路牌一样,可以更高效的看出代码结构,也包括了数据流结构,由于篇幅限制,感兴趣的同学可以看 [原视频](https://youtu.be/JDDxR1a15Yo?t=6579) 了解更多。
|
||||
|
||||
### 写作与写代码
|
||||
|
||||
本章讲了写作(小说)与写代码的关联,总结出如下几个重点:
|
||||
|
||||
- 写小说和写代码都是创造行为。
|
||||
- 写代码需要抽象思维,写小说也要有抽象思维构造人物和情节。
|
||||
- Show, don't tell,写作天然就是申明式的,和数据驱动很相似。
|
||||
|
||||
更多可以去看 [原视频](https://youtu.be/JDDxR1a15Yo?t=9135)。
|
||||
|
||||
### 移动端动画最佳实践
|
||||
|
||||
首先要使用一个真实的手机设备调试,否则可能出现 PC Chrome 一切正常,而手机上实际效果性能很差的情况!
|
||||
|
||||
**手势下拉退出**
|
||||
|
||||
利用 [react-spring](https://github.com/react-spring/react-spring) 和 [react-use-gesture](react-use-gesture) 做一个下滑消失的 Demo:
|
||||
|
||||
```jsx
|
||||
import { animated, useSpring } from "react-spring";
|
||||
import { useDrag } from "react-use-gesture";
|
||||
|
||||
const [{ y }, set] = useSpring(() => {
|
||||
y: 0;
|
||||
});
|
||||
```
|
||||
|
||||
首先定义一个 `y` 纵向位置,通过 `useDrag` 将拖拽操作与 UI 绑定,通过回调将其与 `y` 数据绑定:
|
||||
|
||||
```js
|
||||
const bind = useDrag(({ last, movement: [, movementY], memo = y.value }) => {
|
||||
if (last) {
|
||||
// 拖拽结束时,如果偏移量超过 50 则效果和结束一样,直接将 y 设置为 100
|
||||
const notificationClosed = movementY > 50;
|
||||
|
||||
return set({
|
||||
y: notificationClosed ? 100 : 0,
|
||||
onReset: notificationClosed && removeNotification
|
||||
});
|
||||
}
|
||||
|
||||
// y 的位置区间在 0~100
|
||||
set([{ y: clamp(0, 100, memo + movementY) }]);
|
||||
|
||||
return memo;
|
||||
});
|
||||
```
|
||||
|
||||
将 `useDrag` 与 `y` 绑定后,就可以用在 UI 组件上了:
|
||||
|
||||
```jsx
|
||||
<StyledNotification
|
||||
as={animated.div}
|
||||
onTouchStart={bind().onTouchStart}
|
||||
style={{
|
||||
opacity: y.interpolate([0, 100], [1, 0]),
|
||||
transform: y.interpolate(y => `translateY(${y}px)`)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
将 `opacity` 与 `transform` 与位置 `y` 绑定就可以做出下拉消失的效果。
|
||||
|
||||
**滑动的洞见**
|
||||
|
||||
接着讲到了滑动的三个洞见:
|
||||
|
||||
1. 要立刻响应,任何延迟都会造成用户额外精神负担。
|
||||
2. 滚动速度衰减可以提升用户体验:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1HocDm1H2gK0jSZJnXXaT1FXa-1348-878.gif">
|
||||
|
||||
接着我们需要预测用户的意图,比如在一个类似微信消息列表页左右滑动时:
|
||||
|
||||
- 是否想取消手势交互?
|
||||
- 是否想展示出更多交互按钮?
|
||||
- 是否想删除所有内容?
|
||||
|
||||
这需要更多设计思考。
|
||||
|
||||
1. 橡皮筋滚动,即列表页可以一直向下拉,上面部分像橡皮筋一样可以被拉出空白页的效果。
|
||||
|
||||
在设计手势动画时要考虑三个要点:
|
||||
|
||||
- 使用移动增量作为手势动画的基准点。
|
||||
- 动画和手势应该随时可以被中断,通过 springs 即可实现。
|
||||
- 完成手势后的动画速度应该与手势速度相当,这样视觉体验更自然。
|
||||
|
||||
最后提到了动画兼容性与性能,比如尽量只使用 `transform` 与 `opacity` 可以保证移动端的流畅度,不同移动设备的默认手势效果不同,最好通过 `touch-action` 禁用默认行为以达到更好的兼容性与效果。
|
||||
|
||||
### 唱片与 React
|
||||
|
||||
J.Dash 拥有十年软件开发经验,同时也卖过很多唱片,他介绍了唱片行业与软件开发的共同点。
|
||||
|
||||
唱片行业需要音乐编排能力,这与编码能力类似,都存在良好的设计模式,并且需要团队合作,开发过程中会遇到一些痛苦的经历,但最终完成音乐和项目时都会获得满足的喜悦。
|
||||
|
||||
### 函数式编程
|
||||
|
||||
> Declaratives UIs are the future, and the future is Comonadic. - Phil Freeman
|
||||
|
||||
申明式 UI 是未来,未来则是 Comonadic。
|
||||
|
||||
所谓申明式 UI 可以用下面的公式表达:
|
||||
|
||||
```js
|
||||
type render = (state: State) => View;
|
||||
```
|
||||
|
||||
然后用一段公式介绍了 Comonadic:
|
||||
|
||||
```js
|
||||
class Functor w => Comonad w where
|
||||
extract :: w a -> a
|
||||
duplicate :: w a -> w (w a)
|
||||
extend :: (w a -> a) -> w a -> w b
|
||||
```
|
||||
|
||||
用 JS 版本做一个解释:
|
||||
|
||||
```js
|
||||
const Store = ({ state, render }) => ({
|
||||
extend: f => Store({ state, render: state => f(Store({ state, render })) }),
|
||||
extract: () => render(state)
|
||||
});
|
||||
```
|
||||
|
||||
`extract` 调用后会进行申明式渲染 UI,即 `render(state)`。
|
||||
|
||||
`extend` 表示拓展,接收一个拓展函数作为参数,返回一个新的 Store 对象。这个拓展函数可以拿到 `state`、`render` 并返回新的 `state` 作为 `extract` 时 `render` 的输入。使用例子是这样的:
|
||||
|
||||
```jsx
|
||||
const App = Store({
|
||||
state: { msg: "World" },
|
||||
render: ({ msg }) => <p>Hello {msg}</p>
|
||||
});
|
||||
|
||||
App.extend(({ state }) =>
|
||||
state.msg === "World" ? { msg: "ReactConf" } : state
|
||||
).extract(); // <p> Hello ReactConf </p>
|
||||
```
|
||||
|
||||
然而尴尬的是,笔者看了很久也没看懂 `Store` 函数,最后运行了一下发现这个 Demo 抛出了异常 😂。
|
||||
|
||||
下面是笔者稍微修改后的例子,至少能跑起来:
|
||||
|
||||
```js
|
||||
const Store = ({ state, render }) => ({
|
||||
extend: f => Store({ state, render: state => render(f({ state, render })) }),
|
||||
extract: () => render(state)
|
||||
});
|
||||
|
||||
const app = Store({
|
||||
state: { msg: "Hello World" },
|
||||
render: ({ msg }) => console.log("render " + msg)
|
||||
});
|
||||
|
||||
app
|
||||
.extend(({ state }) => {
|
||||
return { msg: state.msg + " extend1" };
|
||||
})
|
||||
.extend(({ state }) => {
|
||||
return { msg: state.msg + " extend2" };
|
||||
})
|
||||
.extract(); // render Hello World extend2 extend1
|
||||
```
|
||||
|
||||
然而作者的意思仍是未解之谜,希望对函数式了解的同学可以在评论区指点一下。
|
||||
|
||||
### wick editor
|
||||
|
||||
[wick editor](https://www.wickeditor.com/) 是一个开源的动画、游戏制作软件。
|
||||
|
||||
wick editor 是一个动画制作工具,但拓展了一些 js 编程能力,因此可以很好的将动画与游戏结合在一起:
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1hLJpnbr1gK0jSZR0XXbP8XXa-1766-1002.png">
|
||||
|
||||
演讲介绍了 wick editor 的演化过程:
|
||||
|
||||
从很简陋的 MVP 版本开始(1 周)
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB11sdsneL2gK0jSZFmXXc7iXXa-1192-764.png">
|
||||
|
||||
到 Pre-Alpha(4 月)
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1TMJnnXY7gK0jSZKzXXaikpXa-1306-858.png">
|
||||
|
||||
Alpha(5 月)
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1608pnoD1gK0jSZFGXXbd3FXa-1794-1186.png">
|
||||
|
||||
Beta(1.5 年)
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1CKJpnoD1gK0jSZFGXXbd3FXa-1274-854.png">
|
||||
|
||||
重点是 1.0 版本采用 React 重写了!继 Beta 之后又经历了 1 年:
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1ZZhrneH2gK0jSZFEXXcqMpXa-906-596.png">
|
||||
|
||||
这个团队最棒的地方是,将游戏与教育结合,针对不同场景做了很多用户调研并根据反馈持续改进。
|
||||
|
||||
### React Select
|
||||
|
||||
[react-select](https://github.com/JedWatson/react-select) 的作者 [Jed Watson](https://github.com/JedWatson) 被请来啦。作为一个看上去很简单组件(select)的开发者,却拥有如此大的关注量(1.8w star),那作者有着怎样的心路历程呢?
|
||||
|
||||
react-select 看似简单的名字背后其实有挺多的功能,比如作者列举了一些功能层面的内容:
|
||||
|
||||
- autocomplete - 输入时搜索。
|
||||
- 单、多选。
|
||||
- focus 管理。
|
||||
- 下拉框层级与位置,比如可以放在根 DOM 节点,也可以作为当前节点的子元素。
|
||||
- 异步下拉框内容。
|
||||
- 键盘、触控。
|
||||
- Createble,即在搜索时如果没有内容可以动态创建。
|
||||
- 等等。
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB1RYS1mubviK0jSZFNXXaApXXa-1166-226.png">
|
||||
|
||||
在设计层面:
|
||||
|
||||
- 申明式。
|
||||
- 可以被定制。
|
||||
- 性能要求。
|
||||
- 等等。
|
||||
|
||||
随着 Star 逐渐上涨,越来越多的需求被提出,核心库代码量越来越大,甚至许多需求之间都是相互冲突的,而且作者每天都会被上百个 Issue 与 PR 吵醒。做一个业务 Select 可能只要 5 分钟,但做一个开源 Select 却要 5 年,原因是一个简单的 Select 如何满足所有不同业务场景?这绝对是个巨大的挑战。
|
||||
|
||||
比如用户即需要受控也要非受控的组件,如何满足好这个需求同时又让代码更可维护呢?
|
||||
|
||||
假设我们拥有一个受控的组件 `SelectComponent`,那么它的主要 props 是 `value` 与 `onChange`,如果要拓展成一个既支持 `defaultValue`(非受控)又支持 `value`(受控)的组件,我们可以创建一个 `manageState` 组件对 `SelectComponent` 进行封装:
|
||||
|
||||
```jsx
|
||||
const manageState = SelectComponent => ({
|
||||
value: valueProps,
|
||||
onChange: onChangeProp,
|
||||
defaultValue,
|
||||
...props
|
||||
}) => {
|
||||
const [valueState, setValue] = useState(defaultValue);
|
||||
|
||||
const value = valueProps !== undefined ? valueProps : valueState;
|
||||
|
||||
const onChange = (newValue, actionMeta) => {
|
||||
if (typeof onChangeProp === "function") {
|
||||
onChangeProp(newValue, actionMeta);
|
||||
}
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
return <SelectComponent {...props} value={value} onChange={onChange}>
|
||||
};
|
||||
```
|
||||
|
||||
这样就可以组合为一个受控/非受控的综合 Select 组件:
|
||||
|
||||
```js
|
||||
import BaseSelect from "./Select";
|
||||
import manageState from "./manageState";
|
||||
|
||||
export default manageState(Select);
|
||||
```
|
||||
|
||||
同理对异步的封装也可以放在 `makeAsync` 函数中:
|
||||
|
||||
```jsx
|
||||
const makeAsync = SelectComponent => ({
|
||||
getOptions,
|
||||
defaultOptions,
|
||||
...props
|
||||
}) => {
|
||||
const [options, setOptions] = useState(defaultOptions);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onInputChange = async newValue => {
|
||||
setIsLoading(true);
|
||||
const newOptions = await getOptions(newValue);
|
||||
setIsLoading(false);
|
||||
setOptions(newOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectComponent
|
||||
{...props}
|
||||
options={options}
|
||||
isLoading={isLoading}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
可以看到,`SelectComponent` 是一个完全受控的数据驱动的 UI,无论是 `manageState` 还是 `makeAsync` 都是对数据处理的拓展,所以这三者之间才可以融洽的组合:
|
||||
|
||||
```js
|
||||
import BaseSelect from "./Select";
|
||||
import manageState from "./manageState";
|
||||
import makeAsync from "./async";
|
||||
|
||||
export default manageState(Select);
|
||||
|
||||
export const AsyncSelect = manageState(makeAsync(Select));
|
||||
```
|
||||
|
||||
后面还有一些风格化、开源协作的思考,这里就不展开了,对这部分感兴趣的同学可以查看原视频了解更多。
|
||||
|
||||
### React + 政府财政透明项目
|
||||
|
||||
usaspending.gov 这个网站使用 React 建设,可以查看美国政府支持财政的明细,通过流畅的体验让更多用户可以了解国家财政支出,进一步推动财政支出的透明化。由于并不涉及前端技术的介绍,主要是产品介绍,因此精读就不详细展开了。
|
||||
|
||||
顺便说一句,智能分析数据就用 [QuickBI](https://www.alibabacloud.com/zh/product/quickbi),QuickBI 是我们团队研发的一款智能 BI 服务平台,如果你将美国政府的财政支持作为数据集输入,你会分析得更透彻。
|
||||
|
||||
### React + 星舰模拟器
|
||||
|
||||
最后介绍的是使用 React 制作的星舰模拟器,看上去像一个游戏:
|
||||
|
||||
<img width=500 src="https://img.alicdn.com/tfs/TB1HrxAneL2gK0jSZPhXXahvXXa-1946-1104.png">
|
||||
|
||||
有星系图、船体、驾驶员信息、武器装备、燃料、通信等等内容。甚至可以模拟太空驾驶,进行任务,可以实时多人协同。对太空迷们的吸引力很大,感兴趣的同学建议直接观看 [视频](https://youtu.be/JDDxR1a15Yo?t=28638)。
|
||||
|
||||
## 3 总结
|
||||
|
||||
第二天的内容非常全面,涉及了 React API、开发者周边、codemod 工具、代码维护、写作/音乐与代码、动画、函数式编程、看似简单的 React 组件、使用 React 制作的各种脑洞大开的项目,等等。
|
||||
|
||||
React Conf 要展示的是一个完整的 React 世界,第一天提到了 React 是一个桥梁,正因为这个桥梁,连接了各行各业不同的人群以及不同的项目,大家都有一个共同的语言:React。
|
||||
|
||||
"We not only react code, but react the world"。
|
||||
|
||||
> 讨论地址是:[精读《React Conf 2019 - Day2》 · Issue #217 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/217)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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