为什么会出现 'do not mutate vuex store state outside mutation handlers' 错误?

Why 'do not mutate vuex store state outside mutation handlers' error shows up?

我在这里看到了不同的类似主题,但它们没有解决我的问题。我尝试将 Three.js 与 Vue3 (+Vuex) 结合使用。我彻底遵循了本教程:https://stagerightlabs.com/blog/vue-and-threejs-part-one(并且它在该站点上有效)但是我的相同代码不起作用,它抛出错误:

Uncaught (in promise) Error: [vuex] do not mutate vuex store state outside mutation handlers.

我在代码中的任何地方都看不到状态在突变处理程序之外发生突变。我不明白为什么会出现此错误。可能跟Vue3本身有关系?教程里的app估计是用Vue2写的,不知道会不会出问题

这是我的代码。抱歉,如果它看起来很长,但希望它有助于检测根本原因。这也是可重现的例子:https://codesandbox.io/s/black-microservice-y2jo6?file=/src/components/BaseModel.vue

BaseModel.vue

<template>
  <div class="viewport"></div>
</template>


<script>
import { mapMutations, mapActions } from 'vuex'

export default {
  name: 'BaseModel',
  components: {
  },
     
  data() {
    return {
      height: 0
    };
  },
  methods: {
    ...mapMutations(["RESIZE"]),
    ...mapActions(["INIT", "ANIMATE"])
  },

  mounted() {
    this.INIT({
      width: this.$el.offsetWidth,
      width: this.$el.offsetHeight,
      el: this.$el
    })
    .then(() => {
      this.ANIMATE();
      window.addEventListener("resize", () => {
        this.RESIZE({
          width: this.$el.offsetWidth,
          height: this.$el.offsetHeight
        });
      }, true);

    });
  }
}
</script>

<style scoped>
  .viewport {
    height: 100%;
    width: 100%;
  }
</style>

store.js

import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'
import {
    Scene,
    PerspectiveCamera,
    WebGLRenderer,
    Color,
    FogExp2,
    CylinderBufferGeometry,
    MeshPhongMaterial,
    Mesh,
    DirectionalLight,
    AmbientLight,
  } from 'three'


  const state = {
      width: 0,
      height: 0,
      camera: null,
      controls: null,
      scene: null,
      renderer: null,
      pyramids: []
  };

  const mutations = {
    SET_VIEWPORT_SIZE(state, {width, height}){
        state.width = width;
        state.height = height;
    },
    INITIALIZE_RENDERER(state, el){
        state.renderer = new WebGLRenderer({ antialias: true });
        state.renderer.setPixelRatio(window.devicePixelRatio);
        state.renderer.setSize(state.width, state.height);
        el.appendChild(state.renderer.domElement);
    },
    INITIALIZE_CAMERA(state){
        state.camera = new PerspectiveCamera(60, state.width/state.height, 1, 1000);
        state.camera.position.set(0,0,500);
    },
    INITIALIZE_CONTROLS(state){
        state.controls = new TrackballControls(state.camera, state.renderer.domElement);
        state.controls.rotateSpeed = 1.0;
        state.controls.zoomSpeed = 1.2;
        state.controls.panSpeed = 0.8;
        state.controls.noZoom = false;
        state.controls.noPan = false;
        state.controls.staticMoving = true;
        state.controls.dynamicDampingFactor = 0.3;
    },
    INITIALIZE_SCENE(state){
        state.scene = new Scene();
        state.scene.background = new Color(0xcccccc);
        state.scene.fog = new FogExp2(0xcccccc, 0.002);

        var geometry = new CylinderBufferGeometry(0,10,30,4,1);
        var material = new MeshPhongMaterial({
            color: 0xffffff,
            flatShading: true
        });

        for(var i = 0; i < 500; i++){
            var mesh = new Mesh(geometry, material);
            mesh.position.x = (Math.random() - 0.5) * 1000;
            mesh.position.y = (Math.random() - 0.5) * 1000;
            mesh.position.z = (Math.random() - 0.5) * 1000;
            mesh.updateMatrix();
            mesh.matrixAutoUpdate = false;
            state.pyramids.push(mesh);
        };

        state.scene.add(...state.pyramids);

        // create lights
        var lightA = new DirectionalLight(0xffffff);
        lightA.position.set(1,1,1);
        state.scene.add(lightA);
        var lightB = new DirectionalLight(0x002288);
        lightB.position.set(-1, -1, -1);
        state.scene.add(lightB);
        var lightC = new AmbientLight(0x222222);
        state.scene.add(lightC);  
    },
    RESIZE(state, {width, height}){
        state.width = width;
        state.height = height;
        state.camera.aspect = width/height;
        state.camera.updateProjectionMatrix();
        state.renderer.setSize(width, height);
        state.controls.handleResize();
        state.renderer.render(state.scene, state.camera);
    }
  };

  const actions = {
    INIT({ commit, state }, { width, height, el}){
        return new Promise(resolve => {
            commit("SET_VIEWPORT_SIZE", { width, height });
            commit("INITIALIZE_RENDERER", el);
            commit("INITIALIZE_CAMERA");
            commit("INITIALIZE_CONTROLS");
            commit("INITIALIZE_SCENE");

            state.renderer.render(state.scene, state.camera);

            state.controls.addEventListener("change", () => {
                state.renderer.render(state.scene, state.camera);  
            });

            resolve();
        });
    },
    ANIMATE({ dispatch, state }){
        window.requestAnimationFrame(() => {
            dispatch("ANIMATE");
            state.controls.update();
        });
    }
  }

  export default {
    state,
    mutations,
    actions
};

问题是传递给 threejs 初始化的对象附加到 Vuex 状态,并且 threejs 在内部修改对象,导致您观察到的警告。

但是,将对象附加到 Vuex 状态还有另一个副作用,即在使对象具有反应性时会导致大量开销。 ThreeJS 创建了许多内部属性,这些属性都将成为响应式的。这就是应用程序启动严重延迟的原因。

解决方案是使用 markRaw() API 将它们标记为原始数据(因此不需要反应性)。 ThreeJS 仍然可以将属性附加到给定的对象,而无需在它们之间创建反应性连接:

// store/main_three.js
import { markRaw } from "vue";
⋮
export default {
  ⋮
  mutations: {
    INITIALIZE_RENDERER(state, el) {
      const renderer = new WebGLRenderer(⋯);
      ⋮
      state.renderer = markRaw(renderer);
      ⋮
    },
    INITIALIZE_CAMERA(state) {
      const camera = new PerspectiveCamera(⋯);
      ⋮
      state.camera = markRaw(camera);
    },
    INITIALIZE_CONTROLS(state) {
      const controls = new TrackballControls(⋯);
      ⋮
      state.controls = markRaw(controls);
    },
    INITIALIZE_SCENE(state) {
      const scene = new Scene();
      ⋮
      state.scene = markRaw(scene);
      ⋮
      for (var i = 0; i < 500; i++) {
        var mesh = new Mesh(⋯);
        ⋮
        state.pyramids.push(markRaw(mesh));
      }
      ⋮
      // create lights
      var lightA = new DirectionalLight(0xffffff);
      ⋮
      state.scene.add(markRaw(lightA));
      var lightB = new DirectionalLight(0x002288);
      ⋮
      state.scene.add(markRaw(lightB));
      var lightC = new AmbientLight(0x222222);
      state.scene.add(markRaw(lightC));
    },
    ⋮
  },
  ⋮
};

demo