前言
谈起 reactive,大家都会想起 Vue3 的「响应式数据劫持」(或者依赖追踪)
通过 Proxy 创建响应式对象,在对象属性被访问时收集副作用(一个可执行函数),在对象属性被修改时依次执行副作用函数,从而达到数据更新响应通知。
数据劫持思路是通用的,也可用于 React 环境,比如 Formily 表单框架所提供的 @formily/reactive 和 @formily/reactive-react,借助 Proxy 实现依赖追踪,从而达到表单控件的高效更新、按需渲染。
如果,我们也掌握了这一技能,在日常开发中的某一时刻,也可以借用它来优化组件渲染,来提高程序性能。
下面,我们从「使用」到「实现」,来理解 reactive 的核心原理,最后我们来看看如何接入到 React 组件实现按需渲染(数据更新,组件重渲染)。
一、observable & autorun
observable,用于创建一个可观察的响应式对象,在这个对象的属性发生变化时,通知到依赖它的 autorun 中注册的tracker回调;autorun,接收一个 tracker 函数,会初始化时会执行一次,让observable去收集它;当函数内部消费的observable数据发生变化时,tracker 函数会重新执行。
1.使用:
// test.js
const { observable, autorun } = require('./reactive');
const obs = observable({name: '初始值'
});
autorun(() => {console.log(obs.name);
});
obs.name = '变更值';
autorun 接收的 tracker 函数在初始化时执行一次,因为访问了 observable 数据,在数据发生变化时,会再次执行 tracker。输出如下:
初始值
变更值
2.原理:
- 变量定义 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、收集 trackerif (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、执行 trackerconst 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 函数。
1.使用:
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
2.原理:
- 批量更新势必涉及到收集存储,这里定义集合
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(); // 只修改数据,不触发 trackerbatchEnd();
}
// 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。
这种方式的好处是功能分离,数据的使用和数据变化后的逻辑可以分开处理。
1.使用:
const { observable, Tracker } = require('./reactive');
const obs = observable({name: '初始值',
})
const tracker = new Tracker(() => {// 3、数据变化,执行的是 schedulerconsole.log('执行特定逻辑');
})
tracker.track(() => {// 1、触发 track,让 observable 数据收集 schedulerconsole.log(obs.name);
})
// 2、修改数据
obs.name = '更新值';
输出如下:
初始值
执行特定逻辑
2.原理:
- 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 机制来劫持数据(称为响应式数据),当组件所依赖的响应式数据发生变化后,组件进行重新渲染。这样就不再需要进行大量的「组件更新脏检查」,而是一个精确渲染。
最后
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:




文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取
本文介绍了如何利用Vue3的响应式数据劫持思想,通过observable和autorun创建可观察对象,并应用于React组件,实现按需渲染。详细探讨了batch、toJS和Tracker的功能及原理,提供了将MobX与React组件结合的实践示例。
307

被折叠的 条评论
为什么被折叠?



