本文同步在我的博客shymean.com上,歡迎關注html
本文將深刻研究虛擬DOMVNode
相關的技術實現,瞭解前端框架的基礎。前端
本文包含大量的示例代碼,主要實現node
createVNode
,建立vnodeVNode2DOM
,將vnode轉換爲DOM節點VNode2HTML
,將vnode轉換爲HTML字符串diff
算法,可分爲遞歸實現(Vue)和循環實現(React Fiber),因爲篇幅和結構的問題,本文主要實現遞歸diff,關於fiber相關實現,本系列文章列表以下git
排在後面文章內會大量採用前面文章中的一些概念和代碼實現,如createVNode
、diffChildren
、doPatch
等方法,所以建議逐篇閱讀,避免給讀者形成困惑。本文相關示例代碼均放在github上,若是發現問題,煩請指正。github
咱們知道vnode實際上就是一個用於描述UI的對象,包含一些基本屬性,咱們經過type
描述須要渲染的標籤,經過prpos
描述樣式、事件等屬性,經過children
描述子節點,最簡單的實現以下所示面試
function createVNode(type, props = {}, children = []) {
return {
type,
props,
children
}
}
複製代碼
若是要描述下面這個html結構的簡單視圖算法
<div>
<h1>hello title</h1>
<ul class="list-simple">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</div>
複製代碼
使用createVNode
,構建一顆vnode樹,瀏覽器
let data = {
title: 'hello vnode',
list: [1, 2, 3]
}
createRoot(data)
function createRoot(data) {
let listItem = data.list.map(item => {
return createVNode('li', {
onClick() {
console.log(item)
}
}, [item])
})
let list = createVNode('ul', {
class: 'list-simple',
}, listItem)
let title = createVNode('h1', {}, [data.title])
let root = createVNode('div', {}, [title, list])
return root
}
複製代碼
能夠看見VNode樹與DOM樹是一一對應的,相比而言,vnode包含的屬性要比一個真實DOM的屬性少得多。在後面的實現中,咱們會向VNode上添加一些額外的的屬性。前端框架
此處須要注意,對於children
而言,其元素的類型有兩種:能夠是一個VNode,也能夠是原始字面量(視爲文本節點)。爲了統一處理文本節點和元素節點,咱們能夠在createVNode
中對文本節點進行特殊處理併發
const TEXT_NODE = Symbol('__text_node')
// 暴露一個是否爲文本節點的接口
function isTextNode(type) {
return type === TEXT_NODE
}
function createVNode(type, props = {}, children = []) {
let vnode = {
type,
props,
}
vnode.children = children.map((child, index) => {
// 將無type的節點處理爲文本節點,並將其值保存爲nodeValue
if (!child.type) {
child = {
type: TEXT_NODE,
props: {
nodeValue: child
},
children: []
}
}
return child
})
return vnode
}
複製代碼
這樣每一個VNode均可以包含了統一的屬性。
當咱們將整個UI經過VNode樹描述以後,咱們還須要將其渲染爲真實的DOM節點,有兩個實現思路
appendChild
等方式渲染到頁面上innerHTML
渲染到頁面上根據vnode.type
,咱們能夠調用DOM接口實例化真實DOM,而後根據vnode.props
設置相關DOM屬性,最後根據vnode.children
渲染子節點,這個過程最直觀的方法是使用遞歸。
因爲須要正確將子節點插入父節點中,所以須要提早構建父節點,方便起見此處使用先序遍歷。
function VNode2DOM(root, parentDOM) {
let { type, props, children } = root
// 將當前vnode渲染爲對應的DOM節點
let dom
if (isTextNode(type)) {
dom = document.createTextNode(root.props.nodeValue)
} else {
dom = document.createElement(type)
for (var key in props) {
setAttribute(dom, key, props[key])
}
}
// 將子節點也轉換爲dom節點
Array.isArray(children) && children.forEach(child => {
VNode2DOM(child, dom)
})
// 將當前節點插入節點
if (parentDOM) {
parentDOM.appendChild(dom)
}
root.$el = dom
return dom
}
// 向dom元素增長屬性
function setAttribute(el, prop, val) {
// 處理事件
let isEvent = prop.indexOf('on') === 0
if (isEvent) {
let eventName = prop.slice(2).toLowerCase()
el.addEventListener(eventName, val)
} else {
el.setAttribute(prop, val)
}
}
複製代碼
測試一下,能夠看見頁面渲染了真實的DOM節點,同時正確添加了props屬性
let root = createRoot({
title: 'hello vnode',
list: [1, 2, 3]
})
let dom = VNode2DOM(root, null)
document.body.appendChild(dom)
複製代碼
除了渲染DOM節點,咱們也能夠直接拼接HTML字符串(甚至在某些場景下,如SSR,咱們須要的反而僅僅是HTML字符串)。一樣地,咱們可使用遞歸來實現。
因爲構建一個vnode的html片斷須要知道其所有子節點的html片斷,所以此處使用後序遍歷。
function VNode2HTML(root) {
let { type, props, children } = root
let sub = '' // 獲取子節點渲染的html片斷
Array.isArray(children) && children.forEach(child => {
sub += VNode2HTML(child)
})
let el = '' // 當前節點渲染的html片斷
if (type) {
let attrs = ''
for (var key in props) {
attrs += getAttr(key, props[key])
}
el += `<${type}${attrs}>${sub}</${type}>` // 將子節點插入當前節點
} else {
el += root // 純文本節點則直接返回
}
return el
function getAttr(prop, val) {
// 渲染HTML,假設咱們不須要 事件 等props
let isEvent = prop.indexOf('on') === 0
return isEvent ? '' : ` ${prop}="${val}"`
}
}
複製代碼
測試一下
let html = VNode2HTML(root)
console.log(html)
// 輸出結果爲
// <div><h1>hello vnode</h1><ul class="list-simple"><li>1</li><li>2</li><li>3</li></ul></div>
app2.innerHTML = html // 也能夠渲染視圖,儘管貌似少了註冊事件等邏輯
複製代碼
能夠看見,VNode2DOM
和VNode2HTML
均可以達到將VNode描述的UI渲染出來的目的。VNode2HTML
主要用於服務端渲染的場景,而VNode2DOM
能夠在瀏覽器端直接經過DOM接口渲染,更加直觀且靈活,本文主要研究瀏覽器環境中的VNode。關於SSR的相關知識,我會在後面的文章中繼續實現(本次學習框架原理的一個主要目的就是更新博客的同構渲染)。
當vnode發生變化時,咱們能夠經過從新渲染根節點來更新視圖。可是,當vnode結構比較龐大時,咱們就不得不考慮所有從新渲染所帶來的性能問題。
因爲咱們在初始化的時候構建了所有的DOM節點,在vnode發生變化時的理想狀態是:咱們只更新發生了變化的那些vnode,其他未變化的vnode,咱們不必又從新構建一次。
所以如今問題轉化爲:如何找到那些發生了變化的vnode?解決這個問題的算法就被稱爲diff
:從根節點開始,依次對比並更新新舊vnode樹上的節點,並儘量地複用DOM,避免額外開銷。
爲了性能和效率的均衡,diff算法遵循下面約定
基於這些約定,對於vnode樹中的某個節點而言,可能發生的變化有:刪除、新增、更新節點屬性,基於此咱們來實現diff算法。整個diff算法分爲兩步,
diff
與前面的思路差很少,咱們能夠經過遞歸實現diff
// 定義節點可能發生的變化
const [REMOVE, REPLACE, INSERT, UPDATE] = [0, 1, 2, 3];
// 對比新舊節點,經過patches收集變化
function diff(oldNode, newNode, patches = []) {
if (!newNode) {
// 舊節點及其子節點都將移除
patches.push({ type: REMOVE, oldNode })
} else if (!oldNode) {
// 當前節點與其子節點都將插入
patches.push({ type: INSERT, newNode })
diffChildren([], newNode.children, patches);
} else if (oldNode.type !== newNode.type) {
// 使用新節點替換舊節點
patches.push({ type: REPLACE, oldNode, newNode })
// 新節點的字節點都須要插入
diffChildren([], newNode.children, patches);
} else {
// 若是存在有變化的屬性,則使用新節點的屬性更新舊節點
let attrs = diffAttr(oldNode.props, newNode.props) // 發生變化的屬性
if (Object.keys(attrs).length > 0) {
patches.push({ type: UPDATE, oldNode, newNode, attrs })
}
newNode.$el = oldNode.$el // 直接複用舊節點
// 繼續比較子節點
diffChildren(oldNode.children, newNode.children, patches);
}
// 收集變化
return patches
}
function diffAttr(oldAttrs, newAttrs) {
let attrs = {};
// 判斷老的屬性中和新的屬性的關係
for (let key in oldAttrs) {
if (oldAttrs[key] !== newAttrs[key]) {
attrs[key] = newAttrs[key]; // 有可能仍是undefined
}
}
for (let key in newAttrs) {
// 老節點沒有新節點的屬性
if (!oldAttrs.hasOwnProperty(key)) {
attrs[key] = newAttrs[key];
}
}
return attrs;
}
// 按順序對比子節點,在後面咱們會實現其餘方式的新舊節點對比方式
function diffChildren(oldChildren, newChildren, patches) {
let count = 0;
// 比較新舊子樹的節點
if (oldChildren && oldChildren.length) {
oldChildren.forEach((child, index) => {
count++;
diff(child, (newChildren && newChildren[index]) || null, patches);
});
}
// 若是還有未比較的新節點,繼續進行diff將其標記爲INSERT
if (newChildren && newChildren.length) {
while (count < newChildren.length) {
diff(null, newChildren[count++], patches);
}
}
}
複製代碼
使用方式大體以下,對比兩個根節點
let root = createRoot({
title: 'change title',
list: [1,2,3]
})
let root2 = createRoot({
title: 'hello vnode',
list: [3, 2]
})
var patches = diff(root, root2)
console.log(patches) // 能夠看見收集到的變化的節點
複製代碼
doPatch
在doPatch
階段,主要是將收集的變化更新到視圖上
// 將變化更新到視圖上
function doPatch(patches) {
// 特定類型的變化,須要從新生成DOM節點,因爲沒法徹底保證patches的順序,所以在此步驟生成vnode.$el
const beforeCommit = {
[REPLACE](oldNode, newNode) {
newNode.$el = createDOM(newNode)
},
[UPDATE](oldNode, newNode) {
// 複用舊的DOM節點,只須要更新必要的屬性便可
newNode.$el = oldNode.$el
},
[INSERT](oldNode, newNode) {
newNode.$el = createDOM(newNode)
},
};
// 執行此步驟時全部vnode.$el都已準備就緒
const commit = {
[REMOVE](oldNode, newNode) {
oldNode.$parent.$el.removeChild(oldNode.$el)
},
[REPLACE](oldNode, newNode) {
let parent = oldNode.$parent.$el
let old = oldNode.$el
let el = newNode.$el
// 新插入的節點上添加屬性
setAttributes(newNode, newNode.props)
parent.insertBefore(el, old);
parent.removeChild(old);
},
[UPDATE](oldNode, newNode) {
// 只須要更更新diff階段收集到的須要變化的屬性
setAttributes(newNode, newNode.attrs)
// 將newNode移動到新的位置,問題在於前面的節點移動後,會影響後面節點的順序
},
[INSERT](oldNode, newNode) {
// 新插入的節點上添加屬性
setAttributes(newNode, newNode.props)
insertDOM(newNode)
},
}
// 首先對處理須要從新建立的DOM節點
patches.forEach(patch => {
const { type, oldNode, newNode } = patch
let handler = beforeCommit[type];
handler && handler(oldNode, newNode);
})
// 將每一個變化更新到真實的視圖上
patches.forEach(patch => {
const { type, oldNode, newNode } = patch
let handler = commit[type];
handler && handler(oldNode, newNode);
})
}
// 建立節點
function createDOM(node) {
let type = node.type
return isTextNode(type) ?
document.createTextNode(node.props.nodeValue) :
document.createElement(type)
}
// 將節點插入父節點,若是節點存在父節點中,則調用insertBefore執行的是移動操做而不是複製操做,所以也能夠用來進行MOVE操做
function insertDOM(newNode) {
let parent = newNode.$parent.$el
let children = parent.children
let el = newNode.$el
let after = children[newNode.index]
after ? parent.insertBefore(el, after) : parent.appendChild(el)
}
// 設置DOM節點屬性
function setAttributes(vnode, attrs) {
if (isTextNode(vnode.type)) {
vnode.$el.nodeValue = vnode.props.nodeValue
} else {
let el = vnode.$el
attrs && Object.keys(attrs).forEach(key => {
setAttribute(el, key, attrs[key])
});
}
}
複製代碼
能夠看見在doPatch
操做中,咱們須要獲取vnode的DOM實例和其父節點的引用,所以咱們爲vnode增長一個$el
的屬性,引用根據該vnode實例化的真實DOM節點,初始化時爲null,在VNode2DOM
時能夠更新其值
function createVNode(type, props = {}, children = []) {
let vnode = {
// ...
type,props
$el: null
}
vnode.children = children.map(child => {
child.$parent = vnode // 保存對父節點的引用
return child
})
return vnode
}
function VNode2DOM(root, parentDOM) {
// ...
root.$el = dom // 在vnode的DOM實例化後更新vnode.$el
return dom
}
複製代碼
這樣在初始化視圖後,後續更新時,咱們會獲得新的vnode樹,先進行diff收集patches,而後將patches更新到頁面上
let root = createRoot({
title: 'change title',
list: [1,2,3]
})
let dom = VNode2DOM(root) // 此時舊節點的$el已保持對於DOM實例的引用
document.body.appendChild(dom) // 初始化完成
// 數據發生變化,獲取新的vnode樹
let root2 = createRoot({
title: 'hello vnode',
list: [3, 2]
})
var patches = diff(root, root2) // 收集變化的節點
doPatches(patches) // 更新視圖
複製代碼
在上面的例子中,咱們按照下面的流程實現應用
createRoot(data)
初始化根節點root
,並調用VNode2DOM(root)
將vnode渲染爲DOM節點data
變化時,從新調用createRoot(data2)
獲取新的根節點root2
,並經過diff(root, root2)
獲取新舊節點樹中的變化patchs
,最後經過doPatch(patchs)
將變化更新在視圖上整個過程看起來比較簡明,但能夠發現VNode2DOM
與doPatch
中的初始化DOM節點的邏輯是重複的。換個思路,初始化的時候,能夠看作是新舊點與一個爲null的舊節點進行diff操做。
所以,咱們如今能夠直接跳過VNode2DOM
,將初始化與diff
的過程放在一塊兒。
root = createRoot({
title: 'hello vnode',
list: [1, 2, 3]
})
let patches = diff(null, root)
root.$parent = {
$el: app
}
doPatch(patches)
// 視圖更新時與上面相同的例子相同
// let patches =diff(root, root2)
// doPatch(patches)
複製代碼
就這樣,咱們只須要爲根節點手動添加一個root.$parent.$el
屬性用於掛載,除此以外就再也不須要VNode2DOM
這個方法(儘管這個方法是瞭解vnode映射爲真實DOM最簡單直觀的實現了)
在上面的diff算法中,咱們在對比新舊節點時,是經過相同的索引值在父元素中的進行對比的,當兩個節點的類型不相同時,會標記爲REPLACE
,在patch
時會移除舊節點,同時在原位置插入節點。
考慮下面問題,當子節點列表從[h1, ul]
變成了[h1,p,ul]
時,咱們的算法會將新節點中的p標記爲REPLACE
,將ul標記爲INSERT
,這顯然不能達到性能上的優化,最理想的狀態是直接在第二個位置插入p標籤便可。
這個問題能夠轉換爲:在某些時刻,咱們不能簡單地經過默認的索引值來查找並對比新舊節點,反之,咱們應該儘量去對比子節點中vnode.type
相同的節點。
(感謝咱們將整個diff過程分紅了diff
和doPatch
兩個階段,咱們如今只須要修改diffChildren
方法中的一些邏輯便可~)
如下面例子來講,
// 這裏列舉的abcde都是指不一樣的type
oldChildren = [a,b,c,d]
newChildren = [b,e,d,c]
// 爲了儘量地複用舊節點,理想狀態是複用b、d,刪除a,在指定位置插入d以前插入e,將c移動到d以後,整個操做共計3步。
// 咱們上面的具體例子中[h1, ul] -> [h1, p, ul]
// 理想狀態應該是直接將p插入ul節點以前,只須要一步操做
複製代碼
所以咱們從新實現一個diffChildren
方法,並將以前的diffChildren
方法從新命名爲diffChildrenByIndex
// 儘量地與相同type的節點進行比較
// 在這種邏輯下,會盡量地按順序複用子節點中類型相同的節點
// 整個算法的時間複雜度爲O(n),空間複雜度也爲O(n)
// 注意在這種策略下不會再產生REPLACE類型的patch,而是直接將REPLACE拆分紅了INSERT新節點和REMOVE舊節點的兩個patch,對於doPatch階段沒有影響
function diffChildrenByType(oldChildren, newChildren, patches) {
let map = {}
oldChildren.forEach(child => {
let { type } = child
if (!map[type]) map[type] = []
map[type].push(child)
})
for (let i = 0; i < newChildren.length; ++i) {
let cur = newChildren[i]
// 按順序找到第一個類型相同的元素並複用,這種方式存在的問題是當兩個類型相同的節點僅僅是調換位置,他們也會進行UPDATE
// 針對這個問題,能夠進一步判斷,找到類型相同且props和children最接近的元素,從而避免上面的問題,可是這樣作會增長時間複雜度
// 所以,對於類型相同且順序可能發生變化的節點,咱們須要額外的手段來檢測重複的節點,一種方法是使用語義化的標籤,減小類型相同的標籤,二是使用key
if (map[cur.type] && map[cur.type].length) {
let old = map[cur.type].shift()
diff(old, cur, patches)
} else {
// 因爲部分操做如INSERT依賴最終children的順序,所以須要保證patches的順序
// 此處對於同一層級的節點而言,在前面的節點會先進入patches隊列,所以會先插入
diff(null, cur, patches)
}
}
// 剩餘未被使用的舊節點,將其移除
Object.keys(map).forEach(type => {
let arr = map[type]
arr.forEach(old => {
diff(old, null, patches)
})
})
}
複製代碼
測試一下
function diffChildren(oldChildren, newChildren, patches){
// diffChildrenByIndex(oldChildren, newChildren, patches) // 根據索引值查找並diff節點
diffChildrenByType(oldChildren, newChildren, patches) // 根據type查找並diff節點
}
// 變化[h1, ul] -> [h1, p, ul]
複製代碼
通過測試能夠發現,在上面的例子中
diffChildrenByType
會只會產生2個INSERT
類型的patch(一個li節點和一個文本節點),diffChildrenByIndex
1個REPLACE
和8個INSERT
patch,(儘管這個測試用例有點極端,會從新構建整個ul子節點)。在diffChildrenByType
中咱們提到了相同類型元素順序調換會致使兩個元素都進行UPDATE的問題,咱們能夠在建立節點時手動爲節點添加一個惟一標識,從而保證在不一樣的順序中也能快速找到該節點,按照行規咱們將這個惟一標識命名爲key
。
接下來對craeteVNode
和diffChildrenByType
稍做修改,優先根據對比key相同的節點,而後再對比類型相同的節點
// 在diffChildrenByType的基礎上增長了根據key查找舊節點的邏輯
// 根據type和key來進行判斷,避免同類型元素順序變化致使的沒必要要更新
function diffChildrenByKey(oldChildren, newChildren, patches) {
newChildren = newChildren.slice() // 複製一份children,避免影響父節點的children屬性
// 找到新節點列表中帶key的節點
let keyMap = {}
newChildren.forEach((child, index) => {
let { key } = child
// 只有攜帶key屬性的會參與同key節點的比較
if (key !== undefined) {
if (keyMap[key]) {
console.warn(`請保證${key}的惟一`, child)
} else {
keyMap[key] = {
vnode: child,
index
}
}
}
})
// 在遍歷舊列表時,先比較類型與key均相同的節點,若是新節點中不存在key相同的節點,纔會將舊節點保存起來
let typeMap = {}
oldChildren.forEach(child => {
let { type, key } = child
// 先比較類型與key均相同的節點
let { vnode, index } = (keyMap[key] || {})
if (vnode && vnode.type === type) {
newChildren[index] = null // 該節點已被比較,須要彈出
// newChildren.splice(index, 1) // 該節點已被比較,須要彈出
delete keyMap[key]
diff(child, vnode, patches)
} else {
// 將剩餘的節點保存起來,與剩餘的新節點進行比較
if (!typeMap[type]) typeMap[type] = []
typeMap[type].push(child)
}
})
// 剩下的節點處理與diffChildrenByType相同,此時key相同的節點已被比較
for (let i = 0; i < newChildren.length; ++i) {
let cur = newChildren[i]
if (!cur) continue; // 已在在前面與此時key相同的節點進行比較
// ... 找到一個類型相同的節點進行比較
}
// ... 剩餘未被使用的舊節點,將其移除
}
複製代碼
同時增長一種MOVE
的patch類型,在diff方法中,若是新舊節點在父節點中的位置不一致,則會提交一個patch,此外咱們須要在vnode上增長一個index屬性,用於記錄新舊節點在父節點中的位置
function createVNode(type, props = {}, children = []) {
vnode.key = props.key // 增長key
vnode.children = children.map((child, index) => {
// ...
child.index = index // 增長index, 記錄在該節點的索引值
return child
})
}
const [REMOVE, REPLACE, INSERT, UPDATE, MOVE] = [0, 1, 2, 3, 5]; // 增長MOVE類型的patch
function diff(oldNode, newNode, patches = []) {
// 新舊節點類型相同但索引值不一致,則表示節點複用,節點須要移動位置,進行MOVE
if (oldNode.index !== newNode.index) {
patches.push({ type: MOVE, oldNode, newNode })
}
return patches
}
複製代碼
最後,在doPatch
階段須要爲MOVE
類型的節點增長DOM更新處理方法
// 將節點插入父節點,若是節點存在父節點中,則調用insertBefore執行的是移動操做而不是複製操做,
// 所以也能夠用來進行MOVE操做
function insertDOM(newNode) {
let parent = newNode.$parent.$el
let children = parent.children
let el = newNode.$el
let after = children[newNode.index]
after ? parent.insertBefore(el, after) : parent.appendChild(el)
}
// 須要MOVE的元素按照新的索引值排序,保證排在前面的先進行移動位置的操做
patches
.filter(patch => patch.type === MOVE)
.sort((a, b) => a.index - b.index)
.forEach(patch => {
const { oldNode, newNode } = patch
insertDOM(newNode)
})
複製代碼
測試一下
function testKey() {
let list1 = createList([1, 2, 3], true) // true 使用元素值做爲key
let patches = diff(null, list1)
list1.$parent = {
$el: app
}
doPatch(patches)
btn.onclick = function () {
let list2 = createList([4, 3, 2, 1], true)
let patches = diff(list1, list2)
console.log(patches) // 查看收集的變化
doPatch(patches)
}
}
// 測試三種diff策略的影響
function diffChildren(oldChildren, newChildren, patches) {
// diffChildrenByIndex(oldChildren, newChildren, patches)
// diffChildrenByType(oldChildren, newChildren, patches)
diffChildrenByKey(oldChildren, newChildren, patches)
}
複製代碼
一樣進行上面的操做
diffChildrenByKey
包含3個MOVE操做(一、二、3節點不會建立新的文本節點,而是移動li節點),兩個INSERT操做(一個li節點和一個文本節點)diffChildrenByType
包含3個UPDATE操做(更新前三個文本節點),兩個INSERT操做diffChildrenByIndex
,因爲循環節點的類型一致,致使該方法的diff結果與diffChildrenByType
相同能夠看見,增長key以後,會盡量地複用元素節點並移動位置,而不是在原地複用元素節點並更新文本節點(移動位置在性能上並不見得優於原地更新文本節點),所以
使用key並不必定能帶來性能上的提高,而是爲了不原地複用元素節點帶來的影響。
本文從構造vnode節點開始,
VNode2DOM
和VNode2HTML
瞭解瞭如何將vnode樹渲染爲真實的DOM節點diff
算法,主要實現diff
(收集變化)和doPatch
(將變化更新到頁面上)兩個方法,併合並了初始化和更新邏輯,移除了VNode2DOM
type
相同的DOM節點,並發現了順序發生變化的同type
節點存在的問題,key
進一步更新同層新舊子節點的查找和對比,對於key相同的節點,優先使用MOVE操做移動節點,避免原地複用元素節點本文參考了Vue源碼中的一些實現,可是Vue中使用多個遊標進行diff的方式我的感受不是很清楚明瞭,所以按照本身的理解實現了上面的幾種diff策略
通過上面的步驟,大概能瞭解vnode與diff算法的一些核心思想。
因爲上面的diff是遞歸實現的,很難被臨時中斷,在某個時刻又恢復至原來調用的地方,所以當vnode樹過於複雜時,將長時間佔用JavaScript執行線程,致使瀏覽器卡死。在下一篇文章Fiber與循環diff,將參考React中的 fiber,按循環實現diff。
感謝閱讀,九月份經歷了一次很失敗的面試,感受過去挺長一段時間,本身的學習態度和方法都出現了一些問題,總結起來就是:學而不思則罔,學習的時候,應該多思考才行。因爲本人水平有限,文中出現的錯誤,煩請指正。