- 本次掌握:
- 虚拟DOM如何被渲染函数(h函数)产生?
- 要手写h函数
- diff算法原理?
- 要手写diff算法
- 虚拟DOM如何通过diff变为真正的DOM的
- 虚拟DOM变回真正的DOM,是涵盖在diff算法里面的
- 虚拟DOM如何被渲染函数(h函数)产生?
学习的尚硅谷Vue源码解析课程,做个笔记让自己更理解
虚拟DOM
虚拟Dom:用JavaScript对象来描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性
diff算法是发生在虚拟DOM上的,新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出应如何最小量更新,最后反映到真正的DOM
真实DOM如何编译成虚拟DOM见
尚硅谷Vue源码解析mustache课程
h函数
h函数用来产生虚拟节点(vnode)
比如这样调用h函数:
h('a', {props: {href: 'http://www.baidu.com'}}, '百度')
将得到这样的虚拟节点:
{sel: 'a', data: { props: { href: 'http://www.baidu.com' } }, text: '百度', …}
它表示的真正的DOM节点:
<a href="http://www.baidu.com">百度</a>
一个虚拟节点的属性有哪些![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/5466c32820107912b0dad056312825a2.png)
让虚拟节点变为真实dom节点(具体细节后面):
// 创建出patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule])
// 创建虚拟节点
var myVnode1 = h('a', {props: {href: 'http://www.baidu.com'}}, '百度')
console.log(myVnode1);
// 让虚拟节点上树
const container = document.getElementById('container')
patch(container, myVnode1)
h函数可以嵌套使用,从而得到虚拟DOM树(重要)
比如这样嵌套使用h函数:
h('ul', {}, [
h('li', {}, '牛奶'),
h('li', {}, '咖啡'),
h('li', {}, '可乐')
]);
将得到这样的虚拟DOM树:
{
"sel": "ul",
"data": {},
"children": [
{ "sel": "li", "text": "牛奶" },
{ "sel": "li", "text": "咖啡" },
{ "sel": "li", "text": "可乐" }
]
}
h函数的多种用法(利用重载):
h('div')
h('div', '文字')
h('div', [])
h('div', h())
h('div', {}, '文字')
h('div', {}, [])
h('div', {}, h())
手写h函数
// vnode.js(返回节点对象):
export default function(sel, data, children, text, elm) {
return {
sel, data, children, text, elm
}
}
import vnode from './vnode.js';
// 编写低配的h函数,必须接受三个参数:标签,数据对象,c【text/[]/h()】
// 重载功能较弱
// 也就是说,调用的时候形态必须是下面的三种之一:
// 形态① h('div', {}, '文字')
// 形态② h('div', {}, [])
// 形态③ h('div', {}, h())
// 形态2举例:
// h('ul', {}, [
// h('li', {}, '牛奶'),
// h('li', {}, '咖啡'),
// h('li', {}, '可乐')
// ]);
export default function (sel, data, c) {
if (arguments.length !== 3) {
throw new Error('必须传入三个参数')
}
// 检查参数c
if (typeof c === 'string' || typeof c === 'number') {
// 形态①
return vnode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) {
// 形态②
let children = []
// 遍历
for (let i = 0; i < c.length; i++) {
// 检查c[i]必须是一个对象(h函数调用的结果)
if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) {
throw new Error('传入的数组参数中有项不是h函数')
}
// 直接放入children中
children.push(c[i])
}
return vnode(sel, data, children, undefined, undefined)
} else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
// 形态③
let children = [c]
return vnode(sel, data, children, undefined, undefined)
} else {
throw new Error('传入的第三个参数类型不对')
}
}
diff算法
初步理解
-
最小量更新太厉害啦!真的是最小量更新!当然,key很重要。key是这个节点的
唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点。 -
只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。
延伸问题:如何定义是同一个虚拟节点?
答:选择器相同且key相同。 -
只进行同层比较,不会进行跨层比较。 即使是同一片虚拟节点,但是跨层了,对不起,精细化比较不diff你,而是暴力删除旧的、然后插入新的。
diff处理新旧节点不是同一个节点时
- 如何定义“同一个节点”
旧节点的key要和新节点的key相同且旧节点的选择器要和新节点的选择器相同 - 创建节点时,所有子节点需要递归创建的
- 不是同一个节点:
import vnode from './vnode'; import createElement from './createElement' export default function (oldVnode, newVnode) { // 判断第一个参数是否为虚拟节点,不是则包装为虚拟节点 if (oldVnode.sel === '' || oldVnode.sel === undefined) { oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode) } // 判断oldVnode和newVnode是否为同一个虚拟节点【比较key和sel】 if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) { } else { // 直接全部删除,重新创建节点 console.log('直接全部删除,重新创建节点'); let newVnodeElm = createElement(newVnode) // 将新创建的孤儿节点上树 if (oldVnode.elm.parentNode != undefined && newVnodeElm) { oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm) // 删除老节点 oldVnode.elm.remove() } } }
// 创建真实DOM,是孤儿节点不进行插入 export default function createElement (vnode) { // 创建dom节点,此时该节点还是孤儿节点 let domNode = document.createElement(vnode.sel) // 有子节点还是有文本 // 是文本节点就不用递归 if ((vnode.text != '' || vnode.text != undefined) && (vnode.children == undefined || vnode.children.length == 0)) { // 内部是文字 domNode.innerText = vnode.text } else if(Array.isArray(vnode.children) && vnode.children.length > 0){ // 内部是子节点就要递归创建 for (let child of vnode.children) { domNode.appendChild(createElement(child)) } } // 补充elm属性 vnode.elm = domNode // 返回elm, elm属性是一个纯DOM对象 return vnode.elm // return domNode }
diff处理新旧节点是同一个节点时
-
新旧节点text的不同情况
// 判断newVnode和oldVnode是否为同一个对象 if (oldVnode === newVnode) { return } // 判断newVnode是否有text属性 if ((newVnode.text != '' || newVnode.text != undefined) && (newVnode.children == undefined || newVnode.children.length == 0)) { if (newVnode.text != oldVnode.text) { // 替换成新节点的text oldVnode.elm.innerText = newVnode.text } } else { if(oldVnode.children != undefined && oldVnode.children.length > 0) { // 要比较oldVnode和newVnode的子节点【递归】 } else { oldVnode.elm.innerHTML = '' // oldVnode要添加newVnode的子节点 for(let child of newVnode.children) { oldVnode.elm.appendChild(createElement(child)) } } }
-
diff算法的子节点更新策略
经典的diff算法优化策略【四种命中查找】:
① 新前与旧前(新前就是新的虚拟DOM中未处理的开头节点)
② 新后与旧后(新后就是新的虚拟DOM中未处理的最后一个节点)
③ 新后与旧前(此种发生了,涉及移动节点,那么新后指向的节点,移动到所有未处理的旧后之后
④ 新前与旧后(此种发生了,涉及移动节点,那么新前指向的节点,移动到所有未处理的旧前之前命中一种就不再进行命中判断了
如果都没有命中,就需要用循环来寻找了。移动到oldStartIdx之前。-
新增情况
第一种方式命中查找:就比较旧前与新前,节点相同,新前与旧前指针往后移动;依次比较,旧虚拟DOM先遍历完,那么新虚拟DOM中还未遍历到的节点就是需要新增的节点
-
删除情况
-
多删除的情况
-
复杂的情况
旧虚拟DOM:ABCDE; 新虚拟DOM: ECM
新前
E 与旧前
A 比较,未命中;
新后
M 与旧后
E 比较,未命中;
新后
M 与旧前
A 比较,未命中;
新前
E 与旧后
E 比较,命中:将当前新前
指向的节点E插入到旧前
指向的节点A前面,新前++【指向C】,旧后–【指向D】【即将虚拟DOM的E节点置为undefined,移动真实节点E到A前面】;
新前
C 与旧前
A 比较,未命中;
新后
M 与旧后
D 比较,未命中;
新后
M 与旧前
A 比较,未命中;
新前
C 与旧后
D 比较,未命中;
四种命中查找都未命中,则在旧虚拟DOM中循环查找新前
指向的节点C,找到之后将旧虚拟DOM的C节点置为undefined,且移动节点到旧前
节点A前面,新前++【指向M】
再重复进行四种命中查找:
节点M未命中,进行循环查找,没有找到则将M节点插入到所有旧前
节点前;新前++
新前
>新后
退出循环
此时旧虚拟DOM还有剩余节点未遍历处理过,删除剩余节点A、B、D【即旧前
与旧后
之间的真实节点】 -
复杂的情况
-