上集回憶
【React進階系列】從零最先手把手教你完成一個Virtual DOM(二)
上集我們完成了初次襯着從JSX=>Hyperscript=>VDOM=>DOM的歷程,本日我們來看一下當數據更改的時刻怎樣更新DOM,也就是下圖的右半邊部份。
改寫view()
function view(count) {
const r = [...Array(count).keys()]
return <ul id="filmList" className={`list-${count % 3}`}>
{ r.map(n => <li>item {(count * n).toString()}</li>) }
</ul>
}
我們的view函數吸收一個參數count,變量r示意從0到count-1的一個數組。假如count=3, r=[0, 1, 2]。ul的className的值有三種能夠:list-0, list-1, list-2。li的數目取決於count。
改寫render()
function render(el) {
const initialCount = 0
el.appendChild(createElement(view(initialCount)))
setTimeout(() => tick(el, initialCount), 1000)
}
function tick(el, count) {
const patches = diff(view(count + 1), view(count))
patch(el, patches)
if(count > 5) { return }
setTimeout(() => tick(el, count + 1), 1000)
}
render函數有兩個修正,起首挪用view()的時刻傳入count=0。其次,寫了一個定時器,1秒懺悔實行tick函數。tick函數吸收兩個參數,el代表節點元素,count是當前計數值。
tick函數順次做了這幾件事:
- 挪用diff函數,對照新舊兩個VDOM,依據二者的差別獲得須要修正的補丁
- 將補丁patch到實在DOM上
- 當計數器小於即是5的時刻,將count加1,再繼承下一次tick
- 當計數器大於5的時刻,完畢
下面我們來完成diff函數和patch函數。
我們先列出來新舊兩個VDOM對照,會有哪些差別。在index.js文件的最前面聲明一下幾個常量。
const CREATE = 'CREATE' //新增一個節點
const REMOVE = 'REMOVE' //刪除原節點
const REPLACE = 'REPLACE' //替代原節點
const UPDATE = 'UPDATE' //搜檢屬性或子節點是不是有變化
const SET_PROP = 'SET_PROP' //新增或替代屬性
const REMOVE_PROP = 'REMOVE PROP' //刪除屬性
diff()
function diff(newNode, oldNode) {
if (!oldNode) {
return { type: CREATE, newNode }
}
if (!newNode) {
return { type: REMOVE }
}
if (changed(newNode, oldNode)) {
return { type: REPLACE, newNode }
}
if (newNode.type) {
return {
type: UPDATE,
props: diffProps(newNode, oldNode),
children: diffChildren(newNode, oldNode)
}
}
}
- 假如舊節點不存在,我們返回的patches對象, 範例為新增節點;
- 假如新節點不存在,示意是刪除節點;
- 假如二者都存在的話,挪用changed函數推斷他們是不是是有更改;
- 假如二者都存在,且changed()返回false的話,推斷新節點是不是是VDOM(依據type是不是存在來推斷的,由於type不存在的話,newNode要麼是空節點,要麼是字符串)。假如新節點是VDOM,則返回一個patches對象,範例是UPDATE,同時對props和children離別舉行diffProps和diffChildren操縱。
下面我們一次看一下changed, diffProps, diffChildren函數。
changed()
function changed(node1, node2) {
return typeof(node1) !== typeof(node2) ||
typeof(node1) === 'string' && node1 !== node2 ||
node1.type !== node2.type
}
搜檢新舊VDOM是不是有更改的要領很簡單,
- 起首假如數據範例都不一樣,那肯定是更改了;
- 其次假如二者的範例都是純文本,則直接比較二者是不是相稱;
- 末了比較二者的範例是不是相稱。
diffProps()
function diffProps(newNode, oldNode) {
let patches = []
let props = Object.assign({}, newNode.props, oldNode.props)
Object.keys(props).forEach(key => {
const newVal = newNode.props[key]
const oldVal = oldNode.props[key]
if (!newVal) {
patches.push({type: REMOVE_PROP, key, value: oldVal})
}
if (!oldVal || newVal !== oldVal) {
patches.push({ type: SET_PROP, key, value: newVal})
}
})
return patches
}
比較新舊VDOM的屬性的變化,並返迴響應的patches。
- 起首我們採納最大能夠性準繩,將新舊VDOM的一切屬性都兼并賦值給一個新的變量props
- 遍歷props變量的一切Keys,順次比較新舊VDOM關於這個KEY的值
- 假如新值不存在,示意這個屬性被刪除了
- 假如舊值不存在,或許新舊值差別,則示意我們須要從新設置這個屬性
diffChildren()
function diffChildren(newNode, oldNode) {
let patches = []
const maximumLength = Math.max(
newNode.children.length,
oldNode.children.length
)
for(let i = 0; i < maximumLength; i++) {
patches[i] = diff(
newNode.children[i],
oldNode.children[i]
)
}
return patches
}
一樣採納最大能夠性準繩,取新舊VDOM的children的最長值作為遍歷children的長度。然後順次比較新舊VDOM的在雷同INDEX下的每個child。
這裏須要猛烈注重一下
為了簡化,我們沒有引入key的觀點,直接比較的是雷同index下的child。所以假如說一個列表ul有5項,離別是li1, li2, li3, li4, li5; 假如我們刪掉了第一項,新的變成了li2, li3, li4, li5。那末diffchildren的時刻,我們會拿li1和li2比較,順次類推。這樣一來,原本只是刪除了li1, 而li2, li3, li4, li5沒有任何變化,我們得出的diff結論倒是[li替代,li2替代, li3替代, li4替代, li5刪除]。所以react讓人人襯着列表的時刻,必需增添Key。
停止到如今,我們已獲得了我們須要的補丁。下面我們要將補丁Patch到DOM里。
patch()
function patch(parent, patches, index = 0) {
if (!patches) {
return
}
const el = parent.childNodes[index]
switch (patches.type) {
case CREATE: {
const { newNode } = patches
const newEl = createElement(newNode)
parent.appendChild(newEl)
break
}
case REMOVE: {
parent.removeChild(el)
break
}
case REPLACE: {
const {newNode} = patches
const newEl = createElement(newNode)
return parent.replaceChild(newEl, el)
break
}
case UPDATE: {
const {props, children} = patches
patchProps(el, props)
for(let i = 0; i < children.length; i++) {
patch(el, children[i], i)
}
}
}
}
- 起首當patches不存在時,直接return,不舉行任何操縱
- 應用childNodes和Index掏出當前正在處置懲罰的這個節點,賦值為el
- 最先推斷補丁的範例
- 當範例是CREATE時,天生一個新節點,並append到根節點
- 當範例是REMOVE時,直接刪除當前節點el
- 當範例是REPLACE時,天生新節點,同時替代掉原節點
- 當範例是UPDATE時,須要我們特別處置懲罰
- 挪用patchProps將我們之前diffProps獲得的補丁襯着到節點上
- 遍歷之前diffChildren獲得的補丁列表,再順次遞歸挪用patch
末了我們再補充一下patchProps函數
patchProps
function patchProps(parent, patches) {
patches.forEach(patch => {
const { type, key, value } = patch
if (type === 'SET_PROP') {
setProp(parent, key, value)
}
if (type === 'REMOVE_PROP') {
removeProp(parent, key, value)
}
})
}
function removeProp(target, name, value) { //@
if (name === 'className') {
return target.removeAttribute('class')
}
target.removeAttribute(name)
}
這個就不必我詮釋了,代碼很直觀,setProp函數在上一集我們已定義過了。這樣一來,我們就完成了悉數數據更新致使DOM更新的完全歷程。
npm run compile后翻開瀏覽器檢察結果,你應當看到是一個背景色彩在差別變化,同時列表項在逐步增添的列表。
結束撒花
至此,我們的VDOM就悉數完成了。系列初我提出的那幾個題目不知道你如今是不是有了答案。有答案的童鞋能夠在文章批評區將你的看法跟人人分享一下。剖析周全且正確的會收到我的特別嘉獎。😁😁😁😁