(1).了解diff算法特征、虚拟节点、创建项目
1.算法特征
2.虚拟节点
3.建立vdom文件
npm init -y
npm i webpack@4.44.1 webpack-cli@3.3.12 webpack-dev-server@3.11.0
yarn add html-webpack-plugin@4.4.1
const path = require('path'),
HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports={
entry:'./src/js/index.js',
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'dist')
},
devtool:'source-map',
resolve:{
modules:[path.resolve(__dirname,''),path.resolve(__dirname,'node_modules')]
},
plugins:[
new HtmlWebpackPlugin({
template:path.resolve(__dirname,'src/index.html')
})
],
// devServer:{
// contentBase:'./'
// }
}
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev":"webpack-dev-server",
"build":"webpack"
},
const vDom = createElement('ul',{
class:'list',
style:'width:300px;height:300px;background-color:#f40'
},[
createElement('li',{
class:"item",
'data-index':0
},[
createElement('p',{class:'text'},['第一个列表项'])
]),
createElement('li',{
class:"item",
'data-index':1
},[
createElement('p',{class:'text'},[
createElement('span',{class:'title'},['第二个列表项'])
])
]),
createElement('li',{
class:'item',
'data-index':2
},[
'第三个列表项'
])
])
(2).构建虚拟节点、转换真实DOM、渲染DOM节点
1.构建虚拟节点
class Element{
constructor(type,props,children){
this.type = type;
this.props = props;
this.children = children;
}
}
export default Element;
import Element from './Element';
function createElement(type,props,children){
return new Element(type,props,children)
}
export {
createElement
}
import {createElement} from './virtualDom'
const vDom = createElement('ul',{
class:'list',
style:'width:300px;height:300px;background-color:#f40'
},[
createElement('li',{
class:"item",
'data-index':0
},[
createElement('p',{class:'text'},['第一个列表项'])
]),
createElement('li',{
class:"item",
'data-index':1
},[
createElement('p',{class:'text'},[
createElement('span',{class:'title'},['第二个列表项'])
])
]),
createElement('li',{
class:'item',
'data-index':2
},[
'第三个列表项'
])
])
console.log(vDom)
2.转换真实DOM/渲染dom
import {createElement,render,renderDom} from './virtualDom'
...
const rDom = render(vDom);
renderDom(
rDom,
document.getElementById('app')
)
console.log(rDom)
virtualDom.js
import Element from './Element';
function createElement(type,props,children){
return new Element(type,props,children)
}
function setAttrs(node,prop,value){
switch(prop){
case 'value':
if(node.tagName === 'INPUT' || node.tagName=== 'TEXTAREA'){
node.value = value;
}else{
node.setAttribute(prop,value)
}
break;
case 'style':
node.style.cssText = value;
break;
default:
node.setAttribute(prop,value);
break;
}
}
function render(vDom){
const {type,props,children} = vDom,
el = document.createElement(type);
for(let key in props){
setAttrs(el,key,props[key]);
}
children.map((c)=>{
c = c instanceof Element
? render(c)
: document.createTextNode(c)
el.appendChild(c)
})
return el
}
function renderDom(el,rootEl){
rootEl.appendChild(el)
}
export {
createElement,
render,
renderDom,
setAttrs
}
(3).虚拟节点差异分析、创建补丁包
1.虚拟节点差异分析
import {createElement,render,renderDom} from './virtualDom';
import domDiff from './domDiff';
import doPatch from './doPatch';
const vDom1 = createElement('ul', {
class: 'list',
style: 'width: 300px; height: 300px; background-color: orange'
}, [
createElement('li', {
class: 'item',
'data-index': 0
}, [
createElement('p', {
class: 'text'
}, [
'第1个列表项'
])
]),
createElement('li', {
class: 'item',
'data-index': 1
}, [
createElement('p', {
class: 'text'
}, [
createElement('span', {
class: 'title'
}, [])
])
]),
createElement('li', {
class: 'item',
'data-index': 2
}, [
'第3个列表项'
])
]);
const vDom2 = createElement('ul', {
class: 'list-wrap',
style: 'width: 300px; height: 300px; background-color: orange'
}, [
createElement('li', {
class: 'item',
'data-index': 0
}, [
createElement('p', {
class: 'title'
}, [
'特殊列表项'
])
]),
createElement('li', {
class: 'item',
'data-index': 1
}, [
createElement('p', {
class: 'text'
}, [])
]),
createElement('div', {
class: 'item',
'data-index': 2
}, [
'第3个列表项'
])
]);
const rDom = render(vDom1);
renderDom(
rDom,
document.getElementById('app')
)
const patches = domDiff(vDom1,vDom2);
// doPatch(rDom,patches);
console.log(patches);
2.创建补丁包
const patches = {
0:[
{
type:'ATTR',
attr:{}
}
],
2:[
{
type:'ATTR',
attr:{}
}
],
3:[
{
type:'TEXT',
text:'特殊列表项'
}
],
6:[
{
type:'REMOVE',
index:6
}
],
7:[
{
type:'REPLACE',
newNode:newNode
}
]
}
import domDiff from './domDiff'
...
const patches = domDiff(vDom1,vDom2);
console.log(patches);
export const ATTR = 'ATTR';
export const TEXT = 'TEXT';
export const REPLACE = 'REPLACE';
export const REMOVE = 'REMOVE';
import {ATTR,TEXT,REPLACE,REMOVE} from './patchType';
let patches = {},
vnIndex = 0;
function domDiff(oldVDom,newVDom){
let index = 0;
vNodeWalk(oldVDom,newVDom,index);
return patches;
}
function vNodeWalk(oldNode,newNode,index){
let vnPatch = [];
if(!newNode){
vnPatch.push({
type:REMOVE,
index
})
}else if(typeof oldNode === 'string' && typeof newNode === 'string'){
if(oldNode !== newNode){
vnPatch.push({
type:TEXT,
text:newNode
})
}
}else if(oldNode.type === newNode.type){
const attrPatch = attrsWalk(oldNode.props,newNode.props);
if(Object.keys(attrPatch).length>0){
vnPatch.push({
type:ATTR,
attrs:attrPatch
})
}
childrenWalk(oldNode.children,newNode.children)
}else{
vnPatch.push({
type:REPLACE,
newNode:newNode
})
}
if(vnPatch.length > 0){
patches[index] = vnPatch
}
}
function attrsWalk(oldAttrs,newAttrs){
let attrPatch = {};
for(let key in oldAttrs){
if(oldAttrs[key] !== newAttrs[key]){
attrPatch[key] = newAttrs[key]
}
}
for(let key in newAttrs){
if(!oldAttrs.hasOwnProperty(key)){
attrPatch[key] = newAttrs[key];
}
}
return attrPatch;
}
function childrenWalk(oldChildren,newChildren){
oldChildren.map((c,idx)=>{
vNodeWalk(c,newChildren[idx],++vnIndex)
})
}
export default domDiff
(4).给真实DOM打补丁
import doPatch from './doPatch';
...
const rDom = render(vDom1);
renderDom(
rDom,
document.getElementById('app')
)
const patches = domDiff(vDom1,vDom2);
doPatch(rDom,patches);
console.log(patches);
import {ATTR,TEXT,REPLACE,REMOVE} from './patchType';
import {setAttrs,render} from './virtualDom'
import Element from './Element';
let finalPatches = {},
rnIndex = 0;
function doPatch(rDom,patches){
finalPatches = patches;
rNodeWalk(rDom);
}
function rNodeWalk(rNode){
const rnPatch = finalPatches[rnIndex++],
childNodes = rNode.childNodes;
[...childNodes].map((c)=>{
rNodeWalk(c);
})
if(rnPatch){
patchAction(rNode,rnPatch);
}
}
function patchAction(rNode,rnPatch){
rnPatch.map((p)=>{
switch(p.type){
case ATTR:
for(let key in p.attrs){
const value = p.attrs[key];
if(value){
setAttrs(rNode,key,value)
}else{
rNode.removeAttribute(key)
}
}
break;
case TEXT:
rNode.textContent = p.text;
break;
case REPLACE:
const newNode = (p.newNode instanceof Element)
? render(p.newNode)
:document.createTextNode(p.newNode)
rNode.parentNode.replaceChild(newNode,rNode);
break;
case REMOVE:
rNode.parentNode.removeChild(rNode)
break;
default:
break;
}
})
}
export default doPatch;
(5).流程总结
接下来判断两个虚拟dom的不同从而创建补丁包,最后将补丁包打入即可
首先通过函数创建的形式将dom元素相关信息以对象和数组的形式传入,
通过类的实例化将传入的dom元素信息对象化,此时就构成了虚拟节点
接下来通过函数判断传入的第二个参数的类型是style、class、value,或者是data-
如果是value则要判断标签是否为input或textarea,如果是则通过.value设置,不是则通过setAttribute进行设置,如果是style则通过.style.cssText进行设置,其他剩下的也都已setAttrbute进行设置
处理完标签参数后 循环第三个参数children,判断是否为类构建,如果是则进行递归处理,不是则证明只是普通文本,通过createTextNode进行创建即可
此时只需要将处理好的dom节点挂载在指定dom节点上即可完成虚拟dom转换真实dom然后渲染节点
差异比较时会深度比较而不是广度比较,有标签的删除 替换 修改 新增、以及标签参数变更的操作,其中特殊的两个就是
替换不需要删除而是直接替换发生索引移步,新增会出现索引增加等情况。在比较差异的时候就会创建对象形式的补丁包,其中记录了操作类型,具体操作值与索引,这就是diff算法,具体操作流程如下
如果new/old dom类型都为字符串且文本不相同则创建文本变更补丁,
如果new/olddom的标签相同且通过循环判断出标签中的参数有增加或修改则创建参数变更的补丁
其中在发生标签相同的情况下还要将子节点传入这个函数递归进行处理
补丁类型如果是标签变更,有变更值则通过函数重新给dom节点赋值参数内容,如果没有则删除
补丁类型如果是文本变更,则通过dom元素.textContent直接替换
补丁类型如果是替换补丁,则判断是否为element构建,如果是则创建dom替换,如果不是则为普通文本,通过createTextNode创建