vue的基础双向绑定
一、从 new 开始
const app = new Vue({ el: '#app', data: { name: 'fish chan' }, })
通过这段代码,我们可以获取到一个 vue 实例对象,开启我们的 vue 编程,那么简单的 new 背后,究竟是怎样实现数据的双向绑定呢?
二、基础准备
core(vue 简单轮子) 类:
数据劫持 Observer 类:
// 数据的双向绑定 // 涉及其他类: 基础订阅器 类 class Observer { constructor(data) { this.defineReactive(data) } defineReactive(data) { let dep = new Dep(); Object.keys(data).forEach(key => { let val = data[key]; Object.defineProperty(data, key, { get() { Dep.target && dep.addSubscribe(Dep.target); return val; }, set(newVal) { val = newVal; dep.notify(); } }) }) } }
基础订阅器 Dep 和 订阅者 Watcher 类
// 订阅器类以及分发 // 涉及其他类: 无 class Dep { constructor(vue) { this.subs = []; } addSubscribe(subscribe) { this.subs.push(subscribe); } notify() { // 通知所有订阅者更新 let length = this.subs.length; while (length--) { this.subs[length].update(); // 触发更新方法 } } }
// 订阅者, 订阅者要做的事情就是执行某个事件 // 涉及其他类: 订阅器类 class Watcher { constructor(vue, exp, callback) { this.vue = vue; this.exp = exp; this.callback = callback; this.value = this.get(); } get() { Dep.target = this; // 初始化的时候,这里落后于 compile.js 中的读取数据,即第二次读取 // 故这里会触发 Observer 中 Dep.target && dep.addSubscribe(Dep.target); 的执行 // 从而实现订阅者与观察中心的绑定 let value = this.vue._data[this.exp]; Dep.target = null; return value; } update() { this.value = this.get(); this.callback.call(this.vue, this.value); // 将新的数据传回,用于更新视图;这里保证了 this 指向 vue } }
最后是编译类 Complie
// 编译模板 class Complie { constructor(el, vue) { this.$el = document.querySelector(el); this.$vue = vue; } compileText() { // 找到 {{ ... }} 并替换成真实数据 const reg = /\{\{(.*)\}\}/; // 匹配 {{ ... }} 的正则 const fragment = this.node2Fragment(this.$el); const node = fragment.childNodes[0]; // 获取节点对象 if (reg.test(node.textContent)) { let matchedName = RegExp.$1; // 替换数据 // 此时 由于已经执行过数据劫持,故此处读取数据会触发 Observer 中 object.defineProperty 中的 get 逻辑 node.textContent = this.$vue._data[matchedName]; this.$el.appendChild(node); // 编译好的文档碎片放进根节点 new Watcher(this.$vue, matchedName, function(value) { node.textContent = value; console.log(node.textContent); }) } } node2Fragment(node) { const fragment = document.createDocumentFragment(); // 这里好像会取出并改变 node 的 firstChild, 存疑 fragment.appendChild(node.firstChild); // 获取文本节点 return fragment; } }
至此,基础素材已准备完毕,接下来我们逐步分析。
<script src="./Dep.js"></script> <script src="./Watcher.js"></script> <script src="./Observer.js"></script> <script src="./compile.js"></script> <script src="./core.js"></script> <script> const app = new Vue({ el: '#app', data: { name: 'fish chan' }, }) </script>
三、初始化
我们在 html 中引入静态文件的顺序如上所示,但是文件的执行顺序却大致相反:core - Observer - Dep - Complie - Watcher。
从 core 开始,首先是进入数据劫持(new Oberver), 传入 data 对象,从而完成对data 对象所有 key 的 get/set 劫持。同时,这里初始化订阅器 Dep 对象,并设置:在所有数据初次读取(get)的时候,推送到订阅器中留存;在所有数据写入(set)的时候,触发订阅器的通知(notify)方法,并更新数据。当然,这里仅仅是劫持,并不涉及到数据的读写,所以 get/set 方法体均未执行。Oberver 、 Dep 对象初始化完成之后,进入编译类 Complie 的初始化。
接着,执行下 Complie 的初始化(初始化较简单,暂没有需要注意的地方),完成之后,进入 core 中 _compil.compileText() 方法的执行。这里有两个地方需要注意:
(1)17 行 node.textContent = this.$vue._data[matchedName]; 这里会对 _data 进行一次读取,因此,这里会触发之前 Oberver 设置的 get 方法,当然,此次执行 get 因为 Dep.target 的缘故不会执行 dep.addSubscribe(Dep.target); 这句。
(2)21 行针对匹配的 matchedName 初始化 Watcher 对象,即每一个 _data 中的属性都会初始化一个 Watcher 对象。在初始化的过程中, this.value = this.get(); 这个内部函数的调用,会再次触发之前 Oberver 设置的 get 方法,此时, Dep.target 是有值的,所以触发 dep.addSubscribe(Dep.target); 这句, 从而在对应的 Dep 对象添加观察者。
至此,整个初始化过程基本完成。
四、update
然后,我们在浏览器控制台键入 app._data.name = '123' ,文件执行顺序大致是:Observer(set) - Dep(notify) -Watcher(update) - Observer(get) - Complie(callback) 。
键入后,因为是对属性的修改,故先进入 Observer 的 set 劫持,在这里,完成数据赋值和触发订阅器通知更新(notify) 方法,从而通知对应的观察者执行更新(update) 方法,在 update 方法中,Complie 中传入的 回调方法 执行,完成节点值的修改。
在 update 的过程中,会调用一次 Watcher 中的 get(),在这个 get() 中,会触发一次 Observer 中的 get,此时会连锁触发一次订阅,即 dep 的 subs 数组中会再次推入这个对象,虽然按照 notify 的执行逻辑,本次内不会执行新推入的方法,但是在下次更新根值的时候一定会触发。
个人推测 Vue 应该是push subs 数组的时候会进行一次重复检测从而避免数组太长而可能导致的内存泄露。