1 引言
这是继 精读《React Conf 2019 - Day1》 之后的第二篇,补充了 React Conf 2019 第二天的内容。
2 概述 & 精读
第二天的内容更为精彩,笔者会重点介绍比较干货的部分。
Fast refresh
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 和参数名:
// signature: "useState{isLoggedIn}"
function ExampleComponent() {
const [isLoggedIn, setIsLoggedIn] = useState(true);
}
比如当参数名变更时,这个组件的逻辑已发生改动,此时只能销毁并重渲染了。因此实际上通过对签名的对比来判断是否要销毁并重刷新组件:
// 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 场景了:
调试 Suspense
在 Day1 中讲到的 Suspense 特性可以在 React devtools 调试了:
最新版已增强至点击按钮后直接通过 Source 打开源码位置,这样可以快速通过 UI 寻找到代码。同时还可以看到,通过点击 debugger 按钮将当前组件信息打到控制台调试。
除此之外还可以动态修改组件的 props 与 hook state,大大增强了调试能力。
profiler
分析工具也得到了增强,现在可以看到每个组件被渲染了几次以及重新渲染的原因:
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 插件也有相同能力。
理论上来讲,所有 codemode 做的事情都可以替换为 eslint 的 autofix 来完成,比如 sort-comp 就同时被 codemode 和 eslint 支持。
Suspense
要理解 Suspense,就要理解 Suspense 与普通 loading 有什么区别。
从代码角度来说,Suspense 可以类比为 try/catch
的体验。为了简化代码复杂度,我们可以用 try/catch
包裹代码,从而简化 try 区块代码复杂度,并将兜底代码放在 catch 区块:
try {
// 只要考虑正确情况
} catch {
// 错误时 fallback
}
Suspense 也一样,它在渲染 React 组件时如果遇到了 Promise 抛出的 Error,就会进入 fallback
,所以 fallback
含义是 Loading 中状态:
}>
与此同时,实际业务组件中的取数也不需要担心取数是否正在进行中,只要直接处理拿到数据的情况就好了:
function ProfileDetails() {
// 直接使用 user,不用担心失败。
const user = resource.user.read();
return {user.name}
;
}
进一步的,如果要处理组件渲染的异常,再使用 ErrorBoundary
包裹即可,此时的 fallback
含义是组件加载异常的错误状态:
function Home(props) {
return (
}>
}>
);
}
Suspense 模式的取数好处是 “fetch on render”,即渲染与取数同时进行,而普通模式的取数是 “fetch after render”,即渲染完成后再通过 useEffect
取数,此时取数时机已晚。
队列加载
假设 Composer
与 NewsFeed
组件内部都通过 useQuery
取数,那么并行取数时加载机制如下:
同一个 Suspense 可以等待所有子元素都 Ready 后才会一把渲染出 UI,因此可以看到网页被一次性刷新而不是分部刷新。
第二个问题是组件间加载顺序不统一,可能导致先渲染了文章内容,再渲染出文章头部,此时如果区块高度不固定,文章头部可能会撑开,导致文章内容下移,用户的阅读体验会遭到打断。可以通过 suspense ordering
解决这个问题:
function Home(props) {
return (
}>
}>
);
}
比如 forwards
表示从上到下,那么一定会先渲染头部再渲染文章内容,这样文章内容就不会都抖动了。
Render as you fetch
相比 “fetch on render”,更高级别的优化是 “Render as you fetch”,即取数在渲染时机之前。
比如页面路由的跳转、Hover 到一个区块,此时如果取数由这个动作触发,就可以再次将取数时机提前,Facebook 为此创造了一个新的 Hook:usePreloadedQuery
。
用法是,在某个事件中取数,比如点击页面跳转按钮时,通过 preloadQuery
预取数,得到的结果并不是取数结果,而是一个标识,在渲染组件中,把这个标识传给 usePreloadedQuery
可以拿到真实取数结果:
// 组件 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 通过类比地图的方式解释了程序员是如何维护代码的。
首先看我们是如何认路的。认路分为三个层次:
-
随意走走。
- 通过一些地标判断方向。
-
有方向的寻路。
- 通过跟随同伴或者了解更多本地信息找到目的地。
-
地图。
- 通过 GPS 定位。
- 通过模拟地图方式指出路线。
可以看到这三种方式是逐层递进的,那么类比到代码就有意思了:
-
随意走走(滚动查看源代码 + ctrl/f 查找代码 + grep 搜索)。
- 入口(找到入口节点,查看数据结构)。
- 标记(查看代码注释、查看 README)。
- 发信号弹(断点、console.log 等调试行为)
-
找到方向。
- git blame 查看 owner,或直接根据文档找到 codeowners。
-
地图。
- 幸运的话你可以找到一份架构流程图。
可以看到,地图有几种抽象层次,比如忽略了细节的纽约地铁线路图:
抽象到什么层次取决于用户使用的场景,那么代码抽象也是如此。hydrosquall 做了一个工具自动分析出代码调用关系:js-callgraph
这就像路牌一样,可以更高效的看出代码结构,也包括了数据流结构,由于篇幅限制,感兴趣的同学可以看 原视频 了解更多。
写作与写代码
本章讲了写作(小说)与写代码的关联,总结出如下几个重点:
- 写小说和写代码都是创造行为。
- 写代码需要抽象思维,写小说也要有抽象思维构造人物和情节。
- Show, don't tell,写作天然就是申明式的,和数据驱动很相似。
更多可以去看 原视频。
移动端动画最佳实践
首先要使用一个真实的手机设备调试,否则可能出现 PC Chrome 一切正常,而手机上实际效果性能很差的情况!
手势下拉退出
利用 react-spring 和 react-use-gesture 做一个下滑消失的 Demo:
import { animated, useSpring } from "react-spring";
import { useDrag } from "react-use-gesture";
const [{ y }, set] = useSpring(() => {
y: 0;
});
首先定义一个 y
纵向位置,通过 useDrag
将拖拽操作与 UI 绑定,通过回调将其与 y
数据绑定:
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 组件上了:
`translateY(${y}px)`)
}}
/>
将 opacity
与 transform
与位置 y
绑定就可以做出下拉消失的效果。
滑动的洞见
接着讲到了滑动的三个洞见:
- 要立刻响应,任何延迟都会造成用户额外精神负担。
- 滚动速度衰减可以提升用户体验:
type render = (state: State) => View;
然后用一段公式介绍了 Comonadic:
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 版本做一个解释:
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
的输入。使用例子是这样的:
const App = Store({
state: { msg: "World" },
render: ({ msg }) => Hello {msg}
});
App.extend(({ state }) =>
state.msg === "World" ? { msg: "ReactConf" } : state
).extract(); // Hello ReactConf
然而尴尬的是,笔者看了很久也没看懂 Store
函数,最后运行了一下发现这个 Demo 抛出了异常 。
下面是笔者稍微修改后的例子,至少能跑起来:
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 是一个开源的动画、游戏制作软件。
wick editor 是一个动画制作工具,但拓展了一些 js 编程能力,因此可以很好的将动画与游戏结合在一起:
到 Pre-Alpha(4 月)
Beta(1.5 年)
这个团队最棒的地方是,将游戏与教育结合,针对不同场景做了很多用户调研并根据反馈持续改进。
React Select
react-select 的作者 Jed Watson 被请来啦。作为一个看上去很简单组件(select)的开发者,却拥有如此大的关注量(1.8w star),那作者有着怎样的心路历程呢?
react-select 看似简单的名字背后其实有挺多的功能,比如作者列举了一些功能层面的内容:
- autocomplete - 输入时搜索。
- 单、多选。
- focus 管理。
- 下拉框层级与位置,比如可以放在根 DOM 节点,也可以作为当前节点的子元素。
- 异步下拉框内容。
- 键盘、触控。
- Createble,即在搜索时如果没有内容可以动态创建。
- 等等。
这样就可以组合为一个受控/非受控的综合 Select 组件: 同理对异步的封装也可以放在 可以看到, 后面还有一些风格化、开源协作的思考,这里就不展开了,对这部分感兴趣的同学可以查看原视频了解更多。 usaspending.gov 这个网站使用 React 建设,可以查看美国政府支持财政的明细,通过流畅的体验让更多用户可以了解国家财政支出,进一步推动财政支出的透明化。由于并不涉及前端技术的介绍,主要是产品介绍,因此精读就不详细展开了。 顺便说一句,智能分析数据就用 QuickBI,QuickBI 是我们团队研发的一款智能 BI 服务平台,如果你将美国政府的财政支持作为数据集输入,你会分析得更透彻。 最后介绍的是使用 React 制作的星舰模拟器,看上去像一个游戏: 如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。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
import BaseSelect from "./Select";
import manageState from "./manageState";
export default manageState(Select);
makeAsync
函数中: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
是一个完全受控的数据驱动的 UI,无论是 manageState
还是 makeAsync
都是对数据处理的拓展,所以这三者之间才可以融洽的组合:import BaseSelect from "./Select";
import manageState from "./manageState";
import makeAsync from "./async";
export default manageState(Select);
export const AsyncSelect = manageState(makeAsync(Select));
React + 政府财政透明项目
React + 星舰模拟器
讨论地址是: 精读《React Conf 2019 - Day2》 · Issue #217 · dt-fe/weekly
关注
前端精读微信公众号