携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情
这期讲重渲染后生成的新VNode树,和旧树对比去更新dom视图,也就是广义的diff算法,patch函数
patch流程
新节点不存在:删除旧节点
比对:
类型不同 :删除旧节点,挂载新节点
相同类型:若旧节点不存在则渲染新节点,存在则更新
[属性不同:替换更新] [子节点不同:
patchChildren]
改造render函数
使之拥有旧节点值,便于比对
export function render(vnode, container) {//传入新生成的vnode
// 获取容器上的旧vnode
const prevVNode = container._vnode;
//没有新节点,说明删了
if (!vnode) {
if (prevVNode) {
unmount(prevVNode);
}
} else {
// 两者都有,比对
patch(prevVNode, vnode, container);
}
// 渲染后 将vode存放在容器上
container._vnode = vnode;
}
简单流程
function patch(n1, n2, container, anchor) {
if (n1 && !isSameVNodeType(n1, n2)) {
unmount(n1);
n1 = null;
}
// 先不看anchor
const { shapeFlag } = n2;
if (shapeFlag & ShapeFlags.ELEMENT) {
// 元素节点 比较
processElement(n1, n2, container, anchor);
} else if (shapeFlag & ShapeFlags.TEXT) {
processText(n1, n2, container, anchor);
} else if (shapeFlag & ShapeFlags.FRAGMENT) {
processFragment(n1, n2, container, anchor);
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container, anchor);
}
}
function processElement(n1, n2, container, anchor) {
// 旧节点不存在,则渲染新节点
if (n1 == null) {
mountElement(n2, container, anchor);
} else {
//存在,则替换更新
patchElement(n1, n2);
}
}
// 替换更新属性 子节点
function patchElement(n1, n2) {
n2.el = n1.el;
patchProps(n2.el, n1.props, n2.props);
patchChildren(n1, n2, n2.el);
}
patchProps
和上一章的渲染属性类似,只是这次加入比较区别
传入的属性是对象,需要遍历新传入的对象属性,和旧的值比较;再遍历旧的值,如果其中在新对象中不存在,则要去元素中把相应的属性给删掉
export function patchProps(el, oldProps, newProps) {
if (oldProps === newProps) {
return;
}
// 可能为null,null就不能赋值默认参数了,undefined才有效
oldProps = oldProps || {};
newProps = newProps || {};
// 对象,所以赋值新属性覆盖,去掉老属性中新属性没的属性。
for (const key in newProps) {
if (key === 'key') {
continue;
}
const prev = oldProps[key];
const next = newProps[key];
if (prev !== next) {
patchDomProp(el, key, prev, next);
}
}
for (const key in oldProps) {
if (key !== 'key' && !(key in newProps)) {
patchDomProp(el, key, oldProps[key], null);
}
}
}
const domPropsRE = /[A-Z]|^(value|checked|selected|muted|disabled)$/;
function patchDomProp(el, key, prev, next) {
switch (key) {
case 'class':
// 暂时认为class就是字符串
// next可能为null,会变成'null',因此要设成''
el.className = next || '';
break;
case 'style':
// style为对象,如果样式删除
if (!next) {
el.removeAttribute('style');
} else {
// 对象,所以赋值新样式覆盖,去掉老样式中新样式没的属性。
for (const styleName in next) {
el.style[styleName] = next[styleName];
}
if (prev) {
for (const styleName in prev) {
if (next[styleName] == null) {
el.style[styleName] = '';
}
}
}
}
break;
default:
if (/^on[^a-z]/.test(key)) {
// 事件
if (prev !== next) {
const eventName = key.slice(2).toLowerCase();
if (prev) {
el.removeEventListener(eventName, prev);
}
if (next) {
el.addEventListener(eventName, next);
}
}
} else if (domPropsRE.test(key)) {
if (next === '' && typeof el[key] === 'boolean') {
next = true;
}
el[key] = next;
} else {
// 例如自定义属性{custom: ''},应该用setAttribute设置为<input custom />
// 而{custom: null},应用removeAttribute设置为<input />
if (next == null || next === false) {
el.removeAttribute(key);
} else {
el.setAttribute(key, next);
}
}
break;
}
}
patchChildren
新节点若是文本节点,和旧类型一样则更新内容,不一样则
unmount旧节点新节点若是数组节点,旧节点是文本则删除渲染新,一样类型则继续
patch,旧节点不存在则挂载新节点若不存在,删除旧节点
vue 源码的
patchChildren 结构
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1);
}
if (c2 !== c1) {
container.textContent = c2;
}
} else {
// c2 is array or null
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// c1 was array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// c2 is array
// patchArrayChildren()
} else {
// c2 is null
unmountChildren(c1);
}
} else {
// c1 was text or null
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
container.textContent = '';
}
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(c2, container, anchor);
}
}
}
其中 unmount代码,删除不是简单 remove就行,因为 fragment加了两个空白文本节点
function unmount(vnode) {
const { shapeFlag, el } = vnode;
if (shapeFlag & ShapeFlags.COMPONENT) {
unmountComponent(vnode);
} else if (shapeFlag & ShapeFlags.FRAGMENT) {
unmountFragment(vnode);
} else {
el.parentNode.removeChild(el);
}
}
function unmountFragment(vnode) {
// eslint-disable-next-line prefer-const
// el 首节点,详情看下面的内容
let { el: cur, anchor: end } = vnode;
while (cur !== end) {
const next = cur.nextSibling;
cur.parentNode.removeChild(cur);
cur = next;
}
end.parentNode.removeChild(end);
}
patchArrayChildren
下期讲
Fragment 的问题
render(
h('ul', null, [
h('li', null, 'first'),
h(Fragment, null, []),
h('li', null, 'last'),
]),
document.body
);
setTimeout(() => {
render(
h('ul', null, [
h('li', null, 'first'),
h(Fragment, null, [h('li', null, 'middle')]),
h('li', null, 'last'),
]),
document.body
);
}, 2000);
middle 被放在了最后面。
原因是在 mountElement 中,使用了 container.appendChild
所以要添加 anchor属性
anchor 是 Fragment 的专有属性
在 Fragment 位置 前后加入空的文本节点,占位,保证不会把元素插错位置
function processFragment(n1, n2, container, anchor) {
const fragmentStartAnchor = (n2.el = n1
? n1.el
: document.createTextNode(''));
const fragmentEndAnchor = (n2.anchor = n1
? n1.anchor
: document.createTextNode(''));
// fragment没有el,新搞el作为第一个空白文本节点,anchor表示尾节点
if (n1 == null) {
// 表示新节点多了个fragment,插入两个空白文本节点占位,一开始anchor为空,相当于appendChild
container.insertBefore(fragmentStartAnchor, anchor);
container.insertBefore(fragmentEndAnchor, anchor);
// 这样子 挂载时就插入到 第二个空白节点前,就不会插入到父元素最后的元素去了
mountChildren(n2.children, container, fragmentEndAnchor);
} else {
patchChildren(n1, n2, container, fragmentEndAnchor);
}
}
function mountChildren(children, container, anchor) {
children.forEach((child) => {
patch(null, child, container, anchor);
// 因为n1 == null,走 mountElement(n2, container, anchor);
});
}
还有就是 旧节点换成fragment节点时(或fragment换成普通节点),anchor的位置很重要
h1, "" h1 "", h1
h1, h2, h1
anchor等于最后一个h1,把h2插入h1前。
function patch(n1, n2, container, anchor) {
if (n1 && !isSameVNodeType(n1, n2)) {
// n1被卸载后,n2将会创建,因此anchor至关重要。需要将它设置为n1的下一个兄弟节点
anchor = (n1.anchor || n1.el).nextSibling;
。。。
}
。。。
}
function processFragment(n1, n2, container, anchor) {
const fragmentEndAnchor = (n2.anchor = n1
? n1.anchor
: document.createTextNode(''));
if (n1 == null) {
。。。
} else {
patchChildren(n1, n2, container, fragmentEndAnchor);
}
}
function patchChildren(n1, n2, container, anchor) {
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(c2, container, anchor);
}
}