vue.js动态数据绑定学习笔记
对于vue.js的动态数据绑定,经过反复地看源码和博客讲解,总算能够理解它的实现了,心累~ 分享一下学习成果,同时也算是做个记录。完整代码github地址:https://github.com/hanrenguang/dynamic-data-binding。也可以到仓库的 readme 阅读本文,容我厚脸皮地求 star,求 follow。
整体思路
不知道有没有同学和我一样,看着vue的源码却不知从何开始,真叫人头大。硬生生地看了observer, watcher, compile这几部分的源码,只觉得一脸懵逼。最终,从这里得到启发,作者写得很好,值得一读。
关于动态数据绑定呢,需要搞定的是 dep , observer , watcher , compile 这几个类,他们之间有着各种联系,想要搞懂源码,就得先了解他们之间的联系。下面来理一理:
- observer 所做的就是劫持监听所有属性,当有变动时通知 dep
- watcher 向 dep 添加订阅,同时,属性有变化时,observer 通知 dep,dep 则通知 watcher
- watcher 得到通知后,调用回调函数更新视图
- compile 则是解析所绑定元素的 dom 结构,对所有需要绑定的属性添加 watcher 订阅
由此可以看出,当属性发生变化时,是由observer -> dep -> watcher -> update view,compile 在最开始解析 dom 并添加 watcher 订阅后就功成身退了。
从程序执行的顺序来看的话,即 new vue({}) 之后,应该是这样的:先通过 observer 劫持所有属性,然后 compile 解析 dom 结构,并添加 watcher 订阅,再之后就是属性变化 -> observer -> dep -> watcher -> update view,接下来就说说具体的实现。
从new一个实例开始谈起
网上的很多源码解读都是从 observer 开始的,而我会从 new 一个mvvm实例开始,按照程序执行顺序去解释或许更容易理解。先来看一个简单的例子:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>test</title> </head> <body> <div class="test"> <p>{{user.name}}</p> <p>{{user.age}}</p> </div> <script type="text/javascript" src="hue.js"></script> <script type="text/javascript"> let vm = new hue({ el: '.test', data: { user: { name: 'jack', age: '18' } } }); </script> </body> </html>
接下来都将以其为例来分析。下面来看一个简略的 mvvm 的实现,在此将其命名为 hue。为了方便起见,为 data 属性设置了一个代理,通过 vm._data 来访问 data 的属性显得麻烦且冗余,通过代理,可以很好地解决这个问题,在注释中也有说明。添加完属性代理后,调用了一个 observe 函数,这一步做的就是 observer 的属性劫持了,这一步具体怎么实现,暂时先不展开。先记住他为 data 的属性添加了 getter 和 setter。
function hue(options) { this.$options = options || {}; let data = this._data = this.$options.data, self = this; object.keys(data).foreach(function(key) { self._proxydata(key); }); observe(data); self.$compile = new compile(self, options.el || document.body); } // 为 data 做了一个代理, // 访问 vm.xxx 会触发 vm._data[xxx] 的getter,取得 vm._data[xxx] 的值, // 为 vm.xxx 赋值则会触发 vm._data[xxx] 的setter hue.prototype._proxydata = function(key) { let self = this; object.defineproperty(self, key, { configurable: false, enumerable: true, get: function proxygetter() { return self._data[key]; }, set: function proxysetter(newval) { self._data[key] = newval; } }); };
再往下看,最后一步 new 了一个 compile,下面我们就来讲讲 compile。
compile
new compile(self, options.el || document.body) 这一行代码中,第一个参数是当前 hue 实例,第二个参数是绑定的元素,在上面的示例中为class为 .test 的div。
关于 compile,这里只实现最简单的 textcontent 的绑定。而 compile 的代码没什么难点,很轻易就能读懂,所做的就是解析 dom,并添加 watcher 订阅。关于 dom 的解析,先将根节点 el 转换成文档碎片 fragment 进行解析编译操作,解析完成后,再将 fragment 添加回原来的真实 dom 节点中。来看看这部分的代码:
function compile(vm, el) { this.$vm = vm; this.$el = this.iselementnode(el) ? el : document.queryselector(el); if (this.$el) { this.$fragment = this.node2fragment(this.$el); this.init(); this.$el.appendchild(this.$fragment); } } compile.prototype.node2fragment = function(el) { let fragment = document.createdocumentfragment(), child; // 也许有同学不太理解这一步,不妨动手写个小例子观察一下他的行为 while (child = el.firstchild) { fragment.appendchild(child); } return fragment; }; compile.prototype.init = function() { // 解析 fragment this.compileelement(this.$fragment); };
以上面示例为例,此时若打印出 fragment,可观察到其包含两个p元素:
<p>{{user.name}}</p> <p>{{user.age}}</p>
下一步就是解析 fragment,直接看代码及注释吧:
compile.prototype.compileelement = function(el) { let childnodes = array.from(el.childnodes), self = this; childnodes.foreach(function(node) { let text = node.textcontent, reg = /\{\{(.*)\}\}/; // 若为 textnode 元素,且匹配 reg 正则 // 在上例中会匹配 '{{user.name}}' 及 '{{user.age}}' if (self.istextnode(node) && reg.test(text)) { // 解析 textcontent,regexp.$1 为匹配到的内容,在上例中为 'user.name' 及 'user.age' self.compiletext(node, regexp.$1); } // 递归 if (node.childnodes && node.childnodes.length) { self.compileelement(node); } }); }; compile.prototype.compiletext = function(node, exp) { // this.$vm 即为 hue 实例,exp 为正则匹配到的内容,即 'user.name' 或 'user.age' compileutil.text(node, this.$vm, exp); }; let compileutil = { text: function(node, vm, exp) { this.bind(node, vm, exp, 'text'); }, bind: function(node, vm, exp, dir) { // 获取更新视图的回调函数 let updaterfn = updater[dir + 'updater']; // 先调用一次 updaterfn,更新视图 updaterfn && updaterfn(node, this._getvmval(vm, exp)); // 添加 watcher 订阅 new watcher(vm, exp, function(value, oldvalue) { updaterfn && updaterfn(node, value, oldvalue); }); }, // 根据 exp,获得其值,在上例中即 'vm.user.name' 或 'vm.user.age' _getvmval: function(vm, exp) { let val = vm; exp = exp.trim().split('.'); exp.foreach(function(k) { val = val[k]; }); return val; } }; let updater = { // watcher 订阅的回调函数 // 在此即更新 node.textcontent,即 update view textupdater: function(node, value) { node.textcontent = typeof value === 'undefined' ? '' : value; } };
正如代码中所看到的,compile 在解析到 {{xxx}} 后便添加了 xxx 属性的订阅,即 new watcher(vm, exp, callback)。理解了这一步后,接下来就需要了解怎么实现相关属性的订阅了。先从 observer 开始谈起。
observer
从最简单的情况来考虑,即不考虑数组元素的变化。暂时先不考虑 dep 与 observer 的联系。先看看 observer 构造函数:
function observer(data) { this.data = data; this.walk(data); } observer.prototype.walk = function(data) { const keys = object.keys(data); // 遍历 data 的所有属性 for (let i = 0; i < keys.length; i++) { // 调用 definereactive 添加 getter 和 setter definereactive(data, keys[i], data[keys[i]]); } };
接下来通过 object.defineproperty 方法给所有属性添加 getter 和 setter,就达到了我们的目的。属性有可能也是对象,因此需要对属性值进行递归调用。
function definereactive(obj, key, val) { // 对属性值递归,对应属性值为对象的情况 let childobj = observe(val); object.defineproperty(obj, key, { enumerable: true, configurable: true, get: function() { // 直接返回属性值 return val; }, set: function(newval) { if (newval === val) { return; } // 值发生变化时修改闭包中的 val, // 保证在触发 getter 时返回正确的值 val = newval; // 对新赋的值进行递归,防止赋的值为对象的情况 childobj = observe(newval); } }); }
最后补充上 observe 函数,也即 hue 构造函数中调用的 observe 函数:
function observe(val) { // 若 val 是对象且非数组,则 new 一个 observer 实例,val 作为参数 // 简单点说:是对象就继续。 if (!array.isarray(val) && typeof val === "object") { return new observer(val); } }
这样一来就对 data 的所有子孙属性(不知有没有这种说法。。)都进行了“劫持”。显然到目前为止,这并没什么用,或者说如果只做到这里,那么和什么都不做没差别。于是 dep 上场了。我认为理解 dep 与 observer 和 watcher 之间的联系是最重要的,先来谈谈 dep 在 observer 里做了什么。
observer & dep
在每一次 definereactive 函数被调用之后,都会在闭包中新建一个 dep 实例,即 let dep = new dep()。dep 提供了一些方法,先来说说 notify 这个方法,它做了什么事?就是在属性值发生变化的时候通知 dep,那么我们的代码可以增加如下:
function definereactive(obj, key, val) { let childobj = observe(val); const dep = new dep(); object.defineproperty(obj, key, { enumerable: true, configurable: true, get: function() { return val; }, set: function(newval) { if (newval === val) { return; } val = newval; childobj = observe(newval); // 发生变动 dep.notify(); } }); }
如果仅考虑 observer 与 dep 的联系,即有变动时通知 dep,那么这里就算完了,然而在 vue.js 的源码中,我们还可以看到一段增加在 getter 中的代码:
// ... get: function() { if (dep.target) { dep.depend(); } return val; } // ...
这个 depend 方法呢,它又做了啥?答案是为闭包中的 dep 实例添加了一个 watcher 的订阅,而 dep.target 又是啥?他其实是一个 watcher 实例,???一脸懵逼,先记住就好,先看一部份的 dep 源码:
// 标识符,在 watcher 中有用到,先不用管 let uid = 0; function dep() { this.id = uid++; this.subs = []; } dep.prototype.depend = function() { // 这一步相当于做了这么一件事:this.subs.push(dep.target) // 即添加了 watcher 订阅,adddep 是 watcher 的方法 dep.target.adddep(this); }; // 通知更新 dep.prototype.notify = function() { // this.subs 的每一项都为一个 watcher 实例 this.subs.foreach(function(sub) { // update 为 watcher 的一个方法,更新视图 // 没错,实际上这个方法最终会调用到 compile 中的 updaterfn, // 也即 new watcher(vm, exp, callback) 中的 callback sub.update(); }); }; // 在 watcher 中调用 dep.prototype.addsub = function(sub) { this.subs.push(sub); }; // 初始时引用为空 dep.target = null;
也许看到这还是一脸懵逼,没关系,接着往下。大概有同学会疑惑,为什么要把添加 watcher 订阅放在 getter 中,接下来我们来说说这 watcher 和 dep 的故事。
watcher & dep
先让我们回顾一下 compile 做的事,解析 fragment,然后给相应属性添加订阅:new watcher(vm, exp, cb)。new 了这个 watcher 之后,watcher 怎么办呢,就有了下面这样的对话:
watcher:hey dep,我需要订阅 exp 属性的变动。
dep:这我可做不到,你得去找 exp 属性中的 dep,他能做到这件事。
watcher:可是他在闭包中啊,我无法和他联系。
dep:你拿到了整个 hue 实例 vm,又知道属性 exp,你可以触发他的 getter 啊,你在 getter 里动些手脚不就行了。
watcher:有道理,可是我得让 dep 知道是我订阅的啊,不然他通知不到我。
dep:这个简单,我帮你,你每次触发 getter 前,把你的引用告诉 dep.target 就行了。记得办完事后给 dep.target 置空。
于是就有了上面 getter 中的代码:
// ... get: function() { // 是否是 watcher 触发的 if (dep.target) { // 是就添加进来 dep.depend(); } return val; } // ...
现在再回头看看 dep 部分的代码,是不是好理解些了。如此一来, watcher 需要做的事情就简单明了了:
function watcher(vm, exp, cb) { this.$vm = vm; this.cb = cb; this.exp = exp; this.depids = new set(); // 返回一个用于获取相应属性值的函数 this.getter = parsegetter(exp.trim()); // 调用 get 方法,触发 getter this.value = this.get(); } watcher.prototype.get = function() { const vm = this.$vm; // 将 dep.target 指向当前 watcher 实例 dep.target = this; // 触发 getter let value = this.getter.call(vm, vm); // dep.target 置空 dep.target = null; return value; }; watcher.prototype.adddep = function(dep) { const id = dep.id; if (!this.depids.has(id)) { // 添加订阅,相当于 dep.subs.push(this) dep.addsub(this); this.depids.add(id); } }; function parsegetter(exp) { if (/[^\w.$]/.test(exp)) { return; } let exps = exp.split("."); return function(obj) { for (let i = 0; i < exps.length; i++) { if (!obj) return; obj = obj[exps[i]]; } return obj; }; }
最后还差一部分,即 dep 通知变化后,watcher 的处理,具体的函数调用流程是这样的:dep.notify() -> sub.update(),直接上代码:
watcher.prototype.update = function() { this.run(); }; watcher.prototype.run = function() { let value = this.get(); let oldval = this.value; if (value !== oldval) { this.value = value; // 调用回调函数更新视图 this.cb.call(this.$vm, value, oldval); } };
结语
到这就算写完了,本人水平有限,若有不足之处欢迎指出,一起探讨。
参考资料
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: Vue.js学习教程之列表渲染详解
下一篇: 别迟到了