简单实现Vue之指令解析器Compile类
创建html文件
- 文件名为index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue学习demo实现</title>
</head>
<body>
<div id="app">
<h2>大家好!我是{{person.name}},今年{{person.age}}岁。</h2>
<h3>我最喜欢的游戏是{{person.fav}}</h3>
<div v-text="msg"></div>
<div v-html="htmlStr"></div>
<input type="text" v-model="msg">
<br />
<button v-on:click="handlerClick">按钮</button>
<button @click="handlerClick2">按钮2</button>
<br />
<a v-bind:href="url" target="_blank">{{url}}</a>
<a :href="url" target="_blank">百度一下</a>
</div>
<!-- <script src="Vue.js"></script> -->
<script src="MyVue.js"></script>
<script>
vm = new Vue({
el: "#app",
data: {
person: {
name: "小明",
age: 19,
fav: "LOL"
},
msg: "我很喜欢聊天",
htmlStr: "<h4>还喜欢和大家一起学习。</h4>",
url: "https://www.yanfee.com"
},
methods: {
handlerClick() {
console.log("this is a button!");
},
handlerClick2() {
this.$data.person.name = "大明"
}
}
})
</script>
<style>
#app {
background-color: bisque;
}
</style>
</div>
</body>
</html>
将实现的指令为 v-text,v-html,v-model,v-bind(:attr),v-on(@click)。
创建脚本文件
- 文件名:MyVue.js
class MyVue {
// 构造函数
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 实现指令解析类
new Compile(this.$el, this);
}
}
}
在实例化MyVue时,将选项传入并初始化,接下来将实现Compile类,将html文件中各个指令转化为html文本并替换。
class Compile {
constructor(el, vm) {
// 检查el是否为dom节点 如果不是则获取节点
this.el = this.isElmentNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 获取文档碎片
const fragment = this.node2Fragment(this.el);
// 编译文档碎片
this.compile(fragment);
// 追加编译好的元素到根节点
this.el.appendChild(fragment);
}
}
实现Compile类中各个方法
// 实现指令编译方法
compile(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
// 识别节点类型并处理
[...childNodes].forEach(child => {
if (this.isElmentNode(child)) {
// 元素节点处理
this.compileElment(child);
} else {
// 文本节点处理
this.compileText(child)
}
if (child.childNodes && child.childNodes.length) {
this.compile(child)
}
})
}
}
对节点元素指令识别和实现,判断指令类型并提取指令 如 v-text=“msg”,将node中的html文本替换为Vue实例中data下msg的内容
// 处理元素节点
compileElment(node) {
const attributes = node.attributes; //获取元素属性
[...attributes].forEach(attr => {
const { name, value } = attr; // v-text="msg"
// console.log(name, value); // v-text msg
// 是否为指令
if (this.isDirective(name)) { // 得到指令 v-text v-model... v-on:click v-bind
const [, directive] = name.split("-"); // 得到指令 text model bind on:click
// 对on:click 进一步处理
const [directName, attrName] = directive.split(":")
// 编译指令工具类
compileUtil[directName](node, value, this.vm, attrName);
}
// 删除有指令的标签属性
node.removeAttribute('v-' + dirctive)
})
}
/** 识别是否为指令 */
isDirective(attrName) {
return attrName.startsWith("v-")
}
实现编译工具,单独抽出为一个工具对象中
/* 对指令进行编译实现 */
const compileUtil = {
/**
*
* @param {*} node 元素节点
* @param {string} exp 指令表达式 在data中找值需要
* @param {*} vm Vue实例
*/
text(node, exp, vm) {
},
html(node, exp, vm) {
},
model(node, exp, vm) {
},
/**
*
* @param {*} node 元素节点
* @param {*} exp 指令表达式
* @param {*} vm Vue实例
* @param {*} eventName 事件名称
*/
on(node, exp, vm, eventName) {
},
/**
*
* @param {*} node 元素节点
* @param {*} exp 指令表达式
* @param {*} vm Vue实例
* @param {*} attrName 属性名称
*/
bind(node, exp, vm, attrName) {
}
}
对各个指令具体实现
- v-text 实现
/**
* 获取data中的值 如person.name="小明"
*/
getValue(exp, vm) {
// exp = person.fav
return exp.split(".").reduce((data, curValue) => {
return data[curValue]
}, vm.$data)
},
text(node, exp, vm) {
// 获取指令表达式的值
let value = this.getValue(exp, vm);
// 更新
this.updater.textUpdater(node, value);
},
updater: {
// 更新文本内容
textUpdater(node, value) {
node.textContent = value;
},
}
- v-html 实现
html(node, exp, vm) {
let value = this.getValue(exp, vm);
this.updater.htmlUpdater(node, value);
},
updater: {
// ...
// 插入html碎片
htmlUpdater(node, value) {
node.innerHTML = value;
},
}
- v-model 实现
model(node, exp, vm) {
let value = this.getValue(exp, vm);
this.updater.modelUpdater(node, value);
},
updater: {
// ...
// 绑定input的value
modelUpdater(node, value) {
node.value = value;
},
}
- v-bind 实现
/**
*
* @param {*} node 元素节点
* @param {*} exp 指令表达式
* @param {*} vm Vue实例
* @param {*} attrName 属性名称
*/
bind(node, exp, vm, attrName) {
let value = this.getValue(exp, vm);
this.updater.bindUpdater(node, attrName, value);
},
updater: {
// ...
// 绑定属性
bindUpdater(node, attrName, attrValue) {
node.setAttribute(attrName, attrValue)
}
}
- v-on:click实现
/**
*
* @param {*} node 元素节点
* @param {*} exp 指令表达式
* @param {*} vm Vue实例
* @param {*} eventName 事件名称
*/
on(node, exp, vm, eventName) {
let fn = vm.$options.methods && vm.$options.methods[exp];
node.addEventListener(eventName, fn.bind(vm), false)
},
对@click
指令和:src
识别并处理
// 处理元素节点
compileElment(node) {
const attributes = node.attributes; //获取元素属性
[...attributes].forEach(attr => {
const { name, value } = attr; // v-text="msg"
// console.log(name, value); // v-text msg
// 是否为指令
if (this.isDirective(name)) { // 得到指令 v-text v-model... v-on:click v-bind
//...
} else if (this.isBindName(name)) { //绑定属性简写 :src :style...
// 获取绑定属性的key
let [, attrName] = name.split(":");
compileUtil["bind"](node, value, this.vm, attrName);
// 删除带有指令的属性标签
node.removeAttribute(':' + attrName);
}
})
}
对带标签文本进行替换 如<h2>大家好!我是{{person.name}},今年{{person.age}}岁。</h2>
中的{{}}
里的内容
// Compile类中
/**
* 编译文本指令 如 {{person.name}}
*/
compileText(node) {
const content = node.textContent;
// 正则匹配 {{***}} 内容
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil["text"](node, content, this.vm)
}
}
//compileUtil中
/**
*
* @param {*} node 元素节点
* @param {string} exp 指令表达式 在data中找值需要
* @param {*} vm Vue实例
*/
text(node, exp, vm) {
let value;
if (exp.indexOf("{{") !== -1) {
// 对{{person.name}}进行替换
value = exp.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 获取到要替换的表达式 args中第二个元素即为需要的元素
// console.log(args);
return this.getValue(args[1], vm)
})
} else {
// 获取指令表达式的值
value = this.getValue(exp, vm);
}
// 更新
this.updater.textUpdater(node, value);
},
至此,vue中又数据驱动视图更新部分完成。
完整源码如下:
// MyVue.js
/* 对指令进行编译实现 */
const compileUtil = {
/**
* 获取data中的值 如person.name="小明"
*/
getValue(exp, vm) {
// exp = person.fav
return exp.split(".").reduce((data, curValue) => {
return data[curValue]
}, vm.$data)
},
/**
*
* @param {*} node 元素节点
* @param {string} exp 指令表达式 在data中找值需要
* @param {*} vm Vue实例
*/
text(node, exp, vm) {
let value;
if (exp.indexOf("{{") !== -1) {
// 对{{person.name}}进行替换
value = exp.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 获取到要替换的表达式 args中第二个元素即为需要的元素
// console.log(args);
return this.getValue(args[1], vm)
})
} else {
// 获取指令表达式的值
value = this.getValue(exp, vm);
}
// 更新
this.updater.textUpdater(node, value);
},
html(node, exp, vm) {
let value = this.getValue(exp, vm);
this.updater.htmlUpdater(node, value);
},
model(node, exp, vm) {
let value = this.getValue(exp, vm);
this.updater.modelUpdater(node, value);
},
/**
*
* @param {*} node 元素节点
* @param {*} exp 指令表达式
* @param {*} vm Vue实例
* @param {*} eventName 事件名称
*/
on(node, exp, vm, eventName) {
let fn = vm.$options.methods && vm.$options.methods[exp];
node.addEventListener(eventName, fn.bind(vm), false)
},
/**
*
* @param {*} node 元素节点
* @param {*} exp 指令表达式
* @param {*} vm Vue实例
* @param {*} attrName 属性名称
*/
bind(node, exp, vm, attrName) {
let value = this.getValue(exp, vm);
this.updater.bindUpdater(node, attrName, value);
},
updater: {
// 更新文本内容
textUpdater(node, value) {
node.textContent = value;
},
// 插入html碎片
htmlUpdater(node, value) {
node.innerHTML = value;
},
// 绑定input的value
modelUpdater(node, value) {
node.value = value;
},
// 绑定属性
bindUpdater(node, attrName, attrValue) {
node.setAttribute(attrName, attrValue)
}
}
}
class Compile {
constructor(el, vm) {
// 检查el是否为dom节点 如果不是则获取节点
this.el = this.isElmentNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 获取文档碎片
const fragment = this.node2Fragment(this.el);
// 编译文档碎片
this.compile(fragment);
// 追加编译好的元素到根节点
this.el.appendChild(fragment);
}
// 实现指令编译方法
compile(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
// 识别节点类型并处理
[...childNodes].forEach(child => {
if (this.isElmentNode(child)) {
// 元素节点处理
this.compileElment(child);
} else {
// 文本节点处理
this.compileText(child)
}
if (child.childNodes && child.childNodes.length) {
this.compile(child)
}
})
}
// 处理元素节点
compileElment(node) {
const attributes = node.attributes; //获取元素属性
[...attributes].forEach(attr => {
const { name, value } = attr; // v-text="msg"
// console.log(name, value); // v-text msg
// 是否为指令
if (this.isDirective(name)) { // 得到指令 v-text v-model... v-on:click v-bind
const [, directive] = name.split("-"); // 得到指令 text model bind on:click
// 对on:click 进一步处理
const [directName, attrName] = directive.split(":")
// 编译指令工具类
compileUtil[directName](node, value, this.vm, attrName);
// 删除带有指令的属性标签
node.removeAttribute('v-' + directive);
} else if (this.isEventName(name)) { //绑定事件指令 @click
// 获取事件类型
let [, eventName] = name.split("@");
compileUtil["on"](node, value, this.vm, eventName);
// 删除带有指令的属性标签
node.removeAttribute('@' + eventName);
} else if (this.isBindName(name)) { //绑定属性简写 :src :style...
// 获取绑定属性的key
let [, attrName] = name.split(":");
compileUtil["bind"](node, value, this.vm, attrName);
// 删除带有指令的属性标签
node.removeAttribute(':' + attrName);
}
})
}
/**
* 编译文本指令 如 {{person.name}}
*/
compileText(node) {
const content = node.textContent;
// 正则匹配 {{***}} 内容
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil["text"](node, content, this.vm)
}
}
node2Fragment(node) {
const fragment = document.createDocumentFragment();
while (node.firstChild) {
fragment.appendChild(node.firstChild);
}
return fragment;
}
/** 识别是否为属性简写绑定 */
isBindName(attrName) {
return attrName.startsWith(":")
}
/** 识别是否为事件简写 */
isEventName(attrName) {
return attrName.startsWith("@")
}
/** 识别是否为指令 */
isDirective(attrName) {
return attrName.startsWith("v-")
}
/** 识别是否为元素节点 */
isElmentNode(node) {
return node.nodeType === 1;
}
}
class MyVue {
// 构造函数
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 实现指令解析类
new Compile(this.$el, this);
}
}
}
demo 地址:Demo