简单实现Vue功能及原理总结
项目仓库:https://gitee.com/aeipyuan/vue_imitation.git
Vue类
获取数据并对各个工具类进行调度,通Object.defineProptert实现vm[key]=vm.$data[key]这一代理功能和computed、methods代理调用
/* 调度 */
class Vue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
let computed = options.computed;
let methods = options.methods;
if (this.$el) {
/* 监听器 */
new Observer(this.$data);
/* computed */
for (let key in computed) {
Object.defineProperty(this.$data, key, {
get: () => {
/* 执行该函数并返回值 */
return computed[key].call(this);
}
})
}
/* 方法 */
for (let key in methods) {
Object.defineProperty(this, key, {
get() {
/* 返回该函数,不执行 */
return methods[key];
}
})
}
/* 代理 */
this.proxyVm(this.$data);
/* 编译 */
new Compile(this, this.$el);
}
}
/* 代理 */
proxyVm(data) {
for (let key in data) {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newVal) {
data[key] = newVal;
}
})
}
}
}
Compile类
1.获取Vue实例对应的元素,然后将其所有子元素存入文档碎片
文档碎片作用:减少各元素改变时页面渲染次数,防止页面重排次数过多
/* 转为文档碎片 */
node2fragment(node) {
let fragment = document.createDocumentFragment();
let firstChild;
/* while循环将nodez全部加入到fragment */
while (firstChild = node.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
2.根据元素的nodeType来区别文本和元素,进行分类编译
/* 判断是否为element */
isElement(node) { return node.nodeType === 1; }
/* 编译 */
compile(node) {
/* 获取子元素并遍历 */
let childNodes = node.childNodes;
childNodes.forEach(child => {
/* 根据元素类型做不同处理 */
if (this.isElement(child)) {
this.compileElement(child);
/* 编译子元素 */
this.compile(child);
} else {
this.compileText(child);
}
})
}
3.对于element,运用析构赋值,对属性进行拆分,然后在编译工具类执行操作
/* 编译元素 */
compileElement(node) {
/* 取出属性 */
let attributes = node.attributes;
/* 对属性进行遍历 */
[...attributes].forEach(attr => {
let { name, value: valStr } = attr;/* v-on:click showAge */
/* 筛选'v-'开头的 */
if (name.startsWith('v-')) {
/* 删除'v-'取出类型 */
name = name.slice(2);/* on:click */
let [type, event] = name.split(':');/* on click */
/* 调用编译工具执行对应操作 */
CompileUtil[type](node, valStr, this.vm, event);
/* CompileUtil.on(node,value,vm,click) */
}
})
}
4.对于文本,利用正则判断是否具有双括号,传入工具类处理
/* 编译文本 */
compileText(node) {
let content = node.textContent;/* 名字:{{student.name}} */
if (/\{\{(.+?)\}\}/.test(content)) {
CompileUtil.text(node, content, this.vm);
}
}
CompileUtil对象
getVal、getTextVal、setVal等用于参数设置和修改,model、on、html、text调用参数处理,将回来的结果传给render的对应函数modelRender、htmlRender、textRender进行视图更新。
/* 编译工具 */
CompileUtil = {
/* 根据valStr和vm得到数据 */
getVal(vm, valStr) {
/* 获取属性名数组 */
let valArr = valStr.split('.'); /* ["student", "name"] */
/* reduce结合数组获取数据 */
return valArr.reduce((data, item) => {
return data[item];/* 先获取vm.$data[student]传回,再获取student[name] */
}, vm.$data);
},
/* 处理文本得到数据 */
getTextVal(vm, valStr) {/* 名字:{{student.name}} */
return valStr.replace(/\{\{(.+?)\}\}/g, (...args) => {
/* 将括号的值取出替换 */
return this.getVal(vm, args[1]);
})
},
/* 改变数据 */
setVal(vm, valStr, value) {
let valArr = valStr.split('.');
valArr.reduce((data, item, index, arr) => {
//修改数据
if (index == arr.length - 1) {
data[item] = value;
}
return data[item];
}, vm.$data);
},
model(node, valStr, vm) {/* node student.name vm */
/* 获取数据 */
let value = this.getVal(vm, valStr);
/* 调用render里的函数更新视图 */
let rend = this.render['modelRender'];
rend(node, value);
/* 添加订阅者随时改变视图 */
new Watcher(vm, valStr, (newVal) => {
node.textContent = newVal
rend(node, newVal);
})
/* 绑定input事件 */
node.oninput = (e) => {
this.setVal(vm, valStr, e.target.value);
}
},
on(node, valStr, vm, event) {/* node showAge vm click*/
node.addEventListener(event, (e) => {
vm[valStr].call(vm, e);
});
},
html(node, valStr, vm) {/* node msg vm */
let value = this.getVal(vm, valStr);
let rend = this.render['htmlRender'];
rend(node, value);
new Watcher(vm, valStr, newVal => {
rend(node, newVal);
})
},
text(node, valStr, vm) {/* node 名字:{{student.name}} vm*/
let rend = this.render['textRender']; /* 名字:11 */
let value = valStr.replace(/\{\{(.+?)\}\}/g, (...args) => {
/* 对每个数据添加订阅者 */
new Watcher(vm, args[1], newVal => {
rend(node, this.getTextVal(vm, valStr));
})
return this.getVal(vm, args[1]);
})
rend(node, value);
},
render: {
modelRender(node, value) { node.value = value; },
htmlRender(node, value) { node.innerHTML = value; },
textRender(node, value) { node.textContent = value; }
}
}
Observer监听器
利用Object.defineProperty对vm.$data进行监听,每个值设立一个属于自己的订阅器,改变时将通知所有订阅者
/* 监听器 */
class Observer {
constructor(data) {
this.observer(data);
}
/* 遍历data设置监听 */
observer(obj) {
if (obj && typeof obj == 'object') {
/* 获取obj所有子属性 */
let keys = Object.keys(obj);
/* 遍历子属性,全部设置监听 */
keys.forEach(key => {
this.defineRactive(obj, key, obj[key]);
})
}
}
/* 监听 */
defineRactive(obj, key, val) {
/* 若子对象则继续遍历 */
this.observer(val);
/* 给每个值设置一个订阅器 */
let dep = new Dep();
/* 监听改变函数 */
Object.defineProperty(obj, key, {
get() {
/* 判断是否有新订阅者 */
Dep.newSub && dep.addSub();
return val;
},
set: (newVal) => {
/* 相等则没必要更新 */
if (val !== newVal) {
/* 更新val已有的属性的数据,对新加的属性进行监听 */
if (typeof val == 'object') {/* 对象或数组 */
/* 更新内部内容 */
this.updateObj(val, newVal);
} else {
val = newVal;
}
/* 发布 */
dep.notify();
}
}
})
}
/* 对象更新设置子数据更新 */
updateObj(obj1, obj2) {
if (obj1 instanceof Object || obj1 instanceof Array) {
for (let key in obj1) {
if (obj1[key] instanceof Object || obj1[key] instanceof Array) {
this.updateObj(obj1[key], obj2[key]);
} else {
obj1[key] = obj2[key];
}
}
} else {
obj1 = obj2;
}
}
}
Dep订阅器
设立订阅数组subs存储订阅者,addSub向数组插入新的订阅者Dep.newSub(在Observer的get处调用),notify通知订阅者数组所有成员更新数据
/* 订阅器 */
class Dep {
constructor() {
/* 初始化订阅者数组 */
this.subs = [];
}
/* 增加订阅者 */
addSub() {
this.subs.push(Dep.newSub);
}
/* 发布 */
notify() {
/* 通知所有订阅者更新数据 */
this.subs.forEach(sub => {
sub.update();
})
}
}
Watcher订阅者
构造时获取属性值触发get,从而调用Dep的addSub,将自己加入到对应的subs数组中,update用于更新数据并将值传给回调函数
/* 订阅者 */
class Watcher {
constructor(vm, prop, cb) {
this.vm = vm;
this.prop = prop;
this.cb = cb;
/* 加入订阅者数组 */
this.join();
}
/* 加入订阅者数组 */
join() {
/* 将自己设置为订阅器的新订阅者 */
Dep.newSub = this;
/* 读取一次数据触发get,从而间接触发addSub */
this.oldVal = CompileUtil.getVal(this.vm, this.prop);
/* 重置,否则会重复添加 */
Dep.newSub = null;
}
/* 更新 */
update() {
/* 对比新旧值决定是否更新 */
let newVal = CompileUtil.getVal(this.vm, this.prop);
if (this.oldVal !== newVal) {
this.oldVal = newVal;
/* 运行回调函数 */
this.cb(newVal);
}
}
}