精读《React PowerPlug 源码》
1. 引言
React PowerPlug 是利用 render props 进行更好状态管理的工具库。
React 项目中,一般一个文件就是一个类,状态最细粒度就是文件的粒度。然而文件粒度并非状态管理最合适的粒度,所以有了 Redux 之类的全局状态库。
同样,文件粒度也并非状态管理的最细粒度,更细的粒度或许更合适,因此有了 React PowerPlug。
比如你会在项目中看到这种眼花缭乱的 state:
class App extends React.PureComponent {
state = {
name = 1
isLoading = false
isFetchUser = false
data = {}
disableInput = false
validate = false
monacoInputValue = ''
value = ''
}
render () { /**/ }
}
其实真正 App 级别的状态并没有那么多,很多 诸如受控组件 onChange 临时保存的无意义 Value 找不到合适的地方存储。
这时候可以用 Value 管理局部状态:
<Value initial="React">
{({ value, set, reset }) => (
<>
<Select
label="Choose one"
options={["React", "Preact", "Vue"]}
value={value}
onChange={set}
/>
<Button onClick={reset}>Reset to initial</Button>
</>
)}
</Value>
可以看到,这个问题本质上应该拆成新的 React 类解决,但这也许会导致项目结构更混乱,因此 RenderProps 还是必不可少的。
今天我们就来解读一下 React PowerPlug 的源码。
2. 精读
2.1. Value
这是一个值操作的工具,功能与 Hooks 中 useState 类似,不过多了一个 reset 功能(Hooks 其实也未尝不能有,但 Hooks 确实没有 Reset)。
用法
<Value initial="React">
{({ value, set, reset }) => (
<>
<Select
label="Choose one"
options={["React", "Preact", "Vue"]}
value={value}
onChange={set}
/>
<Button onClick={reset}>Reset to initial</Button>
</>
)}
</Value>
源码
- 源码地址
- 原料:无
State 只存储一个属性 value,并赋初始值为 initial:
export default {
state = {
value: this.props.initial
};
}
方法有 set reset。
set 回调函数触发后调用 setState 更新 value。
reset 就是调用 set 并传入 this.props.initial 即可。
2.2. Toggle
Toggle 是最直接利用 Value 即可实现的功能,因此放在 Value 之后说。Toggle 值是 boolean 类型,特别适合配合 Switch 等组件。
既然 Toggle 功能弱于 Value,为什么不用 Value 替代 Toggle 呢?这是个好问题,如果你不担心自己代码可读性的话,的确可以永远不用 Toggle。
用法
<Toggle initial={false}>
{({ on, toggle }) => <Checkbox onClick={toggle} checked={on} />}
</Toggle>
源码
- 源码地址
- 原料:Value
核心就是利用 Value 组件,value 重命名为 on,增加了 toggle 方法,继承 set reset 方法:
export default {
toggle: () => set(on => !on);
}
理所因当,将 value 值限定在 boolean 范围内。
2.3. Counter
与 Toggle 类似,这也是继承了 Value 就可以实现的功能,计数器。
用法
<Counter initial={0}>
{({ count, inc, dec }) => (
<CartItem
productName="Lorem ipsum"
unitPrice={19.9}
count={count}
onAdd={inc}
onRemove={dec}
/>
)}
</Counter>
源码
- 源码地址
- 原料:Value
依然利用 Value 组件,value 重命名为 count,增加了 inc dec incBy decBy 方法,继承 set reset 方法。
与 Toggle 类似,Counter 将 value 限定在了数字,那么比如 inc 就会这么实现:
export default {
inc: () => set(value => value + 1);
}
这里用到了 Value 组件 set 函数的多态用法。一般 set 的参数是一个值,但也可以是一个函数,回调是当前的值,这里返回一个 +1 的新值。
2.4. List
操作数组。
用法
<List initial={['#react', '#babel']}>
{({ list, pull, push }) => (
<div>
<FormInput onSubmit={push} />
{list.map({ tag }) => (
<Tag onRemove={() => pull(value => value === tag)}>
{tag}
</Tag>
)}
</div>
)}
</List>
源码
- 源码地址
- 原料:Value
依然利用 Value 组件,value 重命名为 list,增加了 first last push pull sort 方法,继承 set reset 方法。
export default {
list: value,
first: () => value[0],
last: () => value[Math.max(0, value.length - 1)],
set: list => set(list),
push: (...values) => set(list => [...list, ...values]),
pull: predicate => set(list => list.filter(complement(predicate))),
sort: compareFn => set(list => [...list].sort(compareFn)),
reset
};
为了利用 React Immutable 更新的特性,因此将 sort 函数由 Mutable 修正为 Immutable,push pull 同理。
2.5. Set
存储数组对象,可以添加和删除元素。类似 ES6 Set。和 List 相比少了许多功能函数,因此只承担添加、删除元素的简单功能。
用法
需要注意的是,initial 是数组,而不是 Set 对象。
<Set initial={["react", "babel"]}>
{({ values, remove, add }) => (
<TagManager>
<FormInput onSubmit={add} />
{values.map(tag => (
<Tag onRemove={() => remove(tag)}>{tag}</Tag>
))}
</TagManager>
)}
</Set>
源码
- 源码地址
- 原料:Value
依然利用 Value 组件,value 重命名为 values 且初始值为 [],增加了 add remove clear has 方法,保留 reset 方法。
实现依然很简单,add remove clear 都利用 Value 提供的 set 进行赋值,只要实现几个操作数组方法即可:
const unique = arr => arr.filter((d, i) => arr.indexOf(d) === i);
const hasItem = (arr, item) => arr.indexOf(item) !== -1;
const removeItem = (arr, item) =>
hasItem(arr, item) ? arr.filter(d => d !== item) : arr;
const addUnique = (arr, item) => (hasItem(arr, item) ? arr : [...arr, item]);
has 方法则直接复用 hasItem。核心还是利用 Value 的 set 函数一招通吃,将操作目标锁定为数组类型罢了。
2.6. map
Map 的实现与 Set 很像,类似 ES6 的 Map。
用法
与 Set 不同,Map 允许设置 Key 名。需要注意的是,initial 是对象,而不是 Map 对象。
<Map initial={{ sounds: true, music: true, graphics: "medium" }}>
{({ set, get }) => (
<Tings>
<ToggleCheck checked={get("sounds")} onChange={c => set("sounds", c)}>
Game Sounds
</ToggleCheck>
<ToggleCheck checked={get("music")} onChange={c => set("music", c)}>
Bg Music
</ToggleCheck>
<Select
label="Graphics"
options={["low", "medium", "high"]}
selected={get("graphics")}
onSelect={value => set("graphics", value)}
/>
</Tings>
)}
</Map>
源码
- 源码地址
- 原料:Value
依然利用 Value 组件,value 重命名为 values 且初始值为 {},增加了 set get clear has delete 方法,保留 reset 方法。
由于使用对象存储数据结构,操作起来比数组方便太多,已经不需要再解释了。
值得吐槽的是,作者使用了 != 判断 has:
export default {
has: key => values[key] != null;
}
这种代码并不值得提倡,首先是不应该使用二元运算符,其次比较推荐写成 values[key] !== undefined,毕竟 set('null', null) 也应该算有值。
2.7. state
State 纯粹为了替代 React setState 概念,其本质就是换了名字的 Value 组件。
用法
值得注意的是,setState 支持函数和值作为参数,是 Value 组件本身支持的,State 组件额外适配了 setState 的另一个特性:合并对象。
<State initial={{ loading: false, data: null }}>
{({ state, setState }) => {
const onStart = data => setState({ loading: true });
const onFinish = data => setState({ data, loading: false });
return (
<DataReceiver data={state.data} onStart={onStart} onFinish={onFinish} />
);
}}
</State>
- 源码地址
- 原料:Value
依然利用 Value 组件,value 重命名为 state 且初始值为 {},增加了 setState 方法,保留 reset 方法。
setState 实现了合并对象的功能,也就是传入一个对象,并不会覆盖原始值,而是与原始值做 Merge:
export default {
setState: (updater, cb) =>
set(
prev => ({
...prev,
...(typeof updater === "function" ? updater(prev) : updater)
}),
cb
);
}
2.8. Active
这是一个内置鼠标交互监听的容器,监听了 onMouseUp 与 onMouseDown,并依此判断 active 状态。
用法
<Active>
{({ active, bind }) => (
<div {...bind}>
You are {active ? "clicking" : "not clicking"} this div.
</div>
)}
</Active>
源码
- 源码地址
- 原料:Value
依然利用 Value 组件,value 重命名为 active 且初始值为 false,增加了 bind 方法。
bind 方法也巧妙利用了 Value 提供的 set 更新状态:
export default {
bind: {
onMouseDown: () => set(true),
onMouseUp: () => set(false)
}
};
2.9. Focus
与 Active 类似,Focus 是当 focus 时才触发状态变化。
用法
<Focus>
{({ focused, bind }) => (
<div>
<input {...bind} placeholder="Focus me" />
<div>You are {focused ? "focusing" : "not focusing"} the input.</div>
</div>
)}
</Focus>
源码
- 源码地址
- 原料:Value
依然利用 Value 组件,value 重命名为 focused 且初始值为 false,增加了 bind 方法。
bind 方法与 Active 如出一辙,仅是监听时机变成了 onFocus 和 onBlur。
2.10. FocusManager
不知道出于什么考虑,FocusManager 的官方文档是空的,而且 Help wanted。。
正如名字描述的,这是一个 Focus 控制器,你可以直接调用 blur 来取消焦点。
用法
笔者给了一个例子,在 5 秒后自动失去焦点:
<FocusFocusManager>
{({ focused, blur, bind }) => (
<div>
<input
{...bind}
placeholder="Focus me"
onClick={() => {
setTimeout(() => {
blur();
}, 5000);
}}
/>
<div>You are {focused ? "focusing" : "not focusing"} the input.</div>
</div>
)}
</FocusFocusManager>
源码
- 源码地址
- 原料:Value
依然利用 Value 组件,value 重命名为 focused 且初始值为 false,增加了 bind blur 方法。
blur 方法直接调用 document.activeElement.blur() 来触发其 bind 监听的 onBlur 达到更新状态的效果。
By the way, 还监听了 onMouseDown 与 onMouseUp:
export default {
bind: {
tabIndex: -1,
onBlur: () => {
if (canBlur) {
set(false);
}
},
onFocus: () => set(true),
onMouseDown: () => (canBlur = false),
onMouseUp: () => (canBlur = true)
}
};
可能意图是防止在 mouseDown 时触发 blur,因为 focus 的时机一般是 mouseDown。
2.11. Hover
与 Focus 类似,只是触发时机为 Hover。
用法
<Hover>
{({ hovered, bind }) => (
<div {...bind}>
You are {hovered ? "hovering" : "not hovering"} this div.
</div>
)}
</Hover>
源码
- 源码地址
- 原料:Value
依然利用 Value 组件,value 重命名为 hovered 且初始值为 false,增加了 bind 方法。
bind 方法与 Active、Focus 如出一辙,仅是监听时机变成了 onMouseEnter 和 onMouseLeave。
2.12. Touch
与 Hover 类似,只是触发时机为 Hover。
用法
<Touch>
{({ touched, bind }) => (
<div {...bind}>
You are {touched ? "touching" : "not touching"} this div.
</div>
)}
</Touch>
源码
- 源码地址
- 原料:Value
依然利用 Value 组件,value 重命名为 touched 且初始值为 false,增加了 bind 方法。
bind 方法与 Active、Focus、Hover 如出一辙,仅是监听时机变成了 onTouchStart 和 onTouchEnd。
2.13. Field
与 Value 组件唯一的区别,就是
用法
这个用法和 Value 没区别:
<Field>
{({ value, set }) => (
<ControlledField value={value} onChange={e => set(e.target.value)} />
)}
</Field>
但是用 bind 更简单:
<Field initial="hello world">
{({ bind }) => <ControlledField {...bind} />}
</Field>
源码
- 源码地址
- 原料:Value
依然利用 Value 组件,value 保留不变,初始值为 '',增加了 bind 方法,保留 set reset 方法。
与 Value 的唯一区别是,支持了 bind 并封装 onChange 监听,与赋值受控属性 value。
export default {
bind: {
value,
onChange: event => {
if (isObject(event) && isObject(event.target)) {
set(event.target.value);
} else {
set(event);
}
}
}
};
2.14. Form
这是一个表单工具,有点类似 Antd 的 Form 组件。
用法
<Form initial={{ firstName: "", lastName: "" }}>
{({ field, values }) => (
<form
onSubmit={e => {
e.preventDefault();
console.log("Form Submission Data:", values);
}}
>
<input
type="text"
placeholder="Your First Name"
{...field("firstName").bind}
/>
<input
type="text"
placeholder="Your Last Name"
{...field("lastName").bind}
/>
<input type="submit" value="All Done!" />
</form>
)}
</Form>
源码
- 源码地址
- 原料:Value
依然利用 Value 组件,value 重命名为 values 且初始值为 {},增加了 setValues field 方法,保留 reset 方法。
表单最重要的就是 field 函数,为表单的每一个控件做绑定,同时设置一个表单唯一 key:
export default {
field: id => {
const value = values[id];
const setValue = updater =>
typeof updater === "function"
? set(prev => ({ ...prev, [id]: updater(prev[id]) }))
: set({ ...values, [id]: updater });
return {
value,
set: setValue,
bind: {
value,
onChange: event => {
if (isObject(event) && isObject(event.target)) {
setValue(event.target.value);
} else {
setValue(event);
}
}
}
};
}
};
可以看到,为表单的每一项绑定的内容与 Field 组件一样,只是 Form 组件的行为是批量的。
2.15. Interval
Interval 比较有意思,将定时器以 JSX 方式提供出来,并且提供了 stop resume 方法。
用法
<Interval delay={1000}>
{({ start, stop }) => (
<>
<div>The time is now {new Date().toLocaleTimeString()}</div>
<button onClick={() => stop()}>Stop interval</button>
<button onClick={() => start()}>Start interval</button>
</>
)}
</Interval>
源码
- 源码地址
- 原料:无
提供了 start stop toggle 方法。
实现方式是,在组件内部维护一个 Interval 定时器,实现了组件更新、销毁时的计时器更新、销毁操作,可以认为这种定时器的生命周期绑定了 React 组件的生命周期,不用担心销毁和更新的问题。
具体逻辑就不列举了,利用 setInterval clearInterval 函数基本上就可以了。
2.16. Compose
Compose 也是个有趣的组件,可以将上面提到的任意多个组件组合使用。
用法
<Compose components={[Counter, Toggle]}>
{(counter, toggle) => (
<ProductCard
{...productInfo}
favorite={toggle.on}
onFavorite={toggle.toggle}
count={counter.count}
onAdd={counter.inc}
onRemove={counter.dec}
/>
)}
</Compose>
源码
- 源码地址
- 原料:无
通过递归渲染出嵌套结构,并将每一层结构输出的值存储到 propsList 中,最后一起传递给组件。这也是为什么每个函数 value 一般都要重命名的原因。
在 精读《Epitath 源码 - renderProps 新用法》 文章中,笔者就介绍了利用 generator 解决高阶组件嵌套的问题。
在 精读《React Hooks》 文章中,介绍了 React Hooks 已经实现了这个特性。
所以当你了解了这三种 "compose" 方法后,就可以在合适的场景使用合适的 compose 方式简化代码。
3. 总结
看完了源码分析,不知道你是更感兴趣使用这个库呢,还是已经跃跃欲试开始造轮子了呢?不论如何,这个库的思想在日常的业务开发中都应该大量实践。
另外 Hooks 版的 PowerPlug 已经 4 个月没有更新了(非官方):react-powerhooks,也许下一个维护者/贡献者 就是你。
如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
精读《React PowerPlug 源码》的更多相关文章
- 精读《V8 引擎 Lazy Parsing》
1. 引言 本周精读的文章是 V8 引擎 Lazy Parsing,看看 V8 引擎为了优化性能,做了怎样的尝试吧! 这篇文章介绍的优化技术叫 preparser,是通过跳过不必要函数编译的方式优化性 ...
- 深入浏览器工作原理和JS引擎(V8引擎为例)
浏览器工作原理和JS引擎 1.浏览器工作原理 在浏览器中输入查找内容,浏览器是怎样将页面加载出来的?以及JavaScript代码在浏览器中是如何被执行的? 大概流程可观察以下图: 首先,用户在浏览器搜 ...
- [翻译] V8引擎的解析
原文:Parsing in V8 explained 本文档介绍了 V8 引擎是如何解析 JavaScript 源代码的,以及我们将改进它的计划. 动机 我们有个解析器和一个更快的预解析器(~2x), ...
- 一文搞懂V8引擎的垃圾回收
引言 作为目前最流行的JavaScript引擎,V8引擎从出现的那一刻起便广泛受到人们的关注,我们知道,JavaScript可以高效地运行在浏览器和Nodejs这两大宿主环境中,也是因为背后有强大的V ...
- Chrome V8引擎系列随笔 (1):Math.Random()函数概览
先让大家来看一幅图,这幅图是V8引擎4.7版本和4.9版本Math.Random()函数的值的分布图,我可以这么理解 .从下图中,也许你会认为这是个二维码?其实这幅图告诉我们一个道理,第二张图的点的分 ...
- (译)V8引擎介绍
V8是什么? V8是谷歌在德国研发中心开发的一个JavaScript引擎.开源并且用C++实现.可以用于运行于客户端和服务端的Javascript程序. V8设计的初衷是为了提高浏览器上JavaScr ...
- 浅谈Chrome V8引擎中的垃圾回收机制
垃圾回收器 JavaScript的垃圾回收器 JavaScript使用垃圾回收机制来自动管理内存.垃圾回收是一把双刃剑,其好处是可以大幅简化程序的内存管理代码,降低程序员的负担,减少因 长时间运转而带 ...
- V8引擎嵌入指南
如果已读过V8编程入门那你已经熟悉了如句柄(handle).作用域(scope)和上下文(context)之类的关键概念,以及如何将V8引擎作为一个独立的虚拟机来使用.本文将进一步讨论这些概念,并介绍 ...
- 浅谈V8引擎中的垃圾回收机制
最近在看<深入浅出nodejs>关于V8垃圾回收机制的章节,转自:http://blog.segmentfault.com/skyinlayer/1190000000440270 这篇文章 ...
- 深入出不来nodejs源码-V8引擎初探
原本打算是把node源码看得差不多了再去深入V8的,但是这两者基本上没办法分开讲. 与express是基于node的封装不同,node是基于V8的一个应用,源码内容已经渗透到V8层面,因此这章简述一下 ...
随机推荐
- vty密码登录,到AAA验证登录,以及远程配置网络
华为的的最简易的远程登录方式,就是密码登录了. 配置命令如下图: 最重要的是权限: 访问级(0级).监控级(1级).系统级(2级)和管理级(3级) 在以上基础上,做了一个远程配置方式,通过一台,修改其 ...
- python利用xlrd读取excel文件始终报错原因
1.代码按照网上百度的格式进行书写如下: 但运行后,始终报错如下: 百度了xlrd网页: 分明支持xls和xlsx两种格式的文件,但运行始终报错. 最后找到原因是因为我所读取的文件虽然是以.xls命名 ...
- 登录RabbitMQ的方法
一:(运行RabbitMQ之前需要先打开docker 容器)打开相应的路径,在windows Powershell 管理员下打开 输入:docker-compose -f .\docker-compo ...
- 锐捷交换机配置DHCP SERVER给固定的MAC地址分配静态IP
今天突发奇想,想给自己的手机分配固定地址,使得接入公司无线网络时每次都取到同一ip地址,这样可以排除认证登录问题. 上网溜达一下,记录下锐捷官方的[常见问题]如下,经验证可行. 需求: 给MAC地址为 ...
- NOIP2017感悟
Day1 第一次会做的题这么多却比以前靠的更差. 其实停课期间水平还是提升了很多,但是做题速度和心态问题一就是我最难克服的一个地方. 题目很简单,但是又很恶心,主要是我代码能力太差,第二题调不出来,第 ...
- K8s 入门
中文文档:https://www.kubernetes.org.cn/kubernetes%E8%AE%BE%E8%AE%A1%E6%9E%B6%E6%9E%84 小结大白话 Portainer 挺好 ...
- python scrapy框架爬取豆瓣
刚刚学了一下,还不是很明白.随手记录. 在piplines.py文件中 将爬到的数据 放到json中 class DoubanmoviePipelin2json(object):#打开文件 open_ ...
- java实现多线程使用多个代理ip的方式爬取网页页面内容
项目的目录结构 核心源码: package cn.edu.zyt.spider; import java.io.BufferedInputStream; import java.io.FileInpu ...
- ps入门学习
快捷键 打开 ctrl+O 切换显示窗口 ctrl+tab 隐藏工具栏和面板 tab 只隐藏面板不隐藏工具栏 shift+tab 切换屏幕模式 F 文件的新建与格式 1.新建文档Ctrl+N,存 ...
- webapi 知识点
在web api 中后台的方法必须 加入 [HttpGet] ,[HttpPost],[HttpPut],[HttpDelete] 来区分,这是一种习惯. ps: get 方式参数都存在http协议头 ...