手写简易版vue
目标
实现若干简单指令(含文本替换指令,事件注册指令),实现插值表达式{{}}语法,且实现双向数据绑定。
核心思路
(图片摘自互联网)
实现过程
采取以终为始的方式,由目标倒推实现。
要实现的目标
模板
<div id="app">
<h1>插值表达式</h1>
<h3>{{ msg }}</h3>
<h3>{{ count }}</h3>
<h1>v-text</h1>
<div v-text="msg"></div>
<h1>v-model</h1>
<input type="text" v-model="msg" v-on:input="onTrigger" />
<h1>v-html</h1>
<div v-html="html"></div>
</div>
初始化Vue实例
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue',
count: 100,
person: { name: 'zs' },
test: 'abc', //[1,2,3],
html: '<strong>7777777</strong>',
},
methods: {
onTrigger() {
console.log('event trigger!!!');
},
},
});
实现Vue主类
class Vue {
constructor(options) {
// 1、通过属性保存选项的数据,默认为空对象
this.$options = options || {};
this.$data = options.data || {};
this.$methods = options.methods;
// 绑定DOM对象(字符串则通过DOM查询获取DOM)
this.$el =
typeof options.el === 'string'
? document.querySelector(options.el)
: options.el;
// 2、实例化observer对象,监听数据的变化
new Observer(this.$data);
// 3、实例化compiler对象,解析指令、插值表达式
new Compiler(this);
}
}
实现Observer类,劫持监听Vue实例中所有属性
class Observer {
constructor(data) {
console.log(this);
this.observe(data);
}
observe(data) {
// 如果设置的数据类型为对象就设置为响应式数据
if (data && typeof data === 'object') {
Object.keys(data).forEach((key) => {
//调用设置响应式数据的方法
this.definePReactive(data, key, data[key]);
});
}
}
// 设置属性为响应式数据
definePReactive(obj, key, value) {
// 利递归使深层属性转换为 响应式数据
this.observe(value);
const that = this; // 保存内部this, 方便内部调用
// 负责收集依赖并发送通知
let dep = new Dep();
console.log('dep', dep);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log('你访问了', obj, key);
// 订阅数据变化时, 往Dep中添加观察者, 收集依赖;
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newVal) {
//如果新设置的值也为对象, 也转换为响应式数据
// that.observe(newVal);
console.log(106, value, newVal);
if (newVal !== value) {
value = newVal; // 此处不能写obj[key] = newVal 会出现死递归,将value重置为newVal再次获取时因renturn value;就获取到了最新的属性值,实现属性值更新
// console.log(110, obj);
}
// 发送通知;
dep.notify();
},
});
}
}
实现Compiler类,解析指令,初始化视图
class Compiler {
constructor(vm) {
this.el = vm.$el;
this.vm = vm;
this.compile(this.el); // 构造函数立即执行模板编译
}
// 编译模板,处理文本节点和元素节点
compile(el) {
let childNodes = el.childNodes;
// console.log(childNodes);
// 遍历vm根节点的每个子节点
Array.from(childNodes).forEach((node) => {
// 若子节点为文本节点
if (this.isTextNode(node)) {
// 处理文本节点
this.compileText(node);
// 若子节点为元素节点
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node);
}
// 处理深层子节点,递归调用
// 判断node节点是否有子节点,如果有子节点,递归调用complie
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
// 编译元素节点,处理指令
compileElement(node) {
// 遍历所有的属性节点,判断是否是v-指令
Array.from(node.attributes).forEach((attr) => {
let attrName = attr.name;
// 若属性名为 指令
if (this.isDirective(attrName)) {
if (attrName.indexOf(':') != -1) {
var eventName = attrName.substr(5);
// 事件注册
node['on' + eventName] = this.vm.$methods[attr.value];
} else {
// v-text --> text
attrName = attrName.substr(2);
// 获取属性值(data中的键名)
let key = attr.value;
// 更新节点内容
this.update(node, key, attrName);
}
}
});
}
// 辅助方法,根据指令类型更新内容
update(node, key, attrName) {
// 根据属性名动态调用对应的更新方法---后面有分类定义的更新方法
let updateFn = this[attrName + 'Updater'];
// console.log(updateFn); 触发get方法,在get方法中调用addSub
updateFn && updateFn.call(this, node, this.vm.$data[key], key);
}
// 处理v-text指令
textUpdater(node, value, key) {
// 对于v-text指令,将节点内容替换为data中对应的值
node.textContent = value;
// 实例化对应的监听器
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
// 处理v-html指令
htmlUpdater(node, value, key) {
// 对于v-text指令,将节点内容替换为data中对应的值
node.innerHTML = value;
// 实例化对应的监听器
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
// 处理v-model指令
modelUpdater(node, value, key) {
// 对于v-model指令, 将节点内容替换为data中对应的值
node.value = value;
// 实例化对应的监听器
new Watcher(this.vm, key, (newValue) => {
node.value = newValue;
});
// 双向绑定,视图更新数据也更新
node.addEventListener('input', () => {
console.log('测试185');
this.vm.$data[key] = node.value;
});
}
// 编译文本节点,处理插值表达式
compileText(node) {
// 提取 {{}} 中的 变量 或 表达式
let reg = /\{\{(.+?)\}\}/;
let value = node.textContent;
// 是否可以匹配插值表达式
if (reg.test(value)) {
let key = RegExp.$1.trim();
// 替换成插值表达式对应的值(Vue实例的值) 触发get方法,在get方法中调用addSub
node.textContent = value.replace(reg, this.vm.$data[key]);
// 创建watch对象,当数据改变时改变视图
new Watcher(this.vm, key, (newValue) => {
console.log(newValue, '======');
node.textContent = newValue;
});
}
}
// 判断元素属性是否是指令
isDirective(attrName) {
return attrName.startsWith('v-');
}
// 判断节点是否是文本节点
isTextNode(node) {
return node.nodeType === 3;
}
// 判断节点是否是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
}
实现Dep类,收集依赖,为Watcher类的容器
class Dep {
constructor() {
this.subs = [];
}
// 添加观察者
addSub(sub) {
// sub存在且有update()方法
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 发送通知,遍历并调用每个观察者的update()
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
实现Watcher类,监听视图中数据变化
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
// data中的属性名称
this.key = key;
// 回调函数负责更新视图
this.cb = cb;
// 把watcher对象记录到dep的静态属性target,此时Dep会收集依赖
Dep.target = this;
console.log(this);
// 触发get方法,在get方法中调用addSub
this.oldValue = vm.$data[key];
Dep.target = null;
}
// 当数据变化的时候,更新视图
update() {
console.log('diaoyong');
let newValue = this.vm.$data[this.key];
if (this.oldValue === newValue) {
return;
}
this.cb(newValue);
}
}