基于
Vue + Vuex + Vue-Router
的博客也算是基本完成了,对Vue
的使用也算是比较熟练了。但是,作为一个目标全栈的前端开发工程师来说,目标可不只是仅仅会使用而已,我们要熟悉框架原理,知道其思路,甚至于能手写一个简易框架。这篇博文的目的就是深入理解Vue
的双向绑定原理,分析思路,最终我们要自己手写一个简易的mvvm
框架,效果如下:
本人自认为不是一个适合写作的人,但我还是尝试去写出来,很多技术方面的东西在记录的过程中,会发现自己没有主要到的盲点,能帮助自己进一步学习,所以我会继续尝试写点什么。
这篇文章针对的是 Vue 1.x,虽然在这里我建议大家直接学习 Vue 2.x,但是理解原理上来说,Vue 1.x 够了。
因为是个人的学习总结,难免会出现错误,如果有什么不对的地方,欢迎大家指正。
原理
Vue 的数据绑定可以是单向的,也可以是双向的,无论单向还是双向,其实都属于下面两个过程:
- 数据变化 --> DOM 更新
- DOM 更新 --> 数据变化
这种模式是不是很熟悉,就是 观察者(订阅/发布者)模式,我们平时在对 DOM 操作中都会用到过,比如对 input 执行事件监听,如果有输入就执行一系列操作。但 Vue 的实现可不是基于 DOM 事件,它是一套更加高级的事件系统。下面我们就针对上面两个过程来一个一个的分析。
数据变化 --> DOM 更新
数据的变化怎么能通知 DOM 呢,这里的实现有多种,比如 angular 的脏值检查,在特定的事件发生时执行数据新旧值的检查,如果值发生改变了,那就执行通知。Vue 使用了一种不同的方式 -- setter,通过 Object.defineProperty() 来定义 setter,这样数据改变时就会触发 setter,就能通知 DOM 进行更新了。
但是,当数据更新时,怎么知道要通知哪些 DOM 进行数据更新?这里就需要存储依赖关系,将那些用到数据的引用保存起来,当数据更新时,逐一通知更新。现在就是在哪里进行依赖收集处理了,Vue 是在 getter 中处理的,当触发 getter 读取时,就保存引用数据的源头。
DOM 更新 --> 数据变化
这里就很好处理了,就是对 DOM 添加事件监听,当有值变化时,通知数据进行更新。这里包括 Vue 本身的 model 绑定,也包括我们自己的 DOM 操作,其本质都是一样的,读取新值,调用数据 setter 进行数据更新。
但上面只是很简单的原理分析,实际上 Vue 的实现要复杂得多,下面是 Vue 官方文档中的一张图。
最右侧的绿色的圆代表数据 data,数据的每个属性对象都被定义了 getter 和 setter 两个属性,当通过 watcher 读取数据时,会先执行依赖收集;而通过 setter 更新数据时,会通知 watcher。
最左侧的黄色的方框代表指令集 directive,当 watcher 接收到数据更新时,会通知 directive 执行 update,由 directive 内部实现怎么执行 DOM 更新。
中间的紫色圆就是 watcher,它是连接 data 和 directive 的桥梁,也就是说 directive 不会直接读写 data 里面的数据,数据 data 的变化也不会直接更新到 DOM,watcher 在这里起到一个中介的作用。
从原理的角度上来说,上图的内容是不全的,上图是为了使用者更好的理解 Vue 的响应式,并不适合用来深入研究原理。下图是《Vue.js 权威指南》中源码篇的一个章节中画的图,专门画给研究者看的:
上图比之前的图多了 Dep 和 Observer。一个是上面我们提到的依赖保存,一个是对数据的处理。下面我们聊聊 Directive、Watcher、Dep 和 Observer 的关系及处理逻辑。
首先是 Observer,正如其名 -- 观察者,就是用来监听数据变化并做相应的通知处理。
在 Vue 中,Observer 会观察两种类型的数据,Object 与 Array。
对于 Array 类型的数据,会先重写 Array 的原型方法,重写后能达到两个目的,
- 当数组发生变化时,触发 notify
- 如果是 push,unshift,splice 这些添加新元素的操作,则会使用 observer 观察新添加的数据
重写完原型方法后,遍历拿到数组中的每个数据,使用 observer 观察它。
而对于 Object 类型的数据,则遍历它的每个 key,使用 defineProperty 设置 getter 和 setter,当触发 getter 的时候,observer 则开始收集依赖,而触发 setter 的时候,observer 则触发 notify。
然后是依赖的收集,这时就需要 Dep 出场了。
前面我们说到数据的依赖收集是在 getter 里,没错,但不是所有,因为只有和 DOM 绑定的 getter 才应该被添加到依赖中,Vue 中是只收集通过 watcher 触发 getter 的依赖,而被收集的依赖,其实就是 watcher 实例本身。这里我再强调一次:
当数据的 getter 触发后,会收集依赖,但也不是所有的触发方式都会收集依赖,只有通过watcher 触发的 getter 会收集依赖,而所谓的被收集的依赖就是当前 watcher。
因为只有watcher触发的 getter 才会收集依赖,所以 DOM 中的数据必须通过 watcher 来绑定,就是说 DOM 中的数据必须通过 watcher 来读取!
关于 Dep 的实现,这里提出一部分代码实现:
var uid = 0;
function Dep() {
this.id = uid++;
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
},
// some code
};
当通过 watcher 触发 getter 时,watcher 会使用 dep.addSub(this) 把自己的实例推到 subs 中。
当触发 setter 的时候,会触发 notify,而 notify 则会把 watcher 的 update 方法执行一遍。
到这里 observer dep watcher 的逻辑关系已经清楚了。
至于Directive 如何通过 watcher 的 update 方法改变视图,下面就开始分析 Vue 在模板渲染的过程。
在上图中,左边是上面讲的内容,下面我们讲讲右边的内容。
对于编译部分 vue 分了两种类型,一种是文本节点,一种是元素节点。
文本节点
hello {{name}}
这就是一个文本节点,它包含两部分,普通文本节点 hello 和一个特殊的节点 {{name}}。
接下来 Vue 会通过正则来解析文本节点,解析后的结果如下面结构:
[{
"value": "hello "
}, {
"value": "name",
"tag": true,
"html": false,
"oneTime": false
}]
然后就是遍历 Array,将所有 tag 为 true 的添加扩展对象,扩展属性包括指令方法。
像文本节点的特殊节点只有两种类型,text 和 html,所以简单判断 html 的值就可以知道,应该给扩展类型添加那种指令的接口。
添加扩展对象后大概长成下面的样子。
[{
"value": "hello "
}, {
"value": "name",
"tag": true,
"html": false,
"oneTime": false,
"descriptor": {
"def": {
"update": function(){
// some code
},
"bind": function(){
// some code
}
},
expression: "xx",
filters: "xx",
name: "text"
}
}]
vue 内置的指令都会抛出两个接口 bind 和 update,这两个接口的作用是,编译的最后一步是执行所有用到的指令的 bind 方法,而 update 方法则是当 watcher 触发 update 时,Directive 会触发指令的 update 方法。
observe -> 触发setter -> watcher -> 触发update -> Directive -> 触发update -> 指令。
再然后将所有 tag 为 true 的数据中的扩展对象拿出来生成一个 Directive 实例并添加到 _directives 中(_directives是当前vm中存储所有directive实例的地方)。
this._directives.push(
new Directive(descriptor, this, node, host, scope, frag)
)
最后就是循环 _directives 执行所有 directive实例的 _bind 方法。
Directive 中 _bind 方法的作用有几点:
- 调用所有已绑定的指令的 bind 方法
- 实例化一个 Watcher,将指令的 update 与 watcher 绑定在一起(这样就实现了 watcher 接收到消息后触发的 update 方法,指令可以做出对应的更新视图操作)
- 调用指令的 update,首次初始化视图
这里有一个点需要注意一下,实例化 Watcher 的时候,Watcher会将自己主动的推入Dep依赖中。
总结
响应式原理共有四个部分,observe、Dep、watcher、Directive。
observer可以监听数据的变化。
Dep 可以知道数据变化后通知给谁。
Watcher 可以做到接收到通知后将执行指令的update操作。
Directive 可以把 Watcher 和 指令 连在一起。
不同的指令都会有update方法来使用自己的方式更新 dom。
必须使用watcher触发getter,Dep才会收集依赖。
执行流:
当数据触发 setter 时,会发消息给所有 watcher,watcher 会跟执行指令的 update 方法来更新视图。
当指令在页面上修改了数据会触发 watcher 的 set 方法来修改数据。
实现(DEMO)
因为实现部分思想逻辑不想再写一遍了,所以直接上代码,看注释理解。
Observer
Observer 的实现主要有下面几点:
- 对数据的劫持。通过 getter 和 setter 实现。
- 依赖的添加。依赖添加的时机应该在读取数据时,也就是调用 getter 方法的时候,但不是所有的读取都会添加依赖,需要在编译模板时判断。
- 依赖添加方式。因为 Observer 和 Watcher 是独立的,所以添加依赖的判断是通过一个全局变量 Dep.target 实现的。
/**
* Created by Jay-W on 2017/2/12.
*/
function Observer(data) {
this._data = data;
this.covertData();
}
Observer.prototype = {
covertData: function () {
Object.keys(this._data).forEach(function (key) {
this.defineReactive(key, this._data[key]);
}, this);
},
defineReactive: function (key, value) {
// 监听的是数据,依赖应该直接和数据关联,所以在这里生成依赖处理对象
var dep = new Dep();
// 继续监听子数据
if(value && typeof value === "object") new Observer(value);
// 改写 getter、setter
Object.defineProperty(this._data, key, {
enumerable: true,
configurable: false,
get: function () {
if(Dep.target) {
dep.depend();
}
return value;
},
set: function (newVal) {
if(newVal === value) return;
value = newVal;
dep.notify();
}
})
}
};
function Dep() {
this.deps = [];
}
Dep.prototype = {
depend: function () {
// 调用 Watch 方法,并将 Dep 实例传递过去
Dep.target.addDep(this);
},
addDep: function (dep) {
this.deps.push(dep);
},
notify: function () {
// 当数据更新时,依次调用 watch 的 update 方法
this.deps.forEach(function (dep) {
dep.update();
})
}
};
Watcher
watcher 是连接 Observer 和 Compiler 的桥梁,其实现也比较简单,主要要注意下面两点:
- 生成 Watcher 实例的时候,通过读取 vm 的数据主动触发 getter 方法,实现依赖的添加。
- 将添加依赖和更新 DOM 的方法暴露给 Observer。
/**
* Created by Jay-W on 2017/2/12.
*/
function Watcher(vm, exp, updater) {
this.vm = vm;
this.exp = exp;
this.updater = updater;
this.value = this._getVmValue();
}
Watcher.prototype = {
addDep: function (dep) {
// 调用传递过来的 Dep 实例方法,将 watch 自身添加到依赖中
if(dep.deps.indexOf(this) === -1){
dep.addDep(this);
}
},
update: function () {
var oldValue = this.value;
var value = this._getVmValue();
if(oldValue === value) return;
this.updater && this.updater.call(this.vm, this._getVmValue());
this.value = value;
},
_getVmValue: function () {
// 通过主动调用 getter 方法来将自身添加到 Dep
Dep.target = this;
var val = this.vm.$data;
this.exp.split(".").forEach(function (key) {
val = val[key];
});
Dep.target = null;
return val;
}
};
Compiler
这块主要就是编译模板,将属性转化成指令,同时在添加指令的时候生成 Watcher 实例。
/**
* Created by Jay-W on 2017/2/12.
*/
function Compiler(el, vm) {
this._el = this.isElementNode(el) ? el : document.querySelector(el);
this._vm = vm;
// 开始编译模板
var fragment = this.node2Fragment(this._el);
this.compile(fragment);
this._el.appendChild(fragment);
}
Compiler.prototype = {
node2Fragment: function (el) {
var fragment = document.createDocumentFragment();
while (el.firstChild) fragment.appendChild(el.firstChild);
return fragment;
},
compile: function (el) {
[].slice.apply(el.childNodes).forEach(function (node) {
var nodeContent = node.textContent.trim(),
reg = /\{\{(.*?)\}\}/,
isTextNode = nodeContent.match(reg);
if (this.isElementNode(node)) { // 当子节点为元素节点
this._compileElementNode(node);
} else if (this.isTextNode(node) && isTextNode) { // 当子节点为文本节点
this._compileTextNode(node, isTextNode[1]);
}
// 当仍然存在子节点时,继续迭代
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
}, this);
},
_compileElementNode: function (node) {
[].slice.apply(node.attributes).forEach(function (attr) {
var attrName = attr.name,
reg = /v-([^\:]+)\:?(.*)/,
isDirective = attrName.match(reg);
if (isDirective) {
var dir = isDirective[1],
extra = isDirective[2],
exp = attr.value;
Directive[dir] && Directive[dir](node, this._vm, dir, exp, extra);
// 移除指令属性
node.removeAttribute(attrName);
}
}, this);
},
_compileTextNode: function (node, exp) {
Directive.textNode(node, this._vm, exp);
},
isElementNode: function (el) {
return el.nodeType === 1;
},
isTextNode: function (el) {
return el.nodeType === 3;
}
};
// 指令集
var Directive = {
_bind: function (node, vm, dir, exp) {
var updateFn = this._updates[dir];
updateFn && updateFn(node, this._getVmValue(vm ,exp));
new Watcher(vm, exp, function (value) {
updateFn && updateFn(node, value);
});
},
html: function (node, vm, exp) {
this._bind(node, vm, "html", exp);
},
text: function (node, vm, exp) {
this._bind(node, vm, "nodeText", exp);
},
textNode: function (node, vm, exp) {
this._bind(node, vm, "nodeText", exp);
},
model: function (node, vm, dir, exp) {
this._bind(node, vm, dir, exp);
var oldVal = this._getVmValue(vm, exp),
self = this;
node.addEventListener("input", function () {
var value = this.value;
if(value === oldVal) return;
self._setVmValue(vm, exp, value);
})
},
on: function (node, vm, dir, exp, extra) {
var eventFn = vm.$options.methods && vm.$options.methods[exp];
if (extra && eventFn) {
node.addEventListener(extra, eventFn.bind(vm), false);
}
},
_getVmValue: function (vm, exp) {
var data = vm.$data;
exp.split(".").forEach(function (k) {
data = data[k];
});
return data;
},
_setVmValue: function (vm, exp, value) {
var data = vm.$data,
keys = exp.split(".");
keys.forEach(function (k, i) {
if(i < keys.length - 1){
data = data[k];
} else {
data[k] = value;
}
});
},
// update 更新规则列表
_updates: {
nodeText: function (node, value) {
node.textContent = typeof value === 'undefined' ? '' : value;
},
model: function (node, value) {
node.value = typeof value === "undefined" ? "" : value;
},
html: function (node, value) {
node.innerHTML = typeof value === "undefined" ? "" : value;
}
}
};
最后
就这个简单的 MVVM 框架,我反复看了、写了很多遍,学到了很多。整体来说,实现不难,但这个设计思想,我觉得我现在是肯定想不出来的。。。要走的路还很长嘛
全部代码地址:github