Vue源码解析(一)深入浅出手撕简易VUE.JS和MVVM原理
声明
本文参考了小马哥的视频讲解和代码,结合自己的理解以及其它资料综合得出,水平有限,错误之处还望斧正。
一、最初的最初
先讲结论,Vue是采用数据劫持配合发布者-订阅者模式的方式,通过object.defineproperty()来劫持各个属性的getter和setter,当数据发生变动时,发布消息给依赖收集器,去通知观察者做出对应的回调函数来更新视图(也就是updater对象里面具体的更新方法)。
这其中最关键的在于几个类/对象:compile、observer、dep、watcher、updater,这几个各有处理的任务又相互关联共同实现MVVM。
其中最根本的是负责编译的compile(解析各种指令和{
{}}),并且在其里面安排了updater对象负责数据在视图的渲染和更新(无非是利用innerHTML、content什么的的来呈现数据),在此也实例化了观察者watcher(其中已经绑定了数据更新时对应的函数)与之建立联系。
observer负责对数据的劫持和监听(主要靠defineProperty完成),并且在其中实例化化了工具人Dep(就是声明个数组存储观察者并提供对应方法的类),getter的时候添加观察者,setter的时候利用dep通知观察者及时更新,由此调用观察者的更新方法也就是回调到updater对象完成数据更新。
二、compiler类的实现
声明一个被外界实例化的大类(如Vue),并且判断节点,以后要往里面添加observer和compile两个主要的类。
class MVue {
// 实现一个大类(外面new的类)
constructor(options) {
// 接收传来的el、data、method等等
this.$el = options.el;
this.$data = options.data;
this.$options = options; // 赋值
if (this.$el) {
// 判断是存在el节点就去实现编译类和observer类
new compile(this.$el, this);
}
}
}
那么先来实现compile类,拿到真正的节点并且创建文档碎片。使用文档碎片是为了避免对节点的操作触发过多的重绘和回流。
class compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el); // 元素节点直接给,字符串就去找
this.vm = vm;
const fragment = this.node2Fragment(this.el); // 为根节点el创建文档碎片对象
this.compile(fragment); // 编译子节点
this.el.appendChild(fragment); // 编译完成给真正的根节点去显示编译修改过的子节点们
}
}
node2Fragment (el) {
// 创建文档碎片
const f = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild) {
// 每次while都重新给一个firstChild的值
f.appendChild(firstChild); // 当前的firstChild也会被删除
}
return f;
}
isElementNode (node) {
// 判断是否是元素节点
return node.nodeType === 1;
}
重点是compile方法的实现,分别对元素节点和文本节点进行编译,这里调用的compileUtil是作为编译类compile的辅助对象。
compile (fragment) {
const childNodes = fragment.childNodes;
[...childNodes].forEach((child) => {
if (this.isElementNode(child)) {
// 元素节点
this.compileElment(child);
} else {
// 文本节点
this.compileText(child);
}
if (child.childNodes && child.childNodes.length) {
// 考虑到子节点可能有很多嵌套所以递归
this.compile(child);
}
})
}
compileText (node) {
// 文本节点处理
const content = node.textContent; // 拿到对应的文本内容
if ((/\{\{(.+?)\}\}/).test(content)) {
// 文本内容是否匹配{
{}}
compileUtil['text'](node, content, this.vm); // 是就走text方法
}
}
compileElment(node) {
// 元素节点处理
const attributes = node.attributes;