美文网首页
简易版react dom-diff实现

简易版react dom-diff实现

作者: 田成力 | 来源:发表于2019-12-11 15:18 被阅读0次

声明文件

  • 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;

相关文章

网友评论

      本文标题:简易版react dom-diff实现

      本文链接:https://www.haomeiwen.com/subject/ywwogctx.html