Snake
Snake

Reputation: 121

Unexpected behavior with Object.assign

I use Object.assign() in several projects for a while and so far didn't have any unexpected experiences with the function. Today, I worked on some "auto saving" functionality and then realized that I need to "pause" (disable) the auto-saving feature for a moment to avoid double requests to the server. I went for a simple approach here to add a pause attribute to data.

So this is the solution I came up with (shortened):

    let app = new Vue({
        el: '#app',
        data: {
            pause: false,
            form: {
                title: '',
                slug: '',
                content: ''
            }
        },
        watch: {
            form: {
                deep: true,
                handler (newForm, oldForm) {
                    if (this.pause) { 
                        return
                    }
                    this.debounceSave(newForm, oldForm)
                }
            }
        },
        methods: {
            async save(newForm, oldForm) {
                await this.createPost()
            },
            async createPost() {
                try {
                    let response = await axios.post('https://jsonplaceholder.typicode.com/posts', {
                        title: 'foo',
                        body: this.form.content,
                        userId: 1
                    })
                    // disable auto saving....
                    this.pause = true
                    // I know the assignment doesnt make sense, just for testing reasons
                    Object.assign(this.form, {
                        title: response.data.title,
                        content: response.data.body
                    })
                    // and enable auto saving...
                    this.pause = false
                } catch (error) {
                    //
                }
            },
        },
        created() {
            this.debounceSave = _.debounce((n, o) => this.save(n,o), 300)
        },
        async mounted() {
            //
        }
    })

I noticed tough that if I use Object.assign then the auto saving feature is not disabled and pause remains "false", hence not deactivating the auto saving feature (bad...).

I played around it and could solve the problem by using a promise:

                async createPost() {
                try {
                    let response = await axios.post('https://jsonplaceholder.typicode.com/posts', {
                        title: 'foo',
                        body: this.form.content,
                        userId: 1
                    })
                    // disable auto saving....
                    this.pause = true
                    // I know the assignment doesnt make sense, just for testing reasons
                    new Promise((resolve, reject) => {
                        Object.assign(this.form, {
                            title: response.data.title,
                            content: response.data.body
                        })
                        resolve()
                    })
                    .then(() => this.pause = false)
                    // and enable auto saving...
                    //this.pause = false
                } catch (error) {
                    //
                }
            },
        },

When using a promise I had to make sure to uncomment

// this pause = false

Which just increased my confusing as this shouldn't have any effect (I might be wrong...).

So the question now is: What is going on? Why does the initial approach not work? Why do I have to wrap it inside a Promise to make it ? Is it because of the "shallow copy" nature of Object assign? And if yes, can somebody maybe explain it?

Here's a snippet:

let app = new Vue({
  el: '#app',
  data: {
    pause: false,
    form: {
      title: '',
      slug: '',
      content: ''
    }
  },
  watch: {
    form: {
      deep: true,
      handler(newForm, oldForm) {
        if (this.pause) {
          return
        }
        this.debounceSave(newForm, oldForm)
      }
    }
  },
  methods: {
    async save(newForm, oldForm) {
      await this.createPost()
    },
    async createPost() {
      try {
        let response = await axios.post('https://jsonplaceholder.typicode.com/posts', {
          title: this.form.title,
          body: this.form.content,
          userId: 1
        })
        this.pause = true
        new Promise((resolve, reject) => {
            Object.assign(this.form, {
              title: response.data.title,
              content: response.data.body
            })
            resolve()
          })
          .then(() => this.pause = false)
        //this.pause = false
      } catch (error) {
        //
      }
    },
  },
  created() {
    this.debounceSave = _.debounce((n, o) => this.save(n, o), 300)
  },
  async mounted() {
    //
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js" integrity="sha512-bZS47S7sPOxkjU/4Bt0zrhEtWx0y0CRkhEp8IckzK+ltifIIE9EMIMTuT/mEzoIMewUINruDBIR/jJnbguonqQ==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js" integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>

<div id="app">
  <h1>{{ pause }}</h1>
  Title <input type="text" v-model="form.title"><br> Content <input type="text" v-model="form.content">
  <p>{{ form }}</p>
</div>

Upvotes: 0

Views: 129

Answers (1)

hackape
hackape

Reputation: 19957

This behavior has nothing to do with Object.assign. You need to understand how Vue’s watcher does its job under the hood.

Long story short, the handler callback, by default, is not synchronously called, it’s scheduled asynchronously till before next render or next tick.

When you synchronously set pause=true, trigger the watcher, then set pause=false, by the time the async handler is called, pause is already set back to false. This also explains why the promise version works, cus promise is async too.

If you want handler to be a sync call, you need to also set the flush: "sync" watch option in v3, in v2 I guess it’s sync: true (pls double check yourself).

Upvotes: 1

Related Questions