实现简单版的petite-vue

182 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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)。

NodeTypeNamed Constant
1ELEMENT_NODE
2ATTRIBUTE_NODE
3TEXT_NODE
4CDATA_SECTION_NODE
5ENTITY_REFERENCE_NODE
6ENTITY_NODE
7PROCESSING_INSTRUCTION_NODE
8COMMENT_NODE
9DOCUMENT_NODE
10DOCUMENT_TYPE_NODE
11DOCUMENT_FRAGMENT_NODE
12NOTATION_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我们还需要进行遍历判断。 那就有两种情况:

  1. 如果nodeType=1,那就继续找到它的children
  2. 如果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");

结尾:

第一次写这种思路可能不是很通俗易懂,后面将会进行优化。