开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第三天,点击查看活动详情
题引:
前几天学了一下petite-vue,也看了一些相关的源码,因此打算用自己的思路写一下petite-vue的编译方法。
正文:
第一步:html模板
先把需要用petite-vue控制的html模板写好
<div v-scope>
<div>{{msg}}</div>
</div>
第二步:挂载dom
在这里,我们需要实现一个简单版本的PetiteVue对象,目前该对象需要实现一个方法,就是createApp方法。
像官方的使用方法就是PetiteVue.createApp({age:15}).mount()
那么基于这个使用方法,我们就可以实现下面的代码
const PetiteVue = {
createApp(context){
const app = {
// 在app里存储传过来的上下文,后续需要用到
context,
// 该app需要返回mount挂载方法
mount(dom = 'v-scope'){
const root = document.querySelector(`[${dom}]`);
if(!root){
throw new Error('请提供v-scope属性进行标记');
return;
}
console.log(root);
root.removeAttribute('v-scope');
}
};
// 返回app是为了链式调用mount方法
return app
}
}
PetiteVue.createApp({ msg: "hello!" }).mount("v-scope");
这样,我们就基本实现了dom元素以及对应需要控制的区域,当然里面还有很多边缘判断,但是这里不过多实现,因为目的就是简单demo。
第三步:编译渲染
这一步我们需要遍历该root标签下的children节点,进而找到对应的mustache语法进行替换。
但在这之前,我们需要知道html中节点类型(nodeType)。
| NodeType | Named Constant |
|---|---|
| 1 | ELEMENT_NODE |
| 2 | ATTRIBUTE_NODE |
| 3 | TEXT_NODE |
| 4 | CDATA_SECTION_NODE |
| 5 | ENTITY_REFERENCE_NODE |
| 6 | ENTITY_NODE |
| 7 | PROCESSING_INSTRUCTION_NODE |
| 8 | COMMENT_NODE |
| 9 | DOCUMENT_NODE |
| 10 | DOCUMENT_TYPE_NODE |
| 11 | DOCUMENT_FRAGMENT_NODE |
| 12 | NOTATION_NODE |
而在我们的例子模板中,div的nodeType=1,{{msg}}就是TEXT_NODE即nodeType=3
那么我们就可以简单实现下面的代码
// 根据dom的nodeType判断
// 1是element
// 3是text
function traverse(node,context){
const {nodeType} = node;
if(nodeType === 1){...}
if(nodeType === 3){...}
}
const PetiteVue = {
createApp(context){
const app = {
// 在app里存储传过来的上下文,后续需要用到
context,
// 该app需要返回mount挂载方法
mount(dom = 'v-scope'){
const root = document.querySelector(`[${dom}]`);
if(!root){
throw new Error('请提供v-scope属性进行标记');
return;
}
// 在这里开始编译替换
traverse(root,context);
root.removeAttribute('v-scope');
}
};
// 返回app是为了链式调用mount方法
return app
}
}
PetiteVue.createApp({ msg: "hello!" }).mount("v-scope");
上面的代码中,我们只是实现了一个类型的判断,但是对于node的children我们还需要进行遍历判断。 那就有两种情况:
- 如果nodeType=1,那就继续找到它的children
- 如果nodeType=3,那就是找到了应用mustache语法的地方,我们需要进行正则判断
function traverse(node,context){
const {nodeType} = node;
if(nodeType === 1){
return traverseChildren(node,context);
}
if(nodeType === 3){
const text = node.textContent.trim(); // 提取文本值,在通过正则判断是不是mustache语法
const match = text.match(/[A-Za-z]+/g); // 正则规则并没有考虑过多情况 勿喷
// 如果正则通过
if(match){
node.textContent = "";
match.forEach(item=>{
const dataName = item.trim(); // 找到对应的mustache变量名,从context里面获取属性值
node.textContent += context[dataName]
})
}
}
}
function traverseChildren(node,context){
let chiid;
if(!node.firstChild){
child = node;
}
// 如果该节点有孩子,则获取孩子节点
if(node.firstChild){
child = node.firstChild;
}
while(child){
// 通过上面的判断,child要么是node,要么是node.firstChild。
// 由于标签可以嵌套,我们无法确定node.firstChild是文本节点还是标签,因为需要执行一次traverse
traverse(child,context);
// 像上面的
// <div v-scope>
// <div>{{msg}}</div>
// </div>
//实际上是隐藏了一个text标签
// <div v-scope><text></text>
// <div>{{msg}}</div>
// </div>
// 这个时候就需要直接获取该节点,进而拿取它的兄弟节点(nextSibling)
child = child.nextSibling;
}
// 当child为空的时候,我们需要判断node节点是否还有兄弟节点
if(!child && node.nextSibling){
traverseChildren(node.nextSibling,context);
}
}
以上就是我们的一个遍历过程,但我们会发现,这里面缺少了遍历完成的状态码,会把已经编译好的值重新编译一次,变成undefined,因为我们需要加一个状态码,就可以实现我们想要的效果。
let isFinish = false;
function traverse(node,context){
const {nodeType} = node;
if(nodeType === 1){
return traverseChildren(node,context);
}
if(nodeType === 3){
const text = node.textContent.trim(); // 提取文本值,在通过正则判断是不是mustache语法
const match = text.match(/[A-Za-z]+/g); // 正则规则并没有考虑过多情况 勿喷
// 如果正则通过
if(match){
node.textContent = "";
match.forEach(item=>{
const dataName = item.trim(); // 找到对应的mustache变量名,从context里面获取属性值
node.textContent += context[dateName]
})
}
}
}
function traverseChildren(node,context){
let chiid;
if(!node.firstChild){
child = node;
}
// 如果该节点有孩子,则获取孩子节点
if(node.firstChild){
child = node.firstChild;
}
while(child && !isFinish){
// 通过上面的判断,child要么是node,要么是node.firstChild。
// 由于标签可以嵌套,我们无法确定node.firstChild是文本节点还是标签,因为需要执行一次traverse
traverse(child,context);
// 像上面的
// <div v-scope>
// <div>{{msg}}</div>
// </div>
//实际上是
// <div v-scope><text></text>
// <div>{{msg}}</div>
// </div>
// 这个时候就需要直接获取该节点,进而拿取它的兄弟节点(nextSibling)
child = child.nextSibling;
}
// 当child为空的时候,我们需要判断node节点是否还有兄弟节点
if(!child && node.nextSibling){
traverseChildren(node.nextSibling,context);
isFinish = true;
}
}
const PetiteVue = {
createApp(context){
const app = {
// 在app里存储传过来的上下文,后续需要用到
context,
// 该app需要返回mount挂载方法
mount(dom = 'v-scope'){
const root = document.querySelector(`[${dom}]`);
if(!root){
throw new Error('请提供v-scope属性进行标记');
return;
}
// 在这里开始编译替换
traverse(root,context);
root.removeAttribute('v-scope');
}
};
// 返回app是为了链式调用mount方法
return app
}
}
PetiteVue.createApp({ msg: "hello!" }).mount("v-scope");
结尾:
第一次写这种思路可能不是很通俗易懂,后面将会进行优化。