虛擬DOM
1、新建react項目
//全局安裝腳手架工具
$ npm install create-react-app -g
//創建項目
$ create-react-app dom-diff
// 進入項目目錄
$ cd dom-diff
// 編譯
$ npm run start
2、定義虛擬DOM類
什麼是虛擬DOM? 虛擬DOM就是使用javascript對象來表示真實DOM,是一個樹形結構。
這個對象結構如下:
const virtualDom = {
type: 'ul',
props: {
class: 'lists'
},
children: [{
type: 'li',
props: {},
children: [1]
},
{
type: 'li',
props: {},
children: [2]
},
{
type: 'li',
props: {},
children: [3]
}
]
}
爲了構建這樣的對象,我們需要新建一個 element.js 文件。
- 需要定義對象,用來描述虛擬dom
class Element{
constructor(type,props,childrens){
this.type= type
this.props = props
this.childrens= childrens
}
- }
提供個方法,生成element對象
function createElement(type,props,children){
return new Element(type,props,children)
}
element.js
全部代碼
/**
* 定義虛擬dom對象
*/
class Element{
constructor(type,props,childrens){
this.type= type
this.props = props
this.childrens= childrens
}
}
/**
* 生成虛擬dom
* @param {*} type 元素類型
* @param {*} props 元素屬性
* @param {*} children 子元素
*/
function createElement(type,props,children){
return new Element(type,props,children)
}
3、構建虛擬DOM
在 index.js 文件中
import {createElement} from './element'
let virtualDom = createElement('ul',{className:'lists'},[
createElement('li',{},[1]),
createElement('li',{},[2]),
createElement('li',{},[3])
])
console.log(virtualDom)
變量 virtualDom 在控制檯的打印結果:
4、虛擬DOM生成真實DOM
我們已經生成了構建出了虛擬dom對象,下一步就是將這個對象渲染成真實的dom對象。
我們在 element.js 文件中新增一個render方法。
/**
* 將虛擬dom轉化成真實dom
* @param {Element} virtualDom 虛擬dom
*/
function render(virtualDom){
if(!virtualDom) throw new Error('傳入的虛擬dom爲空')
// 根據type類型來創建對應的元素
let el = document.createElement(virtualDom.type)
//遍歷虛擬dom的屬性
for(let attrKey in virtualDom.props){
const attrValue = virtualDom.props[attrKey]
el.setAttribute(attrKey,attrValue)
}
//處理子元素
if(virtualDom.childrens){
for(let child of virtualDom.childrens){
//判斷是不是element類型。
if(child instanceof Element){
//遞歸
el.appendChild(render(child))
}else{
//如果不是element,則證明是文本節點
el.appendChild(document.createTextNode(child))
}
}
}
return el
}
5、將DOM元素渲染到頁面上
在element.js 中新增一個方法:renderElement(node,element)
//...省略其他代碼
/**
* 將構建好的真實dom插入到目標元素裏
* @param {*} el 目標元素
* @param {*} dom 真實dom
*/
function renderElement(el,dom){
el.appendChild(dom)
}
export {Element,createElement,render,renderElement}
在 index.js 頁面操作,將虛擬dom轉成真實dom,再添加到id爲root的節點下面
import {createElement,render,renderElement} from './element'
let virtualDom = createElement('ul',{class:'lists'},[
createElement('li',{},[1]),
createElement('li',{},[2]),
createElement('li',{},[3])
])
//將虛擬dom轉成真實dom
let actualDom = render(virtualDom)
//將真實dom渲染到頁面上
renderElement(document.getElementById(‘root’),actualDom)
界面效果
DOM-DIFF
- dom-diff 三種優化策略
- 更新的時候只比較平級虛擬節點,依次進行比較並不會跨級比較,比較差異部分,生成對應補丁包,根據補丁包改變的內容更新差異的dom。
- 更新的時候平級比較兩個虛擬DOM,當發現節點已經不存在,則該節點及其子節點會被完全刪除掉,不會用於進一步的比較。如果該刪除的節點之下有子節點,那麼這些子節點也會被完全刪除,它們也不會用於後面的比較。這樣只需要對樹進行一次遍歷,便能完成整個DOM樹的比較。新增的節點,會對應的創建一個新的節點。
- 第三種更新的時候平級比較兩個虛擬DOM,如果只是一層變了,互換了位置,那麼它會複用此虛擬節點,把對應的位置互換一下即可,這個是通過給對應元素添加的key不同來實現的。
-
前置知識
樹的先序深度遍歷
按照 “根-左-右” 的順序進行遍歷 -
代碼實現
3.1 根據虛擬dom差異得到補丁對象
import {
isString
}
from './utils/types.js'
const ATTRS = 'ATTRS'const REPLACE = 'REPLACE'const REMOVE = 'REMOVE'const TEXT = 'TEXT'
//todo
//const ADD ='ADD'
/**
*
* @param {*} newDom 新節點
* @param {*} oldDom 老節點
*/
// 所有都基於一個序號來實現
let num = 0
function diff(oldDom, newDom) {
const patches = {}
num = 0 walk(oldDom, newDom, num, patches) return patches
}
/**
* 比較子節點
* @param {*} oldChilds
* @param {*} newChilds
*/
function diffChildren(oldChilds, newChilds, patches) {
// 比較老的第一個和新的第一個
oldChilds.forEach((old, idx) = >{
walk(old, newChilds[idx], ++num, patches)
});
}
function walk(oldNode, newNode, index, patches) {
//每個元素都有一個補丁
let currentPatch = []
//是否是文本節點
if (isString(oldNode) && isString(newNode)) {
if (oldNode !== newNode) {
currentPatch.push({
type: TEXT,
text: newNode
})
}
}
/**是否新節點爲空:被刪除 */
else if (!newNode) {
currentPatch.push({
type: REMOVE
})
} //類型是否一致
else if (oldNode.type === newNode.type) {
//比較屬性
let attrPatch = diffAttrs(oldNode.props, newNode.props)
//存在屬性差異
if (Object.keys(attrPatch).length > 0) {
currentPatch.push({
type: ATTRS,
attr: attrPatch
})
}
//比較子節點
diffChildren(oldNode.childrens, newNode.childrens, patches)
} else {
//節點被替換
currentPatch.push({
type: REPLACE,
node: newNode
})
}
if (currentPatch.length > 0) {
patches[index] = currentPatch
}
}
/**
* 比較屬性
*/
function diffAttrs(oldProps, newProps) {
const patch = {}
//先遍歷老屬性,比較與新屬性的差異
for (let key in oldProps) {
if (oldProps[key] !== newProps[key]) {
patch[key] = newProps[key]
}
}
//再遍歷新屬性,看是否有新增加的屬性
for (let key in newProps) {
//如果舊屬性裏沒有該值
if (!oldProps.hasOwnProperty(key)) {
patch[key] = newProps[key]
}
}
return patch
}
export
3.2 根據補丁對象,更新dom
import {
render
} from './element'
let allPatches;
let index = 0
function patch(node, patches) {
allPatches = patches
index = 0
walk(node)
}
const ATTRS = 'ATTRS'
const REPLACE = 'REPLACE'
const REMOVE = 'REMOVE'
const TEXT = 'TEXT'
/**
* 開始循環打補丁
*/
function walk(node) {
let current = allPatches[index++]
let childNodes = node.childNodes;
// 先序深度,繼續遍歷遞歸子節點
childNodes.forEach(child => walk(child));
if (current) {
doPatch(node, current);
}
}
function doPatch(node, patches) {
//同一個元素上可能打了多個補丁
patches.forEach(current => {
switch (current.type) {
case ATTRS:
const attrs = current.attr
for (let key in attrs) {
node.setAttribute(key, attrs[key])
}
break
case TEXT:
node.textContent = current.text
break;
case REMOVE:
node.parentNode.removeChild(node)
break;
case REPLACE:
let newNode = current.node
if (newNode instanceof Element) {
node.parentNode.replaceChild(render(newNode), node)
} else {
node.parentNode.replaceChild(document.createTextNode(newNode), node)
}
break;
default:
break;
}
})
}
export default patch
3.3. 模擬數據變化,觸發重新渲染
import {
createElement,
render,
renderElement
} from './element'
import './index.css'
import diff from './diff'
import patch from './patch'
let virtualDom1 = createElement('ul', {
style: 'background:red;color:#fff;'
}, [
createElement('li', {}, ["2"]),
createElement('li', {}, ["2"]),
createElement('li', {}, ["3"])
])
//將虛擬dom轉成真實dom
let actualDom = render(virtualDom1)
//將真實dom渲染到頁面上
renderElement(document.getElementById('root'), actualDom)
//模擬數據變化
let virtualDom2 = createElement('ul', {
style: 'color:red;'
}, [
createElement('li', {}, ["2"]),
createElement('li', {}, ["2"]),
createElement('li', {}, ["4"]),
])
const patchs = diff(virtualDom1, virtualDom2)
patch(actualDom, patchs)