vue+threejs创建各种线

本文介绍了如何利用Vue3的响应式数据劫持思想,通过observable和autorun创建可观察对象,并应用于React组件,实现按需渲染。详细探讨了batch、toJS和Tracker的功能及原理,提供了将MobX与React组件结合的实践示例。

前言

谈起 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 定义每个对象的 getset 逻辑;
  • 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,
} 

这里结合 observableautorun 两个功能一起来介绍,所以代码量上有些多,不过核心逻辑是在 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道面试题,并对每个问题作出了回答和解析。

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

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值