接着上一节我们开始虚拟DOM的更新 话不多说 直接上代码
// vDom.js
const vnodeType = {
HTML:'HTML',
TEXT: 'TEXT',
// 前期不会介绍 component虚拟dom
COMPONENT: 'COMPONENT',
CLASS_COMPONENT: 'CLASS_COMPONENT'
}
const childType = {
EMPTY: 'EMPTY', // 子元素为空
SINGLE: 'SINGLE', // 子元素只有一个
MULTIPLE: 'MULTIPLE' // 子元素有多个
}
// 新建虚拟dom 标签名 属性 子元素
function createElement(tag,data,children= null) {
let flag,childrenFlag
if (typeof tag === 'string'){
// 普通的html标签
flag = vnodeType.HTML
} else if (typeof tag === 'function'){
// 方法式虚拟dom
flag = vnodeType.COMPONENT
} else {
// 普通文本
flag = vnodeType.TEXT
}
if (children === null){
childrenFlag = childType.EMPTY
} else if (Array.isArray(children)){
// 通过长度来判断 childrenFlag的类型
let length = children.length
if (length === 0) {
childrenFlag = childType.EMPTY
} else {
childrenFlag = childType.MULTIPLE
}
}else {
// 其他情况认为是文本
childrenFlag = childType.SINGLE
children = createTextVnode(children + '')
}
// 返回vnode
return {
flag, // vnode类型
tag, // 标签, div 文本没有tag, 组件就是函数
data,
children,
key:data&&data.key,
childrenFlag,
el: null
}
}
// 渲染dom 要渲染的虚拟dom ,容器
function render(vnode,container) {
// 区分首次渲染 和 再次渲染
if (container.vnode){
// 更新
patch(container.vnode,vnode,container)
} else {
mount(vnode,container)
}
// 渲染之后进行储存 判断是否首次渲染
container.vnode = vnode
}
// 更新操作
function patch(prev,next,container){
let nextFlag = next.flag,
prevFlag = prev.flag
// 直接替换
if (nextFlag !== prevFlag) {
replaceVnode(prev,next,container)
} else if (nextFlag === vnodeType.HTML){
patchElement(prev,next,container)
} else if (nextFlag === vnodeType.TEXT){
patchText(prev,next,container)
}
}
// 更新节点
function patchElement(prev,next,container) {
// tag不同 直接替换
if (prev.tag !== next.tag){
replaceVnode(prev,next,container)
return
}
let el = (next.el = prev.el),
prevData = prev.data,
nextData = next.data
// 更新和新建
if (nextData){
for (let key in nextData){
let prevVal = prevData[key],
nextVal = nextData[key]
patchData(el,key,prevVal,nextVal)
}
}
// 删除之前的
if (prevData){
for (let key in prevData){
// 拿到新建的data 然后取值 看之前的data里面是存在
let prevVal = prevData[key]
if(prevVal && !nextData.hasOwnProperty(key)){
patchData(el,key,prevVal,null)
}
}
}
// data更新完毕 更新子元素
patchChildren(
prev.childrenFlag,
next.childrenFlag,
prev.children,
next.children,
el)
}
// 更新子元素
function patchChildren(prevChildFlag,nextChildFlag,prevChildren,nextChildren,container) {
// 需要做的处理
// 1.老的是单独的
// 老的是空的
// 老的是多个
// 2.新的是单独的
// 新的是空的
// 新的是多个
switch (prevChildFlag) {
case childType.SINGLE:
switch (nextChildFlag) {
case childType.SINGLE:
console.log('渲染文本')
patch(prevChildren,nextChildren,container)
break;
case childType.EMPTY:
container.removeChild(prevChildren.el)
break;
case childType.MULTIPLE:
container.removeChild(prevChildren.el)
for (let i = 0;i<nextChildFlag.length;i++){
mount(nextChildren[i],container)
}
break;
}
break;
case childType.EMPTY:
switch (nextChildFlag) {
case childType.SINGLE:
mount(nextChildren,container)
break;
case childType.EMPTY:
break;
case childType.MULTIPLE:
for (let i = 0;i<nextChildFlag.length;i++){
mount(nextChildren[i],container)
}
break;
}
break;
case childType.MULTIPLE:
switch (nextChildFlag) {
case childType.SINGLE:
for (let i = 0;i<prevChildren.length;i++){
container.removeChild(prevChildren[i].el)
}
mount(nextChildren,container)
break;
case childType.EMPTY:
for (let i = 0;i<prevChildren.length;i++){
container.removeChild(prevChildren[i].el)
}
break;
case childType.MULTIPLE:
// 新老都是数组
console.log('新老都是数组')
let lastIndex = 0
for (let i = 0; i<nextChildren.length;i++){
let nextVnode = nextChildren[i]
let j = 0
let find = false
for (j;j<prevChildren.length;j++){
let preVnode = prevChildren[j]
// key相同 认为是同一个元素
if (preVnode.key === nextVnode.key){
find = true
patch(preVnode,nextVnode,container)
// 需要移动
if (j<lastIndex){
let flagNode = nextChildren[i-1].el.nextSibling
container.insertBefore(preVnode.el,flagNode)
break
} else{
lastIndex = j
}
}
}
if (!find){
let flagNode = i == 0 ?prevChildren[0].el : nextChildren[i-1].el.nextSibling
mount(nextVnode,container,flagNode)
}
}
// 移除不需要的元素
for (let i = 0;i<prevChildren.length;i++){
const prevVnode = prevChildren[i]
const has = nextChildren.find(next=>next.key == prevVnode.key)
if (!has){
container.removeChild(prevVnode.el)
}
}
break;
}
break;
}
}
// 直接替换文本
function patchText(prev,next,container) {
let el = (next.el = prev.el)
if (next.children !== prev.children){
el.nodeValue = next.children
}
}
// 直接替换节点
function replaceVnode(prev,next,container) {
container.removeChild(prev.el)
mount(next,container)
}
// 首次挂载元素
function mount(vnode,container,flagNode) {
// 区分flag的类型
let {flag} = vnode
if (flag === vnodeType.HTML){
mountElement(vnode,container,flagNode)
}else if (flag === vnodeType.TEXT){
mountText(vnode,container)
}
}
// 新建文本类型的vnode
function createTextVnode(text){
return {
flag: vnodeType.TEXT,
tag: null,
data: null,
el: null,
children: text,
childrenFlag: childType.EMPTY
}
}
// 直接挂载dom
function mountElement(vnode,container,flagNode) {
let dom = document.createElement(vnode.tag)
vnode.el = dom // 存储之前的dom
let {data,children,childrenFlag} = vnode // 属性
// 挂载data属性
if (data){
for (let key in data ){
// 节点 对应的key 旧值 新值
patchData(dom,key,null,data[key])
}
}
// 判断子元素数量
if (childrenFlag !==childType.EMPTY) {
if (childrenFlag === childType.SINGLE){
mount(children,dom)
}else if (childrenFlag === childType.MULTIPLE){
children.map(item=> mount(item,dom))
}
}
flagNode? container.insertBefore(dom,flagNode): container.appendChild(dom)
}
// 直接挂载text
function mountText(vnode,container) {
let dom = document.createTextNode(vnode.children)
vnode.el = dom
container.appendChild(dom)
}
// 挂载data
function patchData(el,key,prev,next){
switch (key) {
case 'style':
for (let k in next){
el.style[k] = next[k]
}
for (let k in prev){
if (!next.hasOwnProperty(k)){
el.style[k] = ''
}
}
break;
case 'class':
el.className = next
break;
default:
if (key[0] ==='@'){
if (prev) {
el.removeEventListener(key.slice(1),prev)
return
}
if (next){
el.addEventListener(key.slice(1),next)
} else {
el.setAttribute(key,next)
}
}
}
}
然后通过settimeout来模拟更新
let vnode = createElement('div',{id:'test'},[
createElement('p',{key: 'a', style: {color: 'blue'},'@click':()=>{alert('xxx')}},'节点1'),
createElement('p',{key: 'b', style: {color: 'green'}},'节点2'),
createElement('p',{key: 'c', style: {fontSize: '15px'}},'节点3'),
createElement('p',{key: 'd', style: {color: 'yellow'}},'节点4'),
])
let vnode1 = createElement('div',{id:'test'},[
createElement('p',{key: 'd', style: {color: 'yellow'}},'节点4'),
createElement('p',{key: 'a', style: {color: 'blue'},class: 'item-last'},'节点1'),
createElement('p',{key: 'b', style: {color: 'green'}},'节点3'),
createElement('p',{key: 'e', style: {fontSize: '15px'}},'节点5'),
createElement('p',{key: 'f', style: {fontSize: '20px'}},'节点6'),
])
// console.log(JSON.stringify(div,null,2))
render(vnode,document.getElementById('app'))
setTimeout(()=>{
render(vnode1,document.getElementById('app'))
},2000)
这样就实现了dom的更新,本篇文章是通过遍历对比来确定需要更新的节点 没做太多优化。具体步骤都写了注释