1.虚拟DOM的优势
前端三大框架引入虚拟DOM来对真实DOM进行抽象,这样做的好处:
-
直接进行DOM操作是有很多限制的,使用虚拟DOM将元素节点变成JS对象,我们可以操作更多的逻辑,比如:diff、clone等。 -
跨平台性:可以将虚拟节点Vnode以你想要的形式渲染到不同的平台上,Vue是支持你开发属于自己的渲染器的。
2.虚拟DOM的渲染过程
Vue内部将template通过render函数编译成虚拟节点,虚拟节点的children如果是数组(存放虚拟节点)则是虚拟DOM树,最后挂载到真实DOM上。
3.renderer渲染器实现
3.1 生成虚拟节点
// 虚拟节点的本质通过h函数生成一个JS对象
const h = (tag, props, children) => {
return {
tag,
props,
children
}
}
3.2 将虚拟节点挂载到真实DOM上
// 挂载mount函数
const mount = (vnode, container) => {
// vnode中三个参数:tag,props,children
//1.处理tag,创建html元素
const el = document.createElement(vnode.tag)
//保留一份el在vnode中,后续patch更新函数中会使用到
vnode.el = el
//2.处理props: 分为普通属性和事件函数
for (const key in vnode.props) {
const value = vnode.props[key]
if (key.startWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
//3.处理children
if (vnode.children) {
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
} else {
vnode.children.forEach(item => mount(item, el))
}
}
//4.把vnode挂载到真实DOM上
container.appendChild(el)
}
3.3 更新操作patch函数
//对比新旧虚拟节点,进行真实DOM更新
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
//1.对比tag类型
//tag不同直接把旧节点移除,挂载新节点
const n1ElParent = n1.el.parenElement
n1ElParent.removeChild(n1.el)
mount(n2, elParent)
} else {
//2.tag相同处理props
const el = n1.el
n2.el = el
const oldProps = n1.props || {}
const newProps = n2.props || {}
//2.1新增props属性
for (const key in newProps) {
if (newProps[key] !== oldProps[key]) {
//判断属性类型:函数或是普通属性
if (key.startWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), newProps[key])
} else {
el.setAttribute(key, newProps[key])
}
}
}
//2.2删除props属性
for (const key in oldProps) {
//2.2.1把事件直接移除掉:因为每次patch后事件的value为function函数(引用类型),value不会一样
if (key.startWith('on')) {
el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key])
}
//2.2.2把key不在newProps中的移除掉
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
//3.处理children
const oldChildren = n1.children || []
const newChildren = n2.children || []
if (typeof newChildren === 'string') {
if(typeof oldChildren === 'string') {
el.textContent = newChildren
} else {
el.innerHTML = ''
el.textContent = newChildren
}
} else {
if (typeof oldChildren === 'string') {
el.textContent = ''
newChildren.forEach(item => mount(item, el))
} else {
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
//3.1 oldChildren.length > newChildren.length: 移除多余的虚拟节点
if (oldChildren.length > newChildren.length) {
//源码中采用的是unmount方法进行卸载,这里直接移除元素
oldChildren.length.slice(commonLength).forEach(item => el.removeChild(item.el))
}
//oldChildren.length < newChildren.length: 新增新节点
if (oldChildren.length < newChildren.length) {
newChildren.length.slice(commonLength).forEach(item => mount(item, el))
}
}
}
}
}
4.测试效果
<div id="app"></div>
<script src="./renderer.js"></script>
<script>
// 1.通过h函数来创建一个vnode
const vnode = h('div', {class: "title"}, [
h('h2', null, "你好啊,李银河"),
h('button', null, "+1"),
h('h3', null, 'hello')
]) //树结构:相当于一个vdom
// 2.通过mount函数将vnode挂载到真实dom上
mount(vnode, document.querySelector("#app"))
// 3.patch算法:对比虚拟节点更新dom
setTimeout(() => {
const vnode1 = h('div', {class: "pm-title"}, [
h('h2', null, "hello world"),
h('button', null, "--1")
])
patch(vnode, vnode1)
}, 2000)
</script>
-
初次渲染:
-
2秒后:
–灵感来源coderwhy
继续记录前端菜狗的成长!
原文始发于微信公众号(Hephaestuses):Vue源码之简易渲染器renderer实现
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/45041.html