最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - VueJS race condition calling a child's function after setting its props - Stack Overflow

programmeradmin0浏览0评论

UPDATE

This was solved using nextTick() as proposed by @Estus Flask.

import { ref, useTemplateRef, nextTick } from 'vue'

...

const handleClick = () => {
  color.value = 'blue'
  size.value = 50
  nextTick(() => {
    childRef.value?.render()
  })
}

I have Child.vue that draws to canvas. From Parent.vue, I want to be able to change various props of Child.vue, then run its render() function for a redraw.

The problem is when I run handleClick() in Parent.vue, the props for Child.vue have not finished propagating before the render() function is called. This is ascertained by clicking a second time and getting the redraw, and/or by using the hack of adding a delay before calling the render function. Like so:

// Poor person's fix using a delay.
const delay = (ms:number) => new Promise(res => setTimeout(res, ms))
const handleClick = async () => {
  color.value = 'blue'
  size.value = 50
  await delay(10)
  childRef.value?.render()
}

In my real app, I have many more props that will change before calling render(). Sometimes only one changes, sometimes five, sometimes two, etc... Never the same ones.

Is there a proper way to solve this race condition?

<!-- Child.vue -->
<script setup lang="ts">
  import { onMounted, useTemplateRef } from 'vue'

  interface Props {
    color?: string
    size?: number
  }

  const {
    color = 'red',
    size = 10,
  } = defineProps<Props>()

  const canvasRef = useTemplateRef('canvas-ref')
  let canvas:HTMLCanvasElement
  let ctx:CanvasRenderingContext2D

  onMounted(() => {
    canvas = canvasRef.value as HTMLCanvasElement
    ctx = canvas.getContext("2d") as CanvasRenderingContext2D
    render()
  })

  const render = () => {
    ctx.clearRect(0, 0, 100, 100)
    ctx.fillStyle = color
    ctx.fillRect(0, 0, size, size)
  }

  defineExpose({ render })
</script>

<template>
  <canvas ref="canvas-ref" :width="100" :height="100"></canvas>
</template>
<!-- Parent.vue -->
<script setup lang="ts">
  import Child from './Child.vue'
  import { ref, useTemplateRef } from 'vue'

  const childRef = useTemplateRef('child-ref')

  const color = ref()
  const size = ref()

  const handleClick = () => {
    color.value = 'blue'
    size.value = 50
    childRef.value?.render()
  }
</script>

<template>
  <Child
    :color=color
    :size=size
    ref="child-ref"
  />
  <button @click=handleClick>button</button>
</template>

UPDATE

This was solved using nextTick() as proposed by @Estus Flask.

import { ref, useTemplateRef, nextTick } from 'vue'

...

const handleClick = () => {
  color.value = 'blue'
  size.value = 50
  nextTick(() => {
    childRef.value?.render()
  })
}

I have Child.vue that draws to canvas. From Parent.vue, I want to be able to change various props of Child.vue, then run its render() function for a redraw.

The problem is when I run handleClick() in Parent.vue, the props for Child.vue have not finished propagating before the render() function is called. This is ascertained by clicking a second time and getting the redraw, and/or by using the hack of adding a delay before calling the render function. Like so:

// Poor person's fix using a delay.
const delay = (ms:number) => new Promise(res => setTimeout(res, ms))
const handleClick = async () => {
  color.value = 'blue'
  size.value = 50
  await delay(10)
  childRef.value?.render()
}

In my real app, I have many more props that will change before calling render(). Sometimes only one changes, sometimes five, sometimes two, etc... Never the same ones.

Is there a proper way to solve this race condition?

<!-- Child.vue -->
<script setup lang="ts">
  import { onMounted, useTemplateRef } from 'vue'

  interface Props {
    color?: string
    size?: number
  }

  const {
    color = 'red',
    size = 10,
  } = defineProps<Props>()

  const canvasRef = useTemplateRef('canvas-ref')
  let canvas:HTMLCanvasElement
  let ctx:CanvasRenderingContext2D

  onMounted(() => {
    canvas = canvasRef.value as HTMLCanvasElement
    ctx = canvas.getContext("2d") as CanvasRenderingContext2D
    render()
  })

  const render = () => {
    ctx.clearRect(0, 0, 100, 100)
    ctx.fillStyle = color
    ctx.fillRect(0, 0, size, size)
  }

  defineExpose({ render })
</script>

<template>
  <canvas ref="canvas-ref" :width="100" :height="100"></canvas>
</template>
<!-- Parent.vue -->
<script setup lang="ts">
  import Child from './Child.vue'
  import { ref, useTemplateRef } from 'vue'

  const childRef = useTemplateRef('child-ref')

  const color = ref()
  const size = ref()

  const handleClick = () => {
    color.value = 'blue'
    size.value = 50
    childRef.value?.render()
  }
</script>

<template>
  <Child
    :color=color
    :size=size
    ref="child-ref"
  />
  <button @click=handleClick>button</button>
</template>
Share Improve this question edited Mar 3 at 15:59 pelevesque asked Mar 3 at 9:15 pelevesquepelevesque 234 bronze badges 5
  • This is exactly what nextTick is for. Await it instead of random timeouts. It also would make more sense to watch for the props instead in child comp – Estus Flask Commented Mar 3 at 9:19
  • The problem with watching the props is that I will end up rendering many times for nothing. If I watch color and size in the given example, I will get two calls to render() instead of one. In my app, I have like twenty props. I don't want to call render twenty times if I change all the props. I'd like to make all the changes I want, then call render() once. – pelevesque Commented Mar 3 at 9:23
  • The watcher needs to be debounced with timeout 0, this will be enough to prevent multiple calls, watch([() => prop.a, () => prop.b], debounce(() => { canvasRef.value.render() })) – Estus Flask Commented Mar 3 at 9:26
  • Thank you for your help. I was able to make it work with nextTick(). I think that is formally preferable since the idea is to render after the DOM has changed. I also have tons of props. – pelevesque Commented Mar 3 at 9:31
  • Consider posting a self answer showing exactly how you solved the problem, to benefit future visitors. Thanks. – ggorlen Commented Mar 3 at 15:46
Add a comment  | 

1 Answer 1

Reset to default 1

Send overall changes, not each value individually.
Something like this:

/* App.vue */
<script setup>
import { ref } from 'vue';
import Child from './Child.vue'

const canvasProps = ref()

const handleClick = () => {
  canvasProps.value = {
    color: 'blue',
    size:  50
  }
}
</script>

<template>
  <button @click="handleClick">handleClick</button>
  <Child v-bind="canvasProps"></Child>
</template>
/* Child.vue */
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'

interface Props {
  color?: string
  size?: number
}

const props = withDefaults(defineProps<Props>(), {
  color: () => 'red',
  size: () => 100,
});

const canvasRef = ref();

const render = () => {
  if(!canvasRef.value){
    return;
  }
  const ctx = canvasRef.value.getContext('2d');
  ctx.clearRect(0, 0, 100, 100)
  ctx.fillStyle = props.color
  ctx.fillRect(0, 0, props.size, props.size)
}

onMounted(render);
watch(props, render);
</script>

<template>
  <canvas ref="canvasRef" :width="100" :height="100"></canvas>
</template>

Vue SFC Playground

发布评论

评论列表(0)

  1. 暂无评论