不只懂 Vue 语法:为何元件里的 data 必须是函式?建立 data 时能否使用箭头函式?

问题回答

元件里的 data 必须是函式是为了确保元件里的资料不会被别的元件资料所污染。如果 data 是物件,因为 JavaScript 的物件是传址,一旦有元件的资料被修改,别的元件的资料也会被修改。因此,需要用函式,回传一个新物件的做法,确保自己元件的资料自己改,不会被污染。

另外,如果使用箭头函式建立 data,只要 data 物件里没有用到 this,就没有问题。因为这里的 this 不会指向 Vue ,而是 Window 物件,因此如果你打算使用 this 来取得 Vue 里的资料的话就会出错。

以下会作出详细解说。

建立 data 时需要使用函式

平常我们习惯在元件建立 data 时,使用 function return 的方式,假设有一个名为 <Example /> 的元件,里面有以下资料:

Example.vue

data(){
    return {
        foo: 1
    }
}

但以下的写法就会报错:
Example.vue

data: {
    foo: 1
}

Vue 规定需要使用函式回传一个新物件是为了避免元件资料互相污染。因为 JavaScript 物件是传址(pass by reference),因此,当 Example 这元件被重复在多个地方使用时,一旦其中一个元件的资料被修改,其他元件的资料也会一并被修改掉。例如我有 4 个 Example 元件,只要其中一个 Example 元件的资料被修改,其他 Example 元件也会受影响:

<Example />
<Example />
<Example />
<Example />

这个示范模拟了共用同一个物件作为元件资料的情况。

官方文件这里也有相关的示范例子。

注意: 从 Vue 3 开始,不论是根元件或子元件,都必须使用 function return,否则会报错。而在 Vue 2 则容许在根元件直接使用物件,但子元件仍然必须使用 function return。

建立 data 时,能否用箭头函式?

在建立 data 资料时,data 里面如果没有用到 this,就能放心使用箭头函式。原因是箭头函式的 this 会指向 Window 物件,不是 Vue 物件。

这个例子示范了以上提到的情况。

使用箭头函式:

const app = Vue.createApp({})

app.component('Message', {
  template: `
  <p> 目前 this 指向的物件:{{ thisObj }} </p>
  <p> 结果:{{ str }} </p>
  `,
  props: {
    msg: {
      type: String
    }
  },
  data: () => ({
      str: this.msg,
      thisObj: this // Window 物件
  })
})

app.mount('#app')

结果:

结果没显示到 str,但使用 Vue 检查工具时会发现,str 是 undefined:

因为 Window 不会有 str 属性,因此是 undefined
使用传统函式看看:

data() {
    return {
      str: this.msg,
      thisObj: JSON.stringify(this) // Vue 的 data 物件
    }
}

这里使用 JSON.stringify 把 Vue 的物件显示出来,否则会报错。

结果:

查看 Vue 检查工具:

因此,使用箭头函式是没问题。但如果 data 里有用到 this,就会出错。因为 this 会指向 Window 物件,而非 Vue 的物件,因此无法正确取到在 Vue 所建立的资料。

在 data 里使用 computed 资料?

在复习此题目时,想起能不能在 data 的资料里,使用 this 来取得 computed 里的资料。虽然这个做法没必要,因为 computed 里的资料本身是函式,它会回传一个值,所以平常我们只需要直接取 computed 的值来用即可。像是这样:

<p> {{ addSomeText }} </p>
computed: {
    addSomeText() {
        return 'Add some text'
    }
}

结果画面就会显示 "Add some text"。

虽然没必要在 data 取得 computed 的资料,但还是想试试,如果在 data 里取得 computed 里的值会怎样:

<div id="app">
  <Message job="Web developer" />
</div>
const app = Vue.createApp({})

app.component('Message', {
    template: 
        `<p> {{ str }} </p>`
    ,
    props: {
      job: {
        type: String
      }
    },
    data(){
      return{
        str: this.addSomeText,
        name: 'Alysa'
      }
    },
    computed: {
      addSomeText() {
        return 'Add some text'
      }
    }
    })
    
app.mount('#app')

结果 strundefined。即使是使用箭头函式还是这里用到的传统函式,str 的值都一样是 undefined

原因不在於 data 使用箭头函式与否,而是生命周期的问题。因为 Vue 会先建立 data 资料,之後才建立 computed 的资料,因此在 data 里无法取到 addSomeText 的值,因为 addSomeText 在 data 建立时是 undefined

试试以下例子,就会发现 str 能成功取到值:

HTML:

<div id="app">
  <Message job="Web developer" />
</div>

JavaScript:

const app = Vue.createApp({})

app.component('Message', {
        //在画面呼叫 str 函式
        template: 
          `<p> {{ str() }} </p>`
        ,
        props: {
          job: {
            type: String
          }
        },
        data(){
          return {
            // 把 str 改为函式,回传 this.addSomeText
            str() {
              return this.addSomeText
            },
            name: 'Alysa'
          }
        },
        computed: {
          addSomeText() {
            return 'Add some text'
          }
    }
})

app.mount('#app')

这是因为我们在 template 里呼叫 str 函式,意思就是当整个画面都被渲染好後,才会呼叫 str,这时候就一定能取到 computed 里的资料。

题外话,在这情况下,我们用箭头函式建立 data 也会成功取到值,但 str 必须要是传统函式:

JavaScript:

data: () => ({
    str() { 
      return this.addSomeText
    },
    // 以下会回传 undefined
    // str: () => this.addSomeText, 
    name: 'Alysa'
}),

原因是我们在画面中,在 {{ }} 里呼叫 str。这两个大括号是指向 Vue 实体物件里的状态,所以事实上我们是透过 Vue 物件来呼叫 str,因此 str 里的 this 会指向 Vue。

关於 this 的运作,此文章的最後部分会再简单重温一遍。

完整程序码示范

https://codepen.io/alysachan/pen/jOwZRNa?editors=1011

为什麽使用 this 可以取到 Vue 里的资料?

写 Vue 时我们都习惯使用 this 就能取得在 Vue 的资料,包括 datacomputed 以及呼叫在 methods 建立的方法等等。

HTML 的部分:

<div id="app">
  <User job="Web developer" />
</div>

Vue 的部分:

const app = Vue.createApp({})
app.component('User', {
  props: {
    job: {
      type: String
    }
  },
  template: `<p> {{ fullName }} </p>`,
  data: () => ({
      firstName: 'Alysa',
      lastName: 'Chan'
  }),
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`
    }
  },
  methods: {
    greeting(){
      console.log(`Hi I'm ${this.fullName}`)
    }
  },
  mounted() {
    console.log(this) // Proxy 物件
    this.greeting() // Hi I'm Alysa Chan
  }
})
app.mount('#app')

这时候查看 console 会发现一个由 Vue 包装好的 Proxy 物件:

由此可见,Vue 会把所有建立的资料和函式全都放在同一个物件上,并且成为 Proxy 代理的 target。在以上例子中,我在 methods 里使用 this.fullName来引用在 computed 里的 fullName。该 this 会指向这个 target 物件里的 fullName

因此,以上情况就等於以下的写法:

const obj = {
    firstName: 'Chan',
    lastName: 'Alysa',
    fullName() {
        return `${this.lastName} ${this.firstName}`
    },
    greeting(){
        console.log(`Hi I'm ${this.fullName()}`)
    }
}

obj.greeting() // Hi I'm Alysa Chan

补充一点,Vue 是使用 Proxy 来实现响应式更新(Reactivity)。我在前几天的文章有讨论过,有兴趣的话欢迎看看。

简单重温 this 的概念

这里用以上例子,稍为重温 this 的概念,如果把 fullName 改为箭头函式,会出现什麽结果?

const obj = {
    firstName: 'Chan',
    lastName: 'Alysa',
    fullName: () =>`${this.lastName} ${this.firstName}`,
    greeting(){
        console.log(`Hi I'm ${this.fullName()}`)
    }
}

obj.greeting()

结果 console 会出现:Hi I'm undefined undefined

在回答这问题之前,要先知道几个核心概念:

  • this 只存在於函式里。
  • this 的值是取决於怎样呼叫这函式。
  • 传统函式里 this 的值,会指向你呼叫此函式时,所引用的物件。
  • 箭头函式里 this 的值,会继承上一层函式 this 所指向的值。在全域时,就会指向 Window 物件。

最後两点可能比较难理解,但套用到题目里就更清晰了。

题目中,第一步是用 obj.greeting() 来呼叫 greeting 函式。

greeting 是使用传统函式。因此,在 greeting 里的 this.fullName,这个 this 会指向 obj 这物件。上面提过,传统函式里 this 的值,会指向你呼叫此函式时,所引用的物件。在这里,我是用 obj 来呼叫 greeting,所以 greeting 里的 this 会指向 obj

第二步,就是在 greeting 里呼叫 this.fullName(),因为之前提到,这里的 this 是指向 obj,所以意思就是 obj.fullName(),换言之,即是呼叫在 obj里的 fullName 函式。

但是,fullName 是使用箭头函式。 虽然使用 obj.fullName() 来呼叫fullName,但在 fullName 里的 this不会指向 obj。箭头函式里的 this 会往上一层找,看看有没有函式,以及这个函式所指向的 this 是什麽。 但目前 fullName 再往上层找,只有 obj 这物件,并没有函式。直至找到全域,并指向 Window 物件。因为 Window 不会有 lastNamefirstName,所以结果就是 undefined

补充一点,以上提到箭头函式里的 this 需要往上层找函式,更准确的说法是,需要往上层找作用域,而函式会建立一个作用域,但物件不能。因此,在 greeting 里的 this 往上一层找是 obj,但 obj 不会建立一个作用域,最後导致找到全域,并指向 Window 物件。

去年我的铁人赛系列,JavaScript 基本功修炼,有关於箭头函式this 的文章,有兴趣的话也欢迎再参考看看。

总结

  • 建立 data 时,需要用函式,把 data 资料放在此新物件里,并回传出来。原因是避免因为 JavaScript 的物件传址的特性,造成元件之间的资料互相污染。
  • 只要 data 里没有用到 this,就可以使用箭头函式来建立 data。否则,会因为this指向 Window 物件,而非 Vue 物件而造成取值时出错。
  • Vue 会先建立 data 资料,之後才会建立 computed 资料。
  • 传统函式里 this 的值,会指向你呼叫此函式时所引用的物件。箭头函式里 this 的值,会继承上一层函式 this 所指向的值。在全域时,就会指向 Window 物件。

参考资料

重新认识 Vue.js - 1-2 Vue.js 的核心: 实体
重新认识 Vue.js - 2-1 元件系统的特性
Vue JS: Difference of data() { return {} } vs data:() => ({ })
Use computed property in data in Vuejs


<<:  白字黑字记录,足以降低有人要你当替罪羔羊

>>:  EP06 - 从零开始,在 AWS 上建置 Jenkins 使用 Terraform

铁人赛 Day24 -- JavaScript 初体验(二) -- 点击後换图片

前言 最近因为上班进度缓慢,所以内容比较慢,但应该也只能这样了哈哈,我们今天也一样会用到 Oncli...

Day26 vue.js功能展示ep2之有"大麻"烦(cros跨域)

延续昨日 我们今天来完善功能测试 首先设一个runtest的function async runte...

Day 19 - 网页元素DOM - 表单元件的Event,表单的type 设定

制作表单 createRadio(); 沿用上一个文章的参考 加上以下设定 我们可以用radio去做...

EP27 - 建立 VPN 连线,直接连线到 AWS

今天是要来填之前未补之坑, 那就是建立 VPN 连线, 以小公司来说, 其实能够快速加快产品上市比较...

Day 23:「儿子,这是你的零用钱」- 元件间的资料传递

兔大夫: 「请问是兔豪的家属吗?」 兔豪爸: 「是,我就是。 请问我鹅子他...」 兔大夫: 「抱...