阅读提示: 本文适合1-2年工作经验,熟悉vue框架的开发者阅读。如果你没用vue写过实战项目,阅读本文的内容会有些吃力,建议先补足实战经验再来看。
文章里关于vue的一些api,我会讲的比较底层,这样你才能理解vue是怎么处理数据流的。
数据流是什么?
简单来讲,就是组件之间数据的流向。为什么搞清楚这一点很重要?
如果你搞不清楚数据的流向,开发中就很难找到问题所在,我就在调bug的时候,因为找不到是哪个地方修改了数据,耗费了我大量的时间和精力,猛扣头皮!!
要不然就是动了一个地方的数据以后,其他的数据就突然出现异常,但我却不知道为什么。
所以说,了解和设计出一个好的数据流,有利于后期的开发维护。
数据流类型
由于笔者用vue比较多,所以主要讲一下单向数据流和全局数据流,这是vue开发主流的两种数据流类型。
单向数据流
实际上 vue这个框架,是典型的单向数据流。
单向数据流,指的是数据只能从父组件向子组件传递,子组件无法改变父组件的props。如果想要修改只能通过emit事件触发的方式,在父组件中修改。
一言以蔽之,所有数据的更改都是从父组件传递到子组件,都是自上而下的。
单向数据绑定:props
在父传子的前提下,父组件的数据发生改变,会通过props来通知子组件自动更新。
子组件内部,不能直接修改父组件传递过来的props——props是只读的。
这样做的好处是,防止多个子组件都尝试修改父组件状态时,让这一行为变得难以追溯。
如果子组件改了props的值会怎么样呢?这里分情况讨论:
- 如果子组件修改的
props数据类型,是引用数据类型,那么绑定的其实是引用地址,子组件修这个引用类型的值,vue不会报警告,但是建议别这样做。 - 如果绑定的
props是基础类型值,子组件直接修改父组件传递的props,vue会抛出警告。
所以,尽量使用单向数据流,这样做的优点是:
- 单向数据流会使所有状态的改变可记录、可跟踪,源头易追溯;
- 所有数据只有一份,组件数据只有唯一的入口和出口,使得程序更直观更容易理解,有利于应用的可维护性。
双向数据绑定
父组件可以通过props改变子组件的数据,子组件也可以用emit事件通知父组件的改变数据。双方数据可以互相更新,这就是双向数据绑定。
实现方式:props加emit
以下代码示例,父组件代码:
<template>
<div>
<h1>我是父组件</h1>
<Son :info="info" @change="fn"></Son>
</div>
</template>
<script>
import Son from "./Son.vue";
export default {
data() {
return {
info: "我是父组件中的数据",
};
},
components: {
Son,
},
methods: {
fn(info) {
this.info = info + "我是父组件中点击修改后的数据";
},
},
};
</script>
子组件代码:
<template>
<div>
<h2>我是子组件</h2>
<p>{{ info }}</p>
<button @click="fn">修改数据(子)</button>
</div>
</template>
<script>
export default {
props: ["info"],
methods: {
fn() {
//这种直接赋值prop是不可取的,vue会直接报错
//this.info=this.info+"子组件直接赋值prop"
// 修改数据
this.$emit('change',this.info + ",我现在被子组件emit了")
},
},
};
</script>
分析数据流向分析
(1)我们在父组件中定义了数据:一个叫info的字符串;
(2)调用组件的时候,通过:info="info"的方式,将父组件的数据以 prop 的方式传递到子组件中;
(3)子组件读取到info,并将其展示在模板中;
(4)用户点击按钮,info被修改;
(5)子组件监听到这个事件,但它并不直接修改info,而是通过 this.$emit('change'); 以自定义事件的形式,将需要增加的这一个事件报告给父组件;
(6)父组件中,由于通过执行过@click="fn" ,能够监听到子组件报告过来的事件,并在自己的 方法中,实现info数据的更新;
(7)父组件里的数据更新了,通过props的机制,子组件里的数据也将自动更新,同时也将更新界面内容,这一过程由框架自动完成。
整体过程如下图所示:
双向数据绑定语法糖:v-model
在vue中,提供了v-model这个语法糖,相信大家都不陌生下面这段代码。:
<input type="text" v-model="name">
在这里,我们在输入框内输入值,父组件的值也会直接改变。
看上去好像视图的操作直接修改了父组件的值,所以很多人以为这是一种双向数据流,即子组件可以直接修改父组件的值。
但其实不是这样,因为v-model的本质相当于:
<input type="text" :value="name" @input="name = $event.target.value">
我们仔细观察这段代码,可以得出一个结论:
在给<input />元素添加 v-model 属性时,默认会把 value作为元素的属性,然后把input事件作为实时传递 value 的触发事件。
你发现没有,其实这个和上面那个props加emit的案例,是一个套模式,都是子组件通过自定义事件通信父组件改值。
这里vue自己处理了input原生的input 事件,这是 HTML5 新增加的,类似 onchange ,每当输入框内容发生变化,就会触发 input 事件,把最新的value值传给传递给val,完成双向数据绑定的效果 。
相当于vue把input这个组件重写为下面的代码:
<template>
<input type="text" :value="value" @input="handleInput" :placeholder="placehodler" />
</template>
<script>
export default {
name: 'kInput',
props: {
value: ['String', 'Number'],
placeholder: String
},
methods: {
handleInput ($event) {
// 通过input标签的原生事件input将值emit出去,以达到值得改变实现双向绑定
this.$emit('input', $event.target.value)
}
}
}
</script>
<style scoped type="less">
</style>
注意: 在HTML5中,不是所有能进行双向绑定的元素都是 value property 和input事件。
1. text 和 textarea 元素使用 value 作为 prop 和 input 事件;
2. checkbox 和 radio 使用 checked 作为 prop 和 change 事件;
3. select 字段将 value 作为 prop 并将 change 作为事件。
它们都有一个共通点:依赖一个value作为prop,以及emit事件传递新值。
所以,如果我们要在自定义组件中使用 v-model 进行双向绑定,必须满足以下两个条件:
- 将其
valueattribute 绑定到一个名叫value的prop上 - 在数据发生变化时 一个带新值的 input 事件,将新的值通过
$emit把 input 事件抛出
如果我想改值的名字怎么办?我不想用value和input这些名字怎么办?
vue也提供了model这个api,让我们可以更改value和input的名字,我们把第一段案例的代码用自定义model重写一下:
<template>
<div>
<h1>我是父组件</h1>
<!--不用v-model的写法-->
<Son :info="info" @change="fn"></Son>
<!--用v-model的写法,可以省略 @change="fn"-->
<Son v-model="info"></Son>
</div>
</template>
<script>
import Son from "./Son.vue";
export default {
data() {
return {
info: "我是父组件中的数据",
};
},
components: {Son},
methods: {
fn(info) {
this.info = info + "我是父组件中点击修改后的数据";
},
},
};
</script>
子组件代码:
<template>
<div>
<h2>我是子组件</h2>
<p>{{ info }}</p>
<button @click="fn">修改数据(子)</button>
</div>
</template>
<script>
export default {
model:{
// 默认值是value,这里指定info这个prop为v-model
prop: 'info',
// 修改值的自定义事件,默认值为input,我们这里改为change
event: 'change'
}
props: ["info"],
methods: {
fn() {
// 修改数据
this.$emit('change',this.info + ",我现在被子组件emit了")
},
},
};
</script>
可以看到,和第一个案例不同,使用v-model后,父组件不必处理子组件的@change事件,也能实现数据的更新。
之所以用v-model后能简写父组件的@change事件,是因为vue默认帮我们处理了事件的赋值。
相当于vue自动加上去了这句代码:@change="val=>info = val"
看到这里你是不是就明白了,v-model虽然是叫双向数据绑定,但它不是双向数据流(这是很多人的误区,以为子组件的操作不经过任何处理,就直接能修改父组件的值)。
其实v-model本质上还是单向数据流,属于第一种props加emit的处理流程的简写。
v-model只是帮我们减少了父组件控制数据流向的代码,让用户的操作可以通过单向的数据流向,更新到绑定值中,绑定值再通过props的渲染更新到页面上。
让我们用户用起来的感觉,是我们操作视图的变化而改变了数据。 但其实它的数据流依然和第一个案例一样,子组件无法直接改值,数据依然是从父传给子。
流程如下图所示:
v-model优点
优点:双向数据绑定的优点是无需进行和单向数据绑定的那些CRUD(Create,Retrieve,Update,Delete)操作。
双向数据绑定最经常的应用场景就是表单了,这样当用户在前端页面完成输入后,不用任何操作,我们就已经拿到了用户的数据存放到数据模型中了,在表单交互较多的场景下,会简化大量业务无关的代码。
v-model缺点
虽然v-model很方便,但它也是有缺点的:
v-model有数量的限制,一个组件只能有一个v-model。- 双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件两侧都没有明显的变更来源(这点在使用别人组件库时尤为明显),无法追踪局部状态的变化,潜在的行为增加了debug的难度。 由于组件数据变化来源入口变得可能不止一个,数据流转方向易紊乱。为了解决这个问题,我推荐你使用sync指令修饰符。
- 做组件库的时候,如果封装了v-model,就一定要在文档里讲清楚是哪个自定义事件修改的值。 否则父组件改值不可控,子组件说改就改,因为不知道覆盖哪一个自定义事件。
vant2弹出框案例
其实做了v-model,也支持父组件覆盖默认的@change事件
<input type="text" v-model="name" @input="name ='我覆盖了:' +$event.target.value">
但是你不告诉别人是哪个自定义事件改的值,非要人家看你的源码才知道答案,这就很不友好。
双向数据绑定的修饰符:.sync
.sync修饰符可以实现和v-model同样的功能,而且它比v-model更加灵活
- 因为
.sync修饰符是可选的,父组件想加就加,不加就没有双向绑定。 - 不像
v-model那样有数量上的限制,想用几个就用几个。 .sync修饰符的本质
// 正常父传子:
<son :info="str" :title="str2"></son>
// 加上sync之后父传子(.sync没有数量限制):
<son :info.sync="str" .title.sync="str2"></son>
// 它等价于
<son
:info="str" @update:info="val=>str=val"
:title="str2" @update:title="val=>str2=val"></son>
// 相当于多了一个事件监听,事件名是update:info,回调函数中,会把接收到的值赋值给属性绑定的数据项中。
这里面的传值与接收与正常的父向子传值没有区别,唯一的区别在于往回传值的时候$emit所调用的事件名必须是update:属性名,如下所示:
<template>
<div>
<p>{{ info }}</p>
<button @click="fn">修改数据(子)</button>
</div>
</template>
<script>
export default {
props: ["info","title"],
name:'son',
methods: {
fn() {
// 修改数据:`$emit`所调用的事件名必须是`update:属性名`
this.$emit('update:info',this.info + ",我现在被子组件emit了")
},
},
};
</script>
事件名写错不会报错,但是也不会有任何的改变,这点需要多注意。
注意事项:
1.注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用,例如:
v-bind:title.sync=”doc.title + ‘!’”
这是无效的。取而代之的是,你只能提供你想要绑定的 property 名,类似 v-model。
2.不要将 v-bind.sync 用在一个字面量的对象上,例如:
v-bind.sync=”{ title: doc.title }”
这是无法正常工作的,官网给出的理由是:因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
总之,如果你封装组件,除非这个值是input或者checkbox这种,不需要校验就能直接改值的组件,可以直接封装成v-model,否则我都建议你用sync来处理双向绑定,这样更加灵活。
下期我们会讲讲在多个组件中如何维护同一个数据。