简体   繁体   中英

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

I've seen different similar topics here but they don't solve my problem. I try to combine Three.js with Vue3 (+Vuex). I thoroughly followed this tutorial: https://stagerightlabs.com/blog/vue-and-threejs-part-one (and it works on this site) but my identical code doesn't work, it throws the error:

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

I can't see anywhere in the code where state is mutated outside mutation handler. I don't understand why this error comes up. Maybe it has sth to do with Vue3 itself? App in the tutorial was most likely written in Vue2, not sure if it may couse a problem.

Here is my code. Sorry if it looks quite long but hope it helps detecting the root cause. ANd here is reproducible example as well: 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
};

The problem is the objects passed to threejs initialization are attached to the Vuex state, and threejs modifies the objects internally, leading to the warning you observed.

However, attaching the objects to Vuex state has another side effect of causing significant overhead in making the objects reactive. ThreeJS creates many internal properties, which would all be made reactive. That's why the app startup was severely delayed.

The solution is to mark them as raw data (and thus no reactivity needed) with the markRaw() API . ThreeJS could still attach properties to the given objects without creating a reactive connection between them:

// 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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM