初次渲染完毕以后,就到了我们最重要的部分,更新视图时新老节点比较来更新我们的页面。
原理就是用新的虚拟节点和老的虚拟节点做对比,以此来跟新真实dom元素。
下面就是我们要实现的代码,newVnode是更新后的新虚拟节点,oldVnode是更新前的虚拟节点,将新旧虚拟节点传到patch函数中做对比,接下来就是实现一下patch函数了。
import {h,render,patch} from './vdom'
let oldVnode = h('div',{id:'container'},
h('li',{style:{background:'red'},key:'a'},'a'),
h('li',{style:{background:'yellow'},key:'b'},'b'),
h('li',{style:{background:'blue'},key:'c'},'c'),
h('li',{style:{background:'pink'},key:'d'},'d'),
);
let container = document.getElementById('app');
render(oldVnode,container);
let newVnode = h('div',{id:'aa'},
h('li',{style:{background:'pink'},key:'d'},'d'),
h('li',{style:{background:'blue'},key:'c'},'c'),
h('li',{style:{background:'yellow'},key:'b'},'b'),
h('li',{style:{background:'red'},key:'a'},'a'),
)
setTimeout(() => {
patch(oldVnode,newVnode);
}, 1000);
首先我们需要对比的是两个虚拟节点的标签是否一样,如果标签不一样就说明父级不同了,也就没有必要去判断子节点了,这种时候就直接把老的节点干掉,替换成新的节点就好了。
替换的时候要注意一下,需要拿到当前节点的父节点才能替换掉当前节点,这里还有个细节,要将新节点通过createElm方法转换为真实节点再替换。
为什么老节点不用替换成真实节点呢?因为老节点在上一次渲染的时候就已经转换成真实节点了,只需要通过 .el 属性来拿到dom元素就可以了。代码如下
// 创建真实节点
function createElm(vnode){
let {tag,children,key,props,text} = vnode;
if(typeof tag === 'string'){
// 标签 一个虚拟节点 对应着他的真实节点 主要是做一个映射关系
vnode.el = document.createElement(tag);
updateProperties(vnode);
children.forEach(child => { // child是虚拟节点
return render(child,vnode.el); // 递归渲染当前孩子列表
});
}else{
// 文本
vnode.el = document.createTextNode(text);
}
return vnode.el
}
export function patch(oldVnode,newVnode){
// 先比对 标签一样不一样
if(oldVnode.tag !== newVnode.tag){
// 必须拿到当前元素的父亲 才能替换掉自己
oldVnode.el.parentNode.replaceChild(createElm(newVnode),oldVnode.el)
}
}
接下来就是比较标签一样的情况,标签一样有一种情况是两者都为undefined,这时候也需要判断一下。两者可能都为undefined说明都没有标签,那么只需要判断老节点不存在tag属性,然后就是判断新老节点的文本内容是否一致,如果不一致,就将新节点的文本直接赋给老节点就行了。
当标签一样时,标签复用就行,但是可能属性不一样了,到这一步的时候,就要调用我们写的updateProperties方法做属性的对比了(该方法详情见上一篇)。
export function patch(oldVnode,newVnode){
// 1) 先比对 标签一样不一样
if(oldVnode.tag !== newVnode.tag){
// 必须拿到当前元素的父亲 才能替换掉自己
oldVnode.el.parentNode.replaceChild(createElm(newVnode),oldVnode.el)
}
// 2) 比较文本了 标签一样 可能都是undefined
if(!oldVnode.tag){
if(oldVnode.text !== newVnode.text){ // 如果内容不一致直接根据当前新的元素中的内容来替换到文本节点
oldVnode.el.textContent = newVnode.text;
}
}
// 标签一样 可能属性不一样了
let el = newVnode.el = oldVnode.el; // 标签一样复用即可
updateProperties(newVnode,oldVnode.props); // 做属性的比对
}
属性比较完了之后,就该比较孩子节点了。这里才是最为重要的部分。这里分成三种情况,第一种是老的有孩子,新的没有;第二种是老的没孩子,新的有;最后一种是新、老都有孩子。最后一种情况最为复杂,也是vue中最核心的代码,解决比对的问题。代码逻辑如下,实现起来也是比较简单的。
首先要拿到新老节点里的孩子节点,通过length属性判断是否存在孩子,如果新老节点的length属性都大于零就需要调用updateChildren的方法不不停递归比较,这个方法稍后会讲。其他两种情况就比较简单了就不多说了。
export function patch(oldVnode,newVnode){
// 1) 先比对 标签一样不一样
if(oldVnode.tag !== newVnode.tag){
// 必须拿到当前元素的父亲 才能替换掉自己
oldVnode.el.parentNode.replaceChild(createElm(newVnode),oldVnode.el)
}
// 2) 比较文本了 标签一样 可能都是undefined
if(!oldVnode.tag){
if(oldVnode.text !== newVnode.text){ // 如果内容不一致直接根据当前新的元素中的内容来替换到文本节点
oldVnode.el.textContent = newVnode.text;
}
}
// 标签一样 可能属性不一样了
let el = newVnode.el = oldVnode.el; // 标签一样复用即可
updateProperties(newVnode,oldVnode.props); // 做属性的比对
// 必须要有一个根节点
// 比较孩子
let oldChildren = oldVnode.children || [];
let newChildren = newVnode.children || [];
// 老、新都有孩子
if(oldChildren.length > 0 && newChildren.length > 0){
updateChildren(el,oldChildren,newChildren); // 不停的递归比较
}else if(oldChildren.length > 0){ // 老的有孩子 新的没孩子
el.innerHTML = ''
}else if(newChildren.length > 0){ // 老的没孩子 新的有孩子
for(let i = 0; i < newChildren.length ;i++){
let child = newChildren[i];
el.appendChild(createElm(child)); // 将当前新的儿子 丢到老的节点中即可
}
}
}
function isSameVnode(oldVnode,newVnode){
// 如果两个人的标签和key 一样我认为是同一个节点 虚拟节点一样我就可以复用真实节点了
return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
}
function updateChildren(parent,oldChildren,newChildren){
// vue增加了很多优化策略 因为在浏览器中操作dom最常见的方法是 开头或者结尾插入
// 涉及到正序和倒序
let oldStartIndex = 0; // 老的索引开始
let oldStartVnode = oldChildren[0]; // 老的节点开始
let oldEndIndex = oldChildren.length - 1;
let oldEndVnode = oldChildren[oldEndIndex];
let newStartIndex = 0; // 新的索引开始
let newStartVnode = newChildren[0]; // 新的节点开始
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]
}else if(isSameVnode(oldEndVnode,newEndVnode)){ // 从后面比较看是否一样
patch(oldEndVnode,newEndVnode); // 比较孩子
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
}else if(isSameVnode(oldStartVnode,newEndVnode)){
patch(oldStartVnode,newEndVnode);
parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling);
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex]
} // 老的尾巴和新的头去比 将老的尾巴移动到老的头的前面
// 还有一种情况
// 倒叙和正序
}
if(newStartIndex<=newEndIndex){ // 如果到最后还剩余 需要将剩余的插入
for(let i = newStartIndex ; i <=newEndIndex; i++){
// 要插入的元素
let ele = newChildren[newEndIndex+1] == null? null:newChildren[newEndIndex+1].el;
parent.insertBefore(createElm(newChildren[i]),ele);
// 可能是往前面插入 也有可能是往后面插入
// insertBefore(插入的元素,null) = appendChild
// parent.appendChild(createElm(newChildren[i]))
}
}
}