本文参考b站蛋老师的视频希望能从一个更为简单的角度去理解Vue的双向绑定原理,就算没看过视频,也应该从本文收获一个更为基础的理解,同时笔者还是新手,如有不对,希望指正
本文分为两块,一块是对双向绑定原理的逻辑上的梳理,接着是对代码的梳理和理解。
1.
我们把双向绑定所需的函数为4块,分别是Observer,Complie, Dependency,Watcher。
先说说他们的作用,在来谈他们之间的关联和他们是什么。
Observer的作用是将vue中data部分的数据进行监听(数据劫持)
Complie的作用是将vue中data部分的数据解析到页面上
Dependency是发布-订阅模式 中的发布者
Watcher是发布-订阅模式 中的订阅者
通过这四个函数,我们基本可以实现vue的双向绑定原理了。
既然是双向绑定原理MVVM,就有从Model(数据)到View(视图),也有从View到Model。第一部分我们先来理解如何从View到Model。我们需要依靠的就是Complie函数,他将我们的数据解析到页面上,它的逻辑如下:
- 使用文档碎片来临时存放DOM元素
- 根据view替换fragment里文本节点的内容
- 将文档碎片替换成DOM元素
这样我们就解决了数据到视图的更新,但是我们还希望实现一个功能,就是Model变化时,View也能更新,这里用到的就是发布-订阅者模式了,我们需要思考的内容变成了发布-订阅者模式的逻辑是什么
毫无疑问就是当Model变化时,我们希望Compile能再次被触发,再一次将数据解析到页面。那么发布者发布文章的触发条件肯定是Model变化时触发,而订阅者的内容就是Compile再次触发。对于发布者的触发我们可以写在Observer的数据监听的setter中,而订阅者的内容就实现在每次Complie。(文档碎片有多个节点,每个节点都被一次次替换成新的节点,对每个节点的处理时新建一个订阅者)
图一:
如图所示,当数据变化时,Observer会调用Dependency,接着触发Watcher,接着调用Compile进行更新。
我们还需要想如何把Watcher放进Dependency中,这里也是相对有难点的地方,这里的做法是在创建Watcher的时候就已经把Watcher放入Dependency中了。逻辑大概如下
1.我们将watcher中放入临时Dependency属性
2.调用当前watcher监控的属性,此时会触发Observer的getter,在getter中将watcher(临时Dependency属性)放入Dependency
3.删除临时Dependency
图二:
总体的思维逻辑如下
也就是说,Observer中getter的作用是将Watcher加入到Observer,也就是图二,setter的作用则是触发视图更新,也就是图一。可以说设计的很巧妙了。
然后我们说说如何从View到Model,主要是替换绑定了v-model属性的input节点的内容,分为两部分,这部分与前面讲的其实很相似
1.视图上的改为数据(与前面相同)
2.把视图上的绑定addEventListener,当值变化时改变数据。
这里不再赘述。
总体概括如下:
Vue 的响应式是通过 Object.defineProperty 对数据进行劫持,并结合发布订阅者模式实现。 Vue 利用 Object.defineProperty 创建一个 observe 来劫持监听所有的属性,把这些属性全部转为 getter 和 setter。Vue 中每个组件实例都会对应一个 watcher 实例,它会在组件渲染的过程中把使用过的数据属性通过 getter 收集为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
2.
接下来是对代码的梳理
class Vue {
// 参数为对象实例 这个对象用于告知vue需要挂载到哪个元素并挂载数据
constructor(obj_instance) {
// 给实例赋值对象的data属性
this.$data = obj_instance.data;
// 进行数据劫持 监听对象里属性的变化
Observer(this.$data);
Complie(obj_instance.el, this);
}
}
这是Observer
//数据劫持 —— 监听实例里的数据
function Observer(data_instance) {
// 递归出口
if (!data_instance || typeof data_instance !== "object") return;
// 每次数据劫持一个对象时都创建Dependency实例 用于区分哪个对象对应哪个依赖实例和收集依赖
const dependency = new Dependency();
Object.keys(data_instance).forEach((key) => {
// 使用defineProperty后属性里的值会被修改 需要提前保存属性的值
let value = data_instance[key];