好傢伙,
本節來解決我們上一章留下來的問題,
新舊節點同時有兒子的情況本章繼續解決
1.要做什麼?
本章將解決,
1.在相同tag下子元素的替換問題
2.使用雙指標進行元素替換,
實現效果如下:
let vm1 = new Vue({data:{name:'張三'}})
let render1 = compileToFunction(`<ul>
<li style="background:yellow" key="c">我是黃色</li>
</ul>`)
let vnode1 = render1.call(vm1)
document.body.appendChild(createELm(vnode1))
//資料更新
let vm2 = new Vue({data:{name:'李四'}})
let render2 = compileToFunction(`<ul>
<li style="background:blue" key="c">我是藍色</li>
</ul>`)
let vnode2 = render2.call(vm2)
//patch 比對
setTimeout(()=>{
patch(vnode1,vnode2)
},2000)
2.思路
let vm1 = new Vue({
data: {
name: '張三'
}
})
let render1 = compileToFunction(`<ul>
<li style="background:red" key="a">a</li>
<li style="background:pink" key="b">b</li>
<li style="background:blue" key="c">c</li>
</ul>`)
let vnode1 = render1.call(vm1)
document.body.appendChild(createELm(vnode1))
//資料更新
let vm2 = new Vue({
data: {
name: '李四'
}
})
let render2 = compileToFunction(`<ul>
<li style="background:red" key="a">a</li>
<li style="background:pink" key="b">b</li>
<li style="background:blue" key="c">c</li>
<li style="background:yellow" key="d">d</li>
</ul>`)
let vnode2 = render2.call(vm2)
setTimeout(() => {
patch(vnode1, vnode2)
}, 2000)
我們用這個例子來舉例
1.正序(從頭開始)
找到不同(原先沒有的)的項,再將它新增上去
大概的思路就是如此.
但同時,根據不同的情況
我們還有多種比對方法
2.2.逆序
2.3.交叉對比(從頭)
2.4.交叉對比(從尾)
3.程式碼實現
3.1.雙指標
//雙指標 遍歷
let oldStartIndex = 0 //老的開頭索引
let oldStartVnode = oldChildren[oldStartIndex];
let oldEndIndex = oldChildren.length - 1
let oldEndVnode = oldChildren[oldEndIndex]
let newStartIndex = 0 //新的開頭索引
let newStartVnode = newChildren[newStartIndex];
let newEndIndex = newChildren.length - 1
let newEndVnode = newChildren[newEndIndex]
雙指標的寫法非常粗暴,但是好用
3.2.迴圈
(照著上面的圖看)
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
//比對子元素
console.log(666)
if (isSomeVnode(oldStartVnode, newStartVnode)) {
//遞迴
debugger;
//1 從頭部開始
patch(oldStartVnode, newStartVnode);
//移動指標
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = oldChildren[++newStartIndex];
}//2 從尾部開始
else if(isSomeVnode(oldEndVnode, newEndVnode)){
//
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIndex]
newEndVnode = newChildren[--newEndIndex]
}//3 交叉比對 從頭
else if(isSomeVnode(oldStartVnode,newEndVnode)){
patch(oldStartVnode, newEndVnode);
oldStartVnode =oldChildren[++oldStartIndex]
newEndVnode = newChildren[--newEndIndex];
}//4 交叉比對 從尾
else if(isSomeVnode(oldEndVnode,newStartVnode)){
patch(oldEndVnode, newStartVnode);
oldEndVnode =oldChildren[--oldStartIndex]
newStartVnode = newChildren[++newStartIndex];
}
}
3.3.isSomeVnode()
isSomeVnode()方法用於判斷兩個節點是否相同
function isSomeVnode(oldContext, newContext) {
// return true
return (oldContext.tag == newContext.tag) && (oldContext.key === newContext.key);
}
3.4.新增多餘的子兒子
//判斷完畢,新增多餘的子兒子 例子:舊的a b c 新的 a b c d 將d新增到parent
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
parent.appendChild(createELm(newChildren[i]))
}
}
搞定
4.patch.js完整程式碼
如下:
export function patch(oldVnode, Vnode) {
debugger;
//原則 將虛擬節點轉換成真實的節點
// console.log(oldVnode, Vnode)
// console.log(oldVnode.nodeType)
// console.log(Vnode.nodeType)
//第一次渲染 oldVnode 是一個真實的DOM
//判斷ldVnode.nodeType是否為一,意思就是判斷oldVnode是否為屬性節點
if (oldVnode.nodeType === 1) {
console.log(oldVnode, Vnode) //注意oldVnode 需要在載入 mount 新增上去 vm.$el= el
let el = createELm(Vnode) // 產生一個新的DOM
let parentElm = oldVnode.parentNode //獲取老元素(app) 父親 ,body
// console.log(oldVnode)
// console.log(parentElm)
parentElm.insertBefore(el, oldVnode.nextSibling) //當前真實的元素插入到app 的後面
parentElm.removeChild(oldVnode) //刪除老節點
//重新賦值
return el
} else { // diff
// console.log(oldVnode.nodeType)
console.log(oldVnode, Vnode)
//1 元素不是一樣
if (oldVnode.tag !== Vnode.tag) {
//舊的元素 直接替換為新的元素
return oldVnode.el.parentNode.replaceChild(createELm(Vnode), oldVnode.el)
}
//2 標籤一樣 text 屬性 <div>1</div> <div>2</div> tag:undefined
if (!oldVnode.tag) {
if (oldVnode.text !== Vnode.text) {
return oldVnode.el.textContent = Vnode.text
}
}
//2.1屬性 (標籤一樣) <div id='a'>1</div> <div style>2</div>
//在updataRpors方法中處理
//方法 1直接複製
let el = Vnode.el = oldVnode.el
updataRpors(Vnode, oldVnode.data)
//diff子元素 <div>1</div> <div></div>
let oldChildren = oldVnode.children || []
let newChildren = Vnode.children || []
if (oldChildren.length > 0 && newChildren.length > 0) { //老的有兒子 新有兒子
//建立方法
updataChild(oldChildren, newChildren, el)
} else if (oldChildren.length > 0 && newChildren.length <= 0) { //老的元素 有兒子 新的沒有兒子
el.innerHTML = ''
} else if (newChildren.length > 0 && oldChildren.length <= 0) { //老沒有兒子 新的有兒子
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i]
//新增到真實DOM
el.appendChild(createELm(child))
}
}
}
}
function updataChild(oldChildren, newChildren, parent) {
//diff演算法 做了很多最佳化 例子<div>11</div> 更新為 <div>22</div>
//dom中操作元素 常用的 思想 尾部新增 頭部新增 倒敘和正序的方式
//雙指標 遍歷
let oldStartIndex = 0 //老的開頭索引
let oldStartVnode = oldChildren[oldStartIndex];
let oldEndIndex = oldChildren.length - 1
let oldEndVnode = oldChildren[oldEndIndex]
let newStartIndex = 0 //新的開頭索引
let newStartVnode = newChildren[newStartIndex];
let newEndIndex = newChildren.length - 1
let newEndVnode = newChildren[newEndIndex]
console.log(oldEndIndex,newEndIndex)
console.log(oldEndVnode,newEndVnode)
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
//比對子元素
console.log(666)
if (isSomeVnode(oldStartVnode, newStartVnode)) {
//遞迴
debugger;
//1 從頭部開始
patch(oldStartVnode, newStartVnode);
//移動指標
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = oldChildren[++newStartIndex];
}//2 從尾部開始
else if(isSomeVnode(oldEndVnode, newEndVnode)){
//
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIndex]
newEndVnode = newChildren[--newEndIndex]
}//3 交叉比對 從頭
else if(isSomeVnode(oldStartVnode,newEndVnode)){
patch(oldStartVnode, newEndVnode);
oldStartVnode =oldChildren[++oldStartIndex]
newEndVnode = newChildren[--newEndIndex];
}//4 交叉比對 從尾
else if(isSomeVnode(oldEndVnode,newStartVnode)){
patch(oldEndVnode, newStartVnode);
oldEndVnode =oldChildren[--oldStartIndex]
newStartVnode = newChildren[++newStartIndex];
}
}
//判斷完畢,新增多餘的子兒子 a b c 新的 a b c d
console.log(newEndIndex)
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
parent.appendChild(createELm(newChildren[i]))
}
}
}
function isSomeVnode(oldContext, newContext) {
// return true
return (oldContext.tag == newContext.tag) && (oldContext.key === newContext.key);
}
//新增屬性
function updataRpors(vnode, oldProps = {}) { //第一次
let newProps = vnode.data || {} //獲取當前新節點 的屬性
let el = vnode.el //獲取當前真實節點 {}
//1老的有屬性,新沒有屬性
for (let key in oldProps) {
if (!newProps[key]) {
//刪除屬性
el.removeAttribute[key] //
}
}
//2演示 老的 style={color:red} 新的 style="{background:red}"
let newStyle = newProps.style || {} //獲取新的樣式
let oldStyle = oldProps.style || {} //老的
for (let key in oldStyle) {
if (!newStyle[key]) {
el.style = ''
}
}
//新的
for (let key in newProps) {
if (key === "style") {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName]
}
} else if (key === 'class') {
el.className = newProps.class
} else {
el.setAttribute(key, newProps[key])
}
}
}
//vnode 變成真實的Dom
export function createELm(vnode) {
let {
tag,
children,
key,
data,
text
} = vnode
//注意
if (typeof tag === 'string') { //建立元素 放到 vnode.el上
vnode.el = document.createElement(tag) //建立元素
updataRpors(vnode)
//有兒子
children.forEach(child => {
// 遞迴 兒子 將兒子渲染後的結果放到 父親中
vnode.el.appendChild(createELm(child))
})
} else { //文字
vnode.el = document.createTextNode(text)
}
return vnode.el //新的dom
}