好傢伙,
本篇將會解釋要以下效果的實現
我們要實現以下元素替換的效果
gif:
以上例子的程式碼:
//建立vnode
let vm1 = new Vue({data:{name:'張三'}})
let render1 = compileToFunction(`<a>{{name}}</a>`)
let vnode1 = render1.call(vm1)
document.body.appendChild(createELm(vnode1))
//資料更新
let vm2 = new Vue({data:{name:'李四'}})
let render2 = compileToFunction(`<div>{{name}}</div>`)
let vnode2 = render2.call(vm2)
//屬性新增
let vm3 = new Vue({data:{name:'李四'}})
let render3 = compileToFunction(`<div style="color:red">{{name}}</div>`)
let vnode3 = render3.call(vm3)
//patch 比對
setTimeout(()=>{
patch(vnode1,vnode2)
},2000)
setTimeout(()=>{
patch(vnode2,vnode3)
},3000)
以上例子中compileToFunction()方法的詳細解釋
一句話解釋,這是一個將模板變為render函數的方法
開搞:
思路非常簡單,依舊是對不同的情況分類處理
patch.js
export function patch(oldVnode, Vnode) {
//原則 將虛擬節點轉換成真實的節點
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,ewChildren,el){
}
//新增屬性
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
}
三個方法,我們一個個看
export function patch(oldVnode, Vnode) {
//原則 將虛擬節點轉換成真實的節點
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))
}
}
}
}
patch()方法用於根據新的虛擬節點更新舊的虛擬節點以及對應的真實 DOM 元素。
首先判斷舊的虛擬節點是否是一個真實 DOM 元素(即是否為屬性節點),
--1--如果是,則表示這是第一次渲染,需要使用 createELm
函數建立新的 DOM 元素,並將其插入到舊的元素之前,最後再刪除舊的元素,返回新建立的元素。
--2--如果不是第一次渲染,則進行 diff 操作,
--2.1--首先判斷新老節點的標籤是否相同,如果不同,則直接使用新的節點替換舊的節點。
--2.2--如果標籤相同,則需要判斷節點的文字內容和屬性是否發生了變化,如果發生了變化,則通過 updataRpors
函數更新 DOM 元素屬性或文字內容。
--2.3--最後,需要 diff 子元素。
--2.3.1--如果舊節點和新節點均有子元素,則需要將新舊子元素進行比較,通過 updataChild
函數更新舊節點的子元素與新節點的子元素。
--2.3.2--如果舊節點有子元素而新節點沒有,則直接將舊節點的內容清空;
--2.3.3--如果新節點有子元素而舊節點沒有,則直接將新節點的子元素新增到舊節點中。
//新增屬性
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])
}
}
}
updataRpors()是一個更新屬性的方法,其主要功能是更新虛擬節點的屬性,包括刪除不再存在的屬性、更新樣式和類名等。
//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
}
createELm()是一個用於建立和渲染虛擬DOM的函數.
函數名稱為`createELm`,它接收一個引數`vnode`,這個引數是一個虛擬DOM節點物件。
這段程式碼的主要作用是根據傳入的虛擬DOM節點資料結構(`vnode`)建立一個相應的實際DOM元素,並返回該元素。
如果虛擬DOM節點包含子節點,它會遞迴地為每個子節點建立相應的DOM元素並新增到父節點的DOM元素中。