概述
react中diff算法的出现极大地减少了了浏览器修改Dom的性能损耗,diff通过算法算出需要修改的最小节点,并把该节点进行替换,而不是连着父节点一起修改,操作数据结构是永远比操作dom来的快的,所以就可以减少很多不及要的dom操作,进而提高性能
虚拟dom
真实dom在网页上的属性有三个 1.tagName(标签名字)2.attrs(属性:classname,title等等)3.child(标签里面嵌套的孩子节点) 好了既然真实dom的属性我们掌握了,那么我们能不能用js表示出来呢?当然能,毕竟一切皆对象。我们来实现一个虚拟dom类
export class Element{
constructor(tagName,attrs = {},child = []){ //attrs是一串键值对 child是对象数组
this.tagName = tagName
this.attrs = attrs
this.child = child
}}
接着实现一个构造函数
export function newElement(tag,attr,child){
return new Element(tag,attr,child)
}
接着把一个dom结构通过我们的构造函数构造成dom
const VdObj1 = newElement('ul',{id: 'list'},[ newElement('li',{class: 'list',style:'color:red' }, ['clhhh']),
newElement('li',{class: 'list',style:'color:red' }, ['js']),
newElement('li',{class: 'list' }, ['onclick']),
newElement('li',{class: 'list' }, ['onclick'])
])
好了我们来console.log(VdObj1);一下吧
控制台输出
Element { //自己创的节点类
tagName: 'ul', //tagName属性
attrs: { id: 'list' },
child: [
Element { tagName: 'li', attrs: [Object], child: [Array] },
Element { tagName: 'li', attrs: [Object], child: [Array] },
Element { tagName: 'li', attrs: [Object], child: [Array] },
Element { tagName: 'li', attrs: [Object], child: [Array] }
]//思考一下孩子节点中的attrs和child属性为什么打印不出来呢
}
现在我们虚拟dom创造出来了 就要开始把它挂载到真实Dom上了
挂载到真实dom上
首先 把atrrs的对象属性变成标签属性 (我们先示范转换style属性)
const function =SetVdtoDom(node,key,value){
switch(key)
case 'style':
node.style.cssText=value
}
接着在类中实现render函数 进行渲染
export class Element{
constructor(tagName,attrs = {},child = []){ //attrs是一串键值对 child是对象数组
this.tagName = tagName
this.attrs = attrs
this.child = child
}
render(){
let ele =document.createElement(this.tagName)//创建一个document对象
let attrs=this.attrs
for(let key in attrs){
SetVdtoDom(ele,key,attrs[key]) 设置attrs属性
}
let childNodes=this.child
childNodes.foreach(function(child){
let childEle = child instanceof Element ? render
: documen.createTextNode(child)
//如果孩子节点是Element就递归,直到发现文本节点 创造文本节点 结束递归
ele.append(childEle)//给父节点加上孩子节点
})
return ele
}
}
这时候js表示的虚拟dom已经挂载在真实dom上了,尤其是递归那里看不懂的同学要多看几遍噢。 虽然js表示的Dom是虚拟的,但是我们也要把握得住!
diff 算法
const diff=(oldNode,newNode)=>{
let difference={} //来一个对象来表示差异 key为index value为[index:index,type:type,value:{newNode}]
getdiff(oldNode,newNode,index,difference)
return difference
}
const REMOVE = 'remove'
const MODIFY_TEXT = 'modify_text'
const CHANGE_ATTRS = 'change_attrs'
const TAKEPLACE = 'replace'
let initIndex = 0
const getdiff=(oldNode,newNode,index,difference)=>{
let diffResult=[]
//如果新节点不存在
if(!newNode){
diffResult.push(
{
index,
type: REMOVE
}
)
}
//如果是文本节点 直接替换
else if(typeof newNode==='string'&&typeof oldNode==='string'){
diffResult.push({
index,
type:MODIFY_TEXT,
newNode
})
}
//已经是节点了判断属性相同
else if (oldNode.tagName===newNode.tagName){
let storeAttrs={}
for(let key in oldNode.attrs){
if(oldNode.attrs[key]!=newNode.attrs[key]){
storeAttrs[key]=newNode.attrs[key]
}
}
for(let key in newNode.attrs){
if(!oldNode.attrs.hasOwnProperty(key))
storeAttrs[key]=newNode.attrs[key]
console.log(storeAttrs);
}
if(Object.keys(storeAttrs).length>0){
diffResult.push({
index,
type:CHANGE_ATTRS,
value:storeAttrs
})
}
oldNode.child.forEach((child,index)=>{
getdiff(oldNode,newNode,initIndex++,difference)
})
//如果标签都不相同则直接替换
}else if(newNode.tagName!==oldNode.tagName){
diffResult.push({
index,
type:TAKEPLACE,
value:newNode
})
}
if(!oldNode){
diffResult.push({
index,
type:TAKEPLACE,
value:newNode
})
}
if(diffResult.length){
for(let key in diffResult){
difference[key]=diffResult
}
}
diff算法就写好了 完整代码在下面
export class Element{
constructor(tagName,attrs = {},child = []){
this.tagName = tagName
this.attrs = attrs
this.child = child
}
render(){
let ele = document.createElement(this.tagName)
let attrs = this.attrs
for(let key in attrs){
SetVdToDom(ele,key,attrs[key])
}
let childNodes = this.child
childNodes.forEach(function(child){
let childEle = child instanceof Element ?
child.render() : document.createTextNode(child)
ele.appendChild(childEle)
})
return ele
}
}
export function newElement(tag,attr,child){
return new Element(tag,attr,child)
}
export const SetVdToDom = function(node,key,value){
switch(key){
case 'style':
node.style.cssText = value
break
// case 'value':
// let tagName = node.tagName || ''
// tagName = tagName.toLowerCase()
// if(tagName === 'input' || tagName === 'textarea'){//注意input类型的标签
// node.value = value
// }else{
// node.setAttribute(key,value)
// }
// break
// default:
// node.setAttribute(key,value)
// break
}
}
const diff = (oldNode,newNode)=>{
let difference = {} //用来保存两个节点之间的差异
getDiff(oldNode,newNode,0,difference)
return difference
}
const REMOVE = 'remove'
const MODIFY_TEXT = 'modify_text'
const CHANGE_ATTRS = 'change_attrs'
const TAKEPLACE = 'replace'
let initIndex = 0
const getDiff = (oldNode,newNode,index,difference)=>{
let diffResult = []
//新节点不存在的话说明节点已经被删除
if(!newNode){
console.log("节点已经被删除");
diffResult.push({
index,
type: REMOVE
})
//如果是文本节点直接替换就行
}else if(typeof newNode === 'string' && typeof oldNode === 'string'){
if(oldNode !== newNode){
diffResult.push({
index,
value: newNode,
type: MODIFY_TEXT
})
} //如果节点类型相同则则继续比较属性是否相同
}else if(oldNode.tagName === newNode.tagName){
let storeAttrs = {}
for(let key in oldNode.attrs){
if(oldNode.attrs[key] !== newNode.attrs[key]){
storeAttrs[key] = newNode.attrs[key]
}
}
// console.log(storeAttrs);
for (let key in newNode.attrs){
if(!oldNode.attrs.hasOwnProperty(key)){
storeAttrs[key] = newNode[key]
}
}
//判断是否有不同
if(Object.keys(storeAttrs).length>0){
diffResult.push({
index,
value: storeAttrs,
type: CHANGE_ATTRS
})
} //遍历子节点
oldNode.child.forEach((child,index)=>{
//深度遍历所以要保留index
getDiff(child,newNode.child[index],++initIndex,difference)
})
//如果类型不相同,那么无需对比直接替换掉就行
}else if(oldNode.tagName !== newNode.tagName){
diffResult.push({
type: TAKEPLACE,
index,
newNode
})
} //最后将结果返回
if(!oldNode){
diffResult.push({
type: TAKEPLACE,
newNode
})
}
//最后将结果返回
if(diffResult.length){
console.log(index);
difference[index] = diffResult
}
}
const fixPlace = (node,difference)=>{
let pacer = { index: 0 }
pace(node,pacer,difference)
}
/*
接收一个真实DOM(需要更新节点),接收diff过后的最小差异集合
*/
const pace = (node,pacer,difference) =>{
let currentDifference = difference[pacer.index]
let childNodes = node.childNodes
// console.log(difference)
childNodes.forEach((child)=>{
pacer.index ++
pace(child,pacer,difference)
})
if(currentDifference){
doFix(node,currentDifference)
}
}
const doFix = (node,difference) =>{
difference.forEach(item=>{
switch (item.type){
case 'change_attrs':
const attrs = item.value
for( let key in attrs ){
if(node.nodeType !== 1)
return
const value = attrs[key]
if(value){
SetVdToDom(node,key,value)
}else{
node.removeAttribute(key)
}
}
break
case 'modify_text':
node.textContent = item.value
break
case 'replace':
let newNode = (item.newNode instanceof Element) ? item.newNode.render(item.newNode) :
document.createTextNode(item.newNode)
node.parentNode.replaceChild(newNode,node)
break
case 'remove' :
node.parentNode.removeChild(node)
break
default:
break
}
})
}
const VdObj1 = newElement('ul',{id: 'list'},[
newElement('li',{class: 'list',style:'color:red' }, ['clhhh']),
newElement('li',{class: 'list',style:'color:red' }, ['js']),
newElement('li',{class: 'list' }, ['onclick']),
newElement('li',{class: 'list' }, ['onclick'])
])
const VdObj = newElement('ul',{id: 'list'},[
newElement('li',{class: 'list',style:'color:blue' }, ['clh']),
newElement('li',{class: 'list' ,style:'color:grey'}, ['ts']),
newElement('li',{class: 'list' }, ['onclick']),
// newElement('li',{class: 'list-1' }, ['Vue']),
// newElement('input',{value:'id' }, ['Vue'])
])
const RealDom = VdObj1.render()
const renderDom = function(element,target){
target.appendChild(element)
}
export default function start(){
renderDom(RealDom,document.body)
const diffs = diff(VdObj1,VdObj)
console.log(diffs)
fixPlace(RealDom,diffs)
}