简易版react dom-diff实现

声明文件

  • index.js ----手动创建了两个虚拟dom树
  • element.js ----实现了虚拟dom的渲染
  • dom-diff.js ----实现了两个dom树之间的比较,并且生成了补丁patch对象
  • patch.js ----实现了通过比较dom树的不同,将这个补丁包patch对象重新渲染出新的dom树。
-- 本次代码为简易版,代码粗糙,部分优化功能没有实现,比如
- 同级的dom进行比较,如果只是调换位置,不是通过删除,和插入新节点,应通过换位(key的作用)
- 同级的dom进行操作,一个相邻dom进行预比较,或者当新dom树的某一个节点,在老的dom树中存在此节点的情况下,进行优化处理

index.js

import { createElement, render, renderDom } from './element.js';
import diff from './dom-diff.js';
import patch from './patch.js';
let vertualDom = createElement('ul', { class: 'list' }, [
  createElement('li', { class: 'item' }, ['a']),
  createElement('li', { class: 'item' }, ['b']),
  createElement('li', { class: 'item' }, ['c']),
]);
let newVertualDom = createElement('ul', { class: 'list' }, [
  createElement('li', { class: 'item' }, ['1']),
  createElement('li', { class: 'item' }, ['b']),
  createElement('li', { class: 'item' }, ['3']),
]);
let el = render(vertualDom);
renderDom(el, window.root);
console.log(vertualDom);

let patches = diff(vertualDom, newVertualDom)
patch(el, patches);

element.js


class Element {
  constructor(type, props, children) {
    this.type = type;
    this.props = props;
    this.children = children;
  }
}
//设置属性
function setAttr(node, key, value) {
  switch (key) {
    case 'value':
      if (node.tagName.toUpperCase() === 'INPUT' || node.tagName.toUpperCase() === 'TEXTAREA') {
        node.value = value;
      } else {
        node.setAttribute(key, value);
      }
      break;
    case 'style':
      node.style.cssText = value;
      break;
    default:
      node.setAttribute(key, value);
      break;
  }
}
//返回虚拟节点
function createElement(type, props, children) {
  return new Element(type, props, children);
}
//将虚拟dom转换为真实dom
function render(eleObj) {
  let el = document.createElement(eleObj.type);
  for (let key in eleObj.props) {
    setAttr(el, key, eleObj.props[key]);
  }
  //遍历儿子节点,如果屎虚拟dom继续渲染,不是的话就是文本节点
  eleObj.children.forEach((children) => {
    child = (child instanceof Element) ? render(child) : document.createTextNode(child);
    el.appendChild(child);
  })
  return el;
}
//将元素插入到页面中
function renderDom(el, target) {
  target.appendChild(el)
}
export { Element, createElement, render, renderDom }

dom-diff.js

//dom - diff是通过js层面的计算返回一个patch对象(补丁包),然后通过特定的操作解析patch对象,完成页面的重新渲染
//先序深度优先遍历
//dom树同级对比,(节点类型,属性是否相同,新dom节点不存在,则删除,节点类型不相同直接采用替换模式)
//如果同级dom改变,进行删除,然后插入patch对象,就是插入新的dom节点
//同级如果dom如果只是位置发生改变,交换位置(key的作用)
const ATTRS = 'ATTRS';
const TEXT = 'TEXT';
const REMOVE = 'REMOVE';
const REPLACE = 'REPLACE';
let Index = 0;
function diff(oldTree, newTree) {
  let patches = {}
  let index = 0;
  Walk(oldTree, newTree, index, patches)
  return patches;
}
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;
}

function diffChildren(oldChildren, newChildren, index, patches) {
  oldChildren.forEach((child, idx) => {
    //索引不应该是index了
    //index每次传递给walk时,index是递增得,所有得人都基于一个序号来实现
    Walk(child, newChildren[idx], ++Index, patches)
  });

}
function isString(node) {
  return Object.prototype.toString.call(node) === '[object String]';
}

function Walk(oldNode, newNode, index, patches) {
  let currentPatch = [];//每一个元素都有一个补丁对象
  if (!newNode) {
    currentPatch.push({ type: REMOVE, index })
  } else if (isString(oldNode) && isString(newNode)) {//判断文本是否一致
    if (oldNode !== newNode) {
      currentPatch.push({ type: TEXT, text: newNode })
    }
  } else if (oldNode.type === newNode.type) {
    //比较属性是否更改
    let attrs = diffAttr(oldNode.props, newNode.props);
    if (Object.keys(attrs).length > 0) {
      currentPatch.push({ type: ATTRS, attrs })
    }
    //如果有儿子节点,遍历儿子
    diffChildren(oldNode.children, newNode.children, index, patches);
  } else {
    //说明节点被替换了
    currentPatch.push({ type: REPLACE, newNode })
  }
  if (currentPatch.length > 0) {
    //当前元素确实有补丁将元素和补丁对应起来,放到大补丁包中
    patches[index] = currentPatch;
  }
}
export default diff;

patch.js

import { Element, render } from './element.js';
let allPatches;
let index = 0;
function patch(node, patches) {
  //node是真实的dom
  allPatches = patches;
  //给某个元素打补丁
  walk(node);
}
function walk(node) {
  let currentPatch = allPatches[index++];
  let childNodes = node.childNodes;
  childNodes.forEach(child => walk(child));
  if (currentPatch) {
    doPatch(node, currentPatch);
  }
}
function setAttr(node, key, value) {
  switch (key) {
    case 'value':
      if (node.tagName.toUpperCase() === 'INPUT' || node.tagName.toUpperCase() === 'TEXTAREA') {
        node.value = value;
      } else {
        node.setAttribute(key, value);
      }
      break;
    case 'style':
      node.style.cssText = value;
      break;
    default:
      node.setAttribute(key, value);
      break;
  }
}
function doPatch(node, Patches) {
  patches.forEach(patch => {
    switch (patch.type) {
      case 'ATTR':
        for (let key in patch.attrs) {
          let value = patch.attrs[key];
          if (value) {
            setAttr(node, key, value)
          } else {
            node.removeAttribute(key);
          }
        }
        break;
      case 'TEXT':
        node.textContent = patch.text;
        break;
      case 'REMOVE':
        node.parentNode.removeChild(node);
        break;
      case 'REPLACE':
        let newNode = (patch.newNode instanceof Element) ? render(patch.newNode) : document.createTextNode(patch.newNode);
        node.parentNode.replaceChild(newNode, node);
        break;
      default:
        break;
    }
  })

}
export default patch;

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容