Vuejs 手表跳一跳

Vuejs Watch jumping one tick

我正在使用 Vuejs 和 Vuex 开发应用程序。

我有一个名为 settings_operations 的 Vuex 模块。该模块具有以下操作:

async changePassword ({ commit }, { password, id }) {
  commit(CHANGE_PASSWORD_PROCESSING, { id })
  const user = auth.currentUser
  const [changePasswordError, changePasswordSuccess] = await to(user.updatePassword(password))
  if (changePasswordError) {
    commit(CHANGE_PASSWORD_ERROR, { id, error: changePasswordError })
  } else {
    commit(CHANGE_PASSWORD_SUCCESS, changePasswordSuccess)
  }
}

编辑:to() 是 https://github.com/scopsy/await-to-js

具有以下突变:

[CHANGE_PASSWORD_PROCESSING] (state, { id }) {
  state.push({
    id,
    status: 'processing'
  })
},
[CHANGE_PASSWORD_ERROR] (state, { id, error }) {
  state.push({
    id,
    error,
    status: 'error'
  })
}

然后,在组件中我想使用这个状态切片:

computed: {
  ...mapState({
    settings_operations: state => state.settings_operations
  })
},
watch: {
  settings_operations: {
    handler (newSettings, oldSettings) {
      console.log(newSettings)
    },
    deep: false
  }
}

问题在于,当更改密码操作导致错误时,手表不会在 PROCESSING 步骤停止,而是直接转到 ERROR 时刻,因此数组将填充 2 个对象。它确实跳过了 "processing" 观察步骤。

有趣的是,如果我像这样添加 setTimeout:

async changePassword ({ commit }, { password, id }) {
  commit(CHANGE_PASSWORD_PROCESSING, { id })

  setTimeout(async () => {
    const user = auth.currentUser
    const [changePasswordError, changePasswordSuccess] = await to(user.updatePassword(password))
    if (changePasswordError) {
      commit(CHANGE_PASSWORD_ERROR, { id, error: changePasswordError })
    } else {
      commit(CHANGE_PASSWORD_SUCCESS, changePasswordSuccess)
    }
  }, 500)
},

有效!手表停止两次:第一次显示包含处理对象的数组,第二次显示包含 2 个对象的数组;处理一和错误一

我在这里错过了什么?

编辑:

我在这里重现了这个问题:https://codesandbox.io/s/m40jz26npp

这是核心团队成员在 Vue 论坛中给出的回复:

Watchers are not run every time the underlying data changes. They are only run once on the next Tick if their watched data changed at least once.

your rejected Promise in the try block is only a microtask, it doesn’t push execution to the next call stack (on which the watchers would be run), so the error handling happens before the watchers are run.

additionally, when you mutat an object or array, the newValue and oldValue in a deep watcher will be the same. See the docs:

Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the

same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.

and as a final sidenote, I’ve never seen anyone use an aray as the root state of a module, I have no idea if that will work for vuex in all possible circumstances. I certainly would not recommend doing this.

使用来自同一成员的更好更完整的答案进行编辑:

Why watchers are asynchronous at all? Because in the vast majority of use cases, watchers only need to react to the last synchrnous change that was done. In most cases (in the context of a component), it would be couterproductive to to react to every change since you would re-trigger the same behaviour mutliple times even though in the end, only the last state is the important one.

In other words: Running a watcher on each change by default would probably lead to apps that burn a lot of CPU cycles doing useless work. So watchers are implemented with an asynchronous queue that is only flushed on nexTick. And we don’t allow duplicate watchers then because the older instance of a watcher would apply to data that doesn’t “exist” anymore in that state once the queue is flushed.

An important note would be that this only applies to synchronous changes or those done within a microtask, i.e. in an immediatly resolving or failing promise - it would, for example, not happen with an ajax request.

Why are they implemented in a way that they are still not run after a microtask (i.e. an immediatly resolved promise? That’s a bit more coplicated to explain and requires a bit of history.

Originally, in Vue 2.0, Vue.nextTick was implemented as a microtask itself, and the watcher queue is flushed on nextTick. That meant that back then, a watcher watching a piece of data that was changed two times, with a microtask (like a promise) in between, would indeed run two times.

Then, around 2.4 I think, we discovered a problem with this implementation and switched Vue.nextTick to a macroTask instead. under this behaviour, both data chhanged would happen on the current call stack’s microtaks queue, while the watcher queue would be flushed at th beginning of the next call stack, wich means it will only run once.

We found a couple of new problems with this implementation that are much more common than the original issue with microtasks, so we will likely switch back to the microtask implementation in 2.6. Ugly, but necessary.

所以,现在这应该可以解决问题:

await Vue.nextTick();