v-model 不是双向绑定

1,392 阅读3分钟

前言

从 Vue 0.x 开始,Vue 就用 v-model 来实现双向绑定,自 Vue2.0 以后,Vue 已经不是双向绑定,而大家通常所说的双绑只是看起来像双绑而已。

原因是当 FLUX 单向数据流兴起之后,Vue 的作者重新审视了「双向绑定」,发现「双向绑定」的一些问题之后,就更倾向于「单向绑定」了。

v-model 被拆成两部分。

基本用法

// template
 <div id="app">
    <h1>{{ name }}</h1>
    value: <input type="text" :value="name" /><br/> 
    v-model: <input type="text" v-model="name" /><br/> 
    v-model的本质:<input type="text" :value="name" @input="inputEvent" /><br/> 
    <!--还可以不用写下面的 methods 直接写-->
    <input type="text" :value="name" @input=" name = $event.target.value " /><br/> 
  </div>
// script
export default {
  data() {
    return {
      name: "hhh",
    };
  },
  methods: {
    inputEvent(event) {
      this.name = event.target.value;
    },
  },
};

页面由 data 中的数据渲染而来,当 data 中的数据发生改变时,页面会自动更新;当操作页面时,data 中的数据允许改变,页面才会发生改变。这就是 '双向数据绑定',也就是 MVVM。

contenteditable 的 bug

<!--在上面template 中添加如下代码 -->
    BUG:
    <span contenteditable @input="name = $event.target.innerText">
      {{ name }}
    </span>

v-m.gif

我的光标为什么会自动到前面去?

当我在 span里面输入 1 的时候,1 确确实实的写到里面了,这个 span 就会触发 input 事件,从而执行 name = $event.target.innerText

当 name (数据)变化以后,这个 span 就要重新渲染,Vue 记得住这个 span 里面的内容,但是记不住光标的位置。

这个里面当我第一次输入 1,再次输入的时候 span 重新渲染,光标就会自动跑到最前面(Vue忘记了光标的位置)。

这就是 Vue 的 “双向绑定”,实际上是在 {{ name }} 做了一个单向绑定,在 @input="name = $event.target.innerText"做了一个单向绑定。

小栗子1🌰

如果只有一个 input 双向绑定是非常爽的,那如果有两个 input 呢? 截屏2022-01-13 下午12.16.54.png 当我把第一个改成 ‘jack' 的时候,其他人在另一个 input上改成 ‘tom', 现在两个 input 里面的内容是什么? 不确定,因为不确定哪个是最后改的。

graph TD
hhh --> tom --> jack

hhh --> jack -->tom 

因为不确定性,那么在团队协作中就会导致,起点奇怪或者结果奇怪的现象。

这也就是双向绑定存在的问题,你的数据很可能在任何一个时间,被任何人改,改的时候不会通知你,你只知道在用的时候发现不对。虽然出现概率低,但会一直是一个隐患。

小概率事件必然发生 -- 墨菲定律

所以双向绑定在没有同级元素互相篡改的情况的下,是 OK 的。一旦出现了多个改变数据的源,那么数据就会变得不可控。

如何解决?

一个数据只能一个组件改,谁拥有这个数据谁就可以改这个数据。

这个 APP 它把 name 当作数据分别传给两个 input 显示,这两个组件不准改数据,只能显示数据。

如果要改就必须让这个组件通知数据的拥有者(图示的数据是 name),然后这个数据的拥有者才会去改,拥有者在改的时候就会同时改这两个的数据(比如都改成 jack)。 截屏2022-01-13 下午12.40.52.png 如果一个组件把这个改为 tom,那么组件自己不会先去改,而是通知数据拥有者(name),拥有者改了之后,这个组件才会去改成 tom。

<input type="text" 
       :value="name" 
       @input=" name =$event.target.value "/>
data(){
  return {
    name: 'hhh'
  }
}

这样一来,数据的变更权就回到数据拥有者手上了。

用一个 contenteditable 的 bug 来理解单向数据流。

小栗子2🌰

<input type="text" :value="amount" @input="inputEvent"/>
<input type="text" :value="amount" @input="inputEvent"/>
  data() {
    return {
      amount: 500
    };
  },
  methods: {
    inputEvent(event) {
      this.amount = event.target.value;
    },
  },

当改变第一个 input 的值的时候,它会通知它爸爸,我要修改数据,它爸爸同意了,才可以修改值的数据。

什么时候不同意呢?添加条件

  data() {
    return {
      amount: 500
    };
  },
  methods: {
    inputEvent(event) {
      if( event.target.value > 100 ){
        this.amount = event.target.value
      }else{
         event.target.value = 100
      }
    },
  },

这就是单向数据流,因为这么写很麻烦,所以 Vue 提供了 v-model 的语法糖

<input type="text" v-model="amount"/>
<input type="text" v-model="amount"/>

如果以双向绑定的思维去理解的话,就是当我 input 去改 amount ,amount 就变了,实际上不是。

Vue 通过两个单向绑定,来模拟双向绑定,所以我们可以去拦截数据。

小栗子3🌰

源代码

以下代码为完整版本的 Vue

<div id="app">
  爸爸
  <div style="border: 1px solid red">
    <child :selected="value"></child>  
  </div>  
</div>
Vue.component('child', {
  props: ['selected'],
  template: `
    <div>
      selected: {{selected}}  <!-- 这个 selected 不是这个组件拥有的-->
      <hr/>
      <button @click="selected=1">1</button>
      <button @click="selected=2">2</button>
    </div>
  `
})
var vm = new Vue({
  el:"#app",
  data: {
    value: 2   
  }
})

截屏2022-01-13 下午4.00.08.png 当我点击按钮 1 的时候,数据可以改变,但是会出现警告,告诉你应该避免直接修改 prop 数据 a1.gif 截屏2022-01-13 下午3.57.30.png

一个数据只能有一个人去改,这个人就是这个数据的拥有者

Vue.component('child', {
  props: ['selected'],
  template: `
    <div>
      selected: {{selected}}
      <hr/>
      <button @click="$emit("xxx", 1)">1</button>
      <button @click="$emit("xxx", 2)>2</button>
    </div>
  `
})
let vm = new Vue({
  el:"#app",
  data: {
    value: 2
  },
  template:`
    <div>
    爸爸
      <div style="border: 1px solid red">
        <child :selected="value" @xxx="value=$event"></child>
      </div>
    </div>
  `
})

以上代码点击按钮没有警告,说明 Vue 更倾向于单向数据流。

同时为了巩固这一模式,Vue 规定子组件不能修改父组件传给它的 props,一旦发现就会打印出一个警告。

Vue.component('child', {
  props: ['selected'],
  template: `
    <div>
      selected: {{selected}}
      <hr/>
      <button @click="$emit("update:selected", 1)">1</button>
      <button @click="$emit("update:selected", 2)>2</button>
    </div>
  `
})
let vm = new Vue({
  el:"#app",
  data: {
    value: 2
  },
  template:`
    <div>
    爸爸
      <div style="border: 1px solid red">
        <child :selected.sync="value" @xxx="value=$event"></child>
      </div>
    </div>
  `
})
<child :selected.sync="value" @xxx="value=$event"></child>
等价于
<child :selected="value" @update:selected="selected=$event"></child>

.sync 的作用和 v-model 一模一样,就是用双向绑定的语法糖,实现两个单向绑定。

单向数据流的优点

  1. 数据拥有者清楚地知道数据变化的原因和时机(因为是他自己操作数据的)
  2. 数据拥有者可以阻止数据变化

这些都是在双向绑定中很难做到的。

渐进式的 Vue

Vue 的另一个大特点就是「渐进式」,意思就是可以渐渐地用 Vue。而 React 几乎做不到这一点。

  1. 你可以继续操作 DOM
  2. 你可以很方便地做 SEO
  3. 你可以局部做单页面
  4. 你可以整体做单页面

computed V.S methods V.S. watch

  1. 触发时机
    watch: 被 watch 的值变化的时候执行一个函数
    methods: getMessage() 出现在视图里的时候,或视图更新的时候调用 getMessage
    computed: 同时满足两个情况:一、依赖的属性变化了 二、message 出现在视图里了或视图更新了
  2. 使用形式
    watch 的值需要用 data 承载,本身返回值没有用
    methods 的返回值可以直接展示在视图,但是要加括号
    computed 不能加括号