在使用Vue.js自定义组件时,很多时候,我们都期望数据是双向绑定的。
Vue.js实现双向数据绑定的两种方式
1. v-model
调用组件时:
<comp v-model="something"></comp>
或
<comp :value="something"></comp>
在组件内部,必须:
- 接受一个
value
属性 - 在有新的 value 时触发
input
事件
this.$emit('input', newValue)
如:
<comp v-model="something"></comp>
Vue.component('comp', {
...,
props: ['value'],
computed: {
currentValue: {
get () {
return this.value
},
set (val) {
this.$emit('input', val)
}
}
},
...
})
2. props .sync 修饰符
调用组件时:
<comp :foo.sync="bar"></comp>
在组件内部,
- 接受一个
foo
属性 - 需要更新
foo
时,它需要显式地触发一个更新事件
this.$emit('update:foo', newValue)
如:
<comp :foo.sync="bar"></comp>
Vue.component('comp', {
...,
props: ['foo'],
data () {
return {
currentValue: this.foo
}
}
watch: {
currentValue (val, oldVal) {
this.$emit('update:foo', val)
}
}
...
})
Vue实现双向数据绑定的原理分析
v-model
用于双向绑定数据,其本质为语法糖,即
<comp v-model="something"></comp>
相当于
<comp v-bind:value="something" v-on:input="something = $event.target.value"></comp>
props .sync
修饰符 用于双向数据绑定,其本质也为语法糖,即
<comp :foo.sync="bar"></comp>
相当于
<comp :foo="bar" @update:foo="val => bar = val"></comp>
以上,我们可以很容易的看出,实现一个自定义组件的双向数据绑定,其实就是父组件传递一个属性给子组件,在子组件中该属性的值改变时显式的去触发一个事件(v-model触发input事件,.sync触发update:props事件)。
那在Vue.js内部究竟是如何实现双向数据绑定的呢?我们下面继续分析下。
在Vue.js中,采用观察者-订阅者模式来进行双向数据绑定,通过Object.defineProperty()
方法来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
既然是观察者-订阅者模式,那观察者-订阅者是如何实现的呢?我们来看源码
观察者(Observer)关键代码:
class Observer {
constructor (data) {
this.walk(data)
}
walk (data) {
// 遍历 data 对象属性,调用 defineReactive 方法
let keys = Object.keys(data)
for(let i = 0; i < keys.length; i++){
defineReactive(data, keys[i], data[keys[i]])
}
}
}
订阅者(Watcher)关键代码:
class Watcher {
constructor(vm, expOrFn, cb) {
this.cb = cb
this.vm = vm
this.expOrFn = expOrFn
this.value = this.get()
}
update(){
this.run()
}
run(){
const value = this.get()
if(value !==this.value){
this.value = value
this.cb.call(this.vm)
}
}
get(){
const value = this.vm._data[this.expOrFn]
return value
}
}
观察者和订阅者都有了,但是它们如何进行通信呢?
首先,
观察者会遍历 data 对象的所有属性,每个属性通过调用 defineReactive
方法,转换为getter/setter
defineReactive 关键代码如下:
function defineReactive(obj, key, value) {
var dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
if (Dep.target) {
dep.depend() // 依赖收集
}
return value
},
set: function reactiveSetter(newVal) {
if (value === newVal) {
return
}
value = newVal
// 对新值进行观测,如果改变则通知订阅者
dep.notify()
}
})
}
defineReactive 方法将data的属性转换为访问器属性
get时进行依赖收集,
set时,如果数据有改变,则进行订阅通知
通过上面的分析,我们知道了,观察者观测到数据更新时会通知订阅者,但是它是如何通知订阅者(Watcher)的呢?
当然是通过订阅器了!
订阅器(Dep)关键代码如下:
class Dep {
constructor (id, subs) {
this.id = 0++
this.subs = []
}
addSub () {
this.subs.push(sub)
}
removeSub () {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
通过订阅器,订阅者接收到数据改变的通知
由此,Observer
、 Dep
、 Watcher
就形成了一个数据响应系统,也就是Vue.js实现双向数据绑定最核心的原理。