Vue源码:手写实现界面渲染mount和h函数

962 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第10天,点击查看活动详情

今天学习vue源码,跟着老师尝试手写简易的渲染的h函数和mount(没有加载组件,插槽功能)

介绍虚拟节点和h函数

h函数

在vue中,我们想要创建一个dom元素,可以用h函数,他接收三个参数,返回一个对象,也能就是虚拟dom节点

h(tag,obj,string|array)

tag表示想要创建的标签

传入divspan.....

obj是一个对象,可以传入标签的属性和方法

{
    class:"scc",
    OnClick:function(){}
}

第三个传入字符串或者数组

  1. 当传入字符串时,字符串便会当作tag的value,渲染到页面
  2. 当传入数组时,数组可以继续写h函数,为tag的子元素

h函数的实现

const h =(tag,props,children)=>{
    return{
        tag,
        props,
        children
    }
}

就是这么简单,传入三个值,返回三个值

虚拟节点

虚拟节点是一个对象,他包含三个值,tag,属性|方法,value或者子组件
使用虚拟节点有很多好处,比如他虚拟节点都是对象,进行diff算法节省时间
js操作不需要动dom,好处有很多,欢迎大家补充,一起学习

界面渲染

当虚拟节点创建后,我们需要在dom中挂载,我们创建一个文件raderer文件,用来执行界面渲染

mount函数

当元素挂载时,我们需要他传入两个参数,第一个时挂载的虚拟节点,第二个挂载的元素

count mount = (vnode,container)=>{
    // vnode是虚拟节点
    // container是需要挂载的dom节点
}

创建dom节点

当我们拿到vnode时,我们需要把他转换成真是dom,用createElement

count mount = (vnode,container)=>{
    const el = createElement(vnode.tag) // 我们是需要把h函数的返回,传进来的
}

添加属性和方法

这样,虚拟节点创建完了,下一步给他添加方法和属性,添加方法用addEventListener方法,添加属性用setAttribute,所以我们要先判断是属性还是方法

我们知道传入的方法都是on开头,我们就可以根据这个用startWidth方法

const mount = (vnode,container)=>{
    const el = vnode.el = document.createElement(vnode.tag)     //创建元素
    if(vnode.props){                            //因为也可能会传一个null,所以要先判断
        for(const key in vnode.props){          // 传递多个属性,方法要用循环
            const value = vnode.props[key]
            if(key.startsWith("on")){           // 判断是否已on开头
                el.addEventListener(key.slice(2).toLowerCase(),value)
            } else {
                el.setAttribute(key,value)
            }
        }
    }
}

这样,我们就完成了创建dom元素并且把属性和方法给他

添加子元素或value

同样,我们先判断传来的第三个参数是字符串还是数组,如果是数组直接用textContent给节点,如果是数组,就遍历数组添加节点

if(vnode.children){                            //同样先判断传进来了吗
    if(typeof vnode.children === "string"){    //如果是字符串直接赋值
        el.textContent = vnode.children
    } else {                                   //不是就递归创建子元素,挂载到el上面
        vnode.children.forEach(item => { 
            mount(item,el)
        });
    }
}

挂载el

最后一步把el挂载到container上面(container是我们传来的节点)
container.appendChild(el)

完整代码

渲染代码

const h =(tag,props,children)=>{
    return{
        tag,
        props,
        children
    }
}
const mount = (vnode,container)=>{
    const el = vnode.el = document.createElement(vnode.tag)
    if(vnode.props){
        for(const key in vnode.props){
            const value = vnode.props[key]
            if(key.startsWith("on")){
                el.addEventListener(key.slice(2).toLowerCase(),value)
            } else {
                el.setAttribute(key,value)
            }
        }
    }
    if(vnode.children){
        if(typeof vnode.children === "string"){
            el.textContent = vnode.children
        } else {
            vnode.children.forEach(item => {
                mount(item,el)
            });
        }
    }
    container.appendChild(el)
}

html代码

<body>
    <div id="app"></div>
    <script src="./raderer.js"></script>
    <script>
        const vnode = h("div",{class:"scc"},[
            h("span",null,"当前计数:100"),
            h("button",{onClick:function(){console.log(+1);}},"+1"),
            h("button",{onClick:function(){console.log(-1);}},"-1")
        ])
        mount(vnode,document.querySelector("#app"))
    </script>
</body>

界面展示

渲染.gif

结束语

大家觉得有用记得点赞,码字不易