# vue-diff **Repository Path**: gaotengjun/vue-diff ## Basic Information - **Project Name**: vue-diff - **Description**: 部分diff算法 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-05-01 - **Last Updated**: 2024-12-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: Vue, diff ## README ## 1. 初始化项目 ```json { "name": "diff1", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "webpack-dev-server", "webpack": "webpack" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "html-webpack-plugin": "^4.5.0", "webpack": "^4.30.0", "webpack-cli": "^3.3.0", "webpack-dev-server": "^3.7.2" } } ``` ## 2. webpack配置 ```javascript const { resolve } = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: 'development', entry: resolve(__dirname, 'src/js/index.js'), output: { path: resolve(__dirname, 'dist'), filename: 'bundle.js' }, devtool: 'source-map', plugins: [ new HtmlWebpackPlugin({ template: resolve(__dirname, 'src/index.html'), }) ], devServer: { open: true, contentBase: './', // 设置静态文件目录 } } ``` ## 3. 初始化Element类 ```javascript class Element { constructor(type, props, children) { this.type = type; this.props = props; this.children = children; } } export default Element; ``` ## 4. 虚拟dom操作 1. 使用createElement方法创建出虚拟dom 1. 调用render函数渲染虚拟dom,转换成真实dom 1. 根据虚拟dom中的type类型创建出元素标签 1. 遍历属性为标签设置属性 1. 元素设置属性注意value属性和style属性 1. value 1. input标签和textarea标签是直接赋值value属性为value值 1. 其他标签使用setAttribute设置value属性值 2. style 1. 样式使用的是node.style.cssText设置样式字符串属性 3. 其他属性都是使用setAttribute设置属性值 3. 遍历children,递归调用render渲染 1. 判断children数组中每个元素是否是由Element构建,也就是虚拟dom 1. 是则再次使用render函数调用将虚拟dom转换成真实dom 1. 不是则创建文本节点 1. 将每个children元素添加到el标签中 4. 最后返回真实dom元素 ```javascript import Element from "./Element"; function createElement(type, props, children) { return new Element(type, props, children); } // 设置属性 function setAttrs(node, prop, value) { switch (prop) { case 'value': // input 和 textarea 设置value值是直接点value,其他标签才需要设置attr 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(var key in props) { // 就是为 el 元素 设置属性为 key 值为 props[key] setAttrs(el, key, props[key]); } // console.log(el); // 遍历children children.map(c => { // 先判断c是否是Element构造出来的,是则再次render一遍 c = c instanceof Element ? render(c) :document.createTextNode(c); // c为文本节点 console.log(c); // 将c添加到el中 el.appendChild(c); }); return el; } // 渲染到页面 function renderDom(rDom, rootEl) { rootEl.appendChild(rDom); } export { createElement, render, setAttrs, renderDom }; ``` ## 5. 创建虚拟dom测试 ```javascript import { createElement, render, renderDom } from './virtualDom'; const vDom = 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' }, ['第2个列表项']) ]) ]), createElement('li', { class: 'item', 'data-index': 2 }, ['第3个列表项']) ]); // 虚拟dom获取真实dom const rDom = render(vDom); // 真实dom渲染到页面 renderDom(rDom, document.getElementById('app')); console.log(rDom); ``` ## 6. 补丁包格式 ```javascript const patches = { 0: [ { type: 'ATTR', attr: 'list-wrap' } ], 2: [ { type: 'ATTR', attr: 'title' } ], 3: [ { type: 'TEXT', attr: '特殊列表项' } ], 6: [ { type: 'REMOVE', index: 6 } ], 7: [ { type: 'REPLACE', newNode } ] } ``` ## 7. 补丁类型常量 ```javascript export const ATTR = 'ATTR'; // 属性 export const TEXT = 'TEXT'; // 文本 export const REPLACE = 'REPLACE'; // 替换 export const REMOVE = 'REMOVE'; // 删除 ``` ## 8. 比较新旧虚拟节点生成补丁包 ### 1. 新的虚拟节点 ```javascript import { createElement, render, renderDom } from './virtualDom'; import domDiff from './domDiff'; 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' }, ['第2个列表项']) ]) ]), 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个列表项']) ]); // console.log(vDom); // 虚拟dom获取真实dom const rDom = render(vDom1); // 真实dom渲染到页面 renderDom(rDom, document.getElementById('app')); // console.log(rDom); // 生成补丁包 var patches = domDiff(vDom1, vDom2); console.log(patches); ``` ### 2. 生成补丁包 1. 引入补丁包常量 1. 定义存储补丁对象patches 1. 定义索引,深度遍历记录补丁包的位置 1. domDiff方法比较 1. 执行vNodeWalk方法比较新旧节点 1. 定义当前节点的补丁包存储vnPatch 1. 首页判断没有新节点,则表示补丁为删除,记录索引 1. 再判断是文本节点,当文本内容不相同时,文本修改,记录补丁为TEXT类型,text为newNode文本 1. 再判断当前节点的type类型是否相同,相同则需要遍历属性进行比较,如果存在属性补丁就添加到vnPatch中,然后在调用childrenWalk,继续比较新旧节点的children 1. 遍历属性 1. 遍历老节点中和新节点中相同键不同值的情况,则需要添加属性补丁 1. 属性修改 2. 遍历新节点,通过hasOwnPropery判断新节点中是否新增了属性,有就添加属性补丁 1. 添加属性 2. 递归children 1. 遍历children,每一下为一个虚拟节点,Element 1. 通过索引可以拿到新节点对应的老节点的虚拟dom 1. 再次调用vNodeWalk形成递归调用 5. 其他情况为节点替换,添加补丁 5. 当补丁vnPatch的长度大于0时,添加到patches中键为对应索引index 2. 往patches中添加补丁,最后返回 ```javascript import { ATTR, TEXT, REPLACE, REMOVE } from './patchTypes'; let patches = {}, // 补丁包 vnIndex = 0; function domDiff(oldVDOM, newVDOM) { let index = 0; // 也可以直接使用vnIndex 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); console.log(Object.keys(attrPatch)); if(Object.keys(attrPatch).length > 0) { // attrPatch不为空时 添加补丁 vnPatch.push({ type: ATTR, attrs: attrPatch }) } childrenWalk(oldNode.children, newNode.children); } else { vnPatch.push({ type: REPLACE, newNode }) } if(vnPatch.length > 0) { patches[index] = vnPatch; } } // 属性递归 function attrsWalk(oldAttrs, newAttrs) { let attrPatch = {}; // console.log(oldAttrs, newAttrs); 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; } // 遍历children function childrenWalk(oldChildren, newChildren) { oldChildren.map((c, idx) => { // 递归调用 vNodeWalk(c, newChildren[idx], ++ vnIndex) }) } export default domDiff; ``` ## 9. 打补丁 1. 修改了虚拟vDom1 1. 调用doPatch方法往真实dom上打补丁 ```javascript 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个列表项']) ]); // console.log(vDom); // 虚拟dom获取真实dom const rDom = render(vDom1); // 真实dom渲染到页面 renderDom(rDom, document.getElementById('app')); // 生成补丁包 var patches = domDiff(vDom1, vDom2); // console.log(rDom); // 打补丁, 往真实dom上打补丁 doPatch(rDom, patches) console.log(patches); ``` ### 1. 打补丁方法 1. 引入补丁包常量 1. 引入设置真实dom方法,和render方法将虚拟dom渲染成真实dom 1. 引入Element类,用于判断children是否是Element构建 1. 声明finalPatches保存补丁包,用户全局使用,不让需要一直往下传递 1. 声明rnIndex, 记录真实节点对应索引 1. 调用disPatch方法 1. 将补丁包赋值给finalPatches 1. 调用rNodeWalk传入真实dom 1. 获取对应索引的补丁包rnPatch,并将rnIndex++, 目的是下次获取到下一个真实dom对应的补丁 1. 获取真实dom的childNodes 1. 将childNodes解构由类数组转换为数组,并遍历获取到每一个child,调用自身形成递归 1. 每次都要判断当前索引对应的补丁是否存在 1. 存在就将真实节点上添加对应补丁 1. 遍历补丁数组 1. 判断补丁的type类型 1. ATTR 1. 遍历全部属性 1. 获取属性对应的value值 1. 如果存在value值,就将真实节点上使用setAttrs设置对应属性 1. 不存在value值,则需要使用removeAttribute删除对应属性 2. TEXT 1. 文本补丁直接使用textContent修改当前文本值 1. 也可以使用innerText,两者的区别兼容不同 3. REPLACE 1. 需要判断替换的新节点是否是Element构建出来的,如果是则是一个虚拟节点 1. 需要使用render函数转换为真实节点 1. 如果不是则创建一个文本节点并赋值 1. 最后通过获取当前元素的parentNode,获取到父节点,在通过父节点的replaceChild 1. 将旧节点替换为新节点, replaceChild第一个参数是新节点,第二个参数是旧节点 4. REMOVE 1. 获取当前节点的parentNode 1. 在通过父节点removeChild删除当前节点 ```javascript import { ATTR, TEXT, REPLACE, REMOVE } from './patchTypes'; import { setAttrs, render } from './virtualDom'; import Element from './Element'; let finalPatches = {}, // 保存补丁包,方便使用 不用一直往下传递 rnIndex = 0; // 真实节点索引 function doPatch(rDom, patch) { // 保存补丁包 finalPatches = patch; rNodeWalk(rDom); } function rNodeWalk(rNode) { // console.log(rNode); const rnPatch = finalPatches[rnIndex++], // 对应索引补丁包 childNodes = rNode.childNodes; // 类数组 // console.log(rnPatch, childNodes); [...childNodes].map(c => { // 递归处理 rNodeWalk(c); }); // 如果存在补丁就进行处理 if(rnPatch) { // console.log(rNode, rnPatch); patchAction(rNode, rnPatch); } } // 打补丁操作 function patchAction(rNode, rnPatch) { // 遍历补丁 是个数组 rnPatch.map(p => { switch(p.type) { case ATTR: // 遍历属性 for(var key in p.attrs) { const value = p.attrs[key]; // 当value存在时,需要给当前真实节点添加属性 if(value) { setAttrs(rNode, key, value); } else { // 没有value,删除掉这个属性 rNode.removeAttribute(key); } } break; case TEXT: // 文本补丁直接修改 rNode.textContent = p.text; // innerText textContent 兼容性不一样 效果一样 break; case REPLACE: console.log(rNode); // 替换过后的为虚拟节点 // 先要判断是否是Element构造,是怎需要使用render转换为真实节点 const newNode = (p.newNode instanceof Element ) ? render(p.newNode) : document.createTextNode(p.newNode); // 普通文本节点 // 获取当前节点的父节点,父节点进行老节点替换为新节点 rNode.parentNode.replaceChild(newNode, rNode); // 将rNode老节点替换为newNode新节点 break; case REMOVE: // 获取当前节点的父节点,删除rNode子节点 console.log(rNode); rNode.parentNode.removeChild(rNode); break; default: break; } }) } export default doPatch; // vNode = virtual Node // vnPatch = virtual Node patch // rNode = real Node // rnPatch = real Node patch ```