概述:根据vnode生成真实节点el,给真实节点添加属性,遍历children生成子节点,将子节点插入到父节点中,将el插入到html页面中,比较新旧vnode和新旧子vnode的差异,得到补丁(差异对象),遍历每个补丁,根据补丁,给对应旧节点进行更新。
一。VNode部分
第一步,通过createElement方法生成vnode(虚拟节点)
- ul为元素的名称
- class为元素的class属性
- []为子元素列表
let vnode = createElement('ul', {
class: 'list'
}, [
createElement('li', {
class: 'item'
}, ['a']),
createElement('li', {
class: 'item'
}, ['a']),
createElement('li', {
class: 'item'
}, ['a'])
]);
复制代码
第二步,通过定义createElement方法,返回一个VNode的实例化对象
- type为元素的名称
- props为元素的属性
- children为子元素列表
function createElement(type, props, children) {
return new VNode(type, props, children);
}
复制代码
第三步,定义VNode类
class VNode {
constructor(type, props, children) {
this.type = type;
this.props = props;
this.children = children;
}
}
复制代码
第四步,打印vnode,即可得到一个vnode对象
console.log(vnode);
打印结果:
Element {type: "ul", props: {…}, children: Array(3)}
children: (3) [Element, Element, Element]
props: {class: "list"}
type: "ul"
复制代码
第五步,定义render函数,生成真实节点,调用setAttri方法为真实节点添加属性:
function render(vnode) {
//生成节点
let el = document.createElement(vnode.type);
for (let key in vnode.props) {
setAttri(el, key, vnode.props[key]);
}
return el;
}
复制代码
第六步,定义setAttri方法,遍历属性对象,为节点添加属性:
function setAttri(node, key, value) {
switch (key) {
//如果属性为value
case 'value':
//判断node是否是一个input或者textarea元素
if (node.targetName.toUpperCase() === 'INPUT' || node.targetName.toUpperCase() === 'TEXTAREA') {
node.value = value;
} else {
node.setAttribute(key, value);
}
break;
//如果属性为style
case 'style':
node.style.cssText = value;
break;
//其他普通属性
default:
node.setAttribute(key, value);
break;
}
}
复制代码
第七步,此时打印el:
let el = render(vnode);
console.log(el);
打印可得:
<ul class="list" style="width: 100%;"></ul>
复制代码
第八步,在render函数中,遍历children,生成子节点:
vnode.children.forEach(child => {
//判断child是否是vnode,是则调用render函数生成vnode,不是则生成文本节点
child = (child instanceof VNode) ? render(child) : document.createTextNode(child);
el.appendChild(child);
});
复制代码
打印el可得:
<ul class="list" style="width: 100%;">
<li class="item">a</li>
<li class="item">a</li>
<li class="item">a</li>
</ul>
复制代码
第九步,定义mount方法,将el插入到html页面中:
let el = render(vnode);
mount(el,document.querySelector("#app"));
复制代码
mount方法的定义
//将通过vnode生成的el节点插入到页面中,target为页面中的元素
function mount(el, target){
target.appendChild(el);
}
复制代码
二。diff算法部分一
第一步,定义两个vnode,newVnode和oldVnode
let oldVnode = createElement('ul', {
class: 'list',
style: 'width:100%'
}, [
createElement('li', {
class: 'item'
}, ['a']),
createElement('li', {
class: 'item'
}, ['b']),
createElement('li', {
class: 'item'
}, ['c'])
]);
let newVnode = createElement('ul', {
class: 'list-group'
}, [
createElement('li', {
class: 'item'
}, ['A']),
createElement('li', {
class: 'item'
}, ['B']),
createElement('li', {
class: 'item'
}, ['C'])
]);
复制代码
第二步,调用diff方法比较新旧vnode的差异,得到差异对象:
let patchs = diff(oldVnode, newVnode);
第三步,定义diff方法,在diff方法中定义补丁对象存储补丁,调用walk方法进行递归比较新老vnode,将结果放到补丁包中:
- 比较规则:
- 节点类型相同,看属性是否相同,产生一个属性的补丁包:{type: 'ATTRS', attrs: {class: 'list-group'}}
- 节点被删了,产生一个节点被删除的补丁包:{type: 'REMOVE', index: xxx}
- 节点类型不同,产生一个被替换的补丁包:{type: 'REPLACE', newNode: newNode}
- 文本的变化,产生一个文本变化的补丁包:{type: 'TEXT', text:1}
function diff(oldVnode, newVnode) {
//补丁对象
let patches = {};
//比较的第一个元素的下标
let index = 0;
//定义walk方法进行递归比较新老vnode,将结果放到补丁包中
walk(oldVnode, newVnode, index, patches);
return patches;
}
复制代码
第四步,定义walk方法,比较新旧节点,获取补丁:
//节点变化的四种标识
const ATTRS = 'ATTRS';
const TEXT = 'TEXT';
const REMOVE = 'REMOVE';
const REPLACE = 'REPLACE';
//节点序号
let Index = 0;
//判断节点是否是字符串(文本节点)
function isString(node) {
return Object.prototype.toString.call(node) === '[object String]';
}
function walk(oldVnode, newVnode, index, patches) {
let currenPatch = [];// 每个元素都有一个补丁对象
//情况一:节点被删了
if(!newVnode){
currenPatch.push({type: REMOVE, index});
}
//情况二:判断是否是文本节点,且文本节点是否相同
else if(isString(oldVnode) && isString(newVnode)) {
if(oldVnode !== newVnode){
currenPatch.push({type: TEXT, text: newVnode});
}
}
//情况三:判断节点类型是否相同,相同则调用diffAttr方法比较属性是否相同,返回一个attrs属性差异对象
else if (oldVnode.type === newVnode.type) {
//比较属性是否有更改
let attrs = diffAttr(oldVnode.props, newVnode.props);
//判断是否有属性更改
if(Object.keys(attrs).length > 0){
currenPatch.push({type: ATTRS, attrs});
}
//遍历子vnode
diffChildren(oldVnode.children, newVnode.children, patches);
}
//情况四:说明节点被替换了
else {
currenPatch.push({type: REPLACE, newVnode});
}
//得到第一个newVnode和第一个oldVnode的差异对象,存入到patches中
if(currenPatch.length > 0){
//将元素和补丁建立对应关系
patches[index] = currenPatch;
}
}
复制代码
第五步,定义diffAttr方法,比较新旧vnode的属性的差异,返回属性差异对象:
//比较新旧vnode的属性的差异,返回属性差异对象
function diffAttr(oldAttrs, newAttrs) {
let patch = {};
//遍历旧节点属性,比较和新节点的属性的区别
for (let key in oldAttrs) {
if(oldAttrs[key] !== newAttrs[key]){
patch[key] = newAttrs[key];//有可能是undefined,因为新节点中没有这个属性了
}
}
//当旧vnode没有新vnode中的新属性的情况
for (let key in newAttrs) {
if(!oldAttrs.hasOwnProperty(key)){
patch[key] = newAttrs[key];//有可能是undefined,因为新节点中没有这个属性了
}
}
return patch;
}
复制代码
第八步,定义diffChildren,比较子vnode的区别
//遍历新老子vnode,比较子vnode的区别
function diffChildren(oldChildren, newChildren, patches){
// 比较旧vnode的第一个子vnode和新的第一个
oldChildren.forEach((child, _index) => {
//下标累加
walk(child, newChildren[_index], ++Index, patches);
})
}
复制代码
第九步,给el打补丁
//给元素打补丁,重新更新视图
patch(el, patches);
复制代码
第十步,定义patch方法,调用work给节点打补丁
//补丁对象
let allPatches;
//补丁初始化下标
let index = 0;
function patch(node, patches){
allPatches = patches;
//给节点打补丁
work(node);
}
复制代码
第十一步,定义work方法,获取当前补丁,调用doPatch方法给当前元素打补丁,递归子节点,给子节点打补丁
//获取当前补丁,调用doPatch方法给当前元素打补丁,递归子节点,给子节点打补丁
function work(node){
//当前补丁
let currenPatch = allPatches[index++];
//子节点
let childNodes = node.childNodes;
//遍历子节点
childNodes.forEach(child => {
//给子节点打补丁
work(child);
})
//当前补丁存在,则根据补丁更新当前节点
if(currenPatch){
doPatch(node, currenPatch);
}
}
复制代码
第十二步,定义doPatch方法给元素打补丁:
//遍历当前补丁数组,判断补丁类型,根据不同的补丁类型给节点打补丁
function doPatch(node, patches){
patches.forEach(patch=>{
switch (patch.type) {
//属性更改类型的补丁
case 'ATTRS':
for(let key in patch.attrs){
let value = patch.attrs[key];
if(value) {
//添加属性
setAttri(node, key, value);
}
else {
//删除属性
node.removeAttribute(key);
}
}
break;
//文本类型的补丁,替换文本内容
case 'TEXT':
node.textContent = patch.text;
break;
//节点替换类型
case 'REPLACE':
let newNode = (patch.newVnode instanceof VNode) ? render(patch.newVnode) : document.createTextNode(patch.newVnode);
node.parentNode.replaceChild(newNode, node);
break;
//节点被删除了
case 'REMOVE':
node.parentNode.removeChild(node);
break;
}
})
}
复制代码
第十三步,测试结果:小写abc被大写ABC替换。
三。diff算法部分二(平级元素互换位置与新增节点更新):
待续。。。。
学习来源:
- https://www.bilibili.com/video/av31174086/