虚拟DOM算法四部分
1.虚拟DOM创建
2.渲染虚拟DOM(render)
3.虚拟DOM diff(求新旧虚拟DOM差异)
4.虚拟DOM patch(将差异以补丁形式打到旧DOM上)
虚拟DOM创建
//虚拟DOM类
class Element {
constructor(type, props, children) {
this.type = type
this.props = props
this.children = children
}
}
// 创建虚拟DOM,返回虚拟节点(object)
function createElement(type, props, children) {
return new Element(type, props, children)
}
渲染虚拟DOM(render)
//render将虚拟DOM转化为真实DOM
function render(domObj) {
//根据type类型创建对应的元素
let el = document.createElement(domObj.type)
//再去遍历props属性对象,然后给创建的元素el设置属性
for (let key in domObj.props) {
//设置属性的方法
setAttr(el, key, domObj.props[key])
}
//遍历子节点,如果是虚拟DOM,就继续递归渲染
//如果不是就代表是文本节点,直接创建
domObj.children.forEach(child => {
child = (child instanceof Element) ? render(child) : document.createTextNode(child)
//添加金对应的元素内
el.appendChild(child)
})
return el
}
//设置属性
function setAttr(node, key, value) {
switch (key) {
case 'value':
//node是一个input或者textarea就直接设置其value即可
if (node.tagName.toLowerCase() === 'input' ||
node.tagName.toLowerCase() === 'textarea') {
node.value = value
} else {
node.setAttribute(key, value)
}
break
case 'style':
//直接赋值行内样式
node.style.cssText = value
break
default:
node.setAttribute(key, value)
break
}
}
虚拟DOM diff(求新旧虚拟DOM差异)
//DOM-diff
//
function diff(oldTree,newTree){
//声明变量patches用来存放补丁的对象
let patches = {}
//第一次比较应该是树的第零个索引
let index = 0
//递归树 比较后的结果放到补丁里
walk(oldTree,newTree,index,patches)
return patches;
}
function walk(oldNode,newNode,index,patches){
//每一个元素都有一个补丁
let current = []
if(!newNode){
//规则一
current.push({type:'REMOVE',index})
}else if(isString(oldNode) && isString(newNode)){
//判断文本是否一致
if(oldNode !== newNode){
current.push({type:'TEXT',text:newNode})
}
}else if(oldNode.type === newNode.type){
//比较属性是够有更改
let attr = diffAttr(oldNode.props,newNode.props)
if(Object.keys(attr).length > 0){
current.push({type:'ATTR',attr})
}
//如果有子节点,遍历子节点
diffChildren(oldNode.children,newNode.children,patches)
} else {
//说明节点被替换掉了
current.push({type:'REPLACE',newNode})
}
//当前元素确实有补丁存在
if(current.length){
//将补丁和元素对应起来,放到大补丁包中
patches[index] = current
}
}
function isString(obj){
return typeof obj === 'string'
}
function diffAttr(oldAttrs,newAttrs){
let patch = {}
//判断老的属性中和新的属性的关系
for(let key in oldAttrs){
if(oldAttrs[key] !== newAttrs[key]){
patch[key] = newAttrs[key];//有可能还是undefined
}
}
for(let key in newAttrs){
//老节点没有新节点的属性
if(!oldAttrs.hasOwnProperty(key)){
patch[key] = newAttrs[key]
}
}
return patch
}
//所有都基于一个序号来实现
let num = 0
function diffChildren(oldChildren,newChildren,patches){
//比较老的第一个和新的第一个
oldChildren.forEach((child,index)=>{
walk(child,newChildren[index],++num,patches)
})
}
虚拟DOM patch(将差异以补丁形式打到旧DOM上)
//patch补丁更新
//打补丁需要传入两个参数,一个是要打补丁的元素,另一个是要打的补丁
let allPatches;
let index = 0 //默认哪个需要打补丁
function patch(node,patches){
allPatches = patches
//给某个元素打补丁
walk2(node)
}
function walk2(node){
let current = allPatches[index++]
let childNodes = node.childNodes
//先序深度,继续遍历递给子节点
childNodes.forEach(child => walk2(child))
if(current){
doPatch(node,current)//打上补丁
}
}
function doPatch(node,patches){
//遍历所有打过的补丁
patches.forEach(patch =>{
let type = patch.type
if(type === 'ATTR'){
for(let key in patch.attr){
let value = patch.attr[key]
if(value){
setAttr(node,key,value)
}else{
node.removeAttribute(key)
}
}
} else if(type === 'TEXT'){
node.textContent = patch.text
} else if(type === 'REPLACE'){
let newNode = patch.newNode
newNode = (newNode instanceof Element) ? render(newNode) :document.createTextNode(newNode)
node.parentNode.replaceChild(newNode,node)
} else if(type === 'REMOVE'){
node.parentNode.removeChild(node)
}
})
}
锐评该简易虚拟DOM
问题挺多的
问题定位:diff阶段的规则1
remove之后会忽略该节点的子节点,从而导致先序遍历跳过了他的子节点导致index异常
if (!newNode) {
//规则一
current.push({
type: 'REMOVE',
index
})
}
修改后的规则:
如果当前的旧节点不是文本节点,说明还是有子节点的,应该继续diffChildren,但是newNode明显已经不存在,这里传入空数组做空操作,目的是为了让被remove的子节点也应该被遍历,从而保证index一致性
//规则一
current.push({
type: 'REMOVE',
index
})
//如果有子节点,遍历子节点
if (!isString(oldNode)) {
diffChildren(oldNode.children, [], patches)
}
问题二:patch阶段patch异常
问题定位:
在patch阶段,我们虽然使用的是先序遍历,但是打补丁的顺序并不是从最后一个节点打的,而是树遍历到尽头就开始从底部往上打这条分叉上的补丁,从而导致了forEach阶段,nodeList直接被改变了,在数组的forEach中直接改变数组是非常不好的,这会导致无法访问完整这个数组,这里我们先用先序遍历把所有需要打的补丁放进needPatchList({node:ElementNode,current:PatchesItem})
然后打补丁,这样就不会影响到之前的节点
问题代码
function walk(node) {
let current = allPatches[index++];
let childNodes = node.childNodes;
// 先序深度,继续遍历递归子节点
childNodes.forEach(child => walk(child));
if (current) {
doPatch(node, current); // 打上补丁
}
}
更改后的代码
let needPatchList = []
function patch(node, patches) {
allPatches = patches
//给某个元素打补丁
walk2(node)
console.log(needPatchList)
for (let {
node,
current
} of needPatchList) {
doPatch(node, current)
}
}
function walk2(node) {
console.log('补丁', index, node)
let current = allPatches[index++]
let childNodes = node.childNodes
if (current) {
needPatchList.push({
node,
current
})
//doPatch(node,current)//打上补丁
}
//先序深度,继续遍历递给子节点
childNodes.forEach(child => walk2(child))
// if(current){
// needPatchList.push({node,current})
// //doPatch(node,current)//打上补丁
// }
}