前言
无移动domdiff
- 上一篇把属性更新搞定了,但是点击按钮,虽然属性变了,但是数字没变,所以要实现domdiff来改变数字。
- react的domdiff跟vue不太一样,我感觉react的domdiff才是正常人思维,vue那个感觉像是专门搞算法的人写的。
- 首先,需要一个map,来获得老元素的映射。
function getChildrenElementsMap(oldChildrenElements){
let oldChildrenElementsMap={}
if(!oldChildrenElements.length){
oldChildrenElements=[oldChildrenElements]
}
for(let i=0 ;i<oldChildrenElements.length;i++){
let oldkey = oldChildrenElements[i].key ||i.toString()
oldChildrenElementsMap[oldkey]=oldChildrenElements[i]
}
return oldChildrenElementsMap
}
- 这玩意长的样子的就是{key:{xxxxx虚拟domxxx}}这样,如果没有key,就是索引。
- 然后,需要通过遍历新节点,获得新map,其中如果有可以复用的vdom,拿新节点的属性更新老节点然后。
function diff(dom,oldChildrenElements,newChildrenElements){
let oldChildrenElementsMap=getChildrenElementsMap(oldChildrenElements)
getNewChildrenElementsMap(newChildrenElements,oldChildrenElementsMap)
}
function getNewChildrenElementsMap(newChildrenElements,oldChildrenElementsMap){
let newChildrenElementsMap={}
if(!newChildrenElements.length){
newChildrenElements=[newChildrenElements]
}
for (let i=0;i<newChildrenElements.length;i++){
let newChildElement = newChildrenElements[i]
if(newChildElement){
let newKey =newChildElement.key||i.toString()
let oldChildElement = oldChildrenElementsMap[newKey]
if(canDeepCompare(oldChildElement,newChildElement)){
updateElement(oldChildElement,newChildElement)
newChildrenElements[i]=oldChildElement
}
newChildrenElementsMap[newKey]=newChildrenElements[i]
}
}
return newChildrenElementsMap
}
function canDeepCompare(oldChildElement,newChildElement){
if(!!oldChildElement&&!!newChildElement){
return oldChildElement.type===newChildElement.type
}
return false
}
- 同时注意下有个bug需要改一下,就是前面字符串赋值的地方,不能使用type,因为这里还要对比type,所以包装字符串节点需要新增属性,在createDom中也需要进行判断,还有dom对比字符串节点,一共3处地方需要稍微修改下。对于字符串节点,如果里面字相同就不动它:
function updateElement(oldelement,newelement ){
let currentDom =newelement.dom =oldelement.dom
if(oldelement.$$typeof===REACT_TEXT_TYPE&&newelement.$$typeof===REACT_TEXT_TYPE){
if(currentDom.textContent!==newelement.content)currentDom.textContent=newelement.content
}else if(oldelement.$$typeof===REACT_ELEMENT_TYPE){
updateDomProperties(currentDom,oldelement.props,newelement.props)
updateChildrenElements(currentDom,oldelement.props.children,newelement.props.children)
}else if(oldelement.$$typeof===FUNCTION_COMPONENT){
updateFunctionComponent(oldelement,newelement)
}else if(oldelement.$$typeof===CLASS_COMPONENT){
updateClassComponent(oldelement,newelement)
}
}
- 这样就已经可以渲染了。点击加号,数字和属性都能变了。而button就直接复用,没有换新的。
- 所以这个过程是个深度优先递归。
有移动domdiff
- 前面是无移动的情况,还可能要进行dom移动。
- 建个例子:
class Counter extends React.Component{
constructor(props){
super(props)
this.state={show:true}
}
handleClick=()=>{
this.setState((state)=>({show:!state.show}))
}
render(){
if(this.state.show){
return (
<ul onClick={this.handleClick}>
<li key="A">A</li>
<li key="B">B</li>
<li key="C">C</li>
<li key="D">D</li>
</ul>
)
}else{
return(
<ul onClick={this.handleClick}>
<li key="A">A1</li>
<li key="C">C1</li>
<li key="B">B</li>
<li key="E">E1</li>
<li key="F">F</li>
</ul>
)
}
}
}
ReactDOM.render(
<Counter></Counter>,
document.getElementById('root')
);
dep=0
function godeep(){
dep++
godeep()
dep--
if(dep===0)...
}
- 还需要修改下创建孩子虚拟dom时的逻辑,要加个属性,标识这个节点是它父亲的第几个孩子。
function createNativeDOMChildren(parentNode,children){
if(children){
if(Array.isArray(children)){
flatChildren(children).forEach((child,index) => {
child._mountIndex=index
let childDom = createDOM(child)
parentNode.appendChild(childDom)
});
}else{
children._mountIndex=0
let childDom = createDOM(children)
parentNode.appendChild(childDom)
}
}
}
- 前面已经获取了2个map,分别是老节点的map和新节点的map,新节点的map的元素有部分是直接从老map里拉来的已经更新过属性的虚拟dom。所以,得到2个map后,需要做个补丁包,递归结束后得到总共需要打补丁的操作,再进行执行。
let updateDepth=0
let diffQueue =[]
export const MOVE='MOVE'
export const REMOVE='REMOVE'
export const INSERT='INSERT'
function updateChildrenElements(dom,oldChildrenElements,newChildrenElements){
updateDepth++
diff(dom,oldChildrenElements,newChildrenElements,diffQueue)
updateDepth--
if(updateDepth===0){
patch(diffQueue)
diffQueue.length=0
}
}
function patch(){
console.log(JSON.stringify(diffQueue,null,2))
console.log(diffQueue)
}
function diff(parentNode,oldChildrenElements,newChildrenElements,diffQueue){
let oldChildrenElementsMap=getChildrenElementsMap(oldChildrenElements)
let newChildrenElementsMap=getNewChildrenElementsMap(newChildrenElements,oldChildrenElementsMap)
let lastIndex=0
for(let i=0;i<newChildrenElements.length;i++){
let newChildElement=newChildrenElements[i]
if(newChildElement){
let newKey=newChildElement.key||i.toString()
let oldChildElement=oldChildrenElementsMap[newKey]
if(newChildElement===oldChildElement){
if(oldChildElement._mountIndex<lastIndex){
diffQueue.push({
parentNode,
type:MOVE,
fromIndex:oldChildElement._mountIndex,
toIndex:i
})
}
lastIndex=Math.max(oldChildElement._mountIndex,lastIndex)
}else{
diffQueue.push({
parentNode,
type:INSERT,
toIndex:i,
dom:createDOM(newChildElement)
})
}
newChildElement._mountIndex=i
}
}
for(let oldkey in oldChildrenElementsMap){
if(!newChildrenElementsMap.hasOwnProperty(oldkey)){
let oldChildElement=oldChildrenElementsMap[oldkey]
diffQueue.push({
parentNode,
type:REMOVE,
fromIndex:oldChildElement._mountIndex,
})
}else{
let oldChildElement = oldChildrenElementsMap[oldkey];
let newChildElement = newChildrenElementsMap[oldkey];
if (oldChildElement !== newChildElement) {
diffQueue.push({
parentNode,
type: REMOVE,
fromIndex: oldChildElement._mountIndex
});
}
}
}
}
- 其中diffqueue就是个用来收集补丁包的玩意,通过前面深度判断,当深度回到0则进行打补丁操作。
- 而在每层的diff中,遍历新map,在旧节点中寻找相同的虚拟dom,如果全等(因为上一段在新map遍历中会去找老map中对应相同key或者对应相同索引的节点进行比较type,如果相同,更新完属性后直接复用。)那么就代表可以复用,并且得确认位置,制作补丁包。最后进行打补丁。
- 这个补丁包设计上挂个父节点就是到时候好用父节点来操作子节点dom。
- 这样console出来的补丁包就是这样:
[
{
"parentNode": {
"eventStore": {}
},
"type": "MOVE",
"fromIndex": 1,
"toIndex": 2
},
{
"parentNode": {
"eventStore": {}
},
"type": "INSERT",
"toIndex": 3,
"dom": {}
},
{
"parentNode": {
"eventStore": {}
},
"type": "INSERT",
"toIndex": 4,
"dom": {}
},
{
"parentNode": {
"eventStore": {}
},
"type": "REMOVE",
"fromIndex": 3
}
]
function patch(diffQueue){
let deleteMap={}
let deleteChildren =[]
for(let i =0;i<diffQueue.length;i++){
let difference =diffQueue[i]
if(difference.type===MOVE||difference.type===REMOVE){
let fromIndex=difference.fromIndex
let oldChildDom = difference.parentNode.children[fromIndex]
deleteMap[fromIndex]=oldChildDom
deleteChildren.push(oldChildDom)
}
}
deleteChildren.forEach(child=>{
child.parentNode.removeChild(child)
})
for (let i = 0; i < diffQueue.length; i++) {
let { type, fromIndex, toIndex, parentNode, dom } = diffQueue[i];
switch (type) {
case INSERT:
insertChildAt(parentNode, dom, toIndex);
break;
case MOVE://移动就可以从Map中拿出刚才删掉的节点
insertChildAt(parentNode, deleteMap[fromIndex], toIndex);
break;
default:
break;
}
}
}
function insertChildAt(parentNode, newChildDOM, index) {
let oldChild = parentNode.children[index];
oldChild ? parentNode.insertBefore(newChildDOM, oldChild) : parentNode.appendChild(newChildDOM);
}
- 移动实际上就是先删再插,判断当前位置有没有节点,有节点用inserBefore,无节点用append。
- 还有最后复用的地方需要把新的虚拟dom拿来赋给currentdom别忘了。
function compareTwoElement(oldelement,newelement){
let currentDom = oldelement.dom
let currentElement = oldelement
if(newelement===null){
currentDom.parentNode.removeChild(currentDom)
currentDom= null
currentElement=null
}else if(oldelement.type!== newelement.type ){
let newDom = createDOM(newelement)
currentDom.parentNode.replaceChild(newDom,currentDom)
currentElement=newelement
}else{
updateElement(oldelement,newelement)
currentElement=newelement
}
return currentElement
}
- 这样就实现了。感觉循环有点多,不看深度的话,每一层比较需要循环m取得老map,循环n取得新map,遍历新map获取移动插入补丁包,遍历老map获取删除元素补丁包,遍历补丁包获取移动删除元素,遍历删除移动和删除元素,遍历补丁包插入移动和插入的元素。如果老map是m,新map是n,补丁包大小不定,一般不会超过m+n,就当k,还有个需要移动或者删除的元素,不会超过k,就当w,那么总计就是O(2m+2n+2k+w)。如果变态点把k和w换成m+n那就是O(5(m+n))。