渲染系统就是把虚拟DOM转化为真实DOM。
目标
主要实现以下三个功能:
- 功能一: h函数,用于返回一个vnode对象。
- 功能二: mount函数,用于将vnode挂载到DOM上。
- 功能三:patch,用于对vnode进行对比,决定如何处理新的vnode
实现过程
<body>
<div id="app"></div>
<script src="renderer.js"></script>
</body>
创建一个id 为 app 的 div
,用来挂载我们即将创建的节点
renderer 就是渲染
,renderer.js
实现三个函数 h函数
,mount函数
,patch函数
h函数
h函数就是返回一个vnode
,h函数中又有子节点,又是一个h函数,那么也会返回一个vnode,拿着这样来的话,多个vnode组成不就成了vdom嘛
vnode ---> javascript对象 ----> {}
//tag: 标签 props: 属性, children: 子节点
const h = function(tag, props, children) {
return {
tag,
props,
children
}
}
mount函数
-
根据vnode的tag, 利用
document.createElement()
来创建一个节点,顺便在vnode中也保存当前的节点。const mount = function(vnode, box){ const el = vnode.el = document.createElement(vnode.tag) }
-
根据 vnode 的props,来添加属性
- 判断是否存在props
- 遍历拿到,根据key值拿到value值
- 判断value值是函数,还是字符串
if(vnode.props) { for(let key in vnode.props) { const value = vnode.props[key] if(key.startsWith('on')) { //处理监听函数onClick el.addEventListener(key.slice(2).toLowerCase(), value) } else { el.setAttribute(key, value) } } }
-
处理children(目前只考虑是字符串和数组的形式)
- 判断children是否存在
- children是字符串还是数组
if(vnode.children) { if(typeof vnode.children === 'string') { //如果是字符串 el.textContent = vnode.children } else { //数组的形式 vnode.children.forEach(item => { //item函数,又是一个h函数,也需要调用mount函数,递归 mount(item, el) }) } }
-
挂载到容器, 利用
appendChild()
box.appendChild(el)
完整的mount函数:
const mount = function(vnode, box){
const el = vnode.el = document.createElement(vnode.tag)
if(vnode.props) {
for(let key in vnode.props) {
const value = vnode.props[key]
if(key.startsWith('on')) { //处理监听函数onClick----click
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 => {
//item函数,又是一个h函数,也需要调用mount函数,递归
mount(item, el)
})
}
}
box.appendChild(el)
}
patch函数
当节点发生更新的时候,就需要patch,进行比较,更新不同的部分。
这里的patch函数实现,并没有考虑key的存在的性能优化
patch接收两个参数:
- n1: 旧的节点
- n2: 新的节点
const patch = function(n1, n2) {
//由于n2并没有调用mount方法,所以里面并不存在el属性
//引用相同,一起修改
const el = n2.el = n1.el
}
分情况讨论:
- 根据tag是否相同
if(n1.tag !== n2.tag) {
//拿到旧节点的父节点
const n1Parent = n1.el.parentElement
//父节点删除旧节点
n1Parent.removeChild(n1.el)
//添加新的节点
mount(n2, n1Parent)
} else {}
-
tag相同的情况
逻辑一:处理props
- 拿到旧的props 和 新的props
- 把新的props全部添加到节点上
- 对比旧的key是否在新的属性上,如果没有就删除
//对比n1的props和n2的props const oldProps = n1.props || {} const newProps = n2.props || {} //遍历新节点的props添加el for(const key in newProps) { const oldValue = oldProps[key] const newValue = newProps[key] if(newValue !== oldValue) { if(key.startsWith('on')) { //处理监听函数onClick el.addEventListener(key.slice(2).toLowerCase(), newValue) } else { el.setAttribute(key, newValue) } } } //遍历旧的props,查看key值是否在newProps中,如果不存在就移除掉 for(const key in oldProps) { if(!(key in newProps)) { const value = oldProps[key] if(key.startsWith('on')) { //处理监听函数onClick el.removeEventListener(key.slice(2).toLowerCase(), value) } else { el.removeAttribute(key) } } }
逻辑二: 处理children
- 情况1: 如果
newChild
是字符串- 情况1.1:
oldChild
也是字符串 - 情况1.2:
oldChild
是数组
- 情况1.1:
const oldChild = n1.children || [] const newChild = n2.children || [] //情况1 if(typeof newChild === 'string') { //情况1.1 if(typeof oldChild === 'string') { if(newChild !== oldChild) { el.textContent = newChild } } //情况1.2 else { el.innerHTML = newChild } }
-
情况2:如果
newChild
是数组-
情况2.1:
oldChild
是字符串else { //情况2.1 if(typeof oldChild === 'string') { //先置空,添加 el.innerHTML = '' newChild.forEach(item => { mount(item, el) }) }
-
情况2.2:
oldChild
是数组//找出最小长度,然后依次对比(patch) const commonLength = Math.min(oldChild.length, newChild.length) for(const i = 0; i < commonLength; i++) { patch(oldChild[i], newChild[i]) }
-
情况2.2.1:
newChild
的长度大于oldChild
的长度// oldChild: [v1, v2, v3] // newChild: [v1, v2, v3, v4, v5] if(newChild.length > oldChild.length) { newChild.slice(commonLength).forEach(item => { mount(item, el) }) }
-
情况2.2.2 :
newChild
的长度小于oldChild
的长度// oldChild: [v1, v2, v3, v4] // newChild: [v1, v2] if(newChild.length < oldChild.length) { oldChild.slice(commonLength).forEach(item => { el.removeChild(item.el) }) }
-
完整的patch函数
const patch = function(n1, n2) { const el = n2.el = n1.el //n1的tag和n2的tag不相同 if(n1.tag !== n2.tag) { const n1Parent = n1.el.parentElement n1Parent.removeChild(n1.el) mount(n2, n1Parent) } else { //对比n1的props和n2的props const oldProps = n1.props || {} const newProps = n2.props || {} //遍历新节点的props添加el for(const key in newProps) { const oldValue = oldProps[key] const newValue = newProps[key] if(newValue !== oldValue) { if(key.startsWith('on')) { //处理监听函数onClick el.addEventListener(key.slice(2).toLowerCase(), newValue) } else { el.setAttribute(key, newValue) } } } //遍历旧的props,查看key值是否在newProps中,如果不存在就移除掉 for(const key in oldProps) { if(!(key in newProps)) { const value = oldProps[key] if(key.startsWith('on')) { //处理监听函数onClick el.removeEventListener(key.slice(2).toLowerCase(), value) } else { el.removeAttribute(key) } } } //处理children const oldChild = n1.children || [] const newChild = n2.children || [] //情况1: 如果newChild是字符串 if(typeof newChild === 'string') { if(typeof oldChild === 'string') { if(newChild !== oldChild) { el.textContent = newChild } } else { el.innerHTML = newChild } } //情况2: 如果newChild是数组 [v1, v2, v3] else { if(typeof oldChild === 'string') { el.innerHTML = '' newChild.forEach(item => { mount(item, el) }) } //情况2.2: 如果oldChild也是数组 else { //找出最小长度,然后依次对比(patch) const commonLength = Math.min(oldChild.length, newChild.length) for(const i = 0; i < commonLength; i++) { patch(oldChild[i], newChild[i]) } if(newChild.length > oldChild.length) { newChild.slice(commonLength).forEach(item => { mount(item, el) }) } if(newChild.length < oldChild.length) { oldChild.slice(commonLength).forEach(item => { el.removeChild(item.el) }) } } } } }
总结
自己手写一遍,加深印象,收获挺多,需要多看。
-