原生js实现vue指令
1 简介
使用原生js实现vue特性。这里分享一个v-show指令的简单例子,代码会更新到 Gitee 上,只考虑了简单实现并没有考虑真实应用场景和代码优化。
2 需求
先简单构建一个需求,如下图:
在vue中实现这样的功能非常简单,因为vue已经提供了v-show、v-if这样的指令:
<template>
<div>
<button @click="clickHandle">button</button>
<h1 v-show="visible">some content</h1>
</div>
</template>
<script>
export default {
data(){
return {
visible: true,
}
},
methods:{
clickHandle(){
this.visible = !this.visible;
}
}
}
</script>
接下来我们看看原生如何实现像v-show这样的指令。
3 设计与实现
3.1 编码设计
我们参考vue的生命周期,只保留需要实现v-show指令的关键步骤:
- 在初始化时绑定数据和事件
- 在数据变化时更新dom 我们将这些功能拆分为如下几个部分:
3.2 编码实现
- 我们先定义
dataStore和directive分别用于存储数据和指令,这里定义的指令回调接收两个参数,一个是绑定指令的元素el,另一个是指令绑定的dataStore中的属性的值value。
// 用于存储响应式数据
const dataStore = {
visible: true,
};
// directive对象的属性名表示指令名称,属性值是指令的回调函数
const directive = {
"v-show": function (el, value) {
if (value) {
el.style.display = "";
} else {
el.style.display = "none";
}
},
};
- 按照前面章节中的设计将
dom结构画出来,并且根据前面的定义为标签添加指令属性:
<div id="#app">
<button>
<span v-show="visible">隐藏</span>
<span v-show="!visible">显示</span>
</button>
<h1 v-show="visible">this is your content</h1>
</div>
- 为了实现
响应式原理,我们先使用Object.defineProperty来实现一个对象属性监听函数。
/**
* 对象属性监听
* @param {object} obj - 要监听的对象
* @param {string} key - 要监听的对象属性
* @param {object} options - 监听选项
* @param {boolean} [options.immediate] - 是否首次触发回调
* @param {function} options.callBack - 监听回调
* @param {*} options.value - 初始值
*/
function watch(obj, key, options) {
let temp = options.value;
if (
options &&
options.callBack &&
typeof options !== "function" &&
options.immediate
) {
options.callBack(undefined, temp);
}
Object.defineProperty(obj, key, {
set(val) {
const oldVal = temp;
temp = val;
if (options && options.callBack && typeof options !== "function") {
options.callBack(oldVal, val);
}
if (options && typeof options === "function") {
options(oldVal, val);
}
},
get() {
return temp;
},
});
}
- 前面的步骤中我们定义了指令,当然我们需要按照这样的定义去解析我们的指令。这里只实现了对于
dataStore中的属性直接取值或者取反值两种逻辑的解析,并没有像vue一样支持一个完整的表达式,那需要更多的编码,这里只是简单实现。当然如果笔者能坚持继续写下去的话,后期会考虑实现这样的功能。
/**
* parse the directive in tag
* @param {HTMLElement} el
* @param {string} name - directive name
* @param {function} callBack
* */
function directiveParser(el, name, callBack) {
let prop = name;
let flag = true;
if (name.charAt(0) === "!") {
prop = name.slice(1, name.length);
flag = false;
}
let value = flag ? dataStore[prop] : !dataStore[prop];
if (typeof callBack === "function") {
callBack(el, value);
}
}
- 接下来我们还需要一个能够解析
dom树的函数,当然在我们的设计中省略了vue中相当多的内容,所以解析逻辑也变得非常简单。当然这种极简设计也是可以扩展的,这里只是对指令业务进行了简单实现,你完全可以实现更多像directiveParser这样的函数去完成各种各样的业务。
/**
* parse dom
* @param {HTMLElement} node
**/
function parseDomTree(node) {
node.getAttributeNames().forEach((el) => {
if (directive[el]) {
directiveParser(node, node.getAttribute(el), directive[el]);
}
});
Array.from(node.children).forEach((el) => {
parseDomTree(el);
});
}
- 在完成前面的编码后我们简单组织一下代码就可以完成了:
// 省略了前面的步骤中定义的变量、函数以及html结构
// bind data
bind();
// register event
document.querySelector("button").onclick = () => {
dataStore.visible = !dataStore.visible;
};
// bind watch
function bind() {
Object.keys(dataStore).forEach((el) => {
watch(dataStore, el, {
immediate: true,
value: dataStore[el],
callBack: update,
});
});
}
// update dom
function update() {
parseDomTree(document.querySelector("#app"));
}
你可以在Gitee上获取完整的示例。