时至今日,前端对于知识的考量是愈来愈有水平了,逼格高大上了css
各种框架你们已经能够说不管是工做仍是平常中都已经或多或少的使用过了前端
曾经据说不少人被问到过虚拟DOM和DOM-diff算法是如何实现的,有没有研究过?vue
想必问出此问题的也是高手高手之高高手了,不少人都半开玩笑的说:“面试造航母,工做拧螺丝”node
那么,话很少说了,今天就让咱们也来一块儿研究研究这个东东react
好饭不怕晚,沉淀下来收收心!咱们虽然走的慢,可是却从未停下脚步git
首先神奇不神奇的咱们先不去关注,先来简单说说何为虚拟DOMgithub
虚拟DOM简而言之就是,用JS去按照DOM结构来实现的树形结构对象,你也能够叫作DOM对象面试
好了,一句话就把这么伟大的东西给解释了,那么再也不耽误时间了,赶忙进入主环节吧算法
固然,这里还有整个项目的地址方便查看npm
在亲自上阵以前,咱们让粮草先行,先发个图,来看一下整个目录结构是什么样子的
// 全局安装
npm i create-react-app -g
// 生成项目
create-react-app dom-diff
// 进入项目目录
cd dom-diff
// 编译
npm run start
复制代码
如今咱们开始正式写吧,从建立虚拟DOM及渲染DOM起步吧
在element.js文件中要实现如何建立虚拟DOM以及将建立出来的虚拟DOM渲染成真实的DOM
首先实现一下如何建立虚拟DOM,看代码:
// element.js
// 虚拟DOM元素的类,构建实例对象,用来描述DOM
class Element {
constructor(type, props, children) {
this.type = type;
this.props = props;
this.children = children;
}
}
// 建立虚拟DOM,返回虚拟节点(object)
function createElement(type, props, children) {
return new Element(type, props, children);
}
export {
Element,
createElement
}
复制代码
写好了方法,咱们就从index.js文件入手来看看是否成功吧
在主入口文件里,咱们主要作的操做就是来建立一个DOM对象,渲染DOM以及经过diff后去打补丁更新DOM,不啰嗦了,直接看代码:
// index.js
// 首先引入对应的方法来建立虚拟DOM
import { createElement } from './element';
let virtualDom = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'}, ['周杰伦']),
createElement('li', {class: 'item'}, ['林俊杰']),
createElement('li', {class: 'item'}, ['王力宏'])
]);
console.log(virtualDom);
复制代码
createElement方法也是vue和react用来建立虚拟DOM的方法,咱们也叫这个名字,方便记忆。接收三个参数,分别是type,props和children
下面来看一下打印出来的虚拟DOM,以下图
// element.js
class Element {
// 省略
}
function createElement() {
// 省略
}
// render方法能够将虚拟DOM转化成真实DOM
function render(domObj) {
// 根据type类型来建立对应的元素
let el = document.createElement(domObj.type);
// 再去遍历props属性对象,而后给建立的元素el设置属性
for (let key in domObj.props) {
// 设置属性的方法
setAttr(el, key, domObj.props[key]);
}
// 遍历子节点
// 若是是虚拟DOM,就继续递归渲染
// 不是就表明是文本节点,直接建立
domObj.children.forEach(child => {
child = (child instanceof Element) ? render(child) : document.createTextNode(child);
// 添加到对应元素内
el.appendChild(child);
});
return el;
}
// 设置属性
function setAttr(node, key, value) {
switch(key) {
case 'value':
// node是一个input或者textarea就直接设置其value便可
if (node.tagName.toLowerCase() === 'input' ||
node.tagName.toLowerCase() === 'textarea') {
node.value = value;
} else {
node.setAttribute(key, value);
}
break;
case 'style':
// 直接赋值行内样式
node.style.cssText = value;
break;
default:
node.setAttribute(key, value);
break;
}
}
// 将元素插入到页面内
function renderDom(el, target) {
target.appendChild(el);
}
export {
Element,
createElement,
render,
setAttr,
renderDom
};
复制代码
既然写完了,那就赶快来看当作果吧
再次回到index.js文件中,修改成以下代码
// index.js
// 引入createElement、render和renderDom方法
import { createElement, render, renderDom } from './element';
let virtualDom = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'}, ['周杰伦']),
createElement('li', {class: 'item'}, ['林俊杰']),
createElement('li', {class: 'item'}, ['王力宏'])
]);
console.log(virtualDom);
// +++
let el = render(virtualDom); // 渲染虚拟DOM获得真实的DOM结构
console.log(el);
// 直接将DOM添加到页面内
renderDom(el, document.getElementById('root'));
// +++
复制代码
经过调用render方法转为真实DOM,并调用renderDom方法直接将DOM添加到了页面内
下图为打印后的结果:
说到DOM-diff那必定要清楚其存在的意义,给定任意两棵树,采用先序深度优先遍历的算法找到最少的转换步骤
DOM-diff比较两个虚拟DOM的区别,也就是在比较两个对象的区别。
做用: 根据两个虚拟对象建立出补丁,描述改变的内容,将这个补丁用来更新DOM
已经了解到DOM-diff是干吗的了,那就没什么好说的了,继续往下写吧
// diff.js
function diff(oldTree, newTree) {
// 声明变量patches用来存放补丁的对象
let patches = {};
// 第一次比较应该是树的第0个索引
let index = 0;
// 递归树 比较后的结果放到补丁里
walk(oldTree, newTree, index, patches);
return patches;
}
function walk(oldNode, newNode, index, patches) {
// 每一个元素都有一个补丁
let current = [];
if (!newNode) { // rule1
current.push({ type: 'REMOVE', index });
} else if (isString(oldNode) && isString(newNode)) {
// 判断文本是否一致
if (oldNode !== newNode) {
current.push({ type: 'TEXT', text: newNode });
}
} else if (oldNode.type === newNode.type) {
// 比较属性是否有更改
let attr = diffAttr(oldNode.props, newNode.props);
if (Object.keys(attr).length > 0) {
current.push({ type: 'ATTR', attr });
}
// 若是有子节点,遍历子节点
diffChildren(oldNode.children, newNode.children, patches);
} else { // 说明节点被替换了
current.push({ type: 'REPLACE', newNode});
}
// 当前元素确实有补丁存在
if (current.length) {
// 将元素和补丁对应起来,放到大补丁包中
patches[index] = current;
}
}
function isString(obj) {
return typeof obj === 'string';
}
function diffAttr(oldAttrs, newAttrs) {
let patch = {};
// 判断老的属性中和新的属性的关系
for (let key in oldAttrs) {
if (oldAttrs[key] !== newAttrs[key]) {
patch[key] = newAttrs[key]; // 有可能仍是undefined
}
}
for (let key in newAttrs) {
// 老节点没有新节点的属性
if (!oldAttrs.hasOwnProperty(key)) {
patch[key] = newAttrs[key];
}
}
return patch;
}
// 全部都基于一个序号来实现
let num = 0;
function diffChildren(oldChildren, newChildren, patches) {
// 比较老的第一个和新的第一个
oldChildren.forEach((child, index) => {
walk(child, newChildren[index], ++num, patches);
});
}
// 默认导出
export default diff;
复制代码
代码虽然又臭又长,可是这些代码就让咱们实现了diff算法了,因此你们先不要盲动,不要盲动,且听风吟,让我一一道来
根据这些规则,咱们再来看一下diff代码中的walk方法这位关键先生
walk方法都作了什么?
if (!newNode) {
current.push({ type: 'REMOVE', index });
}
复制代码
else if (isString(oldNode) && isString(newNode)) {
if (oldNode !== newNode) {
current.push({ type: 'TEXT', text: newNode });
}
}
复制代码
else if (oldNode.type === newNode.type) {
// 比较属性是否有更改
let attr = diffAttr(oldNode.props, newNode.props);
if (Object.keys(attr).length > 0) {
current.push({ type: 'ATTR', attr });
}
// 若是有子节点,遍历子节点
diffChildren(oldNode.children, newNode.children, patches);
}
复制代码
else {
current.push({ type: 'REPLACE', newNode});
}
复制代码
if (current.length > 0) {
// 将元素和补丁对应起来,放到大补丁包中
patches[index] = current;
}
复制代码
以上就是关于diff算法的分析过程了,没太明白的话不要紧,再反复看几遍试试,意外老是不期而遇的
diff已经完事了,那么最后一步就是你们所熟知的打补丁了
补丁要怎么打?那么让久违的patch出来吧
打补丁须要传入两个参数,一个是要打补丁的元素,另外一个就是所要打的补丁了,那么直接看代码
import { Element, render, setAttr } from './element';
let allPatches;
let index = 0; // 默认哪一个须要打补丁
function patch(node, patches) {
allPatches = patches;
// 给某个元素打补丁
walk(node);
}
function walk(node) {
let current = allPatches[index++];
let childNodes = node.childNodes;
// 先序深度,继续遍历递归子节点
childNodes.forEach(child => walk(child));
if (current) {
doPatch(node, current); // 打上补丁
}
}
function doPatch(node, patches) {
// 遍历全部打过的补丁
patches.forEach(patch => {
switch (patch.type) {
case 'ATTR':
for (let key in patch.attr) {
let value = patch.attr[key];
if (value) {
setAttr(node, key, value);
} else {
node.removeAttribute(key);
}
}
break;
case 'TEXT':
node.textContent = patch.text;
break;
case 'REPLACE':
let newNode = patch.newNode;
newNode = (newNode instanceof Element) ? render(newNode) : document.createTextNode(newNode);
node.parentNode.replaceChild(newNode, node);
break;
case 'REMOVE':
node.parentNode.removeChild(node);
break;
default:
break;
}
});
}
export default patch;
复制代码
看完代码还须要再来简单的分析一下
属性ATTR for in去遍历attrs对象,当前的key值若是存在,就直接设置属性setAttr; 若是不存在对应的key值那就直接删除这个key键的属性
文字TEXT 直接将补丁的text赋值给node节点的textContent便可
替换REPLACE 新节点替换老节点,须要先判断新节点是否是Element的实例,是的话调用render方法渲染新节点;
不是的话就代表新节点是个文本节点,直接建立一个文本节点就OK了。
以后再经过调用父级parentNode的replaceChild方法替换为新的节点
删除REMOVE 直接调用父级的removeChild方法删除该节点
好了,一切都安静下来了。让咱们回归index.js文件中,去调用一下diff和patch这两个重要方法,看看奇迹会不会发生吧
// index.js
import { createElement, render, renderDom } from './element';
// +++ 引入diff和patch方法
import diff from './diff';
import patch from './patch';
// +++
let virtualDom = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'}, ['周杰伦']),
createElement('li', {class: 'item'}, ['林俊杰']),
createElement('li', {class: 'item'}, ['王力宏'])
]);
let el = render(virtualDom);
renderDom(el, window.root);
// +++
// 建立另外一个新的虚拟DOM
let virtualDom2 = createElement('ul', {class: 'list-group'}, [
createElement('li', {class: 'item active'}, ['七里香']),
createElement('li', {class: 'item'}, ['一千年之后']),
createElement('li', {class: 'item'}, ['须要人陪'])
]);
// diff一下两个不一样的虚拟DOM
let patches = diff(virtualDom, virtualDom2);
console.log(patches);
// 将变化打补丁,更新到el
patch(el, patches);
// +++
复制代码
将修改后的代码保存,会在浏览器里看到DOM被更新了,以下图
咱们来梳理一下整个DOM-diff的过程:
行了,就这四句话吧,说多了就有点多此一举了。很久没有写文章了,很感谢小伙伴们的观看,辛苦各位了,886
参考: