Vue.js 核心:
一、响应式的数据绑定系统
二、组件系统。javascript
访问器属性html
访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须经过 defineProperty() 方法单独定义。vue
var obj = { }; // 为obj定义一个名为 hello 的访问器属性 Object.defineProperty(obj, "hello", { get: function () {return sth}, set: function (val) {/* do sth */} }) obj.hello // 能够像普通属性同样读取访问器属性
访问器属性的"值"比较特殊,读取或设置访问器属性的值,其实是调用其内部特性:get和set函数。java
obj.hello // 读取属性,就是调用get函数并返回get函数的返回值 obj.hello = "abc" // 为属性赋值,就是调用set函数,赋值实际上是传参
get 和 set 方法内部的 this 都指向 obj,这意味着 get 和 set 函数能够操做对象内部的值。另外,访问器属性的会"覆盖"同名的普通属性,由于访问器属性会被优先访问,与其同名的普通属性则会被忽略。node
预期达到的效果:
一、随文本框输入文字的变化,span 中会同步显示相同的文字内容;
二、在js或控制台显式的修改 obj.hello 的值,视图会相应更新。这样就实现了 model => view 以及 view => model 的双向绑定。react
模型图:
app
子任务:dom
一、输入框以及文本节点与 data 中的数据绑定
二、输入框内容变化时,data 中的数据同步变化。即 view => model 的变化。
三、data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化。异步
这里须要对 DOM 进行编译,这里引入一个知识点:DocumentFragment。函数
DocumentFragment(文档片断)能够看做节点容器,它能够包含多个子节点,当咱们将它插入到 DOM 中时,只有它的子节点会插入目标节点,因此把它看做一组节点的容器。使用 DocumentFragment 处理节点,速度和性能远远优于直接操做 DOM。Vue 进行编译时,就是将挂载目标的全部子节点劫持(真的是劫持,经过 append 方法,DOM 中的节点会被自动删除)到 DocumentFragment 中,通过一番处理后,再将 DocumentFragment 总体返回插入挂载目标。
<!DOCTYPE html> <html> <head> <title></title> </head> <body> <div id="app"> <input type="text" id="a"> <span id="b"></span> </div> <script type="text/javascript"> var dom = nodeToFragment(document.getElementById('app')); console.log(dom); function nodeToFragment (node) { var flag = document.createDocumentFragment(); var child; while (child = node.firstChild) { flag.append(child); // 劫持node的全部子节点 } return flag; } document.getElementById('app').appendChild(dom); // 返回到app中 </script> </body> </html>
数据初始化绑定
function compile (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 name = attr[i].nodeValue; // 获取v-model绑定的属性名 node.value = vm.data[name]; // 将data的值赋值给该node node.removeAttribute('v-model'); } } } // 节点类型为text if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { var name = RegExp.$1; // 获取匹配到的字符串 name = name.trim(); node.nodeValue = vm.data[name]; } } } function nodeToFragment (node, vm) { var flag = document.createDocumentFragment(); var child; while (child = node.firstChild) { compile(child, vm); flag.append(child); } return flag; } function Vue (options) { this.data = options.data; var id = options.el; var dom = nodeToFragment(document.getElementById(id), this); // 编译完成后,将dom返回到app中 document.getElementById(id).appendChild(dom); } var vm = new Vue({ el: 'app', data: { text: 'hello world' } })
效果
响应式的数据绑定
思路:当咱们在输入框输入数据的时候,首先触发 input 事件(或者 keyup、change 事件),在相应的事件处理程序中,咱们获取输入框的 value 并赋值给 vm 实例的 text 属性。咱们利用 defineProperty 将 data 中的 text 设置为 vm 的访问器属性,所以给 vm.text 赋值,就会触发 set 方法。这里set主要作了跟新属性值得操做。
function defineReactive (obj, key, val) { Object.defineProperty(obj, key, { get: function () { return val }, set: function (newVal) { if (newVal === val) return val = newVal; console.log(val); // console } }); } function observe (obj, vm) { Object.keys(obj).forEach(function (key) { defineReactive(vm, key, obj[key]); }); } function Vue (options) { this.data = options.data; var data = this.data; observe(data, this); var id = options.el; var dom = nodeToFragment(document.getElementById(id), this); // 编译完成后,将dom返回到app中 document.getElementById(id).appendChild(dom); } function compile (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 name = attr[i].nodeValue; // 获取v-model绑定的属性名 node.addEventListener('input', function (e) { // 给相应的data属性赋值,进而触发该属性的set方法 vm[name] = e.target.value; }); node.value = vm[name]; // 将data的值赋给该node node.removeAttribute('v-model'); } }; new Watcher(vm, node, name, 'input'); } // 节点类型为text if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { var name = RegExp.$1; // 获取匹配到的字符串 name = name.trim(); node.nodeValue = vm[name]; } } }
第二部完成效果
订阅/发布模式(subscribe&publish)
订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知全部观察者对象。
发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应操做
var pub = { publish: function() { def.notify(): } } // 三个订阅者subscribers var sub1 = {updata: function () {console.log(1)} } var sub2 = {updata: function () {console.log(2)} } var sub3 = {updata: function () {console.log(3)} } // 一个主题对象 funciton Dep () { this.subs = [sub1, sub2, sub3]; } Dep.prototype.notify = function () { this.subs.forEach(function (sub) { sub.update(); }) } // 发布者发布消息,主题对象执行notify方法,而后会触发订阅者实现更函数 var dep = new Dep(); pub.publish(); // 1, 2, 3
set在这里的做用是:做为发布者发出通知,而文本节点在这里是订阅者,收到消息以后执行相应的更新操做。
双向绑定的实现
每当 new 一个 Vue,主要作了两件事:
1.是监听数据:observe(data),
2.第二个是编译 HTML:nodeToFragement(id)。
在监听数据的过程当中,会为 data 中的每个属性生成一个主题对象 dep。
在编译 HTML 的过程当中,会为每一个与数据绑定相关的节点生成一个订阅者 watcher,watcher 会将本身添加到相应属性的 dep 中。
如今效果:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性的 set 方法。
下一步实现:发出通知 dep.notify() => 触发订阅者的 update 方法 => 更新视图。
关键逻辑:如何将 watcher 添加到关联属性的 dep 中。
在编译 HTML 过程当中,为每一个与 data 关联的节点生成一个 Watcher。
Watcher函数实现思路:
function Watcher (vm, node, name, nodeType) { Dep.target = this; this.name = name; this.node = node; this.vm = vm; this.update(); Dep.target = null; } Watcher.prototype = { update: function () { this.get(); this.node.nodeValue = this.value; }, // 获取data中的属性值 get: function () { this.value = this.vm[this.name]; // 触发相应属性的get } }
首先,将本身赋给了一个全局变量 Dep.target;
其次,执行了 update 方法,进而执行了 get 方法,get 的方法读取了 vm 的访问器属性,从而触发了访问器属性的 get 方法,get 方法中将该 watcher 添加到了对应访问器属性的 dep 中;
再次,获取属性的值,而后更新视图。
最后,将 Dep.target 设为空。由于它是全局变量,也是 watcher 与 dep 关联的惟一桥梁,任什么时候刻都必须保证 Dep.target 只有一个值。
function defineReactive (obj, key, val) { var dep = new Dep(); // !! Object.defineProperty(obj, key, { get: function () { // 添加订阅者watcher到主题对象Dep // !! if (Dep.target) dep.addSub(Dep.target); // !! return val }, set: function (newVal) { if (newVal === val) return val = newVal; // 做为发布者发出通知 dep.notify(); } }); } function Dep () { this.subs = [] } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }); } };
最终效果:
文本内容会随输入框内容同步变化,在控制器中修改 vm.text 的值,会同步反映到文本内容中。
1.异步更新带来的数据响应式误解
<div id="app"> <h2>{{dataObj.text}}</h2> </div> new Vue({ el: '#app', data: { dataObj: {} }, ready: function () { var self = this; /** * 异步请求模拟 */ setTimeout(function () { self.dataObj = {}; self.dataObj['text'] = 'new text'; }, 3000); } })
上面的代码很是简单,咱们都知道vue中在data里面声明的数据才具备响应式的特性,因此咱们一开始在data中声明了一个dataObj空对象,而后在异步请求中执行了两行代码,以下:
self.dataObj = {}; self.dataObj['text'] = 'new text';
模板更新了,应该具备响应式特性,若是这么想那么你就已经走入了误区,一开始咱们并无在data中声明.text属性,因此该属性是不具备响应式的特性的。
但模板切切实实已经更新了,这又是怎么回事呢?
那是由于vue的dom更新是异步的,即当setter操做发生后,指令并不会立马更新,指令的更新操做会有一个延迟,当指令更新真正执行的时候,此时.text属性已经赋值,因此指令更新模板时获得的是新值。
具体流程以下所示:
self.dataObj = {};发生setter操做
vue监测到setter操做,通知相关指令执行更新操做
self.dataObj['text'] = 'new text';赋值语句
指令更新开始执行
因此真正的触发更新操做是self.dataObj = {};这一句引发的,因此单看上述例子,具备响应式特性的数据只有dataObj这一层,它的子属性是不具有的。
2.Vue 不容许在已经建立的实例上动态添加新的根级响应式属性(root-level reactive property)。然而它可使用Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上或者使用$set