virtul DOM 也就是虛擬節(jié)點嘱丢。通過JS的Object對象模擬DOM中的真實節(jié)點對象节视,再通過特定的render方法將其渲染成真實的DOM節(jié)點双戳。
一、渲染步驟
生成vNode---->渲染成真實節(jié)點 --------->掛載到頁面--------->diff比較
1蹋凝、模擬方法和渲染方法
需求,生成一個如下圖所示的DOM結(jié)構(gòu)
image.png
調(diào)用
let virtualDom1 = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'}, ['a']),
createElement('li', {class: 'item'}, ['b']),
createElement('li', {class: 'item'}, ['c']),
])
let virtualDom2 = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'}, ['1']),
createElement('li', {class: 'item'}, ['2']),
createElement('li', {class: 'item'}, ['3']),
])
let el = render(virtualDom);
renderDom(el, window.root);
let patchs = diff(virtualDom1, virtualDom2);
生成虛擬對象的方法createElement
function createElement(type, props, children) {
return new Element(type, props, children)
}
class Element{
constructor(type, props, children){
this.type = type;
this.props = props;
this.children = children
}
}
將虛擬對象渲染成真實DOM的render方法
//render方法將vNode轉(zhuǎn)化成真實DOM
function render(eleObj){
//創(chuàng)建元素
let el = document.createElement(eleObj.type);
//設(shè)置屬性
for(let key in eleObj.props) {
setAttr(el, key, eleObj.props[key]);
}
//遞歸渲染子元素
eleObj.children.foEach(child => {
child = child instanceof Element ? render(child) : document.createTextNode(child);
el.appendChild(child);
})
}
setAttr(node, key, value) {
switch(key) {
case 'value':
if (node.tagName.toUpperCase() === 'INPUT' || node.tagName.toUpperCase() === 'TEXTAREA') {
node.value = value;
}else {
node.setAttribute(key, value);
}
break;
case 'style':
node.style.cssText = value;
break;
default:
node.setAttribute(key, value);
break;
}
}
渲染節(jié)點到頁面的方法renderDom
//將真實DOM渲染到頁面
function renderDom(el, target) {
target.appendChild(el);
}
二总棵、DOM DIFF 算法
1鳍寂、核心思想
DOM DIFF 就是比較兩個虛擬DOM的區(qū)別,實際上就是比較兩個對象的區(qū)別情龄。根據(jù)兩個虛擬對象創(chuàng)建出補丁迄汛,描述改變的內(nèi)容。將這個補丁用來更新DOM骤视。
image.png
【注意】不會更改所有節(jié)點鞍爱,只更改有改變的部分
2、DOM DIFF 兩種優(yōu)化策略
1)分層比較尚胞,一層一層比硬霍,不會跨級對比
2)如果一層的對象只是換了下位置,可以通過key值直接換位置笼裳。
2唯卖、算法實現(xiàn)
差異計算:先序深度優(yōu)先遍歷
image.png
規(guī)則:
1粱玲、若節(jié)點類型不相同,直接采用替換模式拜轨,{type:'REPLACE',newNode:newNode}
2抽减、當(dāng)節(jié)點類型相同時,去看一下屬性是否相同橄碾,產(chǎn)生一個屬性的補丁包卵沉,比如{type:'ATTRS',attrs:{class: 'list-group'}
3、新的DOM節(jié)點不存在法牲,也返回一個不存在的補丁包{type:'REMOVE',index:XXX}
4史汗、文本的變化{type:'TEXT', text:1}
DIff 算法
//diff 算法
let Index = 0;
function diff(oldTree, newTree) {
let patches = {};
let index = 0;
//遞歸數(shù)比較后的結(jié)果放到補丁包中
walk(oldTree, newTree, index, patches);
return patches;
}
function walk(oldTree, newTree, index, patches){
let currentPatch = [];//每個元素都有一個補丁對象
if (!newTree) {
currentPatch.push({type:'REMOVE', index})
}
if (isString(oldTree) && isString(newTree)) {
// 判斷文本是否一致
if (oldTree !== newTree) {
currentPatch.push({type:'TEXT',text:newTree});
}
}else if(oldTree.type === newTree.type) {
//比較屬性是否有更改
let attrs = diffAttr(oldTree.props, newTree.props);
if(Object.keys(attrs).length) {
currentPatch.push({type:'ATTRS', attrs});
}
// 如果有兒子節(jié)點,遍歷子節(jié)點
diffChildren(oldTree.children, newTree.children, index, patches);
} else {
// 節(jié)點類型不同的時候拒垃,直接替換
currentPatch.push({type:'REPLACE', newTree});
}
// 當(dāng)前元素有補丁的情況下停撞,將元素和補丁對應(yīng)起來,放到大補丁包中
if(currentPatch.length) {
patches[index] = currentPatch;
}
}
function diffAttr(oldAttrs, newAttrs) {
let patch = {};
for(let key in oldAttrs) {
if(oldAttrs[key] !== newAttrs[key]) {
patch[key] = newAttrs[key];//有可能是undefined悼瓮,新節(jié)點沒有舊節(jié)點的屬性
}
}
for(let key in newAttrs) {
//老節(jié)點沒有新節(jié)點的屬性
if(! oldAttrs.hasOwnProperty(key)) {
patch[key] = newAttrs[key]
}
}
return patch;
}
function diffChildren(oldChildren, newChildren, index, patches){
// 比較老的第一個和新的第一個
oldChildren.forEach((child, idx) => {
// 記得索引得改
// Index 每次傳遞給walk時戈毒,index是遞增的,所有節(jié)點都基于一個序號實現(xiàn)横堡,因此需要維護一個全局Index
walk(child, newChildren[idx], ++Index, patches);
})
}
function isString(node) {
return Object.prototype.toString.call(node) === '[object string]';
}
function patch(node, patches) {
// 給某個元素打補丁
}