likes
comments
collection
share

天命人!让我们由浅入深,了解formily/reactive由浅入深,讲解formily/reactive 一、背景知识

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

由浅入深,讲解formily/reactive

一、背景知识

1.1、一句话描述reactive

reactive 运用了设计模式的发布订阅模式,目的是为了让数据的变化后的视图驱动,变得更加精准和颗粒度更加小(更新范围完全受控!)

1.2、了解reactive有什么意义?

formily是针对form表单的的成熟解决方案;

  • 优点:

    • 高度集成化

    • 语法简单

    • 性能非常好!

  • 缺点

    • 过度集成化,出现问题很难定位

学习reactive,是为了解其背后原理,方便遇到问题后能够站在原理层面快速分析定位!

1.3、核心概念熟悉

:::  [幼苗]必看

observable

一句话形容:observable(obj) 是为了让obj 偷偷的创建一个看不见的收集池(Collection)!让后续obj的读操作,都能收集reaction(reactive可被收集的载体)

reaction 

可被收集的载体,数据结构如下

export type Reaction = ((...args: any[]) => any) & {
  _boundary?: number // 防止track内部 套track!后续会详细讲解
  _name?: string // 标记,方便定位
  _isComputed?: boolean // 标记是否为computed类型的reaction
  _dirty?: boolean // 数据是否已经更新过
  _context?: any //  大部分情况指向 target
  _disposed?: boolean // 是否已经被卸载(所有的收集)
  _property?: PropertyKey // 绑定的 key
  _computesSet?: ArraySet<Reaction> // compute 卸载专用,
  _reactionsSet?: ArraySet<ReactionsMap> // 卸载专用!这里放的是,有多少个 map 里面 装了我!
  _scheduler?: (reaction: Reaction) => void // 真实的更新的操作!不存在就直接 reaction函数执行!
  _memos?: {  // 类似于hooks 的useMemo ,方便在一些autorun 等函数中,保留记忆功能
    queue: IMemoQueueItem[]
    cursor: number
  }
  _effects?: { // 副作用, 根据deps,判断是否需要更新
    queue: IEffectQueueItem[]
    cursor: number
  }
}

:::

一句话总结:++observable++为对象的每一层的 ++target 和 key++创建了一个++Collection++,然后在reaction operation过程中,读这些target的key的时候,会收集这些++reaction++;最后在写的时候执行这些reaction的++_scheduler++内容;

:::

[休假] reaction的种类

autorun

执行autorun的时候,会创建reaction,它只有++一个函数参数++,当你的诉求只是希望订阅数据,然后同步更新,autorun已经完全够用!

const obj = observable({ name: '' });

  const dispose = autorun(() => {
    console.log(obj.name);
  }); // 返回的dispose用于卸载订阅

  obj.name = 'leimao'; // console一次
  obj.name = 'yaoran'; // console再一次

  dispose(); // 卸载监听

  obj.name = 'haha'; // 不会再打印了

autorun 的函数内部对其他数据进行了更改!不会有任何作用

const obj = observable({ name: '' });

  const dispose = autorun(() => {
    console.log(obj.name);
    obj.name = 'leimao'; // 不起作用!
    obj.name = 'yaoran'; // 不起作用!原因是什么?后续会给解释
  }); // 返回的dispose用于卸载订阅

reaction

reaction的同名函数,和autorun不同的是它有3个参数,args1是函数,args依然是函数、args的options配置;

export function reaction1() {
  const obj = observable({
    age: 5,
    level: 10,
  });

  const dispose = reaction(
    () => {
      console.log('重新计算'); // 立刻执行,因为reaction 内部会直接执行一次!
      return obj.age + obj.level;
    },
    (newValue, oldValue) => {
      console.log('value changed!');
    },
    {
      equals: (oldValue, newValue) => {
        return oldValue === newValue;
      },
      fireImmediately: false,
    },
  );

  obj.age = 20;
  // console  '重新计算' 和  'value changed!'

  batch(() => {
    obj.age = 10;
    obj.level = 20;
  }); // console  '重新计算'
  // no value changed!   因为 obj.age + obj.level总值没有变!

  dispose(); // 发生了卸载
  obj.age = 100; // 没有任何console
}

Tracker

使用方式和autorun非常类似(实际源码实现的也很类似),不同的是它是一个class,而且没有autorun特有的memo和effect;然后就是tracker对于,收集函数和订阅函数进行了拆分

const obj = observable({name: ''})

const tracker = new Tracker(() => {
   console.log('obj.name发生了更改'); 
}) // 订阅的函数

tracker.track(()=> {
   obj.name;
}); // 执行依赖函数,目标是收集!

obs.name = 'leimao'; // console一次

tracker.dispose(); // 卸载监听

obs.name = 'haha'; // 不会再打印了

observe 

它三个参数,分别是 对象和回调,以及配置是否支持深度监听!

他实际并不是真实的reaction,和autorun 、reaction、Tracker的实现方式不太一样,后面讲诉实现方式的时候会提到!

 observable.computed

observable.computed 的基础使用是,参数是一个 callback,返回的是一个应用类型;

const pinxie = observable.computed(() => {

读compute值的时候,会进行双向绑定,如果存在reaction,会被computed收集!同时computed内部也有一个action, callback只会在读的时候会被执行!  当callback执行的时候,computed 会被内部的collection收集

const obj = observable({
    name: 'dingeli',
    age: 25,
  });

  const pinxie = observable.computed(() => {
    console.log('重新计算了!'); // 执行一次
    return `${obj?.name}:${obj.age}`; //  obj?.name 收集了 computed的reaction
  });

  console.log(pinxie.value, '真实值'); // 打印了,但是由于没有reaction,所以computed没有收集到,无法形成收集链

  obj.name = 'yaoran'; // 不会打印重新计算了!

只有在reaction 内部,才会形成收集链

const obj = observable({
    name: 'dingeli',
    age: 25,
  });

  const pinxie = observable.computed(() => {
    console.log('重新计算了!'); // 执行一次
    return `${obj?.name}:${obj.age}`;
  });

  autorun(() => {
    console.log(pinxie.value, '真实值'); // 形成了收集链
  });

  obj.name = 'yaoran'; // 会打印重新计算了! computed的 schedule  1、标记dirty 2、通知自己搜集的重新运行;

当obj.name 设置了值,会通知搜集收集过的 autorun,autorun重新执行,就会读computed的value,就会重新计算了! :::

[羽毛笔]ArraySet

ArraySet 是最终存储reaction的数据结构,所有reaction都是直接存储在Arrayset里面的!基本可以理解为数组,多了部分定制的方法逻辑

export class ArraySet<T> {
  value: T[]
  forEachIndex = 0
  constructor(value: T[] = []) {
    this.value = value
  }

  add(item: T) { // 增加
    if (!this.has(item)) {
      this.value.push(item)
    }
  }

  has(item: T) { // 是否存在
    return this.value.indexOf(item) > -1
  }

  delete(item: T) { // 删除
    const len = this.value.length
    if (len === 0) return
    if (len === 1 && this.value[0] === item) {
      this.value = []
      return
    }
    const findIndex = this.value.indexOf(item)
    if (findIndex > -1) {
      this.value.splice(findIndex, 1)
      if (findIndex <= this.forEachIndex) {
        this.forEachIndex -= 1
      }
    }
  }

  forEach(callback: (value: T) => void) { // 遍历处理
    if (this.value.length === 0) return
    this.forEachIndex = 0
    for (; this.forEachIndex < this.value.length; this.forEachIndex++) {
      callback(this.value[this.forEachIndex])
    }
  }

  batchDelete(callback: (value: T) => void) { // 一次处理, 处理完直接删除
    if (this.value.length === 0) return
    this.forEachIndex = 0
    for (; this.forEachIndex < this.value.length; this.forEachIndex++) {
      const value = this.value[this.forEachIndex]
      this.value.splice(this.forEachIndex, 1)
      this.forEachIndex--
      callback(value)
    }
  }

  clear() { // 全部删除
    this.value.length = 0
  }
}

[本子]Collection

Collection 是用于收集reaction的收集池,对象的target 的每个key 都会有一个独立的 ArraySet 去收集reaction,格式如下:

天命人!让我们由浅入深,了解formily/reactive由浅入深,讲解formily/reactive 一、背景知识

最终的数据结构是一个二维map (RawReactionsMap),第一层是通过target获取的是自身下面所有key组成的一个map(innerMap),每个key里面存放的都是ArraySet!

二、原理剖析

2.1、运行总图

天命人!让我们由浅入深,了解formily/reactive由浅入深,讲解formily/reactive 一、背景知识

运行图可以看出,核心步骤主要是:

创建可搜集对象(proxy实现)

  • observable(obj)

  • observable.box(obj)

  • observable.ref(obj) 

  • observable.deep(obj)

  • observable.shallow(obj) 

  • observable.computed(obj)

运行逻辑,创建reaction

  • autorun

  • reaction

  • Tracker

  • observe

  • observable.computed

更改obj的属性,自动更新订阅的reaction

  • runReactions

2.2、核心步骤的实现原理

2.2.1、创建可搜集对象(proxy实现)

const obj = observable({  name:  '' }); 到底做了什么?

我们从observable开始去剖析:

export function observable<T extends object>(target: T): T {
  return createObservable(null, null, target)
}

上面代码可以看出来observable = createObservable(null, null, target);

所以我们主要是要看createObservable到底做了啥!

我们可以带着问题去学习原理!

第一个问题:怎么实现收集!对象就像一棵树一样,每个属性我们都要去收集完全隔离的依赖!

能做到直接针对属性访问多拦截的只有proxy! object.defineProperty需要研发态强感知对象属性,很显然这不符合我们的预期!

 我们实现一个简单版的 createObservable:(先不用纠结一些函数的细节,先理清楚它的基础架构)

const obj = createObservable(null, null,  someObject )
export const createObservable = (
  target: any,
  key?: PropertyKey,
  value?: any
) => {
  const obj = value;
  const proxy = new Proxy( obj, {
    get(target, key, receiver) {
      const current = getNowReaction(); // 获取当前reaction
      addRawReactionsMap(target, key, current) // 收集依赖 (先不用纠结一些函数的细节,先理清楚它的基础架构)
      const nextTarget = target[key];
      return createObservable(target, key, nextTarget) // 每次get的时候,才会继续下一层观察能力的创建 
    },
    set(target, key, value,receiver) {
      const newValue = createObservable(target, key, value) // 重新建立收集能力
      target[key] = newValue; // 设置值 
      runReactionsFromTargetKey({ // 通知收集过的依赖进行更新! (先不用纠结一些函数的细节,先理清楚它的基础架构)
        target,
        key,
        value: newValue,
        oldValue,
        receiver,
        type: 'set',
      })
    }
  })
  return proxy
}

出现了一个新问题来了:get的时候虽然,很可能之前已经建立过依赖了,不应该重复建立!我们改造一下吧!

思路通过WeakMap 存储建立过联系的数据,已经建立过的直接返回结果就可以了!

if(RawProxy.get(target)) {
  return RawProxy.get(target); // 返回存在的proxy
}
const proxy = new Proxy(...); // 建立新的
RawProxy.set(target, proxy); // 保存关系

将思路和简单版结合起来!

export const createObservable = (
  target: any,
  key?: PropertyKey,
  value?: any,
  shallow?: boolean
) => {
  const obj = value;
  const proxy = new Proxy( obj, {
    get(target, key, receiver) {
      const current = getNowReaction(); // 获取当前reaction
      addRawReactionsMap(target, key, current) // 收集依赖 (先不用纠结一些函数的细节,先理清楚它的基础架构)
      const nextTarget = target[key];

      const observableResult = RawProxy.get(nextTarget) // 【尝试判断是否已经建立过收集能力】新增✅
      if (observableResult) { // 【已经存在收集能力】 新增✅
        return observableResult //  新增✅
      }

      
      return createObservable(target, key, nextTarget) // 每次get的时候,才会继续下一层观察能力的创建 
    },
    set(target, key, value,receiver) {
      const newValue = createObservable(target, key, value) // 重新建立收集能力
      target[key] = newValue; // 设置值 
      runReactionsFromTargetKey({ // 通知收集过的依赖进行更新! (先不用纠结一些函数的细节,先理清楚它的基础架构)
        target,
        key,
        value: newValue,
        oldValue,
        receiver,
        type: 'set',
      })
    }
  });

  // --------------分隔符------------------ 建立双向
  ProxyRaw.set(proxy, target); //  【方便后续根据proxy 获取target   ProxyRaw 是一个WeakMap】 新增✅
  RawProxy.set(target, proxy) // 【方便后续根据 target 获取proxy。   RawProxy 是一个WeakMap】 新增✅
  return proxy
}

当value 不是object,实际是不用建立收集能力的!所以应该增加一个判断!

export const createObservable = (
  target: any,
  key?: PropertyKey,
  value?: any,
  shallow?: boolean
) => {

  if (typeof value !== 'object') return value // 新增✅ 必须是object,否则直接结束

  ... 中间内容省略了
   
  return proxy
}

可以看到函数createObservable存在shallow属性,这意味着有一种需求,就是我们只需要target的表层需要做依赖收集!比如:

const obj = createObservable(null, null, { person: {name: ''} }, true)
autorun(()=> {
  console.log(obj.person.name)
});
obj.person.name = 'leimao'  // 不会触发autorun,因为是shallow模式

那我们要这么实现呢?答案也是建立一个WeakMap,专门收集浅层的监听,这样就能判断哪些数据是做shallow的!

RawShallowProxy.set(target, proxy); // 对象  和proxy设置起来!

:::   思路:第二层我们拦住它继续 createObservable操作!

当我们使用 proxy.person.name 的时候,首先读取的是 proxy.person;

proxy.person在 proxy的get当中,会继续使用 createObservable(target, key, target[key]);

这个时候,我们 RawShallowProxy.get(target) 可以获取到它是一个浅层观察!然后拦截他! :::

export const createObservable = (
  target: any,
  key?: PropertyKey,
  value?: any,
  shallow?: boolean
) => {
   if (typeof value !== 'object') return value // 本次新增 ✅ 必须是obejct,否则直接结束

  if (target) { //  本次新增 ✅
    const isShallowParent = RawShallowProxy.get(target); // 查看是否是浅层   本次新增 ✅
    if (isShallowParent) return value // shallowProxy 就是不要做任何操作? 为啥? 后面再看 本次新增 ✅
  }

  const obj = value;
  
  
  const proxy = new Proxy( obj, {
    get(target, key, receiver) {
      const current = getNowReaction(); // 获取当前reaction
      addRawReactionsMap(target, key, current) // 收集依赖 (先不用纠结一些函数的细节,先理清楚它的基础架构)
      const nextTarget = target[key];

      const observableResult = RawProxy.get(nextTarget) // 【尝试判断是否已经建立过收集能力】
      if (observableResult) { // 【已经存在收集能力】 
        return observableResult // 
      }

      
      return createObservable(target, key, nextTarget) // 每次get的时候,才会继续下一层观察能力的创建 
    },
    set(target, key, value,receiver) {
      const newValue = createObservable(target, key, value) // 重新建立收集能力
      target[key] = newValue; // 设置值 
      runReactionsFromTargetKey({ // 通知收集过的依赖进行更新! (先不用纠结一些函数的细节,先理清楚它的基础架构)
        target,
        key,
        value: newValue,
        oldValue,
        receiver,
        type: 'set',
      })
    }
  });

  ProxyRaw.set(proxy, target); //  【方便后续根据proxy 获取target   ProxyRaw 是一个WeakMap】 
   // RawProxy.set(target, proxy) // 
  if (shallow) { // 本次新增 ✅
    RawShallowProxy.set(target, proxy) // 本次新增 ✅
  } else {
    RawProxy.set(target, proxy) // 【方便后续根据 target 获取proxy。   RawProxy 是一个WeakMap】
  }
  return proxy
}

正常的object类型大致就是这样了!但是如果是 Map、Set、WeakMap、WeakSet类型呢?

他们不会直接读取属性,而是通过 obj.get到方式去获取到,添加也是obj.add 去处理的,答案就是重写get和add等方法!

我们知道get和add方法都是读的操作!,所以我们proxy只需要读!


const instrumentations =  {
  get(key: PropertyKey) {
    const target = ProxyRaw.get(this)
    const proto = Reflect.getPrototypeOf(this) as any
    const current = getNowReaction(); // 获取当前reaction
      addRawReactionsMap(target, key, current) // 收集依赖 (先不用纠结一些函数的细节,先理清楚它的基础架构)
    return findObservable(target, key, proto.get.apply(target, arguments))
  },
  add(key: PropertyKey) {
    const target = ProxyRaw.get(this)
    const proto = Reflect.getPrototypeOf(this) as any
    const hadKey = proto.has.call(target, key)
    // forward the operation before queueing reactions
    const result = proto.add.apply(target, arguments)
    if (!hadKey) {
      runReactionsFromTargetKey({ target, key, value: key, type: 'add' })
    }
    return result
  },
}

function findObservable(target: any, key: PropertyKey, value: any) {
  const observableObj = RawProxy.get(value)
  if (observableObj) {
    return observableObj // 已经监听的情况下,直接放回!
  }
  if (!isObservable(value) && isSupportObservable(value)) {
    return createObservable(target, key, value)
  }
  return value
}

const createCollectionProxy = (target: any, shallow?: boolean) => {
  const proxy = new Proxy(target, {
    get() {
       // instrument methods and property accessors to be reactive
      target = hasOwnProperty.call(instrumentations, key)
        ? instrumentations // get or add 等方法,我们使用工具等方式
        : target
      return Reflect.get(target, key, receiver)
    }
  })
  
  return proxy
}

我们写一个统一版本出来!

export const createObservable = (
  target: any,
  key?: PropertyKey,
  value?: any,
  shallow?: boolean
) => {
  if (typeof value !== 'object') return value // 必须是obejct,否则直接结束

  
  if (target) { 
    const parentRaw = ProxyRaw.get(target) || target // target 存在的先拿出来
    const isShallowParent = RawShallowProxy.get(parentRaw);
    if (isShallowParent) return value // shallowProxy 就是不要做任何操作? 为啥? 后面再看
  }

  buildDataTree(target, key, value) // 
  if (isNormalType(value)) return createNormalProxy(value) // 创建
  if (isCollectionType(value)) return createCollectionProxy(value)
  // never reach
  return value
}

const createNormalProxy = (target: any, shallow?: boolean) => {
  const proxy = new Proxy(target, baseHandlers)
  ProxyRaw.set(proxy, target); //  方便后续根据proxy 获取target
  if (shallow) {
    RawShallowProxy.set(target, proxy)
  } else {
    RawProxy.set(target, proxy) // 方便后续根据 target 获取proxy
  }
  return proxy
}
const createCollectionProxy = (target: any, shallow?: boolean) => {
  const proxy = new Proxy(target, collectionHandlers)
  ProxyRaw.set(proxy, target)
  if (shallow) {
    RawShallowProxy.set(target, proxy)
  } else {
    RawProxy.set(target, proxy)
  }
  return proxy
}

export const collectionHandlers = {
  get(target: any, key: PropertyKey, receiver: any) {
    // instrument methods and property accessors to be reactive
    target = hasOwnProperty.call(instrumentations, key)
      ? instrumentations
      : target
    return Reflect.get(target, key, receiver)
  },
}

const instrumentations =  {
  get(key: PropertyKey) {
    const target = ProxyRaw.get(this)
    const proto = Reflect.getPrototypeOf(this) as any
     const current = getNowReaction(); // 获取当前reaction
      addRawReactionsMap(target, key, current) // 收集依赖 (先不用纠结一些函数的细节,先理清楚它的基础架构)
    return findObservable(target, key, proto.get.apply(target, arguments))
  },
  add(key: PropertyKey) {
    const target = ProxyRaw.get(this)
    const proto = Reflect.getPrototypeOf(this) as any
    const hadKey = proto.has.call(target, key)
    // forward the operation before queueing reactions
    const result = proto.add.apply(target, arguments)
    if (!hadKey) {
      runReactionsFromTargetKey({ target, key, value: key, type: 'add' })
    }
    return result
  },
}

const baseHandlers: ProxyHandler<any> = {
  get(target, key, receiver) {
    if (!key) return
    const result = target[key] // use Reflect.get is too slow
    if (typeof key === 'symbol' && wellKnownSymbols.has(key)) {
      return result
    }
    // receiver
     const current = getNowReaction(); // 获取当前reaction
      addRawReactionsMap(target, key, current) // 收集依赖 (先不用纠结一些函数的细节,先理清楚它的基础架构)
    const observableResult = RawProxy.get(result)
    if (observableResult) {
      return observableResult
    }
    if (!isObservable(result) && isSupportObservable(result)) {
      const descriptor = Reflect.getOwnPropertyDescriptor(target, key)
      if (
        !descriptor ||
        !(descriptor.writable === false && descriptor.configurable === false)
      ) {
        return createObservable(target, key, result)
      }
    }
    return result
  },
  set(target, key, value, receiver) {
    const hadKey = hasOwnProperty.call(target, key)
    const newValue = createObservable(target, key, value)
    const oldValue = target[key]
    target[key] = newValue // use Reflect.set is too slow
    if (!hadKey) {
      runReactionsFromTargetKey({
        target,
        key,
        value: newValue,
        oldValue,
        receiver,
        type: 'add',
      })
    } else if (value !== oldValue) {
      runReactionsFromTargetKey({
        target,
        key,
        value: newValue,
        oldValue,
        receiver,
        type: 'set',
      })
    }
    return true
  },
  deleteProperty(target, key) {
    const oldValue = target[key]
    delete target[key]
    runReactionsFromTargetKey({
      target,
      key,
      oldValue,
      type: 'delete',
    })
    return true
  },
}



到了这里,基础架构就基本讲完了!

我们来看看一些细节是这么实现的!

::: 收集依赖 addRawReactionsMap

通知更新 runReactionsFromTargetKey :::

还记得我们之前说的addRawReactionsMap是怎么样一个数据结构么?他是一个二维的WeakMap,让我们看看具体实现细节!

const addRawReactionsMap = (
  target: any,
  key: PropertyKey,
  reaction: Reaction
) => {
  const reactionsMap = RawReactionsMap.get(target) // 通过target 获取 Map = reactionsMap
  if (reactionsMap) { // 第一层key 获取的Map
    const reactions = reactionsMap.get(key) // 通过Map 获取 ArraySet
    if (reactions) { //   ArraySet 存储的都是 reaction
      reactions.add(reaction) // 增加一个
    } else {
      reactionsMap.set(key, new ArraySet([reaction])) // 创建新的ArraySet
    }
    return reactionsMap
  } else { // 创建新Map
    const reactionsMap: ReactionsMap = new Map([
      [key, new ArraySet([reaction])],
    ])
    RawReactionsMap.set(target, reactionsMap)
    return reactionsMap
  }
}

addRawReactionsMap明显是存的操作,而runReactionsFromTargetKey 则是取和执行的操作!

export const runReactionsFromTargetKey = (operation: IOperation) => {
  let { key, type, target, oldTarget } = operation
  batchStart() // 这里可以不着急理解它!
  if (type === 'clear') {
    // 删除之前,也要通知运行一次!
    oldTarget.forEach((_: any, key: PropertyKey) => {
      runReactions(target, key)
    })
  } else {
    runReactions(target, key) // 取和执行操作!
  }
  if (type === 'add' || type === 'delete' || type === 'clear') {
    // 当 add key 不存在的时候,or 
    const newKey = Array.isArray(target) ? 'length' : ITERATION_KEY
    runReactions(target, newKey)  // 取和执行操作!
  }
  batchEnd() // 这里可以不着急理解它!
}
const runReactions = (target: any, key: PropertyKey) => {
  const reactions = getReactionsFromTargetKey(target, key) // 在二维map中获取,key收集的所有reations
  const prevUntrackCount = UntrackCount.value
  UntrackCount.value = 0 // 
  for (let i = 0, len = reactions.length; i < len; i++) {
    const reaction = reactions[i]
    if (reaction._isComputed) {
      reaction._scheduler(reaction) // 正常执行
    } else if (isScopeBatching()) {
      PendingScopeReactions.add(reaction) //  批量收集! scope 是一波流
    } else if (isBatching()) { // 不会立刻执行
      PendingReactions.add(reaction) // 批量收集!,是一次性运行
    } else {
      if (isFn(reaction._scheduler)) {
        reaction._scheduler(reaction)   // 正常执行
      } else {
        reaction() // 也可以运行函数本身
      }
    }
  }
  UntrackCount.value = prevUntrackCount
}

const getReactionsFromTargetKey = (target: any, key: PropertyKey) => {
  const reactionsMap = RawReactionsMap.get(target)
  const reactions = []
  if (reactionsMap) {
    const map = reactionsMap.get(key)
    if (map) {
      map.forEach((reaction) => {
        if (reactions.indexOf(reaction) === -1) {
          reactions.push(reaction)
        }
      })
    }
  }
  return reactions
}

天命人!让我们由浅入深,了解formily/reactive由浅入深,讲解formily/reactive 一、背景知识

2.2.2、reaction  的实现原理

[向右]2.2.2.1、autorun

autorun是一个函数类型收集的方案,典型的用法:

export function autorunDemo() {
  const obj = observable({ name: '' });

  const dispose = autorun(() => {
    console.log(obj.name);
  }); // 返回的dispose用于卸载订阅

  obj.name = 'leimao'; // console一次
  obj.name = 'yaoran'; // console再一次

  dispose(); // 卸载监听

  obj.name = 'haha'; // 不会再打印了
}

很显然autorun 本质是创建了一个reaction,并且会首次创建就执行(开始被访问的属性的Collection收集)!后续的这些属性修改就会让autorun自动被执行!

简单一些,我们写一个简易版!

const autorun = (tracker) => {
  function reaction () {
    if(!isFunction(tracker)) { // 非函数
      return;
    }
    try {
       ReactionStack.push(reaction); // 推入最前栈
       callback(); // 执行函数,且开始搜集  最前栈的autorun 的reaction
    } finally {
       ReactionStack.pop();
    }
  }

  reaction();
  return () => {
    // ... 卸载逻辑,稍后再讲
  }
}

 autorun 还要达到一个条件,就是 autorun的参数内部,对于属性的修改是按批执行的!

export function autorunIsBatch() {
  const obj = observable({ name: '1' });

  autorun(() => {
    console.log(obj.name, '我是第一个autorun'); // 初始化执行一次 1 ,  下面的会执行一次 6
  });

  autorun(() => {
    console.log(obj.name, '我是第二个autorun'); // 初始化执行一次1, 后续不会执行
    obj.name = '2';
    obj.name = '3';
    obj.name = '4';
    obj.name = '5';
    obj.name = '6';
  });
}

打印结果如下:

天命人!让我们由浅入深,了解formily/reactive由浅入深,讲解formily/reactive 一、背景知识

::: 两个问题需要思考:

1、为什么?obj.name 做了那么多设值,结果第一个autorun只执行了一次!

原因是autorun的设计是按批执行的!也就是函数内部会统一一次处理!

2、第二个autorun,obj.name 收集了 它的reaction,紧接着修改了obj.name,理论上它应该会又出发了callback的执行, callback执行又会重新导致obj.name 被修改,从而死循环?

设置 _boundary 避免死循环! :::

batch应该怎么实现?

思路:

::: 1、设置batch计数!每次函数执行前 batch ++

2、然后函数内部的所有设置,都要判断 batch 是否> 0, 大于0咱们要把需要执行的reaction 存储起来;

3、函数执行后,一次遍历执行完成! :::

BatchCount.value++


tracker(); // 触发的runReactions要做判断!

BatchCount.value--
PendingReactions.batchDelete((reaction) => {
    if (isFn(reaction._scheduler)) {
      reaction._scheduler(reaction)
    } else {
      reaction()
    }
  })

const runReactions = (target: any, key: PropertyKey) => {
  const reactions = getReactionsFromTargetKey(target, key)
  for (let i = 0, len = reactions.length; i < len; i++) {
    const reaction = reactions[i]
    if (。。。) {
      。。。
    } else if (isBatching()) { 
      PendingReactions.add(reaction) // 批量收集!,是一次性运行
    } else {
      // never reach
      if (isFn(reaction._scheduler)) {
        reaction._scheduler(reaction) 
      } else {
        reaction()
      }
    }
  }
}

为了方便,我们把每一步设计成统一函数:

 const BatchCount = { value: 0 }

 const isBatching = () => BatchCount.value > 0

 const batchStart = () => {
  BatchCount.value++
}
 const batchEnd = () => {
  BatchCount.value--
  if (BatchCount.value === 0) {
    executePendingReactions()
  }
}

autorun 进一步改造:

const autorun = (tracker) => {
  function reaction () {
    if(!isFunction(tracker)) { // 非函数
      return;
    }
    try {
        batchStart()
        ReactionStack.push(reaction)
        tracker()
      } finally {
        ReactionStack.pop()
        batchEnd()
     }
  }

  reaction();
  return () => {
    // ... 卸载逻辑,稍后再讲
  }
}

死循环怎么办?是通过_boundary避免的!

const autorun = (tracker) => {
  function reaction () {
    if(!isFunction(tracker)) { // 非函数
      return;
    }
    if (reaction._boundary > 0) return // 这样能够防止死循环!
    try {
        batchStart()
        ReactionStack.push(reaction)
        tracker()
      } finally {
        ReactionStack.pop()
        reaction._boundary++ // 执行前增加 _boundary
        batchEnd() // 最终批次会执行tracker
        reaction._boundary = 0
     }
  }

  reaction();
  return () => {
    // ... 卸载逻辑,稍后再讲
  }
}

为了安全考虑再增加一个判断来加固!

const autorun = (tracker) => {
  function reaction () {
    if(!isFunction(tracker)) { // 非函数
      return;
    }
    if (reaction._boundary > 0) return // 这样能够防止死循环!

    if (ReactionStack.indexOf(reaction) === -1) {
      try {
        batchStart()
        ReactionStack.push(reaction)
        tracker()
      } finally {
        ReactionStack.pop()
        reaction._boundary++ // 执行前增加 _boundary
        batchEnd() // 最终批次会执行tracker
        reaction._boundary = 0
       }
    }

  }

  reaction();
  return () => {
    // ... 卸载逻辑,稍后再讲
  }
}

现在讲一讲卸载逻辑:其核心是找到谁搜集了当前reaction!然后通知这些搜集当前reaction的Arrayset,去除当前reaction!

const dispose = autorun(() => {
    console.log(obj.name);
  }); // 返回的dispose用于卸载订阅

dispose(); // 卸载监听

也就是dispose的逻辑!

我们上面说了核心是找到哪些搜集了当前reaction的Arrayset!那么怎么找到呢?

答案是前面我们搜集的时候,就要做好互相记录的准备!重新改写一下搜集逻辑才行!

改写之前 请不记得addRawReactionsMap(target, key, currentReaction)逻辑的,回到前面章节再熟悉一下这块逻辑!

addRawReactionsMap最终回返回  reactionsMap,这里面存放的是 target 下面所有key的 Arrayset,也就是

天命人!让我们由浅入深,了解formily/reactive由浅入深,讲解formily/reactive 一、背景知识

我们对它的返回记录到reaction里面去!


const reaction = nowReaction;
const reactionsMap = addRawReactionsMap(target, key, currentReaction);
const bindSet = reaction._reactionsSet // 核心点  _reactionsSet 反向存放搜集了当前reaction 的  reactionsMap
  if (bindSet) {
    bindSet.add(reactionsMap)  // 添加!
  } else {
    reaction._reactionsSet = new ArraySet([reactionsMap]) // 新建
  }
  return bindSet

改造的好看一些:

const addReactionsMapToReaction = (
  reaction: Reaction,
  reactionsMap: ReactionsMap
) => {
  const bindSet = reaction._reactionsSet
  if (bindSet) {
    bindSet.add(reactionsMap)
  } else {
    reaction._reactionsSet = new ArraySet([reactionsMap])
  }
  return bindSet
}
// 最终proxy的get地方:
 new Proxy(obj, {
   get() {
     ...
     addReactionsMapToReaction(current, addRawReactionsMap(target, key, current))
     ...
   }
 })

反向记录了搜集了当前reaciton的reactionMaps了,需要删除的时候拿出来删了!

const dispose = () => {
  reaction._reactionsSet?.forEach((reactionsMap) => {
    reactionsMap.forEach((reactions) => {
      // 不用知道当前key,全部执行一遍,保证删除干净!
      reactions.delete(reaction) 
    })
  });
  delete reaction._reactionsSet;
}

autorun 核心逻辑到此为止!实际还有 autorun.effect 和 autorun.memo 的逻辑,这里不介绍了!

[向右] 2.2.2.2、computed

computed 是比较特殊的一个reaction种类!因为它除了自身是一个reaction,同时它还具备搜集能力!

const obj = observable({
    name: 'dingeli',
    age: 25,
  });

  const pinxie = observable.computed(() => {
    console.log('重新计算了!'); // 执行一次
    return `${obj?.name}:${obj.age}`;
  });

  autorun(() => {
    console.log(pinxie.value, '真实值'); // 形成了收集链
  });

  obj.name = 'yaoran'; // 会打印重新计算了! computed的 schedule  1、标记dirty 2、通知自己搜集的重新运行;

autorun 被computed收集了起来!

computed被 obj.name  + obj.age 都收集起来了!

::: obj.name 发生了更改

pinxie 设置了dirty 

通知所有搜集的reaction更新

autorun 更新

重新计算 pingxie.value :::

export const computed: IComputed = (value) => {
  const store: IValue = {};

  const proxy = {};

  const context = target ? target : store;
  const property = target ? key : 'value';
  const descriptor = getPrototypeDescriptor(target, property, value);

  function compute() {
    store.value = descriptor.get?.call(context);
  }
  function reaction() {
    if (ReactionStack.indexOf(reaction) === -1) {
      releaseBindingReactions(reaction);
      try {
        ReactionStack.push(reaction); // 推入栈种,准备让别人收集我!
        compute(); // 开始计算,别人要收集我了!
      } finally {
        ReactionStack.pop();
      }
    }
  }
  reaction._name = 'ComputedReaction';
  reaction._scheduler = () => {
    // 别人通知我刷新,我直接转播
    reaction._dirty = true; // 设置脏值
    runReactionsFromTargetKey({
      // 直接转播
      target: context,
      key: property,
      value: store.value,
      type: 'set',
    });
  };
  reaction._isComputed = true;
  reaction._dirty = true;
  reaction._context = context;
  reaction._property = property;

  function get() {
    if (hasRunningReaction()) {
      //  收集别人之前,也让别人记录一下,我收集了它!
      bindComputedReactions(reaction); // _computesSet.push(nowCurrentReaction)
    }
    if (!isUntracking()) {
      //如果允许untracked过程中收集依赖,那么永远不会存在绑定,因为_dirty已经设置为false
      // 第一次都是走的这个逻辑!
      if (reaction._dirty) {
        // 脏了,才会重新计算,也就是有缓存
        reaction(); //
        reaction._dirty = false;
      }
    } else {
      // 不做收集!
      compute();
    }
    // 收集起来!收集别人!
    bindTargetKeyWithCurrentReaction({
      target: context,
      key: property,
      type: 'get',
    });
    return store.value;
  }

  function set(value: any) {
    try {
      batchStart();
      descriptor.set?.call(context, value);
    } finally {
      batchEnd();
    }
  }
  if (target) {
    Object.defineProperty(target, key, {
      get,
      set,
      enumerable: true,
    });
    return target;
  } else {
    Object.defineProperty(proxy, 'value', {
      set,
      get,
    });
    buildDataTree(target, key, store);
    proxy[ObModelSymbol] = store; // store
  }
  return proxy;
};
[向右]2.2.2.3、observe

后续会单独开个章节来讲他的逻辑比较独立!

结束语

reactive是整个formily最底层的逻辑,核心点是通过proxy的特殊拦截能力, 使得对象的每个key都有一个独立的ArraySet去订阅reaction;

proxy的 set,则是发布订阅的发布步骤;

至此完成了自闭环!

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