什么是虚拟Dom (Virtual DOM)

  1. vue2.x才有虚拟Dom
  2. 它的本质是js对象,是跨平台的 可以在node运行

    虚拟Dom在vue中做什么

    掘金DOM和DOM-diff- https://juejin.cn/post/6844903806132568072#heading-9

    vue的渲染过程:

  3. 将真实Dom转换为虚拟Dom(js对象)=>这一步拖慢了页面渲染速度,但提供了虚拟dom更新时对比

  4. 更新时进行对比

    虚拟Dom转换为真实Dom 初始化patch

    vue 中初始化方法 patch(container, vnode)(容器,虚拟Dom)

过程图

初次生成虚拟Dom
image.png

代码

创建虚拟Dom的js对象

  1. // element.js
  2. //type: 指定元素的标签类型,如'li', 'div', 'a'等
  3. //props: 表示指定元素身上的属性,如class, style, 自定义属性等
  4. //children: 表示指定元素是否有子节点,参数以数组的形式传入
  5. // 虚拟DOM元素的类,构建实例对象,用来描述DOM
  6. class Element {
  7. constructor(type, props, children) {
  8. this.type = type;
  9. this.props = props;
  10. this.children = children;
  11. }
  12. }
  13. // 创建虚拟DOM,返回虚拟节点(object)
  14. function createElement(type, props, children) {
  15. return new Element(type, props, children);
  16. }
  17. export {
  18. Element,
  19. createElement
  20. }

调用生成一个虚拟Dom的js对象

  1. // index.js
  2. // 首先引入对应的方法来创建虚拟DOM
  3. import { createElement } from './element';
  4. let virtualDom = createElement('ul', {class: 'list'}, [
  5. createElement('li', {class: 'item'}, ['周杰伦']),
  6. createElement('li', {class: 'item'}, ['林俊杰']),
  7. createElement('li', {class: 'item'}, ['王力宏'])
  8. ]);
  9. console.log(virtualDom);

image.png

渲染虚拟Dom 转换为真实Dom document.createElement 设置属性node.setAttribute

  1. // element.js
  2. class Element {
  3. // 省略
  4. }
  5. function createElement() {
  6. // 省略
  7. }
  8. // render方法可以将虚拟DOM转化成真实DOM
  9. function render(domObj) {
  10. // 根据type类型来创建对应的元素
  11. let el = document.createElement(domObj.type);
  12. // 再去遍历props属性对象,然后给创建的元素el设置属性
  13. for (let key in domObj.props) {
  14. // 设置属性的方法
  15. setAttr(el, key, domObj.props[key]);
  16. }
  17. // 遍历子节点
  18. // 如果是虚拟DOM,就继续递归渲染
  19. // 不是就代表是文本节点,直接创建
  20. domObj.children.forEach(child => {
  21. child = (child instanceof Element) ? render(child) : document.createTextNode(child);
  22. // 添加到对应元素内
  23. el.appendChild(child);
  24. });
  25. return el;
  26. }
  27. // 设置属性
  28. function setAttr(node, key, value) {
  29. switch(key) {
  30. case 'value':
  31. // node是一个input或者textarea就直接设置其value即可
  32. if (node.tagName.toLowerCase() === 'input' ||
  33. node.tagName.toLowerCase() === 'textarea') {
  34. node.value = value;
  35. } else {
  36. node.setAttribute(key, value);
  37. }
  38. break;
  39. case 'style':
  40. // 直接赋值行内样式
  41. node.style.cssText = value;
  42. break;
  43. default:
  44. node.setAttribute(key, value);
  45. break;
  46. }
  47. }
  48. // 将元素插入到页面内
  49. function renderDom(el, target) {
  50. target.appendChild(el);
  51. }
  52. export {
  53. Element,
  54. createElement,
  55. render,
  56. setAttr,
  57. renderDom
  58. };

调用结果

  1. // index.js
  2. // 引入createElement、render和renderDom方法
  3. import { createElement, render, renderDom } from './element';
  4. let virtualDom = createElement('ul', {class: 'list'}, [
  5. createElement('li', {class: 'item'}, ['周杰伦']),
  6. createElement('li', {class: 'item'}, ['林俊杰']),
  7. createElement('li', {class: 'item'}, ['王力宏'])
  8. ]);
  9. console.log(virtualDom);
  10. // +++
  11. let el = render(virtualDom); // 渲染虚拟DOM得到真实的DOM结构
  12. console.log(el);
  13. // 直接将DOM添加到页面内
  14. renderDom(el, document.getElementById('root'));

image.png

简易整体思路代码

image.png
image.png

虚拟Dom更新对比 更新update

vue 中更新方法 update(oldVnode,newVnode)

  1. 局部更新(节点数据)
  2. DOM-diff比较两个虚拟DOM的区别,也就是在比较两个对象的区别。

    对比过程图

    虚拟Dom直接进行对比
    image.png

    代码

    DOM-diff那一定要清楚其存在的意义,给定任意两棵树,采用先序深度优先遍历的算法找到最少的转换步骤 DOM-diff比较两个虚拟DOM的区别,也就是在比较两个对象的区别。 作用: 根据两个虚拟对象创建出补丁,描述改变的内容,将这个补丁用来更新DOM

  1. // diff.js
  2. function diff(oldTree, newTree) {
  3. // 声明变量patches用来存放补丁的对象
  4. let patches = {};
  5. // 第一次比较应该是树的第0个索引
  6. let index = 0;
  7. // 递归树 比较后的结果放到补丁里
  8. walk(oldTree, newTree, index, patches);
  9. return patches;
  10. }
  11. function walk(oldNode, newNode, index, patches) {
  12. // 每个元素都有一个补丁
  13. let current = [];
  14. if (!newNode) { // rule1
  15. current.push({ type: 'REMOVE', index });
  16. } else if (isString(oldNode) && isString(newNode)) {
  17. // 判断文本是否一致
  18. if (oldNode !== newNode) {
  19. current.push({ type: 'TEXT', text: newNode });
  20. }
  21. } else if (oldNode.type === newNode.type) {
  22. // 比较属性是否有更改
  23. let attr = diffAttr(oldNode.props, newNode.props);
  24. if (Object.keys(attr).length > 0) {
  25. current.push({ type: 'ATTR', attr });
  26. }
  27. // 如果有子节点,遍历子节点
  28. diffChildren(oldNode.children, newNode.children, patches);
  29. } else { // 说明节点被替换了
  30. current.push({ type: 'REPLACE', newNode});
  31. }
  32. // 当前元素确实有补丁存在
  33. if (current.length) {
  34. // 将元素和补丁对应起来,放到大补丁包中
  35. patches[index] = current;
  36. }
  37. }
  38. function isString(obj) {
  39. return typeof obj === 'string';
  40. }
  41. function diffAttr(oldAttrs, newAttrs) {
  42. let patch = {};
  43. // 判断老的属性中和新的属性的关系
  44. for (let key in oldAttrs) {
  45. if (oldAttrs[key] !== newAttrs[key]) {
  46. patch[key] = newAttrs[key]; // 有可能还是undefined
  47. }
  48. }
  49. for (let key in newAttrs) {
  50. // 老节点没有新节点的属性
  51. if (!oldAttrs.hasOwnProperty(key)) {
  52. patch[key] = newAttrs[key];
  53. }
  54. }
  55. return patch;
  56. }
  57. // 所有都基于一个序号来实现
  58. let num = 0;
  59. function diffChildren(oldChildren, newChildren, patches) {
  60. // 比较老的第一个和新的第一个
  61. oldChildren.forEach((child, index) => {
  62. walk(child, newChildren[index], ++num, patches);
  63. });
  64. }
  65. // 默认导出
  66. export default diff;

patch补丁更新 打补丁需要传入两个参数,一个是要打补丁的元素,另一个就是所要打的补丁了

  1. import { Element, render, setAttr } from './element';
  2. let allPatches;
  3. let index = 0; // 默认哪个需要打补丁
  4. function patch(node, patches) {
  5. allPatches = patches;
  6. // 给某个元素打补丁
  7. walk(node);
  8. }
  9. function walk(node) {
  10. let current = allPatches[index++];
  11. let childNodes = node.childNodes;
  12. // 先序深度,继续遍历递归子节点
  13. childNodes.forEach(child => walk(child));
  14. if (current) {
  15. doPatch(node, current); // 打上补丁
  16. }
  17. }
  18. function doPatch(node, patches) {
  19. // 遍历所有打过的补丁
  20. patches.forEach(patch => {
  21. switch (patch.type) {
  22. case 'ATTR':
  23. for (let key in patch.attr) {
  24. let value = patch.attr[key];
  25. if (value) {
  26. setAttr(node, key, value);
  27. } else {
  28. node.removeAttribute(key);
  29. }
  30. }
  31. break;
  32. case 'TEXT':
  33. node.textContent = patch.text;
  34. break;
  35. case 'REPLACE':
  36. let newNode = patch.newNode;
  37. newNode = (newNode instanceof Element) ? render(newNode) : document.createTextNode(newNode);
  38. node.parentNode.replaceChild(newNode, node);
  39. break;
  40. case 'REMOVE':
  41. node.parentNode.removeChild(node);
  42. break;
  43. default:
  44. break;
  45. }
  46. });
  47. }
  48. export default 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);
// +++

简易整体思路代码

image.png