前言
今天来模拟一下vue3的渲染系统的实现,不太了解这方面的可以认真看看,希望能对大家有帮助。
渲染系统
众所周知vue框架是通过render函数将vue的template模板通过h函数解析成vnode(虚拟节点)慢慢变成了vdom(虚拟dom) 最后转换成真实元素,我们才能真正的在浏览器上看到。今天我们就来手动实现一个简单的渲染器,来探究探究其真正的原理。
graphTDtemplate-->渲染函数render-function-->虚拟节点vnode-->真实元素-->浏览器显示
手写一个虚拟节点 来帮助我们实验
//1.通过h函数创建vnode形成vdomconstvnode=h('div',{class:'home',id:'one'},[h('h2',null,'哈哈,我是渲染器渲染出来的元素'),h('button',{onClick(){}},'按钮点击')])
我们现在要做的就是将虚拟节点中的内容转化成真正的元素
h函数 转化成vnode对象
传递参数
tag 标签名 如div span
props 属性名如class id
children 字符串为直接填入内容 数组可嵌套多个子节点
//将vnode转换成vnodeconsth=(tag,props,children)=>{return{tag,props,children}}
它干的活很简单,直接转化成对象就行了 (实际上vue内部源码还增加了其它属性,来应对其它的情况) 我们来看看转换后的vnode 实际上现在是一个小的vdom了
实质上就对象里面套对象的数据结构,现在我们就是考虑将其转换为真实dom再进行挂载 接下来我们将其生成的vdom挂载到特定的容器上
//2.通过mount函数,将vnode挂载到div#app上mount(vnode,document.querySelector('#app'))
实现mount挂载函数 将VNode挂载到DOM上
传递参数 vnode 虚拟节点 container 挂载到的容器上
//解析vnode节点形成真正的元素节点vnode->elementconstmount=(vnode,container)=>{//1.创建元素节点constel=vnode.el=document.createElement(vnode.tag)//2.判断属性是否有值有则遍历再判断属性是否为函数分别处理if(vnode.props){for(keyinvnode.props){constvalue=vnode.props[key]//取值if(key.startsWith('on')){//匹配是否以on开头的属性//添加监听删除'on'再小写eg:onClick->clickel.addEventListener(key.slice(2).toLowerCase(),value)}else{el.setAttribute(key,value)//添加属性}}}//3.处理childrenif(vnode.children){if(typeofvnode.children==='string'){//字符串直接填入el.textContent=vnode.children}else{vnode.children.forEach(item=>{//遍历直接执行递归mount(item,el)});}}container.appendChild(el)}
这个实现起来稍微复杂了点,主要是需要考虑的情况比较多,这里也没有写完整,主要步骤实现就是这样来做的。
这样执行mount函数后页面上就能真正的显示我们的dom元素了 如下:
但是当我们更新了dom时它又是怎么进行及时更新呢?
patch函数,用于对两个VNode进行对比,决定如何处理新的VNode
情况一tag标签不同
//通过h函数创建vnode形成vdomconstvnode=h('div',{class:'home',id:'one'},[h('h2',null,'哈哈,我是渲染器渲染出来的元素'),h('button',{onClick(){}},'按钮点击')])console.log(vnode);//2.通过mount函数,将vnode挂载到div#app上mount(vnode,document.querySelector('#app'))//3.通过patch函数进行diff算法更新节点setTimeout(()=>{constnewVnode=h('p',{class:'home-new',id:'one-new'},[h('h3',null,'哈哈,我是渲染器渲染出来的元素'),])patch(vnode,newVnode)},2000)
patch 处理
//主要思路是比较出不同点,更新domconstpatch=(n1,n2)=>{//1.节点标签不一致简单粗暴将旧节点移除新节点挂载上去做一个替换效果if(n1.tag!==n2.tag){constn1Parent=n1.el.parentElement//获取父节点n1Parent.removeChild(n1.el)//移除n1子节点mount(n2,n1Parent)//将n2挂载到父节点}else{}}
效果展示 成功替换
情况二 props中的属性发生了变化 增删改
//通过h函数创建vnode形成vdomconstvnode=h('div',{class:'home',id:'one'},[h('h2',null,'哈哈,我是渲染器渲染出来的元素'),h('button',{onClick(){}},'按钮点击')])console.log(vnode);//2.通过mount函数,将vnode挂载到div#app上mount(vnode,document.querySelector('#app'))//3.通过patch函数进行diff算法更新节点setTimeout(()=>{//情况二:props中的属性发生了变化增删改constnewVnode=h('div',{class:'home-new',name:'kzj',onClick(){console.log('hahha');}},[h('h3',null,'呵呵,我是渲染器渲染出来的元素'),])patch(vnode,newVnode)},2000)
我们这里改变了class类名 增加了name属性 删除了id属性 patch 处理
//主要思路是比较出不同点,更新domconstpatch=(n1,n2)=>{//1.节点标签不一致简单粗暴将旧节点移除新节点挂载上去做一个替换效果if(n1.tag!==n2.tag){constn1Parent=n1.el.parentElement//获取父节点n1Parent.removeChild(n1.el)//移除n1子节点mount(n2,n1Parent)//将n2挂载到父节点}else{//取出element对象并在n2中保存constel=n2.el=n1.el//2.属性新增修改删除constoldProps=n1.props||{}constnewProps=n2.props||{}//修改和新增处理for(keyinnewProps){constoldValue=oldProps[key]constnewValue=newProps[key]if(oldValue!==newValue){if(key.startsWith('on')){//更新函数el.addEventListener(key.slice(2).toLowerCase(),newValue)}else{el.setAttribute(key,newValue)//更新属性}}}//删除处理for(keyinoldProps){if(!(keyinnewProps)){//如果该属性不在新对象中if(key.startsWith('on')){//移除方法constvalue=oldProps[key]el.removeEventListener(key.slice(2).toLowerCase(),value)}else{el.removeAttribute(key)//移除该属性}}}}}
效果展示 成功实现 增删改都能实现 点击也能触发函数
这里可能有细心的同学对下面子元素怎么没改变有点疑问了,那是因为我们现在还没有对children处理呢,所以上面肯定是不会改变的。那为啥第一种情况标签不同就能改变呢,这个不用多说了吧,虽然它没有去做props children处理,但是它简单粗暴呀,把根节点直接给替换了,你说能不改变吗? 下面开始对children开始处理
情况三 children 发生改变
//通过h函数创建vnode形成vdomconstvnode=h('div',{class:'home',id:'one'},[h('h2',null,'哈哈,我是渲染器渲染出来的元素'),h('button',{onClick(){}},'按钮点击')])console.log(vnode);//2.通过mount函数,将vnode挂载到div#app上mount(vnode,document.querySelector('#app'))//3.通过patch函数进行diff算法更新节点setTimeout(()=>{//情况三:children发生改变constnewVnode=h('div',{class:'home-new',name:'kzj',onClick(){console.log('hahha');}},[h('h3',null,'呵呵,我是渲染器渲染出来的元素'),h('h4',null,'嘻嘻'),h('h5',null,'嘿嘿'),])patch(vnode,newVnode)},2000)
我们这里新增了两个h标签 patch处理 这里细分几种情况处理
判断是不是字符串 处理
数组处理 2.1旧节点大于新节点--移除多余旧节点 2.2旧节点小于新节点--增加多余新节点
//1.通过h函数创建vnode形成vdomconstvnode=h('div',{class:'home',id:'one'},[h('h2',null,'哈哈,我是渲染器渲染出来的元素'),h('button',{onClick(){}},'按钮点击')])0
效果展示 成功实现
这样我们就封装一个完整的渲染系统了,理解了实现过程也能更好的帮助我们了解vue内部是如何来实现渲染系统的。