1.什么是双向绑定
所谓的双向绑定就是数据驱动,数据驱动是vue.js最大的特点,在vue中,用户界面发生的变化,开发者不需要手动的去修改DOM。
比如,我们点击一个button,需要元素的文本做一个“是/否”的切换操作,在传统jQuery中,对于页面修改的流程通常是:对button绑定事件,然后获取文案对应元素的dom对象,最后根据切换来修改dom对象的文本值。
2.Vue实现数据驱动(双向绑定)原理
Vue实现数据双向绑定主要采用数据劫持,配合发布者-订阅者模式我,通过
Object.defineProperty()
来劫持各个属性的setter
和getter
,在数据变动时发布消息给订阅者,触发相应监听回调
当一个普通JavaScript对象传给Vuie实例作为他的data选项时,Vue将遍历它的属性,用Object.defineProperty
将它们转为getter/setter。用户看不到getter/setter,但是在内部它们让vue追踪依赖,在属性被访问和修改时通知变化
vue的数据双向绑定将MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己Model的数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 => 视图更新;视图交互变化 => 数据model变更 双向绑定的效果
3.模拟vue数据驱动,实现input:v-model双向绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="num">
{{num}}
</div>
<script>
//发布者
class Dep {
constructor() {
this.subs = []
}
//注册订阅
addSub(sub) {
this.subs.push(sub)
}
//发布通知
notify() {
this.subs.map(sub => {
sub.update()
})
}
}
//订阅者
class Watcher {
constructor(vm, node, name) {
this.name = name
this.node = node
this.vm = vm
Dep.target = this
this.update()
Dep.target = null
}
//订阅者接口
update() {
this.node.nodeValue = this.vm[this.name]
this.node.value = this.vm[this.name]
}
}
class Vue {
constructor({el, data}) {
this.data = data
observe(this.data, this)
let ele = document.querySelector(el)
let dom = nodeToFragement(ele, this)
ele.appendChild(dom)
}
}
const app = new Vue({
el: '#app',
data: {
num: 1
}
})
//把vm.data的属性直接挂载到vm上,并对属性值进行代理检测
function observe(obj, vm) {
Object.entries(obj).map(([key, val]) => {
defineReactive(vm, key, val)
})
}
//代理检测
function defineReactive(obj, key, val) {
let dep = new Dep()
Object.defineProperty(obj, key, {
get() {
console.log('读取数据', val);
//TODO添加订阅者
if(Dep.target) {
dep.addSub(Dep.target)
}
return val
},
set(newVal) {
if(newVal === val) {
return
}
val = newVal
//TODO通知订阅者
dep.notify()
console.log('更新数据', val);
}
})
}
function nodeToFragement(node, vm) {
let flag = document.createDocumentFragment()
let child
while(child = node.firstChild) {
compile(child, vm)
flag.append(child)
}
return flag
}
//处理模板,提取{{插值}} 创建观察者
function compile(node, vm) {
let reg = /{{(.*)}}/
//nodeType === 1 => 元素类型
if(node.nodeType === 1) {
let attrs = node.attributes
for(let i = 0, len = attrs.length; i < len; i++) {
if(attrs[i].nodeName === 'v-model') {
let name = attrs[i].nodeValue
//TODO创建观察者,等待通知
new Watcher(vm, node, name)
node.addEventListener('input', e => {
console.log('123');
//触发setter,联动发布者发布通知
vm[name] = e.target.value
})
node.removeAttribute('v-model')
}
}
}
//nodeType === 3 text类型
if(node.nodeType === 3) {
if(reg.test(node.nodeValue)) {
let name = RegExp.$1
name = name.trim()
//TODO创建观察者
new Watcher(vm, node, name)
//触发getter,联动订阅者添加
node.nodeValue = vm[name]
}
}
}
</script>
</body>
</html>
<!--
核心思路在compile里面
function compile(node, vm) {
let reg = /{{(.*)}}/
//nodeType === 1 => 元素类型
if(node.nodeType === 1) {
let attrs = node.attributes
for(let i = 0, len = attrs.lengthl; i < len; i++) {
if(attrs[i].nodeName === 'v-model') {
let name = attrs[i].nodeValue
//TODO创建观察者,等待通知
node.addEventListener('input', e => {
//触发setter,联动发布者发布通知
vm[name] = e.target.value
})
node.removeAttribute('v-model')
}
}
}
首先检测
<input type="text" v-model="num">
提取v-model的值
-->
实现效果