思维导图
0. 从常见问题引入
- 虚拟dom是什么?
- 如何创建虚拟dom?
- 虚拟dom如何渲染成真是dom?
- 虚拟dom如何patch(patch)
- 虚拟DOM的优势?(性能)
- Vue中的key到底有什么用,为什么不能用index?
- Vue中的diff算法实现
- diff算法是深度还是广度优先遍历
1. 生成虚拟dom
1. h方法实现
virtual dom ,也就是虚拟节点
- 它通过js的Object对象模拟dom中的节点
- 再通过特定的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
}
}
2. render方法实现
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) {
vnode.domElement = document.createElement(type)
updateProperties(vnode)
children.forEach(childVnode => render(childVnode, vnode.domElement))
} else {
}
return vnode.domElement
}
function updateProperties(newVnode, oldProps = {}) {
let domElement = newVnode.domElement
let newProps = newVnode.props;
for (let oldPropName in oldProps) {
if (!newProps[oldPropName]) {
delete domElement[oldPropName]
}
}
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的真实节点,插入当前的真实节点里去
3. 再次渲染
刚刚可能会有点不解,为什么要把新的节点和老的节点属性进行比对,因为刚刚是首次渲染,现在讲一下二次渲染
比如说现在构建了一个新节点newNode,我们需要和老节点进行对比,然而并不是简单的替换,而是需要尽可能多地进行复用 首先判断父亲节点的类型,如果不一样就直接替换 如果一样
- 文本类型,直接替换文本值即可
- 元素类型,需要根据属性来替换
这就证明了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
}
let domElement = newVnode.domElement = oldVNode.domElement
updateProperties(newVnode, oldVNode.props)
let oldChildren = oldVNode.children
let newChildren = newVnode.children
if (oldChildren.length > 0 && newChildren.length > 0) {
} else if (oldChildren.length > 0) {
domElement.innerHTML = ''
} else if (newChildren.length > 0) {
for (let i = 0; i < newChildren.length; i++) {
let ele = createDomElementFrom(newChildren[i])
domElement.appendChild(ele)
}
}
}
2. diff算法
刚刚的渲染方法里,首先是对最外层元素进行对比,对于儿子节点,分为三种情况
- 老的有儿子,新的没儿子(那么直接把真实节点的innerHTML设置为空即可)
- 老的没儿子,新的有儿子(那么遍历新的虚拟节点的儿子列表,把每一个都利用createElementFrom方法转化为真实dom,append到最外层真实dom即可)
- 老的有儿子,新的有儿子,这个情况非常复杂,也就是我们要提及的diff算法
1. 对常见的dom做优化
以最常见的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'),
);
情况1:末尾追加一个元素(头和头相同)
新的虚拟节点
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
}
function updateChildren(parent, oldChildren, newChildren) {
let oldStartIndex = 0
let oldStartVnode = oldChildren[oldStartIndex];
let oldEndIndex = oldChildren.length - 1
let oldEndVnode = oldChildren[oldEndIndex];
let newStartIndex = 0
let newStartVnode = newChildren[newStartIndex];
let newEndIndex = newChildren.length - 1
let newEndVnode = newChildren[newEndIndex];
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (isSameVnode(oldStartVnode, newStartVnode)) {
patch(oldStartVnode, newStartVnode)
oldStartVnode = oldChildren[++oldStartIndex]
newStartVnode = newChildren[++newStartIndex]
}
}
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
parent.appendChild(createDomElementFrom(newChildren[i]))
}
}
}
情况2:队首添加一个节点(尾和尾)
头和头+尾和尾的处理方法: 我们通过parent.insertBefore(createDomElementFrom(newChildren[i]), beforeElement)使得末尾添加和头部添加采用同一种处理方法
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
let beforeElement = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].domElement parent.insertBefore(createDomElementFrom(newChildren[i]), beforeElement)
}
}
图解:
MVVM=>数据一变,就调用patch
情况3:翻转类型(头和尾)
尾和头就不画图了
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 {
情况4: 暴力比对复用
else {
let index = map[newStartVnode.key]
console.log(index);
if (index == null) {
parent.insertBefore(createDomElementFrom(newStartVnode),
oldStartVnode.domElement)
} else {
let toMoveNode = oldChildren[index]
patch(toMoveNode, newStartVnode)
parent.insertBefore(toMoveNode.domElement, oldStartVnode.domElement)
oldChildren[index] = undefined
}
newStartVnode = newChildren[++newStartIndex]
}
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
}
对于key的探讨
1. 为什么不能没有key
2. 为什么key不能是index
3. diff的遍历方式
采用的是深度优先,只会涉及到dom树同层的比较,先对比父节点是否相同,然后对比儿子节点是否相同,相同的话对比孙子节点是否相同
|