响应式数据 Reactive
前言
谈起 reactive
,大家都会想起 Vue3 的「响应式数据劫持」(或者依赖追踪)
通过 Proxy
创建响应式对象,在对象属性被访问时收集副作用(一个可执行函数),在对象属性被修改时依次执行副作用函数,从而达到数据更新响应通知。
数据劫持思路是通用的,也可用于 React
环境,比如 Formily
表单框架所提供的 @formily/reactive
和 @formily/reactive-react
,借助 Proxy 实现依赖追踪,从而达到表单控件的高效更新、按需渲染。
如果,我们也掌握了这一技能,在日常开发中的某一时刻,也可以借用它来优化组件渲染,来提高程序性能。
下面,我们从「使用」到「实现」,来理解 reactive
的核心原理,最后我们来看看如何接入到 React 组件实现按需渲染(数据更新,组件重渲染)。
一、observable & autorun
observable
,用于创建一个可观察的响应式对象,在这个对象的属性发生变化时,通知到依赖它的 autorun 中注册的tracker
回调;autorun
,接收一个 tracker 函数,会初始化时会执行一次,让observable
去收集它;当函数内部消费的observable
数据发生变化时,tracker 函数会重新执行。
- 使用:
// test.js
const { observable, autorun } = require('./reactive');
const obs = observable({
name: '初始值'
});
autorun(() => {
console.log(obs.name);
});
obs.name = '变更值';
autorun 接收的 tracker 函数在初始化时执行一次,因为访问了 observable
数据,在数据发生变化时,会再次执行 tracker。输出如下:
初始值
变更值
- 原理:
- 变量定义 1,
RawReactionsMap
是一个WeakMap
,存储每个对象属性收集过来的副作用函数(tracker);使用WeakMap
的好处是可以接受对象作为 key; - 变量定义 2,
currentReaction
记录了 autorun 中的 tracker 函数; - 创建 Proxy,核心是执行
createObservable
定义每个对象的get
和set
逻辑; - get 收集工作,对于上面示例,访问
obs.name
是发生在 autorun/tracker 函数中,会将 tracker 收集在RawReactionsMap
集合; - set 通知工作,在
RawReactionsMap
中查找并执行收集到的 autorun/tracker 函数。
observable
实现如下:
// reactive.js
// 1. 第一部分,定义变量
const RawReactionsMap = new WeakMap(); // 存储副作用
const RawProxy = new WeakMap(); // 记录对象是否被 Proxy 过
let currentReaction; // autorun 中的 tracker 函数
// 2、存储 tracker 函数
const addRawReactionsMap = (target, key, reaction) => {
const reactionsMap = RawReactionsMap.get(target);
if (reactionsMap) {
const reactions = reactionsMap.get(key);
if (reactions) {
reactions.add(reaction);
} else {
reactionsMap.set(key, new Set([reaction]));
}
return reactionsMap;
} else {
const reactionsMap = new Map([
[key, new Set([reaction])], // Set 去重
])
RawReactionsMap.set(target, reactionsMap);
return reactionsMap;
}
}
// 将 reaction 和 map 建立关联,用于销毁使用
const addReactionsMapToReaction = (reaction, reactionsMap) => {
const bindSet = reaction._reactionsSet;
if (bindSet) {
bindSet.add(reactionsMap);
} else {
reaction._reactionsSet = new Set([reactionsMap]);
}
return bindSet;
}
// 3、数据劫持核心实现
const handler = {
get(target, key) {
const result = target[key];
// 1、收集 tracker
if (currentReaction) {
addReactionsMapToReaction(currentReaction, addRawReactionsMap(target, key, currentReaction));
}
const observableResult = RawProxy.get(result);
if (observableResult) return observableResult;
return createObservable(result); // 深度劫持
},
set(target, key, value) {
const oldValue = target[key];
target[key] = value;
if (oldValue !== value) {
// 2、执行 tracker
const runReactions = (target, key) => {
const reactions = RawReactionsMap?.get(target)?.get(key);
reactions && reactions.forEach(reaction => reaction());
}
runReactions(target, key);
}
return true;
}
}
const createObservable = (target) => {
if (typeof target !== 'object') return target;
const rawProxy = RawProxy.get(target);
if (rawProxy) return rawProxy;
const proxy = new Proxy(target, handler);
RawProxy.set(target, proxy);
return proxy;
}
function observable(target) {
return createObservable(target);
}
module.exports = {
observable,
}
autorun
的实现如下:
const autorun = (tracker) => {
const reaction = () => {
if (typeof tracker !== 'function') return;
try {
currentReaction = reaction; // 保存
tracker();
} finally {
currentReaction = null;
}
}
reaction();
return () => {
reaction._reactionsSet?.forEach((reactionsMap) => {
reactionsMap.forEach((reactions) => {
reactions.delete(reaction);
});
});
delete reaction._reactionsSet;
}
}
module.exports = {
observable,
autorun,
}
这里结合 observable
和 autorun
两个功能一起来介绍,所以代码量上有些多,不过核心逻辑是在 handler
中的 get 和 set。
另外,autorun 返回一个销毁 tracker 订阅的函数,通过执行销毁函数,将 tracker 从 RawReactionsMap
中移除,来脱离 observable
对象响应。
...
const dispose = autorun(() => {
console.log(obs.name);
});
obs.name = '变更值';
dispose();
obs.name = '再次变更'; // autorun tracker 不会再执行
二、batch
batch
是批量更新,当一个 tracker
中依赖多次 observable
数据时,若这些数据同一时间被修改,tracker 将会被执行多次。
batch 可以批量更新,只会执行一次 tracker 函数。
- 使用:
const { observable, autorun, batch } = require('./reactive');
const obs = observable({});
autorun(() => {
console.log(obs.aa, obs.bb, obs.cc, obs.dd);
})
batch(() => {
obs.aa = 'aaa'
obs.bb = 'bbb'
obs.cc = 'ccc'
obs.dd = 'ddd'
})
输出如下:
undefined undefined undefined undefined
aaa bbb ccc ddd
- 原理:
- 批量更新势必涉及到收集存储,这里定义集合
PendingReactions
,使用Set
好处是可以去重; - 当执行
batch
时标记处于批量更新状态,即isBatching()
返回 true; - 当更新数据进入
set
后,由于是batch
状态,不会立刻执行tracker
,而是存储在PendingReactions
中; - 最后调用
executePendingReactions
统一执行 tracker 函数。
// reactive.js
...
const PendingReactions = new Set([]);
const BatchCount = { value: 0 };
const isBatching = () => BatchCount.value > 0;
const batchStart = () => {
BatchCount.value++;
}
const batchEnd = () => {
BatchCount.value --;
executePendingReactions(); // 触发 tracker
}
const executePendingReactions = () => {
PendingReactions.forEach(reaction => reaction())
PendingReactions.clear();
}
const batch = (fn) => {
batchStart();
fn(); // 只修改数据,不触发 tracker
batchEnd();
}
// handler set 逻辑调整如下:
const handler = {
get(target, key) { ... },
set(target, key, value) {
...
const runReactions = (target, key) => {
const reactions = RawReactionsMap?.get(target)?.get(key);
reactions && reactions.forEach(reaction => {
// 不执行,只存储
+ if (isBatching()) {
+ PendingReactions.add(reaction);
+ } else {
reaction();
}
});
}
runReactions(target, key);
}
}
module.exports = {
...
batch
}
三、toJS
toJS
会深度递归将 observable 对象转换成普通 JS 对象,实现上比较简单,代码如下:
// reactive.js
const toJS = values => {
const _toJS = values => {
if (Array.isArray(values)) {
const res = [];
values.forEach(item => {
res.push(_toJS(item));
})
return res;
} else if (Object.prototype.toString.call(values) === '[object Object]') {
const res = {};
for (const key in values) {
res[key] = _toJS(values[key]);
}
}
return values;
}
return _toJS(values);
}
module.exports = {
...
toJS,
}
四、Tracker
Tracker
,与 autorun 的 tracker 函数相似,Tracker.track 方法用于应用 observable
对象,而真正作为数据更新执行的 tracker
函数则是 Tracker.scheduler
。
这种方式的好处是功能分离,数据的使用和数据变化后的逻辑可以分开处理。
- 使用:
const { observable, Tracker } = require('./reactive');
const obs = observable({
name: '初始值',
})
const tracker = new Tracker(() => {
// 3、数据变化,执行的是 scheduler
console.log('执行特定逻辑');
})
tracker.track(() => {
// 1、触发 track,让 observable 数据收集 scheduler
console.log(obs.name);
})
// 2、修改数据
obs.name = '更新值';
输出如下:
初始值
执行特定逻辑
- 原理:
- Tracker.track 用于使用
observable
数据; - Tracker.track._scheduler 才是
observable
数据变化后需要执行的tracker
函数。
// reactive.js
...
class Tracker {
constructor(scheduler) {
this.track._scheduler = scheduler;
}
// 这里要定义实例方法,若是原型方法,会被创建多个实例给覆盖掉
track = (tracker) => {
if (typeof tracker !== 'function') return;
let result;
try {
currentReaction = this.track;
result = tracker();
} finally {
currentReaction = null;
}
return result;
}
}
// handler set 逻辑调整如下:
const handler = {
get(target, key) { ... },
set(target, key, value) {
...
const runReactions = (target, key) => {
const reactions = RawReactionsMap?.get(target)?.get(key);
reactions && reactions.forEach(reaction => {
if (isBatching()) {
PendingReactions.add(reaction);
+ } else if (typeof reaction._scheduler === 'function') {
+ reaction._scheduler(reaction); // Tracker
+ } else {
reaction();
}
});
}
runReactions(target, key);
}
}
module.exports = {
...
Tracker,
}
Tracker 的特性可用于衔接 React 函数组件,在 observable
数据更新后,触发组件的重渲染。
五、应用于 React 组件
上面介绍了那么多,都是 JS 数据操作,这跟 React 会有什么关联呢?
有没有一种可能:React 组件内部应用到了 observable
数据,在数据发生更新时,这个 React 组件自动进行重渲染呢?
我们来看一个例子:
import React from 'react';
import observer from './reactive-react';
import { observable } from './reactive';
const state = observable({
title: 'reactive',
content: '响应式数据'
});
const Head = observer(() => {
console.log('head render.');
return <div>
{state.title},
<input value={state.title} onChange={e => state.title = e.target.value} />
</div>;
});
const Content = observer(() => {
console.log('content render.');
return <div>
{state.content},
<textarea value={state.content} onChange={e => state.content = e.target.value} />
</div>;
});
const App = () => {
console.log('app render.');
return <div>
<Head name="head" />
<Content name="content" />
</div>
}
export default App;
示例中,定义了一个 observable
数据 state,其中 Head 组件依赖了 state.title,Content 依赖 state.content。
你会发现,Head 中的 Input 发生 change,Head 组件会重渲染并输出 head render.
,而 Content 组件以及 App 组件都未更新。
而实现这种按需渲染,只需通过 observer
包裹函数组件即可达成。即,observer
是衔接 observable
和 React 组件的桥梁。
它基于上面讲述的 Tracker
来实现,我们来看看它的原理:
// reactive-react.jsx
import { useReducer, useRef } from 'react';
import { Tracker } from './reactive';
const observer = (FunctionComponent) => {
const wrappedComponent = (props) => {
const [, forceUpdate] = useReducer(v => v + 1, 0);
const trackerRef = useRef(new Tracker(forceUpdate));
return trackerRef.current.track(() => FunctionComponent(props));
}
return wrappedComponent;
}
export default observer;
看到这里相信你会一目了然:
通过 tracker.track
执行函数组件去访问 observable
变量,让变量将这个 Tracker
实例的 scheduler
方法进行收集;当 observable
数据更新时执行 scheduler
方法。
而 scheduler
对应的就是外层组件的 forceUpdate
触发重渲染 API。
注意,observer 只能接收函数组件,且不要使用 React.memo 包裹组件(它是一个对象,不是方法)。
总结一下: 借助 ES6 Proxy 机制来劫持数据(称为响应式数据),当组件所依赖的响应式数据发生变化后,组件进行重新渲染。这样就不再需要进行大量的「组件更新脏检查」,而是一个精确渲染。
最后
谢谢阅读,如有不足之处,欢迎指出。
转载自:https://juejin.cn/post/7149442332168257549