当我们学会使用一个东西的时候,就开始想要去知道这个东西是怎么实现的,这个也是我们一直继续探究下去的动力,博主学了vue的时间也比较长了,自己也写了一个demo,还在不定时更新,有兴趣的小伙伴可以去看看,如果这个项目能让您有所收获,那也是博主希望看到的,接下来也是聊聊自己开始学习vue一些实现原理的过程。
刚刚接触vue的时候,我们就发现了这样一个有趣的功能,当我们在input输入框输入内容时,旁边也会对应的显示同样内容
看起来似乎其实是个非常简单的功能,但是vue背后实现却比较复杂,博主也是查阅了很多资料,从中也是学习到了很多之前忽略的东西,vue的响应式原理首先就是利用了对象的访问器属性,就是setter/getter方法,并且利用es6的Object.defineProperty()的方法就能去设置这个方法。
先来看下html部分的代码
<div id="app">
<input type="text" v-model="text">
{{text}}
</div>
非常简单的代码,相信大家都能看懂,我们以上面的html页面为例,一步一步来解析vue响应式原理的实现步骤
function defineReactive (obj, key, val) {
var dep = new Dep();
// 2.给所有的data对象的属性附加上访问器get/set属性
Object.defineProperty(obj, key, {
get: function () {
// 13.此时Dep.target就是指向了Watcher,所以会执行addSub的方法
if (Dep.target) dep.addSub(Dep.target);
return val;
},
set: function (newValue) {
if (val === newValue) return;
val = newValue;
// 21.此时dep调用notify方法发布通知
dep.notify();
}
})
}
function observe (obj, vm) {
Object.keys(obj).forEach(function(key){
defineReactive(vm, key, obj[key]);
})
}
function nodeToFragment (node, vm) {
var flag = document.createDocumentFragment();
var child;
while (child = node.firstChild) {
// 4.取出节点上的data中对应的属性的值进行编译处理
// 7.再次循环子节点,取到包含text变量的文本节点
compile(child, vm);
flag.append(child);
}
// 18.node子节点遍历完成,退出循环,返回dom片段
return flag;
}
function compile (node, vm) {
var reg = /\{\{(.*)\}\}/;
//节点类型为元素
if (node.nodeType === 1) {
var attrs = node.attributes;
for (var i = 0;i < attrs.length;i++) {
if (attrs[i].name === 'v-model') {
var name = attrs[i].nodeValue; // 如果是属性,则其nodeValue返回就是属性的值
// 5.找到input子节点,添加事件监听
// 20.这个时候如果我们修改input里面的值,事件监听就会重新设置vm实例的值,触发访问器的set方法
node.addEventListener('input',function(e){
vm[name] = e.target.value;
});
// 6.把实例中data对象对应上面的name属性的值赋值给子节点的value值,触发访问器get
// 注意此时的Dep.target还是undefined
node.value = vm[name]
node.removeAttribute('v-model')
}
}
}
//节点类型为文本
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配的正则表达式的第一个括号的表达式
name = name.trim();
// 8.符合文本的条件,创建观察者实例,进入观察者构造函数
new Watcher(vm, node, name);
}
}
}
function Watcher (vm, node, name) {
// 9.此时将Watcher实例赋给了Dep.target
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
// 10.执行Watcher原型上的update方法
this.update();
// 17.清空Dep.target
Dep.target = null;
}
Watcher.prototype = {
// 23.从11步开始重复执行,将改变的vm中data的值赋给了文本节点
update: function () {
// 11.先执行原型上的get()方法
this.get();
// 16.将这个value为'hello vue'的值赋给了文本节点的nodeValue值
this.node.nodeValue = this.value;
},
get: function () {
// 12.get方法将vm中的name属性值赋给value属性,触发vm上data的访问器get
// 15.返回了val的属性'hello vue'
this.value = this.vm[this.name];
}
}
function Dep () {
this.subs = [];
}
Dep.prototype = {
// 14.将Watcher放入subs的数组中
addSub: function (sub) {
this.subs.push(sub);
},
// 22.执行存储在subs数组中的Watcher观察者的update方法
notify: function () {
this.subs.forEach(function(sub){
sub.update();
})
}
}
function Vue (options) {
this.data = options.data;
var data = this.data;
// 1.遍历data对象上的所有属性
observe(data, this);
var id = options.el;
// 3.找到app根节点下的子节点,创建dom片段
var dom = nodeToFragment(document.getElementById(id), this);
// 19.将dom片段挂载到这个根元素app下
document.getElementById(id).append(dom);
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello vue'
}
})
博主也是将详细的步骤也是写在了注释上面,如果有的同学不理解的话,可以利用Chrome浏览器的断点调试功能来查看代码的执行步骤,此外这里也是应用到了一种叫做发布订阅的模式,博主也是采用了设计模式一书中的概括
一个或多个观察者对目标的状态感兴趣,它们通过将自己依附在目标对象上一边注册所感兴趣的内容,目标状态发生改变并且观察者可能对这些改变感兴趣,就会发送一个通知消息,调用每个观察者的更新方法。当观察者不再对目标状态感兴趣时,它们可以简单地将自己从中分离。
看到这个定义是不是对于上面的代码有了一些认识的呢,正是利用了这种模式来实现一个简单的vue响应式原理,就是这样简单的代码其中包含的东西也是非常多,需要有很多知识的铺垫才能理解,所以前路很长,还有很多等待我们去学习。