大家好,我是HoMeTown,今天聊一聊老生长谈的Vue之双向数据绑定。
首先我们先看什么是双向数据绑定?
对于不是很了解设计模式的朋友,你可以先理解一下单向数据绑定,就是把数据绑定到视图,每次触发操作修改了数据,视图就会更新,数据 -> 视图,可以理解为MV,数据驱动视图。
举个🌰:

通过点击按钮set name,触发点击事件,手动更新变量name的值为HoMeTown,但是当我改变input输入框里的值,变量 name的值却不变,如下图:

那么双向数据绑定就是在单向的基础上,通过操作更新视图,数据自动更新,那上面的🌰来讲,就是我输入Input,变量name的值动态改变。视图 -> 数据,可以理解为VM,视图驱动数据。
Vue中的双向数据绑定由三个重要部分组成:
这个分层的架构方案,用专业术语来讲就是MVVM。
ViewModel干了两件事儿:
它由两个重要部分组成:
在Vue中,双向数据绑定的流程为:
Observe中Compile中Watcher,将来对应数据变化时,Watcher调用更新函数Dep来管理多个WatcherDep,然后通知这个Dep下所有的Watcher执行更新函数参考下图:

首先定义一个Vue类, 做三件事
class Vue {
constructor(options) {
this.$data = options.data
this.$options = options
// 数据劫持
observe(this.$data)
// 属性代理
proxy(this.$data)
// 模板编译
compile(el, this)
}
}
接下来开始实现observe函数,做三件事
function observe(obj) {
// 递归终止条件
if(!obj || typeof obj !== 'object') return // 是空的 && 不是一个对象
Object.keys(obj).forEach( key => {
// 当前key对应的value
const value = obj[key]
// value能到这里,有可能是object,需要递归劫持
observe(value)
// 为当前的key所对应的属性添加getter & setter
Object.defineProrerty(obj, key, {
// 当且仅当该属性的 `enumerable` 键值为 `true` 时,该属性才会出现在对象的枚举属性中。
enumerable: true,
// 当且仅当该属性的 `configurable` 键值为 `true` 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
configurable: true,
get() {
// 将new出来的Watcher实例进行收集
Dep.target ? dep.addSub(Dep.target) : null
},
set(newValue) {
if( val !== newValue ) dep.notify() // 通知执行
}
})
})
}
接下来实现Dep类,做两件事儿:
//依赖收集的类
class Dep {
constructor() {
// 所有Watcher实例存在这里
this.subs = []
}
// 添加Watcher实例
addSub(watcher) {
this.subs.push(watcher)
}
// 通知Watcher实例执行更新函数
notify() {
this.subs.forEach( w => w.update())
}
}
接下来实现订阅者Watcher类,做两件事:
class Watcher {
// callback中,记录了当前watcher如何更新自己的文本内容
// 与此同时,需要拿到最新的数据,所以,在new Watcher的时候,需要传递vm进来
// 因为需要知道在vm很多属性中,哪个数据,才是当前自己所需要的数据,所以,new Watcher的时候,需要指定key
constructor(vm, key, callback) {
this.vm = vm
this.key = key
this.callback = callback
// 把创建的watcher实例,在Dep.addSub时,存进Dep的subs里
Dep.target = this; // 自定义target属性
key.split(".").reduce((newobj, k) => newobj[k], vm);
Dep.target = null;
}
// 发布者通知Watcher更新的方法
update() {
const value = this.key.split(".").reduce((newobj, k) => newobj[key], this.vm);
this.callback(value)
}
}
最后实现compile,对HTML结构进行模板编译的方法:
function compile(el, vm) {
// 获取elDom元素
vm.$el = document.querySelector(el);
// 创建文档碎片,提高Dom操作性能
const fragment = document.createDocumentFragment();
// 取出来
while ((childNode = vm.$el.firstChild)) {
fragment.appendChild(childNode);
}
// 进行模板编译
replace(fragment)
// 放进去呀
vm.$el.appendChild(fragement)
function replace(node) {
// 定义匹配插值表达式的正则
const regMustache = /\{\{\s*(\S+)\s*\}\}/;
// \S匹配任何非空白字符
// \s匹配任何空白字符,包括空格、制表符、换页符等等。
// ()非空白字符提取出来,用一个小括号进行分组
// 当前的node节点是一个文本子节点,需要进行替换
if(node.nodeType == 3) {
const text = node.textContent // 文本子节点的字符串内容
const execResult = regMustache.exec(text) //为一个数组,索引为0的为{{name}},为1的为name,exec() 方法用于检索字符串中的正则表达式的匹配。
if(execResult) {
const value = execResult[1].split(".").reduce((newobj, k) => newobj[k], vm)
node.textContent = text.replace(regMustache, value)
// 此时,就可以创建Watcher实例,将这个方法存到watcher上,调用update就执行
new Watcher(vm, execResult[1], (newValue) => {
node.textContent = text.replace(regMustache, newValue)
});
// good good
}
// 递归结束
return
}
// 判断当前的node节点是否为input输入框
if(node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT' ){
// 首先要做v-model,就得先拿到属性节点
const attrs = Array.from(node.attributes);
const findResult = attrs.find((x) => x.name === "v-model");
if(findResult) {
// 当前有v-model,获取值
const expStr = findResult.value;
const value = expStr.split(".").reduce((newobj, k) => newobj[k], vm);
node.value = value;
// 创建Watcher实例
new Watcher(vm, expStr, (newValue) => {
node.value = newValue
})
// 监听input事件,拿到文本框最新的值,然后更新到vm上
node.addEventListener("input", e => {
const keys = expStr.split(".")
const keysLen = keys.length
const obj = keys.slice(0, keysLen - 1).reduce((newobj, k) => newobj[k], vm);
obj[keys[keysLen - 1]] = e.target.value
})
}
}
// 走到这,证明不是文本节点,递归处理
node.childNodes.forEach( child => replace(child))
}
}
还是用最开始我们的那个🌰,修改如下:
HTML
<div id="app">
<p>name:<span id="nameBox">{{name}}span>p>
<input v-model="name" id="ipt" type="text" />
<button id="set">Set namebutton>
div>
JS
const vm = new Vue({
el: "#app",
data: {
name: "No name yet!",
},
});
const setBtn = document.getElementById("set");
setBtn.onclick = function () {
vm.name = "Is HoMeTown!!";
};
点击按钮,修改Vue实例vm的属性name = 'Is HoMeTown!!':

可以看到已经成功了!这是单向,然后我们试一试,修改输入框的内容,上方name的值不会不跟着改变:

SUCCESS!!!
Vue中,双向数据绑定的原理总结的来说有几点:
Watcher,setter时通知Watcher.update依赖收集与通知执行订阅者执行更新v-model,给input添加事件完结~