alibaba/formily

[Bug Report] reactiveField toJS函数会deep clone对象,导致组件watch effect重复执行

Opened this issue · 6 comments

  • I have searched the issues of this repository and believe that this is not a duplicate.

Reproduction link

!sandbox

Steps to reproduce

如link中的源码。
组件在watch中触发emit,导致parent渲染的reactiveField重新渲染,并构造了一个新的props。
再次渲染子组件的过程中,导致了watch effect函数重复执行。

image
image
image

What is expected?

watch函数只触发一次。

What is actually happening?

watch函数触发了n次

Package

@formily/vue@2.1.11


Reproduction link 显示 devBox not found, 看看链接是不是有问题?

看标题描述应该与 #2794 这个问题相关。 可以通过 useField 获取 field 中获取到组件 props, 这个引用不会变

Reproduction link 显示 devBox not found, 看看链接是不是有问题?

原链接是私有的,现在开放了

看标题描述应该与 #2794 这个问题相关。 可以通过 useField 获取 field 中获取到组件 props, 这个引用不会变

对但vue的响应式系统下,props的引用地址变更也会触发watch effect。react我理解useEffect的dep也是简单的shallow diff。我觉得props地址是不能变更的,虽然两个对象打印后是一样。

源码是在packages/vue/src/components/ReactiveField.ts
originData对象里的value都是deep clone的,经过这个toJs这个函数处理

export const toJS = <T>(values: T): T => {
  const visited = new WeakSet<any>()
  const _toJS: typeof toJS = (values: any) => {
    if (visited.has(values)) {
      return values
    }
    if (values && values[RAW_TYPE]) return values
    if (isArr(values)) {
      if (isObservable(values)) {
        visited.add(values)
        const res: any = []
        values.forEach((item: any) => {
          res.push(_toJS(item))
        })
        visited.delete(values)
        return res
      }
    } else if (isPlainObj(values)) {
      if (isObservable(values)) {
        visited.add(values)
        const res: any = {}
        for (const key in values) {
          if (hasOwnProperty.call(values, key)) {
            res[key] = _toJS(values[key])
          }
        }
        visited.delete(values)
        return res
      }
    }
    return values
  }

  return _toJS(values)
}

看标题描述应该与 #2794 这个问题相关。 可以通过 useField 获取 field 中获取到组件 props, 这个引用不会变

对但vue的响应式系统下,props的引用地址变更也会触发watch effect。react我理解useEffect的dep也是简单的shallow diff。我觉得props地址是不能变更的,虽然值一样。

源码是在packages/vue/src/components/ReactiveField.ts originData对象里的value都是deep clone的,经过这个toJs这个函数处理

export const toJS = <T>(values: T): T => {
  const visited = new WeakSet<any>()
  const _toJS: typeof toJS = (values: any) => {
    if (visited.has(values)) {
      return values
    }
    if (values && values[RAW_TYPE]) return values
    if (isArr(values)) {
      if (isObservable(values)) {
        visited.add(values)
        const res: any = []
        values.forEach((item: any) => {
          res.push(_toJS(item))
        })
        visited.delete(values)
        return res
      }
    } else if (isPlainObj(values)) {
      if (isObservable(values)) {
        visited.add(values)
        const res: any = {}
        for (const key in values) {
          if (hasOwnProperty.call(values, key)) {
            res[key] = _toJS(values[key])
          }
        }
        visited.delete(values)
        return res
      }
    }
    return values
  }

  return _toJS(values)
}

是的, vue是自带响应式的。 出现这个问题的原因是当前版本的 formily 在设计的时候,将响应式放在@formily/reactive 这一层了,没有利用 vue自身的能力。 目前 @formily/vue 中的 ReactiveField 实际上还是参考了 @formily/react 中的实现,vue 组件更像一个 functional Component, 是否重新渲染是由 reactive 层去控制的。 在你不脱离@formily/vue 去开发情况下,通过 provide/inject 可以规避这个问题。

看标题描述应该与 #2794 这个问题相关。 可以通过 useField 获取 field 中获取到组件 props, 这个引用不会变

对但vue的响应式系统下,props的引用地址变更也会触发watch effect。react我理解useEffect的dep也是简单的shallow diff。我觉得props地址是不能变更的,虽然值一样。
源码是在packages/vue/src/components/ReactiveField.ts originData对象里的value都是deep clone的,经过这个toJs这个函数处理

export const toJS = <T>(values: T): T => {
  const visited = new WeakSet<any>()
  const _toJS: typeof toJS = (values: any) => {
    if (visited.has(values)) {
      return values
    }
    if (values && values[RAW_TYPE]) return values
    if (isArr(values)) {
      if (isObservable(values)) {
        visited.add(values)
        const res: any = []
        values.forEach((item: any) => {
          res.push(_toJS(item))
        })
        visited.delete(values)
        return res
      }
    } else if (isPlainObj(values)) {
      if (isObservable(values)) {
        visited.add(values)
        const res: any = {}
        for (const key in values) {
          if (hasOwnProperty.call(values, key)) {
            res[key] = _toJS(values[key])
          }
        }
        visited.delete(values)
        return res
      }
    }
    return values
  }

  return _toJS(values)
}

是的, vue是自带响应式的。 出现这个问题的原因是当前版本的 formily 在设计的时候,将响应式放在@formily/reactive 这一层了,没有利用 vue自身的能力。 目前 @formily/vue 中的 ReactiveField 实际上还是参考了 @formily/react 中的实现,vue 组件更像一个 functional Component, 是否重新渲染是由 reactive 层去控制的。 在你不脱离@formily/vue 去开发情况下,通过 provide/inject 可以规避这个问题。

后续这个问题会修复吗?我这边可以先规避下