My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

Mobx 运行机制深入研究

发表于 2024-03-03

追踪原理

官方文档

MobX 会对在执行 跟踪函数 期间 读取的任何现有的可观察属性做出反应

“读取” 是对象属性的间接引用,可以用过 . (例如 user.name) 或者 [] (例如 user[‘name’]) 的形式完成。

“追踪函数” 是 computed 表达式、observer 组件的 render() 方法和 when、reaction 和 autorun 的第一个入参函数。

“过程(during)” 意味着只追踪那些在函数执行时被读取的 observable 。这些值是否由追踪函数直接或间接使用并不重要。

换句话说,MobX 不会对其作出反应:

从 observable 获取的值,但是在追踪函数之外

在异步调用的代码块中读取的 observable

Mobx 5 以下 MobX 不会追踪还不存在的索引或者对象属性(当使用 observable 映射(map)时除外)。

所以建议总是使用 .length 来检查保护基于数组索引的访问。

所有数组的索引分配都可以检测到,但前提条件必须是提供的索引小于数组长度。

核心概念

追踪属性访问,而不是值

1
2
3
4
5
6
7
8
9
let message = observable({
title: "Foo",
author: {
name: "Michel"
},
likes: [
"John", "Sara"
]
})

mobx会追踪箭头有没有变化

如果箭头发生变化,就会执行追踪函数

使用注意

处理数据时

1.更改没有被obserable的箭头,追踪函数不执行

2.追踪函数里使用间接引用指向obserable属性,追踪函数不执行

3.对新增的属性,可以使用set,get实现obserable

4.在异步代码中访问的obserable属性,不会引起追踪函数执行

1.更改没有被obserable的箭头,追踪函数不执行
autorun(() => {
    console.log(message.title)
})
message = observable({ title: "Bar" }) //指向message的箭头没有被obervable
autorun(() => {
    message.likes;//箭头没变,又没有访问数组里面的属性
})
message.likes.push("Jennifer");

2.追踪函数里使用间接引用指向obserable属性,追踪函数不执行
var title = message.title;
autorun(() => {
    console.log(title) //访问箭头没有变,还是指向老值的位置
})
message.title = "Bar" //箭头改了,但autorun里没有用到
const author = message.author;
autorun(() => {
    console.log(author.name) 
})
message.author.name = "Sara";//会执行跟踪函数,autorun里有访问name属性,这里指向name值得箭头改了
message.author = { name: "John" };//不会执行,没有访问author属性的箭头

正确使用

A:
autorun(() => {
    console.log(message.author.name)
})
message.author.name = "Sara";
message.author = { name: "John" };
B:
function upperCaseAuthorName(author) {
    const baseName = author.name;
    return baseName.toUpperCase();
}
autorun(() => {
    console.log(upperCaseAuthorName(message.author))
})
message.author.name = "Chesterton"

3.异步
const message = observable({ title: "hello" })
autorun(() => {
    console.log(message) //会执行两次,因为console.log是异步的,请确保始终传递不变数据 ( immutable data ) 或防御副本给 console.log。
})
message.title = "Hello world"
autorun(() => {
    setTimeout(
        () => console.log(message.likes.join(", ")), //异步执行,访问原始数据打印一次
        10
    )
})
message.likes.push("Jennifer");//不会引起autorun执行

4.MobX 5 可以追踪还不存在的属性
autorun(() => {
    console.log(message.postDate)
})
message.postDate = new Date()

组件使用时

子组件问题

MobX 只会为数据是直接通过 render 存取的 observer 组件进行数据追踪

所以当需要将数据传递给子组件时,要保证子组件也是一个obserable组件,可以做出反应

解决办法:

1.将子组件使用obserable函数处理

它用 mobx.autorun 包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件

2.使用mobx-react的Obserable组件包裹子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
方法一:将子组件使用obserable函数处理
const MyComponent = observer(({ message }) =>
<SomeContainer
title = {() => <TitleRenderer message={message} />}
/>
)
const TitleRenderer = observer(({ message }) =>
<div>{message.title}</div>}
)
message.title = "Bar"
方法二:使用mobx-react的Obserable组件包裹子组件
const MyComponent = ({ message }) =>
<SomeContainer
title = {() =>
<Observer>
{() => <div>{message.title}</div>}
</Observer>
}
/>
message.title = "Bar"

避免在本地字段中缓存 observable

1
2
3
4
5
6
7
8
9
10
@observer class MyComponent extends React.component {
author;
constructor(props) {
super(props)
this.author = props.message.author;//message.author发生变化时不会引起render
}
render() {
return <div>{this.author.name}</div> //.name可以引起render
}
}

优化,使用计算属性,或者在render函数中进行间接引用

@observer class MyComponent extends React.component {
    @computed get author() {
        return this.props.message.author
    }

其他

1.从性能上考虑,越晚进行间接引用越好

2.数组里面的是对象而不是字符串,那么对于发生在某个具体的对象中发生的变化,渲染数组的父组件将不会重新渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Message = observer(({ message }) =>
<div>
{message.title}
<Author author={ message.author } />
<Likes likes={ message.likes } />
</div>
)
const Author = observer(({ author }) =>
<span>{author.name}</span>
)
const Likes = observer(({ likes }) =>
<ul>
{likes.map(like =>
<li>{like}</li>
)}
</ul>
)
变化 重新渲染组件
message.title = “Bar” Message
message.author.name = “Susan” Author (.author 在 Message 中进行间接引用, 但没有改变)*
message.author = { name: “Susan”} Message, Author
message.likes[0] = “Michel” Likes

一些 对比

autorun vs compute

当使用 autorun 时,所提供的函数总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发

computed(function) 创建的函数只有当它有自己的观察者时才会重新计算,否则它的值会被认为是不相关的

如果一个计算值不再被观察了,例如使用它的UI不复存在了,MobX 可以自动地将其垃圾回收。

而 autorun 中的值必须要手动清理才行

autorun vs reaction

reaction(() => data, (data, reaction) => { sideEffect }, options?)

它接收两个函数参数,第一个(数据函数)是用来追踪并返回数据作为第二个函数(效果函数)的输入。

传入 reaction 的第二个函数(副作用函数)当调用时会接收两个参数。

第一个参数是由 data 函数返回的值。

第二个参数是当前的 reaction,可以用来在执行期间清理 reaction

reaction 返回一个清理函数。

不同于 autorun 的是当创建时 **效果 **函数不会直接运行,只有在数据表达式首次返回一个新值后才会运行。

在执行 效果函数时访问的任何 observable 都不会被追踪。

效果函数仅对数据函数中访问的数据作出反应,这可能会比实际在效果函数使用的数据要少。

此外,效果 函数只会在表达式返回的数据发生更改时触发。 换句话说: reaction需要你生产 效果函数中

所需要的东西。

useObserver vs Observer vs observer

相关文档

1.虽然只是在返回DOM的地方使用 useObserver(), 但是,当dom中数据改变的时候,整个component都会重新render

1
2
3
4
5
6
7
8
9
10
function Person() {
console.log('in useObserver');//点击按钮会触发执行
const person = useLocalStore(() => ({ name: 'John' }));
return useObserver(() => (
<div>
{person.name}
<button onClick={() => (person.name = 'Mike')}>No! I am Mike</button>
</div>
));
}

2.Observer 标签组件可以更精准的控制想要重新渲染的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function ObservePerson() {
console.log('in Observer');//点击按钮不会执行
const person = useLocalStore(() => ({name: 'John'}))
return (
<div>
The old name is: {person.name} //点击按钮不会更新
<div>
<Observer>{() => <div>{person.name}</div>}</Observer> //点击按钮会更新
<button onClick={() => (person.name = 'Mike')}>
I want to be Mike
</button>
</div>
</div>
)
}

3.与useObserver相比,除了使用方法不同,目前不知道区别在哪,有时间需要探究一下

const ObserverLowercasePerson: React.FC<any> = observer(() => {
    console.log('in Observer') //点击按钮也会执行
    const person = useLocalStore(() => ({name: 'John'}));
    return (
        <div>
            <div>The name is: {person.name}</div>
            <button onClick={() => (person.name = 'Mike')}>
                Change name
            </button>
        </div>
    )
})
```
My Little World

趋势图卡片实现原理

发表于 2024-03-02

背景

春节看板项目中有对数据进行趋势分析的展示,其中一种卡片的展示形式经过思考后可以提炼成组件向外提供服务,于是进行封装上传Semi物料市场

设计

  1. 既要支持一个卡片的展现,也要支持多个卡片的展示,所以数据源应该是一个数组 list
  2. 每个卡片的大小应该是一样的,所以应该给一个统一设置卡片大小的属性 size
  3. 卡片可以静态展示,也可以有响应事件,这里先支持一个点击事件 onClick
  4. 单个卡片的功能需要展示趋势折线图,标题,提示,数量,还要支持定制颜色

所以单个卡片的数据结构应该是这样的

name 标题名 string 或者 ReactNode
tip 提示(可选) string 或者 ReactNode
tipNormalShow 提示图标展示方式,默认值false,鼠标滑过才展示 boolean FALSE
hoverLayer 鼠标hover是否展示蒙层效果,默认false,不展示 boolean FALSE
lineColor 折线的颜色,涉及渐变色计算,配置成十六进制格式 string #E91E63 或者 #00B3A1根据卡片位置奇偶情况切换默认颜色
value 标题下的数据 string 或者 ReactNode
xData 折线图x轴数据 Array[string或者number]
yData 折线图y轴数据 Array[string或者number]
loading 数据加载状态开启,默认false,不开启 boolean
noDataTip 没有数据时的提示 string 或者 ReactNode 抱歉,没有数据可展示
errorInfo 错误展示 {text: ‘xxx’, color: ‘xxxx’}

实现

将List 传进来的数据,循环成多个卡片,将单个卡片信息,onClick 和size都传递给卡片组件

卡片根据传递进来属性的不同状态,展示相关信息

另外折线图依靠echart来画,所以需要根据颜色和卡片位置生产曲线配置,这里依靠getChartOption

计算渐变颜色同转换成rgb格式,设置透明度来实现渐变

体验

在线体验地址:https://semi.bytedance.net/material/zh-CN/playground/219

My Little World

拖拽渲染问题的深入研究

发表于 2024-03-01

背景

使用拖拽组件进行拖拽排序
1.原展示模块内容需要进行缩略展示,具备收缩展开的能力
2.排序的内容复杂,需要异步获取数据,循环的时候传进去的关键值(如id)作为参数拉取数据,渲染图表

方案一

保留原组件渲染逻辑,同时将数据源传入排序组件(排序组件显示标题类信息代表原模块),
然后根据是否进入排序状态,保留二者其一,就是排序时展示排序组件,非排序时展示模块内容
问题

从排序状态回到正常展示状态时,因为正常展示的组件DOM在进入排序状态时被销毁
这时再回来,相当于从无到有要重新创建,会引起数据重新请求

方案二

将原展示模块组件作为排序组件项进行渲染,在进入排序状态时将展示模块高度减小,仅保留标题部分充当缩略信息展示
因为展示模块DOM始终存在,所以可以解决掉方案一展示模块DOM消失再创建的数据拉取问题
原理

DOM的新建跟更新流程不同,在这种情况下,新建过程会需要去请求接口拉数据,而如果仅仅是更新的话,可以依赖react的key的关键作用减少DOM 的重建过程,只是进行调换顺序即可
这里在将数据源列表渲染出来的时候,将数据的特征值赋值给key,即排序前后,展示模块key不变就不会被重新新建渲染, 只是进行排序处理
解决方案一产生的问题

在进入排序状态时,将展示模块组件高度设置为0,overflow:hidden,就看不到展示组件,但DOM 依然存在
这时再使用排序组件展示缩略信息即可

小结

无论哪种方案,在结束排序后,都要更新数据源,但数据源里面的对象不能变,因为展示模块会依赖其中的具体对象里的信息进行数据拉取
即

My Little World

学习关于产品的一些思维

发表于 2022-09-03

产品经理决策力工具

象限法:
把想做的事情拆成两个指标去做
让这个两个指标做xy轴
在xy轴包围的空间内,分成四个象限
把要做的事情按照xy轴的值分布在四个象限中
然后决定要在四个象限中寻找最优解

假设思维: 把未来要做的事情一步一步的假设出来
用户思维: 用使用者的思维设计功能

产品路线图roadmap4个核心要素

  1. 里程碑是要有意义的
  2. 跟各个方向的工作协同进行
  3. 可能不是串行的而是并行的,需要准备多种方案
  4. 基于产品框架

五张图说明产品

  1. 核心功能体验图,主要功能的流程图
  2. 模块图,将功能具体的实现划分不同模块,即可以概览具备的的功能,也方便进行任务分配
  3. 功能树,一个模块具体具备的功能内容,相当于再细分
  4. 页面关系图,页面的操作流程,可以跟功能树对比查看是否有功能遗漏,上面提到的模块功能树都会最终落到页面上
  5. 交互设计图,不是最重要的,但要有自解释性,每个人都能看懂

用户留存率—-> 指标之王
算法

  1. 新增留存率 所有新用户中有多少比例下个时间周期会出现
  2. 活跃留存率 所有用户中(活跃用户),包括新用户,有多少比例会在下个时间周期出现,即有多少人会成为下个周期的活跃用户
    统计分析
    用户活跃度
    cohort, 横纵都是第1-n周
    每一行代表当前周用户留存率再往后几周的留存率请款
    每一列代表当前周中,阁用户留存率情况,可以看到每周留存率在这一周的变化情况
    对角线从左上到右下,上面数据表示次周留存率,如果呈下降趋势,说明产品在新客中粘性在下降,留存率整体在下降
    将对角线数据处理形成折线图可直观看到用户留存趋势

RFM,用户贡献值(下了多少单,总消费金额…),根据用户贡献值可采取不同的营销策略

DAU,WAU,MAU,日、周、月活跃用户,一般让DAY/MAU的值作为一个用户粘性的指标
以DAY/MAU为y轴,DAU为x轴,形成折线趋势图,让趋势保持稳定上升是一个产品的发展方向

如何提升留存?
不要去想现有总用户如何去留存,去观察哪些用户值得留存,想办法让这些用户实现留存提升

设计一套CRM系统
CRM系统: 维护公司与客户关系 ===> 用户运营战略执行系统

My Little World

redis 学习的一些笔记

发表于 2022-08-09

基础常识
磁盘
寻址:ms
带宽: G/M
内存
寻址:ns(纳秒级)
带宽:byte/s
秒>毫秒>微秒>纳秒
磁盘寻址上比内存少了10w倍

I/O BUffer:成本问题
磁盘有磁道和扇区,一个扇区512byte
会造成索引成本增大
因此进行4K对齐,操作系统无论读多少都最少从磁盘里面拿4k出来

数据库的表很大,性能会下降吗?
如果表有索引,
那么对于增删改的操作肯定会变慢
查询速度
如果是1个或者少量查询依然很快
但如果是并发大的时候会受到硬盘带宽的影响,从而影响速度

数据在内存和磁盘中体积不一样

redis出现原因:
内存 ==> 贵
磁盘 ==> 慢
两个基础设施限制:
冯诺依曼体系的硬件制约 ===> 硬盘io带宽问题
以太网,tcp/ip的网络 ===> 不稳定

https://db-engines.com/en/

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

上述数据类型是指value的数据类型
memcache 和redis区别在于,memcache value没有类型
将返回value所有数据到client端,会受到server 网卡IO的限制,而且client要有解码的逻辑
redis因为value有类型,所以对于数据的请求根据不同情况直接调用相应类型方法返回少量数据即可
做到了计算向数据移动

redis是单进程,单线程,单实例的,通过epoll快速处理并发请求
epoll是同步非阻塞的多路复用机制
jvm: 一个线程成本是1MB
线程多了会增加调度成本,从而使CPU浪费,也会增加内存成本
BIO—>NIO(同步非阻塞)—>多路复用—>epoll(引入共享空间避免fd相关数据考来考去)

redis 对不同数据类型不同encoding类型的数值具有不同的方法
key有两个属性:
type标识值的类型
encoding标识值的编码类型
二者决定对值的操作可以使用哪些方法,从而加速计算

String 类型
字符串的相关操作:
set,get,append,setRange,getRange,strlen
适用场景:
使用内存存储的session,对象,小文件

数值的相关操作:
incr,decr
适用场景:
限流,计数
秒杀一般会用数据库

bitmap的相关操作:
setbit, bitcount,bitpos,bittop
使用场景:web, 离线数据
1.用户系统,统计用户登录天数,且窗口随机
key标识用户,每个用户准备365位,每一位表示当天是否登录,登录置一
setbit sean 1 1
setbit sean 7 1
setbit sean 364 1
STRLEN sean ===>46 每个用户只需要46个字节来存储这些信息
BITCOUNT sean -2 -1 计算多少天范围内一共登录了几天

  1. 电商做618活动,需要给活跃用户登录后送礼物,请问应该备货多少礼物
    假设这里活跃用户统计规则为20220901-20220903三天内登录的用户,三天内只要登录一次就算活跃用户
    key标识当天用户登录情况,将用户编号映射到二进制位的相应位置上,每一位二进制代表一位用户是否登录
    setbit 20220901 1 1
    setbit 20220902 1 1
    setbit 20220902 7 1
    bittop or destkey 20220901 20220902
    bitcount destkey 0 -1

  2. 存储oa权限信息

  3. 布隆过滤器,模块调用

List 类型相关操作
按不同放入顺序排列
栈:同向命令
队列:反向命令
数组
阻塞,单播队列
ltrim
使用场景:
数据共享,迁出
无状态

Hash类型
对filed进行数值计算
场景:
点赞,收藏,详情页
聚合场景

Set类型
无序去重集合
元素变多扩容,会触发rehash,造成原顺序颠倒不稳定
集合操作多
随机事件:
RANDMEMBER key count
正数:取出一个去重的结果集(不能超过已有集)
负数:取出一个带重复的结果集,一定满足你要的数量
如果是0,不返回
应用场景:
随机事件==>抽奖:
10个精品,参与人数> 10 时,传正数,得到10个随机不重复值
10个精品,参与人数< 10 时,传负数,得到10个可重复的值
中奖是否重复
Spop 不重复取出一个
推荐系统:
共同好友:交集
推荐好友:差集

Sorted set
排序
物理内存左小右大(根据分值从左到右从小到大)
不随命令发生变化
集合操作(并集,交集)权重、聚合指令
排序是怎么实现的。 ==> skip List 跳跃表
增删改查的速度
场景:
排行榜,
有序事件
评论+分页

redis持久化
持久化意味着性能会下降
两个指标
快照:rdb,恢复的速度快,但缺失的多
日志:aof, ,
满足完整性好,恢复速度变慢,===>采用不同日志策略避免
指令追加造成冗余量比较大 ===> 使用重写

redis 分布式集群
可用性:
单点故障可通过主从主备一变多集群构建镜像,需要同步
强一致性,会破坏使用性
弱一致性,可用性强,但同步一致性低(默认)
最终一致性,使用黑盒可靠集群做中间缓存,保证主从数据最终一致性
数据存储压力问题(装不下),采用分片式集群代理集群,也是一变多,但不需要同步

AkF拆分原则
根据业务划分数据到不同redis实例

My Little World

系统设计案例

发表于 2022-02-02

案例:用户将存储内容粘贴到站点,站点给用户返回一个短地址,用户通过短地址,可以访问之前粘贴的内容或者跳转之前的原始站点

功能

  1. 对于用户来说操作要简单,生成的短地址要简单,而且要唯一,不同用户即使内容一样也要生成唯一的短地址

  2. 时间有效性,从存储角度来说,不可能一直帮用户存储所有生成的短地址,不然存储会越来越大,所以通过设置短地址有效的访问时间,可以减少存储成本

技术

  1. 可用性(high Availability), 保障用户功能可用
  2. 低延时(low latency),用户拿到短地址或者通过短地址跳转其他网站时,重定向时间不宜过长
  3. 安全性(non guessable),不能被猜出来,用户在生成一定短地址时如果携带一些个人信息,不应体现在短地址中,否则会造成用户信息泄露
  4. 对于ins/微博/小红书之类的社交功能还要保障一致性,博主发的照片,follower看到的内容应该是一样的

容量负载能力假设

对一个用户来说,可以抽象出两个主要请求

  1. 请求生成短url,我们要把请求参数或者原始信息存储起来 inbound
  2. 请求访问url,把生成的url返回给用户使用,进行重定向 outbound

假设一个短地址按500byte大小存储

容量

假设一个月会有100万个新短地址生成,
那么5年会产生

100万5年12个月 ==> 约等于60亿个短地址

60亿*500byte ==> 3TB 会有需要3TB大小的容量存储

负载能力

假设一个月有100个用户,每个用户会进行100万次访问短地址进行重定向的操作

那么每秒钟会有
(100 100万)/(30D 24H * 3600s) ==> 约等于4000个短url 要给到用户
同时会有
4000/ 100 ==> 大概40个短url 需要被生成

那么服务所需要的带宽就可以计算出来

inbound : 40 500 byte 约等于 20kb/s
outbound: 4000
500 byte 约等于 20MB/s

API 数据库设计

api

假设会用到简单的增删
生成短url createURL(api-key, originUrl, expired-Date, userId)
删除url deleteURL(api-key,shortUrl)

DataBase

对于shortUrl

pk: hash
originUrl,
expired-date,
userId
…

对于user
pk: userId
name
…

生成shortUrl

假如计划生成一个6个字符的短url,使用base64的加密算法的话可以生成64 ^ 6 大约640个短url,

满足之前5年会产生60个亿的唯一性需求

整体逻辑

用户 —>request shortUrl generation —> app

app —> base64 encoding + 从key generation DB中拿一个nonUse 的key —> 得到shortUrl —>DB

DB —> app –>用户

其他

Cache

2-8原则

用存储的20% 的url做cache内容,可以满足80% 的访问需求

load balance

均衡流量

过期后的url处理,key的处理

分布式存储,分片

对于社交功能的newFeed的推送

pull / push / pull + push hybrid

My Little World

一些其他小问题

发表于 2021-11-29

1.
在 useEffect 中如果使用了某些变量,却没有在依赖项中指定,会发生什么呢?

依赖那里没有传任何参数的话,会每次render都执行。
依赖项有传值但是,有部分依赖没有传,那么没有传的那部分,数据即使变化也不会执行副作用。

对于这节课中显示的 Blog 文章的例子,我们在 useEffect 中使用了 setBlogContent 这样一个函数,本质上它也是一个局部变量,那么这个函数需要被作为依赖项吗?为什么?

函数应该是不会变化的,所以不需要监听。

2.
useState 其实也是能够在组件的多次渲染之间共享数据的,那么在 useRef 的计时器例子中,能否用 state 去保存 window.setInterval() 返回的 timer 呢?

可以,只是没有 useRef 更优,因为在更新 state 值后会导致重新渲染,而 ref 值发生变化时,是不会触发组件的重新渲染的,这也是 useRef 区别于 useState 的地方。

3.
componentWillUnmount 近似的实现:组件销毁和文章 id 变化时执行。那么在函数组件中如果要实现严格的 componentWillUnmount,也就是只在组件销毁时执行,应该如何实现?\

1
2
3
4
5
useEffect(() => {
return () => {
// 这里只会在组件销毁前(componentWillUnmount)执行一次
}
}, [])

4.
每次调用 useArticle 这个 Hook 的时候都会触发副作用去获取数据。
但是有时候,我们希望在有些组件自动获取,但有的组件中需要点击某个按钮才去获取数据,那么你会如何设计这个 Hook?(可能这道题有一点难度。)

useArticle Hook 可以提供一个参数,用来标记本地调用是否默认触发副作用去获取数据;对于点击按钮才触发请求的功能,可以在 Hook 中将获取数据的方法 retrn 出去,供外部自由调用。

  1. Hook 一般都是使用的 useState 保存了状态数据,也就意味着状态的范围限定在组件内部,组件销毁后,数据就没了。那么如果希望数据直接缓存到全局状态,应该如何做呢?

可以借助 redux,配合 useContext 等 api ,将状态数据存储至全局中。

My Little World

如何提升应用打开速度

发表于 2021-11-29

使用 import 语句,定义按需加载的起始模块

对于这个需求,ECMA Script 标准有一个提案,专门用于动态加载模块,语法是 import(someModule)。
注意,这里的 import 和我们一般用于引入模块的静态声明方式不同,比如 import something from ‘somemodule’ 。
但这里的 import 是作为一个函数动态运行的,这个 import() 函数会返回一个 Promise。
这样,在模块加载成功后,就可以在 Promise 的 then 回调函数中去使用这个模块了。

虽然这只是一个提案,并没有成为标准,但是 Webpack 等打包工具利用了这样的语法去定义代码的分包。

也就是说,Webpack 实现了这样的语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ProfilePage() {
// 定义一个 state 用于存放需要加载的组件
const [RealPage, setRealPage] = useState(null);

// 根据路径动态加载真正的组件实现
import('./RealProfilePage').then((comp) => {
setRealPage(Comp);
});
// 如果组件未加载则显示 Loading 状态
if (!RealPage) return 'Loading....';

// 组件加载成功后则将其渲染到界面
return <RealPage />
}

import() 这个语句完全是由 Webpack 进行处理的。

按需加载的实现原理:Webpack 利用了动态 import 语句,自动实现了整个应用的拆包。
而在实际开发中,其实并不需要关心 Webpack 是如何做到的,
而只需要考虑:该在哪个位置使用 import 语句去定义动态加载的拆分点。

总体需要采用的策略是:按业务模块为目标去做隔离,尽量在每个模块的起始页面去定义这个拆分点。

react-loadable,专门用于 React 组件的按需加载。

1
2
3
4
5
6
7
8
9
10
11
12
import Loadable from "react-loadable";


// 创建一个显示加载状态的组件
function Loading({ error }) {
return error ? 'Failed' : 'Loading';
}
// 创建加载器组件
const HelloLazyLoad = Loadable({
loader: () => import("./RealHelloLazyLoad"),
loading: Loading,
});

可以看到 Loadable 这个高阶组件主要就是两个 API。

loader:用于传入一个加载器回调,在组件渲染到页面时被执行。
在这个回调函数中,我们只需要直接使用 import 语句去加载需要的模块就可以了。

loading:表示用于显示加载状态的组件。在模块加载完成之前,加载器就会渲染这个组件。
如果模块加载失败,那么 react-loadable 会将 errors 属性传递给 Loading 组件,方便你根据错误状态来显示不同的信息给用户。

按需加载可以说是减少首屏加载时间最为有效的手段,它可以让用户在打开应用时,无需加载所有代码就能开始使用,从而提升用户体验。

使用 service worker 缓存前端资源

和浏览器自动的资源缓存机制相比,Service Worker 加上 Cache Storage 这个缓存机制,具有更高的准确性和可靠性。

因为它可以确保两点:
缓存永远不过期。你只要下载过一次,就永远不需要再重新下载,除非主动删除。
永远不会访问过期的资源。换句话说,如果发布了一个新版本,那么你可以通过版本化的一些机制,来确保用户访问到的一定是最新的资源。

My Little World

重新认识路由

发表于 2021-11-28

路由管理,就是让你的页面能够根据 URL 的变化进行页面的切换,这是前端应用中一个非常重要的机制

路由的核心逻辑就是根据 URL 路径这个状态,来决定在主内容区域显示什么组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 自定义路由
const MyRouter = ({ children }) => {
const routes = _.keyBy(
children.map((c) => c.props),
"path",
);
const [hash] = useHash();
const Page = routes[hash.replace("#", "")]?.component;
// 如果路由不存在就返回 Not found.
return Page ? <Page /> : "Not found.";
};


const Route = () => null;

// 应用
function SamplePages {
return (
<div className="sample-pages">
{/* 定义了侧边导航栏 */}
<div className="sider">
<a href="#page1">Page 1</a>
<a href="#page2">Page 2</a>
<a href="#page3">Page 3</a>
<a href="#page4">Page 4</a>
</div>
<div className="exp-15-page-container">
{/* 定义路由配置 */}
<MyRouter>
<Route path="page1" component={Page1} />
<Route path="page2" component={Page2} />
<Route path="page3" component={Page3} />
<Route path="page4" component={Page4} />
</MyRouter>
</div>
</>
);
};

使用React Router

简单应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 从 react-router-dom 引入路由框架提供的一些组件
import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
// 引入了两个课程示例页面
import Counter from "./01/Counter";
import UserList from "./01/UserList";


// 使用数组定义了页面组件和导航的标题,从而方便下面的渲染逻辑
const routes = [
["01 Counter", Counter],
["01 UserList", UserList],
];
function App() {
return (
<BrowserRouter>
<div className="app">
<ul className="sider">
{routes.map(([label]) => (
<li>
<Link to={`/${label.replace(" ", "/")}`}>{label}</Link>
</li>
))}
</ul>
<div id="pageContainer" className="page-container">
<Switch>
{routes.map(([label, Component]) => (
<Route key={label} path={`/${label.replace(" ", "/")}`}>
<Component />
</Route>
))}
{/* 定义一个默认的路由 */}
<Route path="/" exact>
<h1>Welcome!</h1>
</Route>
<Route path="*">Page not found.</Route>
</Switch>
</div>
</div>
</BrowserRouter>
);
}

相关组件作用

BrowserRouter:标识用标准的 URL 路径去管理路由,比如 /my-page1 这样的标准 URL 路径。
除此之外,还有 MemoryRouter,表示通过内存管理路由;
HashRouter,标识通过 hash 管理路由。
自己实现的例子其实就是用的 hash 来实现路由。

Link:定义一个导航链接,点击时可以无刷新地改变页面 URL,从而实现 React Router 控制的导航。

Route: 定义一条路由规则,可以指定匹配的路径、要渲染的内容等等。

Switch:在默认情况下,所有匹配的 Route 节点都会被展示,但是 Switch 标记可以保证只有第一个匹配到的路由才会被渲染。

react router

使用嵌套路由:实现二级导航页面

需要路由框架具备两个能力:

能够模糊匹配。比如 /page1/general 、/page1/profile 这样两个路由,需要都能匹配到 Page1 这样一个组件。
然后 Page1 内部呢,再根据 general 和 profile 这两个子路由决定展示哪个具体的页面。

Route 能够嵌套使用。在我们自定义 Route 的例子中,Route 组件仅用于收集路由定义的信息,不渲染任何内容。
如果需要路由能嵌套使用,那就意味着需要在 Route 下还能嵌套使用 Route。而这在 React Router 是提供支持的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

import { BrowserRouter, Route, Link } from "react-router-dom";


const Page1 = () => {
return (
<div className="exp-15-page1">
<div className="exp-15-page1-header">
<Link to="/page1/general">General</Link>
<Link to="/page1/profile">Profile</Link>
<Link to="/page1/settings">Settings</Link>
</div>
<div className="exp-15-page1-content">
<Route path="/page1/general">General Page</Route>
<Route path="/page1/profile">Profile Page</Route>
<Route path="/page1/settings">Settings Page</Route>
</div>
</div>
);
};
const Page2 = () => "Page 2";
const Page3 = () => "Page 3";


function NestedRouting() {
return (
<BrowserRouter>
<h1>Nested Routing</h1>
<div className="exp-15-nested-routing">
<div className="exp-15-sider">
<Link to="/page1">Page 1</Link>
<Link to="/page2">Page 2</Link>
<Link to="/page3">Page 3</Link>
</div>
<div className="exp-15-page-container">
<Route path="/page1"><Page1 /></Route>
<Route path="/page2"><Page2 /></Route>
<Route path="/page3"><Page3 /></Route>
</div>
</div>
</BrowserRouter>
);
}

在 URL 中保存页面状态(页面参数)

在url携带页面相关信息,将页面的一些状态存放到 URL 中,
一方面可以提升用户体验,另一方面也可以简化页面之间的交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

<Route path="/tabs-page/:activeTab" component={TabsPage} />

import { useCallback } from "react";
import { Tabs, Table } from "antd";
import { useHistory, useParams } from "react-router-dom";
import data from "../10/data";
import { useSearchParam } from "react-use";


const { TabPane } = Tabs;


export default () => {
// 通过 React Router 的 API 获取 activeTab 这个参数信息
const { activeTab = "users" } = useParams();
// 通过查询字符串获取当前的页码信息
const page = parseInt(useSearchParam("page"), 10) || 1;

// 通过 React Router 提供的 history 对象来操作 URL
const history = useHistory();
const handleTabChange = useCallback(
(tab) => history.push(`/15/TabsPage/${tab}`),
[history],
);
// 定义表格的翻页功能
const pagination = {
pageSize: 3,
current: page,
onChange: (p) => {
history.push(`/15/TabsPage/${activeTab}?page=${p}`);
},
};
return (
<div>
<h1>Tabs Page</h1>
<Tabs activeKey={activeTab} onChange={handleTabChange}>
<TabPane tab="Users" key="users">
<Table
dataSource={data}
columns={[
{ dataIndex: "name", title: "User Name" },
{ dataIndex: "city", title: "City" },
]}
pagination={pagination}
/>
</TabPane>
<TabPane tab="Jobs" key="jobs">
<Table
dataSource={data}
columns={[{ dataIndex: "job", title: "Job Title" }]}
pagination={pagination}
/>
</TabPane>
</Tabs>
</div>
);
}

注意

这里遵循了唯一数据源的原则,避免定义中间状态去存储 tab 和页码的信息,而是直接去操作 URL,这样可以让代码逻辑更加清晰和直观。

路由层面实现权限控制

利用前端路由的动态特性。路由是通过 JSX 以声明式的方式去定义的,这就意味着路由的定义规则是可以根据条件进行变化的,也就是所谓的动态路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65


import { useState } from "react";
import { Button } from "antd";
import { Route, Link } from "react-router-dom";


// 定义了两个示例页面组件
const Page1 = () => "Page 1";
const Page2 = () => "Page 2";


// 定义了一个组件用户展示未登录状态
const UnauthedPage = () => (
<span style={{ color: "red" }}>Unauthorized, please log in first.</span>
);
export default () => {
// 模拟用户是否登录的状态,通过一个按钮进行切换
const [loggedIn, setLoggedIn] = useState(false);

// 定义了两套路由,一套用于登录后,一套用于未登录状态
const routes = loggedIn
? [
{
path: "/15/RouterAuth",
component: Page1,
},
{
path: "/15/RouterAuth/page1",
component: Page1,
},
{
path: "/15/RouterAuth/page2",
component: Page2,
},
]
// 如果未登录,那么对于所有 /15/RouterAuth 开头的路径,显示未授权页面
: [{ path: "/15/RouterAuth", component: UnauthedPage }];


return (
<div>
<h1>Router Auth</h1>
<Button
type={loggedIn ? "primary" : ""}
onClick={() => setLoggedIn((v) => !v)}
>
{loggedIn ? "Log Out" : "Log In"}
</Button>


<div className="exp-15-router-auth">
<div className="exp-15-sider">
<Link to="/15/RouterAuth/page1">Page 1</Link>
<Link to="/15/RouterAuth/page2">Page 2</Link>
</div>
<div className="exp-15-page-container">
{/* */}
{routes.map((r) => (
<Route path={r.path} component={r.component} />
))}
</div>
</div>
</div>
);

代码中核心的机制就在于我们根据登录状态,创建了不同的路由规则,这样就能在源头上对权限进行集中控制,避免用户未经授权就访问某些受保护的页面。

同时呢,因为在相同的 URL 下进行了信息提示,那么也就更容易实现用户登录后还能返回原页面的功能。

My Little World

Hooks 在forms上的使用

发表于 2021-11-27

应用思想就是在这个 Hook 去维护整个表单的状态,并提供根据名字去取值和设值的方法,从而方便表单在组件中的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 包含验证功能validators 的hooks, 处理表单状态管理
const useForm = (initialValues = {}, validators) => {
const [values, setValues] = useState(initialValues);
// 定义了 errors 状态
const [errors, setErrors] = useState({});

const setFieldValue = useCallback(
(name, value) => {
setValues((values) => ({
...values,
[name]: value,
}));

// 如果存在验证函数,则调用验证用户输入
if (validators[name]) {
const errMsg = validators[name](value);
setErrors((errors) => ({
...errors,
// 如果返回错误信息,则将其设置到 errors 状态,否则清空错误状态
[name]: errMsg || null,
}));
}
},
[validators],
);
// 将 errors 状态也返回给调用者
return { values, errors, setFieldValue };
};

// 应用

import { useCallback } from "react";
import useForm from './useForm';

export default () => {
const validators = useMemo(() => {
return {
name: (value) => {
// 要求 name 的长度不得小于 2
if (value.length < 2) return "Name length should be no less than 2.";
return null;
},
email: (value) => {
// 简单的实现一个 email 验证逻辑:必须包含 @ 符号。
if (!value.includes("@")) return "Invalid email address";
return null;
},
};
}, []);
// 使用 useForm 得到表单的状态管理逻辑
const { values, errors, setFieldValue } = useForm({}, validators);
// 处理表单的提交事件
const handleSubmit = useCallback(
(evt) => {
// 使用 preventDefault() 防止页面被刷新
evt.preventDefault();
console.log(values);
},
[values],
);
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name: </label>
<input
value={values.name || null}
onChange={(evt) => setFieldValue("name", evt.target.value)}
/>
</div>

<div>
<label>Email:</label>
<input
value={values.email || null}
onChange={(evt) => setFieldValue("email", evt.target.value)}
/>
</div>
<button type="submit">Submit</button>
</form>
);
};

把表单的状态管理单独提取出来,成为一个可重用的 Hook。
这样在表单的实现组件中,就只需要更多地去关心 UI 的渲染,而无需关心状态是如何存储和管理的,从而方便表单组件的开发。

Form 最为核心的机制就是我们将表单元素的所有状态提取出来,
这样表单就可以分为状态逻辑和 UI 展现逻辑,从而实现数据层和表现层的分离。

1…456…26
YooHannah

YooHannah

260 日志
1 分类
23 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces