场景

  1. 父组件 Parent 。有一个 form 属性
  2. 子组件 childrenAv-model 方式接收父组件的 form 属性,并提供一个 submit 方法
  3. 子组件 childrenB,props 属性方式接收父组件 form 属性,并提供一个 print 方法供父组件调用

相关代码(便于阅读写到一起了):
  1. <!-- 子组件 A -->
  2. <template>
  3. <div>
  4. <div>A</div>
  5. <div>
  6. <input type="text" v-model="formData.text">
  7. <button @click="submit">submit</button>
  8. </div>
  9. <div>
  10. <span>A 组件 props form.text: {{this.value.text}}</span>
  11. </div>
  12. </div>
  13. </template>
  14. <script>
  15. export default {
  16. name: 'A',
  17. props: {
  18. // 接收 v-model 绑定的值
  19. value: {
  20. type: Object,
  21. default () { return {} }
  22. }
  23. },
  24. data () {
  25. return {
  26. formData: Object.assign({}, this.value)
  27. }
  28. },
  29. watch: {
  30. value (newVal) {
  31. this.formData = Object.assign({}, this.value)
  32. }
  33. },
  34. methods: {
  35. // 通过 input 事件修改 v-model 的值,然后触发 父组件监听的 submit 事件
  36. submit () {
  37. this.$emit('input', this.formData)
  38. this.$emit('submit', this.formData)
  39. }
  40. }
  41. }
  42. </script>
  43. <!--父组件-->
  44. <template>
  45. <div id="app">
  46. <!-- 传递 form 属性,子组件 A 会修改 form 属性,并同时触发 submit 事件 -->
  47. <com-a v-model="form" @submit="submitHandle"></com-a>
  48. <!-- 子组件 B 接收一个 form 属性 -->
  49. <com-b :form="form" ref="b"></com-b>
  50. </div>
  51. </template>
  52. <script>
  53. import ComA from './components/component-a.vue'
  54. import ComB from './components/component-b.vue'
  55. export default {
  56. name: 'App',
  57. components: {
  58. ComA,
  59. ComB
  60. },
  61. data () {
  62. return {
  63. form: {
  64. text: 0
  65. }
  66. }
  67. },
  68. methods: {
  69. // 子组件 A 触发 submit 事件后,父组件调用子组件 B 的 print 方法
  70. submitHandle () {
  71. this.$refs.b && this.$refs.b.print()
  72. }
  73. }
  74. }
  75. </script>
  76. <!-- 子组件 B -->
  77. <template>
  78. <div>
  79. <div>B</div>
  80. <div>
  81. B 组件 props form.text: {{form.text}}
  82. </div>
  83. <!-- text 为父组件调用 print 方法后 累加 form.text 的值 -->
  84. <div>
  85. {{text}}
  86. </div>
  87. <button @click="text = ''">清除</button>
  88. </div>
  89. </template>
  90. <script>
  91. export default {
  92. name: 'B',
  93. props: {
  94. form: {
  95. type: Object,
  96. default () { return {} }
  97. }
  98. },
  99. data () {
  100. return {
  101. text: ''
  102. }
  103. },
  104. methods: {
  105. print () {
  106. // 累加 form.text 的值
  107. this.text += this.form.text + '\t'
  108. }
  109. }
  110. }
  111. </script>

问题表现

在输入框中随意输入一个值,点击 submit 按钮(子组件 A 中的)。

问题:print 方法中调用到的 this.form.text 并不是最新的(打印的 text 值是 form.text 上一次的值)。

原因

点击子组件 A 中按钮方法调用走向:

  1. 子组件 A 内部调用自己的 submit 方法
  2. 通过 $emit input 事件修改 v-model 的值
  3. 通过 $emit submit 给父组件发出事件
  4. 父组件触发 submitHandle 方法
  5. submitHandle 方法调用子组件B 内部的 print 方法

问题最终的原因是因为 vue 的 defineProperty set 后的执行更新的响应机制 默认是 异步处理 的过程。

这里之所以说是默认是因为 vue 在非生成环境可以配置的,通过 Vue.config.async = false 来改变默认的异步更新的执行机制。

```javascript /**

  • Push a watcher into the watcher queue.
  • Jobs with duplicate IDs will be skipped unless it’s
  • pushed when the queue is being flushed. */ export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) {
     i--
    
    } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true
  // 这里非生成环境可以通过配置 config 来改变更新策略
  if (process.env.NODE_ENV !== 'production' && !config.async) {
    flushSchedulerQueue()
    return
  }
  // 默认使用 nextTick 来执行更新策略
  nextTick(flushSchedulerQueue)
}

} }


> 在 vue config.js 源码中有注明 async 是私有属性,会配合 `vue test utils` 使用。


也就是说父组件的 form 发生变化后,子组件 B 的响应处理被放到了微任务队列中了(以 chrome Promise 来说),而从子组件 A 从 submit 执行开始 第 1 步到第 5 部 print 方法都是在主线程上同步执行的。

所以才会出现上面尴尬的问题。

<a name="b104b85f"></a>
### 处理方案

> 将 `Vue.config.async = false 是不可取的,会造成开发环境与生产环境两种不同的表现形式。


1. 
在父组件 submitHandle 调用子组件 B 的 print 方法之前增加一个 `$nextTick`
```javascript
methods: {
  // 子组件 A 触发 submit 事件后,父组件调用子组件 B 的 print 方法
  async submitHandle () {
    // 此处增加 $nextTick 在队列中会排在 props 响应处理的后面,所以下面的调用可以取到预期中的值
    await this.$nextTick()
    this.$refs.b && this.$refs.b.print()
  }
}
  1. 不在父组件中调用 print 方法,在子组件 B 中采用 watch 属性来触发 print 方法

当然使用哪两种方式取决于业务场景,并非谁对谁错。