请问vue中双向数据绑定是如何实现的?MVVM原理是什么?
vue中的双向数据绑定是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter;在数据变动时发布消息给订阅者,触发相应的回调更新函数。通过Observer来监听model数据变化,通过Compile来解析编译模板指令;当数据发生变化时,Observer发布消息给Watcher(订阅者),订阅者通过调用更新函数来更新视图。Watcher搭起了Observer和Compile之间的通信桥梁,从而达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
实现一个Compile(编译器)
用于解析指令,并初始化视图。并在此时创建订阅者,绑定更新函数,在数据变化时更新视图或数据。
class Compile{
constructor(el, vm) {
// 判断el是否是一个元素节点对象
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// 1 获取文档碎片对象 放入内存中 减少页面的回流和重绘
const fragement = this.node2Fragment(this.el)
// 2 编译模板
this.compile(fragement)
// 3 追加子元素到根元素上
this.el.appendChild(fragement)
}
compile(fragement) {
// 1 获取所有子节点
let childNodes = fragement.childNodes
childNodes = [...childNodes]
childNodes.forEach(child => {
if (this.isElementNode(child)) {
// 是元素节点
// 编译元素节点
// console.log('元素节点', child)
this.compileElement(child)
} else {
// console.log('文本节点', child)
// 编译文本节点
this.compileText(child)
}
if (child.childNodes && child.childNodes.length) {
// 递归遍历编译所有子节点
this.compile(child)
}
})
}
// 编译解析元素节点
compileElement(node){
// 元素节点 v-html v-model v-text等指令或者事件绑定
let attributes = node.attributes
attributes = [...attributes]
// 拿到所有的属性 解析出指令
attributes.forEach(attr => {
const {name, value} = attr // 例如:v-text msg
// console.log(attr, name, value)
if (this.isDirective(name)) {
// 判断是否是v-开始 表示是一个指令 v-text v-html v-model v-on:click
const [, dirctive] = name.split('-') // text html model on:click
const [dirName, eventName] = dirctive.split(':') // dirName: text html model on eventName: click
// 更新数据 数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName)
// 删除带有指令标签的属性
node.removeAttribute('v-'+dirctive)
} else if (this.isEventName(name)) {
// @click="handleClick"
let [,eventName] = name.split('@')
compileUtil['on'](node, value, this.vm, eventName)
// 删除带有@的属性
node.removeAttribute('@'+eventName)
} else if (this.isBindName(name)) {
let [, attrName] = name.split(':')
compileUtil['bind'](node, value, this.vm, attrName)
// 删除带有:的属性
node.removeAttribute(':'+attrName)
}
})
}
// 编译解析文本节点
compileText(node) {
// {{}} 对应类似v-text
const content = node.textContent
if (/\{\{(.+?)\}\}/.test(content)) {
// 正则匹配含有双大括号的文本 并且
// console.log(content)
compileUtil['text'](node, content, this.vm)
}
}
实现一个Update(更新方法)
通过解析指令以及文本,在数据变化时,通过操作dom节点,更新视图;修改data,更新数据。
// 更新函数
updater: {
textUpdater(node, value) {
node.textContent = value
},
htmlUpdater(node, value) {
node.innerHTML = value
},
modelUpdater(node, value) {
node.value = value
},
bindUpdater(node, attrName, value) {
node.setAttribute(attrName, value)
}
}
}
实现一个Watcher(订阅者)
数据发生变化时,调用回调函数,更新视图或数据。
class watcher{
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
// 先保存旧值 用于判断新值传入时 是否有变化
this.oldVal = this.getOldVal()
}
getOldVal() {
Dep.target = this
const oldVal = compileUtil.getVal(this.expr, this.vm)
// 在调用getVal时会触发observer中defineReactive中 object.defineProperty 中的get函数
// 在get函数中拿到该watcher并添加到dep中
Dep.target = null
return oldVal
}
update() {
const newVal = compileUtil.getVal(this.expr, this.vm)
if (this.oldVal !== newVal) {
this.oldVal = newVal
this.cb(newVal)
}
}
}
编译模板指令时,初始化一个watcher实例并绑定更新函数
text(node, expr, vm) {
let value
if (expr.indexOf('{{') !== -1) {
// 处理 存在双大括号的文本 {{personalbar.name}} {{msg}}
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// replace 替换回调函数参数分别有:0 匹配到的字符串 1在使用组匹配 组匹配到的值 匹配值在原字符串中的索引 原字符串
// 绑定观察者 将来数据发生变化 触发这里的回调 进行更新
new watcher(vm, args[1], (newVal) => {
// console.log('newVal', newVal, this.getContentVal(expr, vm))
// 在此有个疑问 newVal和getContentVal重新解析原表达式获取的值是一样的 不知作者为啥要重新解析一遍?
this.updater.textUpdater(node, this.getContentVal(expr, vm))
})
return this.getVal(args[1], vm)
})
} else {
// 处理v-text expr: msg vm: 整个实例
new watcher(vm, expr, (newVal) => {
this.updater.textUpdater(node, newVal)
})
value = this.getVal(expr, vm)
}
this.updater.textUpdater(node, value)
},
html(node, expr, vm) {
const value = this.getVal(expr, vm)
new watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal)
})
this.updater.htmlUpdater(node, value)
},
model(node, expr, vm) {
const value = this.getVal(expr, vm)
// 创建监听者 并通过watcher中的update来绑定回调这个更新函数 数据 =》 视图
new watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal)
})
// 视图 =》 数据 =》 视图
node.addEventListener('input', (e) => {
// 设置值
this.setVal(expr, vm, e.target.value)
})
this.updater.modelUpdater(node, value)
},
on(node, expr, vm, eventName) {
// 找到对应的函数方法 绑定监听函数
let fn = vm.$options.methods && vm.$options.methods[expr]
// 修改函数this指向为当前vue实例
node.addEventListener(eventName, fn.bind(vm), false)
},
bind(node, expr, vm, attrName) {
const value = this.getVal(expr, vm)
this.updater.bindUpdater(node, attrName, value)
},
实现一个Dep(依赖收集器)
Observer中将订阅者收集在数组中,当数据发生变化时,遍历数组,通知订阅者调用回调更新函数更新视图或者数据。
class Dep{
constructor() {
this.subs = []
}
// 收集订阅者
addSub(watcher) {
this.subs.push(watcher)
}
// 通知观察者去更新视图
notify() {
this.subs.forEach(w => {
w.update()
})
}
}
实现一个Observer(观察者)
劫持监听所有的属性,在初始化数据时(编译解析指令的时候,创建watcher,先挂载了wather到dep上,后获取数据调用get),此时将订阅者收集到依赖收集器中。数据变化时,在setter函数通知依赖收集器中的订阅者数据发生变化,调用更新函数。
class Observer{
constructor(data) {
this.observe(data)
}
observe(data) {
if (data && typeof data === 'object') {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
}
defineReactive(obj, key, value) {
// 递归遍历 value中是否还是对象
this.observe(value)
const dep = new Dep()
// 劫持并监听所有的属性
Object.defineProperty(obj, key, {
enumerable: true, // 表示能否通过for-in循环返回属性
configurable: false, // 表示能否通过delete删除属性从而重新定义属性
get() {
// 初始化 编译解析指令的时候 获取数据时就会调用get
// 订阅数据变化时, 往dep中添加订阅者 查看数据是否变化 更新对应视图
// 订阅者在 新建watcher的时候挂载到Dep上
Dep.target && dep.addSub(Dep.target)
return value
},
set:(newVal) => {
// 对新值劫持 并进行监听
this.observe(newVal)
if (newVal !== value) {
value = newVal
}
// 通知数据变化
dep.notify()
}
})
}
}
流程图
流程分析
- 创建编译器,解析指令,初始化视图;创建订阅者,订阅数据变化,绑定更新函数。
- 创建观察者,劫持监听所有属性,在getter中收集订阅者;在setter中监听数据变化,通知收集器中的订阅者,调用更新函数。
- 创建依赖收集器,添加订阅者。
以上是我在实现MVVM时理解的流程,只包含部分代码;具体实现请戳gitHub
有什么不明白的地方欢迎留言,我很乐意解答,和大家一起进步~