不只懂 Vue 语法:什麽是单向资料流和双向绑定?

问题回答

双向绑定(two-way data bindings)是指把画面上的 DOM 与资料透过 Vue 实体来绑定。当其中一方有更动时,另一方都会随即更新。

单向资料流是 Vue 官方提倡的一种管理元件资料状态模式。重点在於,当子层元件透过 props 来接收父层元件传来的资料时,子层不应该直接修改 props,不然开发时就难以追踪资料状态的变化。建议做法是透过 emit,触发事件来修改在父层的资料。也就是常见口诀:「props in, event out」的意思。

以下会再详细解说双向绑定、单向资料流、以及使用 props 时须注意的事。

双向绑定

双向绑定的重点就是当画面或资料有更新,对方也会随之更新。最明显的例子就是 v-model,我们很常在 input 栏位上 v-model 来绑定画面中输入栏目前的内容,以及在 data 里的资料。一旦用户输入内容,我们的 Vue 里的 data 资料也会同步拥有相同的资料,反之亦然。

单向资料流

单向资料流的概念是针对父子层元件的资料传递时的模式,它跟双向绑定的概念并没有冲突。当要把资料从父元件传递到子元件时,子元件会使用 props 来接收,但子元件不应该直接修改 props 来修改父元件的资料 ,而是使用 emit。

假如你是传入基本型别的资料,例如数字、字串等,你在子元件不会修改到父元件的资料,因为当父元件再被渲染时,它依旧要指向父元件所定义的资料,像以下例子:

HelloWorld.vue(子层)

<input v-model="childMsg" />
export default {
  props: {
    value: String,
    childMsg: {
      type: String,
      default: "default msg"
    }
  }
};

App.vue (父层)

<template>
  <div>
    {{ parentMsg }}
    <HelloWorld :childMsg="parentMsg" />
  </div>
</template>
export default {
  name: "Home",
  components: {
    HelloWorld
  },
  data() {
    return {
      parentMsg: "Hello vue!"
    };
  }
};

警告:

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.

父层的 parentMsg 并没有被修改,依旧为 "Hello vue!"。但 Vue 会把你以上的行为视作尝试修改父层资料,因此跳警告提示此举是无效。

如果是资料是物件呢?会否跳警告?

在父元件加入物件资料:

App.vue

data() {
    return {
      parentMsg: "Hello vue!",
      parent: {
        a: 123
      }
    };
}

绑定 props:

<template>
  <div>
    <p>{{ parentMsg }}</p>
    <p>{{ parent }}</p>
    <HelloWorld :childMsg="parentMsg" :child="parent" />
  </div>
</template>

HelloWorld.vue(子元件):

<template>
  <div>
    <input v-model="childMsg" />
    <input v-model="child.a" />
  </div>
</template>

<script>
export default {
  props: {
    value: String,
    childMsg: {
      type: String,
      default: "default msg"
    },
    child: {
      type: Object,
      default: {}
    }
  }
};
</script>

结果会直接改掉父层资料,而且 console 没有跳警告

官方文件有解释,这是因为物件和阵列是传址(pass by reference),因此会直接修改到父元件的资料。官方不建议这做法。

在 Vue 3,如果你的 Vue CLI 专案有加入 ESLint,当你的子元件的 input ,使用 v-model 直接绑定 props时,ESLint 就都会报错:

Unexpected mutation of "child" prop

但如果没有加入 ESLint,不管是修改物件还是基本型别资料,也不会报错,而且也能把父元子的资料改掉。

在 props 传送物件型别资料时,须注意的事项

为了保持单向资料流,就不能在子元件绑定 props。

如果 props 是基本型别资料,那就在 data 里建立属性来拷贝 props 的值即可,或者使用 computed 来处理资料再掉回画面使用,这些都是官方文件有提到的做法,因此就不作解释了。

但如果资料是物件或阵列,考虑到传址的问题,可以用以下方式解决:

1. 使用 v-bind,自动解构赋值

如果物件资料的属性不多的话,可以用 v-bind 来处理。示范如下:

假设在父元件有一笔物件资料,并传送到子元件的 input 里:

App.vue 资料(父元件):

  data() {
    return {
      user: {
        name: "Tom",
        age: 20,
      },
    };
  },

传入子元件(Child1):

<Child1 v-bind="user" />

在子元件里,设定 nameage 这两个 props,解构 user 物件并赋值到这两个 props 里,最後再在 data 拷贝这两个 props:

<template>
  <div>
    <input type="text" v-model="userName">
  </div>

  <div>
    <input type="text" v-model="userAge">
  </div>
</template>

<script>
export default {
  props: ["name", "age"],
  data() {
    return {
      userName: this.name,
      userAge: this.age,
    }
  }
};
</script>

但如果该物件资料的属性很多,就需要写很多个 props,因此此方法就不适合。

2. 使用 JSON.parse 处理

遇上物件属性很多的情况,我们可以直接把整个物件传进去子元件,并在 data 里使用 JSON.parse()JSON.stringify() 深拷贝这个物件,最後在 input 绑定拷贝的结果:

Child2(子元件):

<template>
  <div>
    <input type="text" v-model="user.name" />
  </div>

  <div>
    <input type="text" v-model="user.age" />
  </div>
</template>

<script>
export default {
  props: ["userInfo"],
  data() {
    return {
      user: JSON.parse(JSON.stringify(this.userInfo)),
      // 用展开语法也可以,但注意不要用在处理多层物件,但因为只能做浅拷贝
      // user: {...this.userInfo}
    };
  },
};
</script>

完整程序码示范两种方法

https://codesandbox.io/s/vue-chuan-wu-jian-props-shi-de-v-bind-jie-gou-he-json-parse-fang-fa-ghxfr?file=/src/components/Child.vue

props 的补充知识

使用 v-bind 才能传入数字

当我们要以 props 传入数字,必须要使用 v-bind 才可以。没有使用 v-bind 的话,一律当作传入纯字串。

字串:

<HelloWorld num="3" />

数字

<HelloWorld :num="3" />

注意,:v-bind 的缩写。

props 验证设定

官方在 style guide 提及,不建议以下的写法:

props: ['status']

Vue 建议用物件方式,加入型别检查、预设值等等:

props: {
  status: {
    type: String,
    required: true,
    validator: function (value) {
      return [
        'syncing',
        'synced',
        'version-conflict',
        'error'
      ].indexOf(value) !== -1
    }
  }
}

这是为了更严谨传入 props,减少错误。因为当错误传入 props时,Vue 会跳警告提醒。在例子中,如果 validator 回传 false,同样会跳黄字警告。

另外,有些情况我亦会使用 default 属性来设定预设值。例如在呼叫 API 时,需要等一笔阵列资料回传过来,再放入 props 里传给子元件,再在子元件使 v-for 显示阵列里每一笔的资料。如果在子元件的 props 先建立预设值,那麽使用 v-for 来绑定 props 时就不会报错。

总结

  • 双向绑定是指把画面上的 DOM 与资料透过 Vue 实体来绑定。只要对方有更新,另一方也会更新。
  • 单向资料流是管理父子元件资料状态的模式,资料只能由父元件传列子元件,子元件透过 props 来接收,但子元件不能以直接修改 props 的方式,改变父元件传来的资料。
  • 在子元件要改变父元件的资料,就要使用 emit 来实现。
  • 如果 props 的资料是物件型别,并需要把它放到 data 里使用的话,可以使用 v-bind 自动解构赋值,或者是深拷贝方法 JSON.parse()JSON.stringify 来处理。
  • 设定 props 时,建议用物件方式来建立,而非阵列。从而更严谨和仔细处理 props 的资料。

参考资料

重新认识 Vue.js - 2-2 元件之间的沟通传递


<<:  D13 删除特定的使用者文件

>>:  Day 4 jest的生命周期

Day25-实作

终於到了30天的尾声,该学的都学了! 接下来就是运用在实际的案例上。剩下的这几天我要跟着「重新认识V...

Day 26 (Js)

1. = =是判别左右相等为真 != 是判别左右不相等为真 2.function同名字,会执行後面的...

Day19:SwiftUI—Button

前言 今天来学习SwiftUI 的按钮 — Button。 实作 宣告一个 text 按钮 打开一个...

可爱的小企鹅

本篇文章分享笔者第一次接触到 Linux 作业系统的故事。 进入正题 相信大部分的读者看到标题就会知...

Day7 SQL

终於要进入到我熟悉的後端了,在Day5安装XAMPP时有勾选了PHP、MySQL、phpMyAdmi...