前言
無移動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))。