我们都知道,Vue 是一套用于构建用户界面的渐进式框架,是目前前端领域主流框架之一,作为前端框架,它有两大核心:
- 数据双向绑定:当数据发生改变,视图可以自动更新,可以不用关心 dom 操作,而专心数据操作;
- 可组合的视图组件:把视图按照功能切分成若干基本单元,组件可以一级一级组合整个应用形成倒置组件树,可维护,可重用,可测试。
Vue 从之前的 1 版本到现在基本所有公司都用到的 2 版本经历了一次重大变革,到 2020 年 10 月 5 日,Vue3 的源码正式发布。3 版本的发布其中比较变化大的一个点就是 Vue 的数据双向绑定的原理发生了变化,那么我们知道,** 数据双向绑定的原理在前端的面试中基本上是必问的,可见其重要性。** 那么接下来我们着重讨论一下 Vue2 和 Vue3 版本的数据双向绑定原理的区别及其各自的优缺点。
# 一、什么是 MVVM 模式
在讨论 Vue 的数据双向绑定原理之前,我们还得知道咱们平时工作中用 Vue 开发的开发模式是什么,那就是 MVVM 模式。
大多数的开发模式,可能通过最多的是 MVC 开发模式,当然还有更多的 MVP 模式,发布订阅者模式等等,那么 Vue 为什么要采用 MVVM 的开发模式,其实主要还是因为 MVVM 模式可以实现数据的双向绑定,它是如何实现的?
MVVM 可以分为,模型层,视图层,视图模型层,模型层主要干的事情就是负责与业务数据相关的操作,就像我们 data 函数中返回的数据一样,主要负责数据方面的工作,view 层顾名思义就是主要负责视图相对应的操作,神秘的 vm 层也就是视图模型层主要是干什么的?视图模型层就类似一座桥梁,链接模型(数据层)和视图层,它的工作是双向的一边监听数据一边观测试图,无论是数据层发生变化还是视图层发生变化,他都会同步给对方,让视图层和数据层保持一致。实现数据的双向绑定。
通过下面这张图可以看到他们三者之间的一个工作流程:
# 二、Vue2 数据双向绑定原理的实现
Vue2 采用数据劫持并结合发布者 - 订阅者模式的方式,通过 ES6 的 object.defineProperty()
方法去劫持各个属性的 setter/getter
方法,在数据发生变化的时候,发布消息给订阅者,触发相应的监听回调。
具体步骤如下:
- 需要 observe (观察者) 的数据对象进行遍历,包括子属性对象的属性,都加上 setter 和 getter, 这样的话,给这个对象的某个值赋值,就会触发 setter, 那么就能监听到数据的变化。
- compile (解析) 解析模版指令,将模版中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
- watcher (订阅者) 是 observer 和 compile 之间通信的桥梁,主要做的事情是:
- 在实例化时往属性订阅器 (dep) 里添加自己;
- 自身必须有一个
update()
方法; - 待属性变动
dep.notice()
通知时,能够调用自身的update()
方法,并触发 compile 中绑定的回调。
- MVVM 作为数据绑定入口,整合
observer
,compile
和watcher
来监听自己的 model 数据变化,通过compile
来解析编译模版,最终利用watcher
搭起observer
和compile
之间的通信桥梁,达到数据变化 -> 更新视图:视图交互变化 -> 数据 model 变更的双向绑定效果。
结合上面所说可以看下面这张图,能有个直观的感受:
# 三、源码剖析
那么 Vue2 的源码是如何具体实现它的数据双向绑定原理的呢?我们对源码进行剖析,发现它的代码实现步骤如下:
# 1. observer 实现对 vue 各个属性进行监听
function defineReactive( obj, key, val ) { | |
// 每个属性建立个依赖收集对象,get 中收集依赖,set 中触发依赖,调用更新函数 | |
var dep = new Dep(); | |
Object.defineProperty(obj, key, { | |
enumerable: true, | |
configurable: true, | |
get: function() { | |
// 收集依赖 Dep.target 标志 | |
Dep.target && dep.addSub(Dep.target) | |
return val | |
}, | |
set: function(newVal){ | |
if(newVal === val) return | |
// 触发依赖 | |
dep.notify() | |
val = newVal | |
} | |
}) | |
} |
# 2. dep 实现
function Dep(){ | |
this.subs = [] | |
} | |
Dep.prototype = { | |
constructor: Dep, | |
addSub: function(sub){ | |
this.subs.push(sub) | |
}, | |
notify: function(){ | |
this.subs.forEach(function(sub){ | |
sub.update() // 调用的 Watcher 的 update 方法 | |
}) | |
} | |
} |
# 3.compiler 实现对各个指令模板的解析器
通过 compiler 实现对 vue 各个指令模板的解析器,生成抽象语法树,编译成 Virtual Dom,渲染视图。
// 编译器 | |
function compiler(node, vm){ | |
var reg = /\{\{(.*)\}\}/; | |
// 节点类型为元素 | |
if(node.nodeType ===1){ | |
var attr = node.attributes; | |
// 解析属性 | |
for(var i=0; i< attr.length;i++){ | |
if(attr[i].nodeName == 'v-model'){ | |
var _value = attr[i].nodeValue | |
node.addEventListener('input', function(e){ | |
// 给相应的 data 属性赋值,触发修改属性的 setter | |
vm[_value] = e.target.value | |
}) | |
node.value = vm[_value] // 将 data 的值赋值给 node | |
node.removeAttribute('v-model') | |
} | |
} | |
new Watcher(vm,node,_value,'input') | |
} | |
// 节点类型为 text | |
if(node.nodeType ===3){ | |
if(reg.test(node.nodeValue)){ | |
var name = RegExp.$1; | |
name = name.trim() | |
new Watcher(vm,node,name,'input') | |
} | |
} | |
} |
# 4.Watcher 连接 observer 和 compiler
通过他们之间的连接,接受每个属性变动的通知,绑定更新函数,更新视图。
function Watcher(vm,node,name, nodeType){ | |
Dep.target = this; //this 为 watcher 实例 | |
this.name = name | |
this.node = node | |
this.vm = vm | |
this.nodeType = nodeType | |
this.update() // 绑定更新函数 | |
Dep.target = null // 绑定完后注销 标志 | |
} | |
Watcher.prototype = { | |
get: function(){ | |
this.value = this.vm[this.name] // 触发 observer 中的 getter 监听 | |
}, | |
update: function(){ | |
this.get() | |
if(this.nodeType == 'text'){ | |
this.node.nodeValue = this.value | |
} | |
if(this.nodeType == 'input') { | |
this.node.value = this.value | |
} | |
} | |
} |
# 总结
以上就是关于 Vue2 的数据双向绑定原理的刨析,如果你仔细阅读了,相信一定会让你有所收获,接下来咱们会在下篇中讨论 Vue3 的数据双向绑定原理,这里咱们先做个思考:是不是新版的数据双向绑定原理就优于旧版的?可以给大家提前透露一下,并不是这样的,两者各有优缺点。