likes
comments
collection
share

响应式数据 Reactive

作者站长头像
站长
· 阅读数 62

前言

谈起 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。输出如下:

初始值
变更值
  1. 原理:
  • 变量定义 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、收集 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,
}

这里结合 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
  1. 原理:
  • 批量更新势必涉及到收集存储,这里定义集合 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

这种方式的好处是功能分离,数据的使用和数据变化后的逻辑可以分开处理。

  1. 使用:
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 = '更新值';

输出如下:

初始值
执行特定逻辑
  1. 原理:
  • 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 机制来劫持数据(称为响应式数据),当组件所依赖的响应式数据发生变化后,组件进行重新渲染。这样就不再需要进行大量的「组件更新脏检查」,而是一个精确渲染。

最后

谢谢阅读,如有不足之处,欢迎指出。

参考: @formily/reactive

转载自:https://juejin.cn/post/7149442332168257549
评论
请登录