<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
virtual dom ,也就是虛擬節點
1.它通過js的Object物件模擬dom中的節點
2.再通過特定的render方法將其渲染成真實的dom節點
eg:
<div id="wrapper" class="1"> <span style="color:red">hello</span> world </div>
如果利用h方法生成虛擬dom的話:
h('div', { id: 'wrapper', class: '1' }, h('span', { style: { color: 'red' } }, 'hello'), 'world');
對應的js物件如下:
let vd = { type: 'div', props: { id: 'wrapper', class: '1' }, children: [ { type: 'span', props: { color: 'red' }, children: [{}] }, { type: '', props: '', text: 'world' } ] }
自己實現一個h方法
function createElement(type, props = {}, ...children) { // 防止沒有傳值的話就賦值一個初始值 let key; if (props.key) { key = props.key delete props.key } // 如果孩子節點有字串型別的,也需要轉化為虛擬節點 children = children.map(child => { if (typeof child === 'string') { // 把不是節點型別的子節點包裝為虛擬節點 return vNode(undefined, undefined, undefined, undefined, child) } else { return child } }) return vNode(type, props, key, children) } function vNode(type, props, key, children, text = undefined) { return { type, props, key, children, text } }
render的作用:把虛擬dom轉化為真實dom渲染到container容器中去
export function render(vnode, container) { let ele = createDomElementFrom(vnode) //通過這個方法轉換真實節點 if (ele) container.appendChild(ele) }
把虛擬dom轉化為真實dom,插入到容器中,如果虛擬dom物件包含type值,說明為元素(createElement),否則為節點型別(createTextnode),並把真實節點賦值給虛擬節點,建立起兩者之間的關係
function createDomElementFrom(vnode) { let { type, key, props, children, text } = vnode if (type) {//說明是一個標籤 // 1. 給虛擬元素加上一個domElemnt屬性,建立真實和虛擬dom的聯絡,後面可以用來跟新真實dom vnode.domElement = document.createElement(type) // 2. 根據當前虛擬節點的屬性,去跟新真實dom的值 updateProperties(vnode) // 3. children中方的也是一個個的虛擬節點(就是遞迴把兒子追加到當前元素裡) children.forEach(childVnode => render(childVnode, vnode.domElement)) } else {//說明是一個文字 } return vnode.domElement } function updateProperties(newVnode, oldProps = {}) { let domElement = newVnode.domElement //真實dom, let newProps = newVnode.props; //當前虛擬節點中的屬性 // 如果老的裡面有,新的裡面沒有,說明這個屬性被移出了 for (let oldPropName in oldProps) { if (!newProps[oldPropName]) { delete domElement[oldPropName] //新的沒有,為了複用這個dom,直接刪除 } } // 如果新的裡面有style,老的裡面也有style,style可能還不一樣 let newStyleObj = newProps.style || {} let oldStyleObj = oldProps.style || {} for (let propName in oldStyleObj) { if (!newStyleObj[propName]) { domElement.style[propName] = '' } } // 老的裡面沒有,新的裡面有 for (let newPropsName in newProps) { // 直接用新節點的屬性覆蓋老節點的屬性 if (newPropsName === 'style') { let styleObj = newProps.style; for (let s in styleObj) { domElement.style[s] = styleObj[s] } } else { domElement[newPropsName] = newProps[newPropsName] } } }
根據當前虛擬節點的屬性,去更新真實dom的值
由於還有子節點,所以還需要遞迴,生成子節點虛擬dom的真實節點,插入當前的真實節點裡去
剛剛可能會有點不解,為什麼要把新的節點和老的節點屬性進行比對,因為剛剛是首次渲染,現在講一下二次渲染
比如說現在構建了一個新節點newNode,我們需要和老節點進行對比,然而並不是簡單的替換,而是需要儘可能多地進行復用
首先判斷父親節點的型別,如果不一樣就直接替換
如果一樣
1.文字型別,直接替換文字值即可
2.元素型別,需要根據屬性來替換
這就證明了render方法裡我們的oldProps的必要性,所以這裡把新節點的真實dom賦值為舊節點的真實dom,先複用一波,待會再慢慢修改
updateProperties(newVnode, oldVNode.props)
export function patch(oldVNode, newVnode) { // //判斷型別是否一樣,不一樣直接用新虛擬節點替換老的 if (oldVNode.type !== newVnode.type) { return oldVNode.domElement.parentNode.replaceChild( createDomElementFrom(newVnode), oldVNode.domElement ) } // 型別相同,且是文字 if (oldVNode.text) { return oldVNode.document.textContent = newVnode.text } // 型別一樣,不是文字,是標籤,需要根據新節點的屬性更新老節點的屬性 // 1. 複用老節點的真實dom let domElement = newVnode.domElement = oldVNode.domElement // 2. 根據最新的虛擬節點來更新屬性 updateProperties(newVnode, oldVNode.props) // 比較兒子 let oldChildren = oldVNode.children let newChildren = newVnode.children // 1. 老的有兒子,新的有兒子 if (oldChildren.length > 0 && newChildren.length > 0) { // 對比兩個兒子(很複雜) } else if (oldChildren.length > 0) { // 2. 老的有兒子,新的沒兒子 domElement.innerHTML = '' } else if (newChildren.length > 0) { // 3. 新增了兒子 for (let i = 0; i < newChildren.length; i++) { // 把每個兒子加入元素裡 let ele = createDomElementFrom(newChildren[i]) domElement.appendChild(ele) } } }
剛剛的渲染方法裡,首先是對最外層元素進行對比,對於兒子節點,分為三種情況
1.老的有兒子,新的沒兒子(那麼直接把真實節點的innerHTML設定為空即可)
2.老的沒兒子,新的有兒子(那麼遍歷新的虛擬節點的兒子列表,把每一個都利用createElementFrom方法轉化為真實dom,append到最外層真實dom即可)
3.老的有兒子,新的有兒子,這個情況非常複雜,也就是我們要提及的diff演演算法
以最常見的ul列表為例子
舊的虛擬dom
let oldNode = h('div', {}, h('li', { style: { background: 'red' }, key: 'A' }, 'A'), h('li', { style: { background: 'blue' }, key: 'B' }, 'A'), h('li', { style: { background: 'yellow' }, key: 'C' }, 'C'), h('li', { style: { background: 'green' }, key: 'D' }, 'D'), );
新的虛擬節點
let newVnode = h('div', {}, h('li', { style: { background: 'red' }, key: 'A' }, 'A'), h('li', { style: { background: 'blue' }, key: 'B' }, 'B'), h('li', { style: { background: 'yellow' }, key: 'C' }, 'C1'), h('li', { style: { background: 'green' }, key: 'D' }, 'D1'), h('li', { style: { background: 'black' }, key: 'D' }, 'E'), );
eg:
// 比較是否同一個節點 function isSameVnode(oldVnode, newVnode) { return oldVnode.key == newVnode.key && oldVnode.type == newVnode.type } // diff function updateChildren(parent, oldChildren, newChildren) { // 1. 建立舊節點開頭指標和結尾 let oldStartIndex = 0 let oldStartVnode = oldChildren[oldStartIndex]; let oldEndIndex = oldChildren.length - 1 let oldEndVnode = oldChildren[oldEndIndex]; // 2. 建立新節點的指標 let newStartIndex = 0 let newStartVnode = newChildren[newStartIndex]; let newEndIndex = newChildren.length - 1 let newEndVnode = newChildren[newEndIndex]; // 1. 當從後面插入節點的時候,希望判斷老的孩子和新的孩子 迴圈的時候,誰先結束就停止迴圈 while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { // 注意:比較物件是否相等,你不能用==,因為指向的位置可能不一樣,可以用type和key if (isSameVnode(oldStartVnode, newStartVnode)) { //patch比對更新 patch(oldStartVnode, newStartVnode) // 移動指標 oldStartVnode = oldChildren[++oldStartIndex] newStartVnode = newChildren[++newStartIndex] } } if (newStartIndex <= newEndIndex) { for (let i = newStartIndex; i <= newEndIndex; i++) { parent.appendChild(createDomElementFrom(newChildren[i])) } } }
頭和頭+尾和尾的處理方法:
我們通過parent.insertBefore(createDomElementFrom(newChildren[i]), beforeElement)
使得末尾新增和頭部新增採用同一種處理方法
// 如果是從前往後遍歷說明末尾新增了節點,會比原來的兒子後面新增了幾個 // 也可以時從後往前遍歷,說明比原來的兒子前面新增了幾個 if (newStartIndex <= newEndIndex) { for (let i = newStartIndex; i <= newEndIndex; i++) { // 取得第一個值,null代表末尾 let beforeElement = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].domElement parent.insertBefore(createDomElementFrom(newChildren[i]), beforeElement) } }
圖解:
MVVM=>資料一變,就呼叫patch
尾和頭就不畫圖了
else if (isSameVnode(oldStartVnode, newEndVnode)) { // 頭和尾巴都不一樣,拿老的頭和新的尾巴比較 patch(oldStartVnode, newEndVnode) // 把舊節點的頭部插入到舊節點末尾指標指向的節點之後一個 parent.insertBefore(oldStartVnode.domElement, oldEndVnode.domElement.nextSibling) // 移動指標 oldStartVnode = oldChildren[++oldStartIndex] newEndVnode = newChildren[--newEndIndex] } else if (isSameVnode(oldEndVnode, newStartVnode)) { // 頭和尾巴都不一樣,拿老的頭和新的尾巴比較 patch(oldEndVnode, newStartVnode) // 把舊節點的頭部插入到舊節點末尾指標指向的節點之後一個 parent.insertBefore(oldEndVnode.domElement, oldStartVnode.domElement) // 移動指標 oldEndVnode = oldChildren[--oldEndIndex] newStartVnode = newChildren[++newStartIndex] } else {
else { // 都不一樣,就暴力比對 // 需要先拿到新的節點去老的節點查詢是否存在相同的key,存在則複用,不存在就建立插入即可 // 1. 先把老的雜湊 let index = map[newStartVnode.key]//看看新節點的key在不在這個map裡 console.log(index); if (index == null) {//沒有相同的key // 直接建立一個,插入到老的前面即可 parent.insertBefore(createDomElementFrom(newStartVnode), oldStartVnode.domElement) } else {//有,可以複用 let toMoveNode = oldChildren[index] patch(toMoveNode, newStartVnode)//複用要先patch一下 parent.insertBefore(toMoveNode.domElement, oldStartVnode.domElement) oldChildren[index] = undefined // 移動指正 } newStartVnode = newChildren[++newStartIndex] } // 寫一個方法,做成一個雜湊表{a:0,b:1,c:2} function createMapToIndex(oldChildren) { let map = {} for (let i = 0; i < oldChildren.length; i++) { let current = oldChildren[i] if (current.key) { map[current.key] = i } } return map }
採用的是深度優先,只會涉及到dom樹同層的比較,先對比父節點是否相同,然後對比兒子節點是否相同,相同的話對比孫子節點是否相同
本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45