场景
- 父组件
Parent。有一个form属性 - 子组件
childrenA,v-model方式接收父组件的 form 属性,并提供一个submit方法 - 子组件
childrenB,props 属性方式接收父组件form属性,并提供一个print方法供父组件调用
相关代码(便于阅读写到一起了):
<!-- 子组件 A --><template><div><div>A</div><div><input type="text" v-model="formData.text"><button @click="submit">submit</button></div><div><span>A 组件 props form.text: {{this.value.text}}</span></div></div></template><script>export default {name: 'A',props: {// 接收 v-model 绑定的值value: {type: Object,default () { return {} }}},data () {return {formData: Object.assign({}, this.value)}},watch: {value (newVal) {this.formData = Object.assign({}, this.value)}},methods: {// 通过 input 事件修改 v-model 的值,然后触发 父组件监听的 submit 事件submit () {this.$emit('input', this.formData)this.$emit('submit', this.formData)}}}</script><!--父组件--><template><div id="app"><!-- 传递 form 属性,子组件 A 会修改 form 属性,并同时触发 submit 事件 --><com-a v-model="form" @submit="submitHandle"></com-a><!-- 子组件 B 接收一个 form 属性 --><com-b :form="form" ref="b"></com-b></div></template><script>import ComA from './components/component-a.vue'import ComB from './components/component-b.vue'export default {name: 'App',components: {ComA,ComB},data () {return {form: {text: 0}}},methods: {// 子组件 A 触发 submit 事件后,父组件调用子组件 B 的 print 方法submitHandle () {this.$refs.b && this.$refs.b.print()}}}</script><!-- 子组件 B --><template><div><div>B</div><div>B 组件 props form.text: {{form.text}}</div><!-- text 为父组件调用 print 方法后 累加 form.text 的值 --><div>{{text}}</div><button @click="text = ''">清除</button></div></template><script>export default {name: 'B',props: {form: {type: Object,default () { return {} }}},data () {return {text: ''}},methods: {print () {// 累加 form.text 的值this.text += this.form.text + '\t'}}}</script>
问题表现
在输入框中随意输入一个值,点击 submit 按钮(子组件 A 中的)。
问题:print 方法中调用到的 this.form.text 并不是最新的(打印的 text 值是 form.text 上一次的值)。
原因
点击子组件 A 中按钮方法调用走向:
- 子组件 A 内部调用自己的
submit方法 - 通过 $emit input 事件修改 v-model 的值
- 通过 $emit submit 给父组件发出事件
- 父组件触发 submitHandle 方法
- 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) {
} queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = truei--
// 这里非生成环境可以通过配置 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()
}
}
- 不在父组件中调用 print 方法,在子组件 B 中采用 watch 属性来触发 print 方法
当然使用哪两种方式取决于业务场景,并非谁对谁错。
