Subscribe on changes!

`watchEffect ` Should not runs a function immediately,please add to the queued runs by onBeforeUpdate

avatar
Sep 25th 2021

Version

3.2.18

Reproduction link

codepen.io

Steps to reproduce

    const obj = reactive({})
    const foo = ref()
      Vue.watchEffect(() => {
        foo.value = obj.deep.foo  // error
      })

    obj.deep = {foo:'foo'}

What is expected?

副作用函数不应该立即执行,而应该挂到列队在onBeforeUpdate中统一执行,避免因传入的数据还未创建完成出现错误,或者重复执行。

What is actually happening?

现在只能将 watchEffect 放到 onMounted 勾子中执行才能避免问题 js Vue.onMounted(() => { Vue.watchEffect(() => { foo.value = obj.deep.foo // no problem }) })

即使不出现上面错误,例如:

    const obj = Vue.reactive({deep:{foo:''})
    const foo = Vue.ref()
      Vue.watchEffect(() => {
        foo.value = obj.deep.foo  // double run
      })
    obj.deep = {foo:'foo'}  // fired effect

也会造成副作用函数执行两次,而且这本就是注册一个监听勾子,可是却立即执行,不是很好理解! 虽然有watchPostEffect, 但执行时机是在更新后, 比如以下情况

const boo = ref()
const bar = ref()
watchPostEffect(() => {
   bar.value = boo.value ?  'some' : 'none'
})
const thd = computed(() => {
   return 'hello,' + (boo.value  ?  bar.value : 'world')
})

当boo 变化时 computed会执行,更新后才会触发bar变化,然后又会使computed勾子执行。所这这也不能解决问题!

avatar
Sep 25th 2021

It runs immediately once because it needs to collect reactive dependencies. then it will adhere to the flush setting.

The "double run" is the intended behavior by our design.

avatar
Sep 25th 2021

first run in the onBeforeUpdate , Still can be collect dependencies

Running twice of create stage doesn't make sense

avatar
Sep 25th 2021

Running twice of create stage doesn't make sense

That totally depends on your use case, though.

  • You might want to have the effect run immediately, maybe it triggers an API call you want to have going ASAP.
  • you might want to have it run again if a reactive dependency did change before mounted, which might or might be changed depending on other state/effects in your component.

The way things are now, you can guard yourself against a double update with a flag like:

const isBeforeMount = ref(false)
onBeforemount(() => isBeforeMount.value = true)
watchEffect(() => {
  if (isBeforeMount.value) {
    //implementation.
  }
})

or maybe you have a situation where you want the effect to be run after the render cycle with flush: 'post'?

avatar
Sep 25th 2021

in my super form, user can be configure, example:

[
      {
        type: 'Input',
        prop: 'name',
        label: '姓名',
      },
      {
        type: 'Input',
        prop: 'age',
        label: '年龄',
        initialValue: 20,
      },
      {
        type: 'Input',
        prop: 'idNumber',
        label: '证件号',
        disabled:(formData) => formData.age<16
      },
  {
     type:'group',
     prop: 'group',   //  formData.group. 
     items:[...]
]

this formData is build in create stage,

function build(option, parentData, formData) {
  // parentData  is  reactive(formData[anyname]) , formData is reactivity
  parentData[option.prop] = option.initalValue
  const disabledRef = ref(!option.disabled)
  if (typeof option.disabled === 'function') {
      watchEffect(() => {
        disabledRef.value = !option.disabled(formData)
      })
  }
  // ...
}

if effect run immediately, formData is not ready yet

avatar
Sep 25th 2021

Maybe something like:

function build(option, parentData, formData) {
  // parentData  is  reactive(formData[anyname]) , formData is reactivity
  parentData[option.prop] = option.initalValue
  const disabledRef = ref(!option.disabled)
  if (typeof option.disabled === 'function') {
      onBeforeMount(() => watchEffect(() => {
        disabledRef.value = !option.disabled(formData)
      }))
  }
  // ...
}

... or use watch()instead of watchEffect(), for example.

Point being: There are other ways to achieve what you want, and we won't do a breaking change in the behavior of watchEffect

avatar
Sep 25th 2021

watchEffect can be called inside onBeforeMount or onMounted if needed (it will still be tracked by the component's effect scope), you have full control over when to set it up.