1.图解说明
- 概要
2.代码共享
- GitHub: MyVue2.x
- 简单演示: MyVue2.x_Demo
3.代码实现
根据上图的概要逻辑,代码进行简单实现
- 模拟Vue实例
/**
* Vue模拟实现
*/
class MyVue {
constructor(options) {
// 1. 保存数据
this.$options = options;
this.$data = options.data;
this.$el = options.el;
if (this.$el) {
// 2. 创建Observer来监控数据的变化
new Observer(this.$data);
// 3. 代理 核心就是vue.data的属性 赋值给 vue
Object.keys(this.$data).forEach(attr => {
this._proxy(attr);
}
);
// 4. 创建Compile
new Compiler(this.$el, this);
}
}
// 对vue实例中的data对象进行代理
_proxy(attr) {
Object.defineProperty(this, attr, {
enumerable: true,
configurable: true,
set(newValue) {
this.$data[attr] = newValue;
},
get() {
return this.$data[attr];
}
})
}
}
- 创建观察者Observer来监控数据变化
/**
* 观察者 观察,监听
*/
class Watcher {
constructor(vm, attrValue, callback) {
this.vm = vm;
this.attrValue = attrValue;
this.callback = callback;
// 先将旧值保存起来
this.oldValue = this.getOldVal();
}
getOldVal() {
// 给Dep设置一个target属性,指向Watcher
Dep.target = this;
const oldValue = commonUtils.getVal(this.attrValue, this.vm);
// 这里需要设为Null,第一次解析后,当message值改变后
// 还会调用get方法,那时Dep.target不为null的话会,
// 会一直向dep.addSub添加watcher
Dep.target = null;
return oldValue;
}
update() {
const newValue = commonUtils.getVal(this.attrValue, this.vm);
if (newValue !== this.oldValue) {
this.callback(newValue);
}
}
}
/**
* 发布者
*/
class Dep {
constructor() {
this.subs = [];
}
// 添加观察者
addSub(watcher) {
this.subs.push(watcher);
}
// 通知观察者去更新node
notify() {
console.log("通知观察者", this.subs);
this.subs.forEach(watcher => {
watcher.update();
})
}
}
/**
* Observer 监视数据变化
*/
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (data && typeof data === 'object') {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
})
}
}
// 监视数据 Object.defineProperty
defineReactive(obj, key, value) {
// 递归遍历
this.observe(value);
// 每一个属性,对应一个Dep对象
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
// Dep.target 就是Watcher
dep.addSub(Dep.target);
}
return value;
},
set: (newValue) => {
this.observe(newValue);
if (newValue === value) {
return;
}
value = newValue;
dep.notify();
}
})
}
}
- 编译模板Dom
/**
* 1.编译dom, 初始化赋值
* 2.新建Watcher
*/
class Compiler {
constructor(el, vm) {
// 获取到当前传入的dom元素。例如: #app
this.el = commonUtils.isElementNode(el) ? el : document.querySelector(el);
// 保存当前vue实例
this.vm = vm;
// 1.创建文档碎片对象,放入内存中,减少页面的回流(reflow)和重绘(repaint)
const fragment = this._createFragment();
// 2.编译模板
this._compile(fragment);
// 3.将片段加入到dom中
this.el.appendChild(fragment);
}
// 创建片段
_createFragment() {
// 能做什么: 创建一个虚拟的节点对象,或者说,是用来创建文档碎片节点
// 什么特点: DocumentFragment存在于内存中,并不在DOM中
// 为什么用: 当需要添加多个dom元素时,如果先将这些元素添加到DocumentFragment中,再统一将DocumentFragment添加到页面
// 减少浏览器对dom渲染次数,效率会明显提升
const frag = document.createDocumentFragment();
let child;
while(child = this.el.firstChild) {
// 如果添加的child是文档中存在的元素,则通过appendChild在为其添加子元素时,会从文档中删除之前存在的元素
frag.appendChild(child);
}
return frag;
}
// 编译模板, 匹配dom和Watcher
_compile(fragment) {
const childNodes = fragment.childNodes;
childNodes.forEach(childNode => {
if (commonUtils.isElementNode(childNode)) {
// 是否是元素节点
this._compileElement(childNode);
}
if(commonUtils.isTextNode(childNode)) {
// 是否是文本节点
this._compileText(childNode);
}
// 元素节点的子元素也取出<h2>{{msg}}</h2>
if (childNode.childNodes && childNode.childNodes.length) {
this._compile(childNode);
}
})
}
// 编译属性节点
_compileElement(node) {
// <div v-text="message"></div>
// 为了遍历, 将伪数组转为数组
const attributeList = Array.prototype.slice.call(node.attributes);
attributeList.forEach(attr => {
// attr: v-text="message"
// 判断是否以v-开头, v-text, v-html, v-model
if (commonUtils.isStartWithVPrefix(attr.name)) {
// 取出text, html, model等
const attrName = attr.name.split("-")[1];
// 数据驱动视图 value: v-text="message"中的message等
commonUtils[attrName](node, attr.value, this.vm);
// 删除v-属性
node.removeAttribute("v-" + attrName);
}
})
}
// 编译文本节点
_compileText(node) {
// {{}}
const content = node.textContent;
if (commonUtils.regex.test(content)) {
commonUtils['text'](node, content, this.vm);
}
}
}
- 共通方法等
// 共通属性, 方法定义
const commonUtils = {
// 匹配mustache
regex: /\{\{(.+?)\}\}/g,
// 判断是dom中元素对象还是字符串
isElementNode(node) {
return node.nodeType === 1;
},
// 判断是dom中文本对象还是字符串
isTextNode(node) {
return node.nodeType === 3;
},
// 判断是否是v-前缀开头
isStartWithVPrefix(attrName) {
return attrName.startsWith("v-");
},
// 取出vue data中定义的属性值
getVal(attrValue, vm) {
return attrValue.split(".").reduce(function(result, currentValue) {
return result[currentValue];
},vm);
},
// 取出vue data中定义的属性值
setVal(attrValue, vm, inputVal) {
let length = attrValue.split(".").length;
return attrValue.split(".").reduce(function (result, currentValue, currentIndex) {
// person.name中的最后的name进行赋值
if (length - 1 === currentIndex) {
result[currentValue] = inputVal;
}
return result[currentValue];
}, vm)
},
// 为了替换 {{person.name}} -- {{person.age}}
getContentVal(attrValue, vm) {
return attrValue.replace(this.regex, (...args) =>{
return this.getVal(args[1], vm);
})
},
// 取出文本属性值
text(node, attrValue, vm) {
let value;
if (attrValue.indexOf("{{") !== -1) {
// {{person.name}}--{{person.age}}
let temp = this;
value = attrValue.replace(this.regex, function (target, m1) {
// 绑定观察者, 将来数据发生变化时会去调用
new Watcher(vm, m1, () => {
temp.updater.textUpdater(node, temp.getContentVal(attrValue, vm));
});
return temp.getVal(m1, vm);
});
} else {
// v-text="person.age"中的person.age
value = this.getVal(attrValue, vm);
// 绑定观察者, 将来数据发生变化时会去调用
new Watcher(vm, attrValue, (newValue) => {
this.updater.textUpdater(node, newValue);
});
}
this.updater.textUpdater(node, value);
},
// 取出html属性值
html(node, attrValue, vm) {
const value = this.getVal(attrValue, vm);
// 绑定观察者, 将来数据发生变化时会去调用
new Watcher(vm, attrValue, (newValue) => {
this.updater.htmlUpdater(node, newValue);
});
this.updater.htmlUpdater(node, value);
},
// 取出model属性值
model(node, attrValue, vm) {
const value = this.getVal(attrValue, vm);
// 绑定观察者, 将来数据发生变化时会去调用 数据 -> 视图
new Watcher(vm, attrValue, (newValue) => {
this.updater.modelUpdater(node, newValue);
});
// 视图 -> 数据 -> 视图
node.addEventListener("input", e => {
// 设置值
this.setVal(attrValue, vm, e.target.value);
})
this.updater.modelUpdater(node, value);
},
updater: {
textUpdater(node, value) {
// <div v-text="message">value</div>
node.textContent = value;
},
htmlUpdater(node, value) {
// <div v-html="message">value</div>
node.innerHTML = value;
},
modelUpdater(node, value) {
// <input type="text" v-model="message">
node.value = value;
}
}
}