虚拟dom实现
通过转换为对象去处理
定义类型
// 节点类型
const vnodeType = {
HTML: 'HTML',
TEXT: 'TEXT',
COMPONENT: 'COMPONENT',
CLASS_COMPONENT: 'CLASS_COMPONENT',
};
// 子节点的个数
const childType = {
EMPTY: 'EMPTY',
SINGLE: 'SINGLE',
MULTIPLE:"MULTIPLE"
}
创建虚拟dom
- 用对象去存储虚拟dom,(标签,属性,子元素)
- 返回值代表的是存储本节点的属性
// create vdom
function createElement(tag, data, children) {
let flag;
if(typeof tag == 'string'){
// 普通标签
flag = vnodeType.HTML;
}else if(typeof tag == 'function'){
flag = vnodeType.COMPONENT;
}else {
flag = vnodeType.TEXT;
}
let childrenFlag;
if(children == null){
childrenFlag = childType.EMPTY;
}else if(Array.isArray(children)){ // 元素
let len = children.length;
if(len ==0){
childrenFlag = childType.EMPTY;
}else {
childrenFlag = childType.MULTIPLE;
}
}else {
// 认为是文本
childrenFlag = childType.SINGLE;
children = createTextVnode(children + '');
}
return {
flag, // vnode 类型
tag, // 标签
data, // 属性
key:data && data.key, // 唯一关键字
children, // 子元素
childrenFlag, // 标识子元素的个数
el:null, // 存放子元素的dom
}
}
渲染
if(container.vnode){
// 更新
patch(container.vnode, vnode, container )
}else { //首次
mount(vnode, container);
}
container.vnode = vnode;// 为了判断是二次渲染
首次挂载
function mount(vnode, container, flagNode){
let {flag} = vnode;
if(flag == vnodeType.HTML){ // 普通标签
mountElement(vnode, container, flagNode);
}else if(flag == vnodeType.TEXT){ // 文本标签
mountText(vnode, container);
}
}
元素挂载
* 属性的复制
- 子元素的挂载
function mountElement(vnode, container, flagNode){
let dom = document.createElement(vnode.tag);
vnode.el = dom;
let { data, children, childrenFlag } = vnode;
//挂载 data属性
if(data) {
for(let key in data) {
// 节点 名字 老值, 新值
patchDate(dom, key, null, data[key]);
}
}
if(childrenFlag !== childType.EMPTY){ // 挂载子元素
if(childrenFlag == childType.SINGLE){
mount(children, dom);
}else if(childrenFlag == childType.MULTIPLE) {
for(let i=0; i<children.length; i++){
mount(children[i], dom);
}
}
}
flagNode? container.insetBefore(dom, flagNode) : container.appendChild(dom);
}
文本的挂载
function mountText(vnode, container) {
let dom = document.createTextNode(vnode.children);
container.appendChild( dom );
}
更新
/**
*
* 元素的更新
*
* @param { 先前渲染的节点 } prev
* @param { 当前节点 } next
* @param { 容器 } container
*/
function patch(prev, next, container) {
console.log(prev, next, container);
let nextFlag = next.flag;
let prevFlag = next.flag;
if(nextFlag !== prevFlag){ // pre 是text next是p 直接替换
replaceVnode(prev, next, container);
}else if(nextFlag == vnodeType.HTML){ // 同为元素节点
patchElement(prev, next, container);
}else if(nextFlag == vnodeType.TEXT) {
// patchText(prev, next);
}
}
元素的更新
- 这里分很多情况去处理
-
老的是单独的 老的是多个 老的是空的
-
新的是单独的 新的是空的 新的是多个
-
function patchChildren(
prevChildrenFlag,
nextChildrenFlag,
prevChildren,
nextChildren,
container, //当前的容器
) {
//更新子元素
/**
* 1. 老的是单独的
* 老的是多个
* 老的是空的
*
* 2. 新的是单独的
* 新的是空的
* 新的是多个
*/
switch(prevChildrenFlag) {
case childType.SINGLE :
switch(nextChildrenFlag) {
case childType.SINGLE:
patch(prevChildren, nextChildren, container);
break;
case childType.EMPTY:
container.removeChild(prevChildren);
break;
case childType.MULTIPLE:
container.removeChild(prevChildren.el);
for(let i=0; i<nextChildren.length;i++){
mount(nextChildren[i], container);
}
break;
}
break;
case childType.EMPTY:
switch(nextChildrenFlag) {
case childType.SINGLE :
console.log(nextChildren, container);
console.log('+++');
mount(nextChildren, container);
break;
case childType.EMPTY :
break;
case childType.MULTIPLE :
for(let i=0; i<nextChildren.length;i++){
mount(nextChildren[i], container);
}
break;
}
break;
case childType.MULTIPLE:
switch(nextChildrenFlag) {
case childType.SINGLE :
for(let i=0; i<prevChildren.length;i++){
container.removeChild(prevChildren[i].el);
}
mount(nextChildren, container);
break;
case childType.EMPTY :
for(let i=0; i<prevChildren.length;i++){
container.removeChild(prevChildren[i].el);
}
break;
case childType.MULTIPLE :
let lastIndex = 0;
for(let i=0;i<nextChildren.length; i++) {
let nextVnode = nextChildren[i];
let j = 0;
let find = false;
for(;j<prevChildren.length;j++) {
// key 相同, 认为同一个元素
let prevVnode = prevChildren[j];
if(prevVnode.key === nextVnode.key) {
find = true;
patch(prevVnode, nextVnode, container);
if(j< lastIndex) {
// 需要移动
// abc a移动到b之后
let flagNode = nextChildren[i-1].el.nextSibling;
// console.log(container);
// console.log('===')
container.insertBefore(prevVnode.el, flagNode);
}else {
lastIndex = j;
}
}
}
if(!find){ // 需要新增
let flagNode = i==0 ? prevChildren[0].el : nextChildren[i-1].el.nextSibling;
mount(nextVnode, container, flagNode);
}
}
// 移除不需要的元素
for(let i=0;i<prevChildren.length; i++){
const prevVnode = prevChildren[i];
const has = nextChildren.find(next=> next.key == prevVnode.key)
if(!has){
container.removeChild(prevVnode.el);
}
}
break;
}
break;
}
}
完整代码
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>虚拟dom</title>
<script src="./vdom.js"></script>
<style>
.item {
font-size: 50px;
color: orange;
}
</style>
</head>
<body>
<div id="app">
</div>
<script>
let vnode = createElement('div', {id: 'test'},[
createElement('p', {key: 'a', style:{color: 'blue'}}, '节点1'),
createElement('p', {key: 'b', onClick:()=>console.log('alert') }, '节点2'),
createElement('p', {key: 'c', class:"item"}, '节点3'),
createElement('p', {key: 'd'}, '节点4'),
]);
let vnode1 = createElement('div', {id: 'test'},[
createElement('p', {key: 'd'}, '节点4'),
createElement('p', {key: 'a', style: {color: 'blue'}}, '节点1'),
createElement('p', {key: 'b'}, '节点2'),
createElement('p', {key: 'e'}, '节点5'),
createElement('p', {key: 'f', style: {color: 'gray', 'font-size': '100px'}}, '节点6'),
]);
console.log(vnode1)
// console.log( JSON.stringify(div, null, 2) );
render(vnode, document.getElementById('app'));
setTimeout(()=>{
render(vnode1, document.getElementById('app'));
},1000)
</script>
</body>
</html>
vdom.js
const vnodeType = {
HTML: 'HTML',
TEXT: 'TEXT',
COMPONENT: 'COMPONENT',
CLASS_COMPONENT: 'CLASS_COMPONENT',
};
const childType = {
EMPTY: 'EMPTY',
SINGLE: 'SINGLE',
MULTIPLE:"MULTIPLE"
}
// create vdom
function createElement(tag, data, children) {
let flag;
if(typeof tag == 'string'){
// 普通标签
flag = vnodeType.HTML;
}else if(typeof tag == 'function'){
flag = vnodeType.COMPONENT;
}else {
flag = vnodeType.TEXT;
}
let childrenFlag;
if(children == null){
childrenFlag = childType.EMPTY;
}else if(Array.isArray(children)){ // 元素
let len = children.length;
if(len ==0){
childrenFlag = childType.EMPTY;
}else {
childrenFlag = childType.MULTIPLE;
}
}else {
// 认为是文本
childrenFlag = childType.SINGLE;
children = createTextVnode(children + '');
}
return {
flag, // vnode 类型
tag,
data,
key:data && data.key,
children,
childrenFlag,
el:null, // 存放子元素的dom
}
}
// 节点类型不一致 直接替换
function replaceVnode(prev, next, container) {
container.removeChild(prev.el);
mount(next, container);
}
function patchText(prev, next) {
let el = (next.el = prev.el);
if(next.children !== prev.children){
el.nodeValue = next.children;
}
}
function patchChildren(
prevChildrenFlag,
nextChildrenFlag,
prevChildren,
nextChildren,
container, //当前的容器
) {
//更新子元素
/**
* 1. 老的是单独的
* 老的是多个
* 老的是空的
*
* 2. 新的是单独的
* 新的是空的
* 新的是多个
*/
switch(prevChildrenFlag) {
case childType.SINGLE :
switch(nextChildrenFlag) {
case childType.SINGLE:
patch(prevChildren, nextChildren, container);
break;
case childType.EMPTY:
container.removeChild(prevChildren);
break;
case childType.MULTIPLE:
container.removeChild(prevChildren.el);
for(let i=0; i<nextChildren.length;i++){
mount(nextChildren[i], container);
}
break;
}
break;
case childType.EMPTY:
switch(nextChildrenFlag) {
case childType.SINGLE :
console.log(nextChildren, container);
console.log('+++');
mount(nextChildren, container);
break;
case childType.EMPTY :
break;
case childType.MULTIPLE :
for(let i=0; i<nextChildren.length;i++){
mount(nextChildren[i], container);
}
break;
}
break;
case childType.MULTIPLE:
switch(nextChildrenFlag) {
case childType.SINGLE :
for(let i=0; i<prevChildren.length;i++){
container.removeChild(prevChildren[i].el);
}
mount(nextChildren, container);
break;
case childType.EMPTY :
for(let i=0; i<prevChildren.length;i++){
container.removeChild(prevChildren[i].el);
}
break;
case childType.MULTIPLE :
let lastIndex = 0;
for(let i=0;i<nextChildren.length; i++) {
let nextVnode = nextChildren[i];
let j = 0;
let find = false;
for(;j<prevChildren.length;j++) {
// key 相同, 认为同一个元素
let prevVnode = prevChildren[j];
if(prevVnode.key === nextVnode.key) {
find = true;
patch(prevVnode, nextVnode, container);
if(j< lastIndex) {
// 需要移动
// abc a移动到b之后
let flagNode = nextChildren[i-1].el.nextSibling;
// console.log(container);
// console.log('===')
container.insertBefore(prevVnode.el, flagNode);
}else {
lastIndex = j;
}
}
}
if(!find){ // 需要新增
let flagNode = i==0 ? prevChildren[0].el : nextChildren[i-1].el.nextSibling;
mount(nextVnode, container, flagNode);
}
}
// 移除不需要的元素
for(let i=0;i<prevChildren.length; i++){
const prevVnode = prevChildren[i];
const has = nextChildren.find(next=> next.key == prevVnode.key)
if(!has){
container.removeChild(prevVnode.el);
}
}
break;
}
break;
}
}
function patchElement(prev, next, container) {
if(prev.tag !== next.tag){ // pre 是div next是p 直接替换
replaceVnode(prev, next, container);
return ;
}
let el = (next.el = prev.el);
let prevData = prev.data;
let nextData = next.data;
if(nextData) {
for(let key in nextData) {
let prevVal = prevData[key];
let nextVal = nextData[key];
patchDate(el, key, prevVal, nextVal);
}
}
if(prevData) {
for(let key in prevData) {
let prevVal = prevData[key];
if(prevVal && !nextData.hasOwnProperty(key)){
patchDate(el, key, prevVal, null);
}
}
}
//更新完毕 更新子元素
patchChildren(
prev.childrenFlag,
next.childrenFlag,
prev.children,
next.children,
el, //当前的容器
);
}
/**
*
* 元素的更新
*
* @param { 先前渲染的节点 } prev
* @param { 当前节点 } next
* @param { 容器 } container
*/
function patch(prev, next, container) {
console.log(prev, next, container);
let nextFlag = next.flag;
let prevFlag = next.flag;
// pre 是text next是p 直接替换
if(nextFlag !== prevFlag){
replaceVnode(prev, next, container);
}else if(nextFlag == vnodeType.HTML){
patchElement(prev, next, container);
}else if(nextFlag == vnodeType.TEXT) {
// patchText(prev, next);
}
}
/**
* 虚拟dom
* 父容器
* */
function render(vnode, container) {
// 首次渲染和再次渲染
if(container.vnode){
// 更新
patch(container.vnode, vnode, container )
}else { //首次
mount(vnode, container);
}
container.vnode = vnode;
}
// 首次挂在元素
function mount(vnode, container, flagNode){
let {flag} = vnode;
if(flag == vnodeType.HTML){ // 普通标签
mountElement(vnode, container, flagNode);
}else if(flag == vnodeType.TEXT){
mountText(vnode, container);
}
}
function patchDate(el, key, prev, next) {
switch(key) {
case 'style':
for(let k in next) {
el.style[k] = next[k];
}
for(let k in prev) { //删除没有出现的
if(!next.hasOwnProperty(k)) {
el.style[k] = '';
}
}
break;
case 'class':
el.className = next;
break;
default:
if(key === 'onClick'){
if(prev) {
el.removeEventListener(key.slice(2).toLocaleLowerCase(), prev);
}
if(next){
el.addEventListener(key.slice(2).toLocaleLowerCase(), next);
}
}else {
el && el.setAttribute(key, next);
}
break;
}
}
function mountElement(vnode, container, flagNode){
let dom = document.createElement(vnode.tag);
vnode.el = dom;
let { data, children, childrenFlag } = vnode;
//挂载 data属性
if(data) {
for(let key in data) {
// 节点 名字 老值, 新值
patchDate(dom, key, null, data[key]);
}
}
if(childrenFlag !== childType.EMPTY){ // 挂载子元素
if(childrenFlag == childType.SINGLE){
mount(children, dom);
}else if(childrenFlag == childType.MULTIPLE) {
for(let i=0; i<children.length; i++){
mount(children[i], dom);
}
}
}
flagNode? container.insetBefore(dom, flagNode) : container.appendChild(dom);
}
function mountText(vnode, container) {
let dom = document.createTextNode(vnode.children);
container.appendChild( dom );
}
// 文本类型的vnode
function createTextVnode(text){
return {
flag: vnodeType.TEXT,
tag: null,
data: null,
children: text,
childrenFlag: childType.EMPTY,
}
}