How to wirte your own Virtual DOM(译)

前言

React在处理界面更新的时候,是经过对比虚拟DOM(js对象)之间的差别来更新UI的.这种方式能必定程度上的减小对DOM的操做.经过虚拟DOM这个中间层结合多平台的renderer使React实现了跨平台.本文梳理了在medium上关于Virtual DOM的两篇文章来介绍如何简单实现一个Virtual DOM.前端

为何虚拟DOM

  1. UI = F(data) Virtual DOM使数据,操做,属性能够集中在一块儿,这种方式能必定程度上下降项目长期维护的复杂性.
  2. 页面性能 经过虚拟DOM的对比,进行差别的更新能提高页面的性能.

如何实现虚拟DOM

虚拟DOM能够理解是真实DOM的映射,如何实现虚拟DOM主要须要考虑一下几点:node

  1. 如何描述虚拟DOM(create)
  2. 如何绘制虚拟DOM(render)
  3. 如何差别化的更新虚拟DOM而且更新UI(update)

下面主要从上面的三点来逐步实现一个简易版的虚拟DOM实现.git

建立Virtual DOM

DOM的节点能够经过type(节点类型),props(styles, event), children(子元素)来描述.能够经过下面的函数来建立虚拟节点.github

// 建立虚拟节点
function h(type, props, children) {
  return { type, props: props || [], children: children || [] };
}
const root = h('ul', { name: 100, onClick: () => { console.log(1); } }, [
  h('li', {}, ['sss'])
])
复制代码

virtualDom

绘制Virtual DOM

在下面的实现中,以$开头的元素指代真实的DOM节点,node指代虚拟节点.算法

绘制元素

在绘制元素的时候,若是节点的内容是文本,就直接建立文本节点.不然就建立当前类型的DOM节点而且遍历它的children节点递归的调用自身而且添加到建立的节点.性能优化

function createElement(node) {
  if(typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children.map(createElement).forEach($el.appendChild.bind($el));
  return $el;
}
const root = h("ul", { name: '111', className: 'test' }, [
  h('li', { name: 'child' }, ['text'])
]);
// app是页面中已经存在的容器节点
const container = document.getElementById('app');
container.appendChild(createElement(root)); // 已经能绘制到页面
复制代码

添加属性

在添加属性的时候,有如下的节点须要注意:bash

  • 对DOM节点上不存在的属性名字进行转换,例如ClassName
  • 布尔属性值的设置
  • 增长属性过滤功能来实现特有的实现
实现
//设置布尔属性
function setBooleanProp($target, name, value) {
  if(value) {
    $target.setAttribute(name, value);
    $target[name] = value;
  } else {
    $target[name] = false;
  }
}
// 属性过滤
function isCustomProp(name) {
  return false;
}
// 设置全部属性的入口
function setProps($target, props) {
  Object.keys(props).forEach(name => {
    setProp($target, name, props[name]);
  });
}
// 对单一属性的设置,实现过滤,转换
function setProp($target, name, value) {
  if(isCustomProp(name)) {
    return;
  } else if(name === 'className') {
    $target.setAttribute('class', value);
  } else if(typeof value === 'boolean') {
    setBooleanProp(name, value);
  } else {
    $target.setAttribute(name, value);
  }
}
复制代码

经过将设置属性的操做加入到以前的createElement函数中,来实现DOM属性的添加.微信

function createElement(node) {
  if(typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  setProps($el, node.props);
  node.children.map(createElement).forEach($el.appendChild.bind($el));
  return $el;
} 
复制代码

经过运行以前的代码,发现属性已经添加到DOM中了.
domWithPropapp

添加事件

在对事件的添加上,因为具体的事件也是在建立Virtual DOM的时候添加到props的,若是不想经过以前声明的setProps函数进行事件的处理,就须要将这些属性过滤出来,具体实现以下:dom

//判断是不是event属性
function isEventProp(name) {
  return /^on/.test(name);
}
// 获取属性的后缀  例如 onClick => click
function extractEventName(name) {
  return name.slice(2).toLowerCase();
}
// 修改以前的过滤属性函数,加入对event属性的过滤
function isCustomProp(name) {
  return isEventProp(name)
}
// 添加属性函数
function addEventListeners($target, props) {
  Object.keys(props).forEach(name => {
    if(isEventProp(name)) {
      $target.addEventListener(extractEventName(name), props[name]);
    }
  })
}
复制代码

将添加事件的函数增长到createElement函数中,完成对事件的添加.

function createElement(node) {
  if(typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  setProps($el, node.props);
  addEventListeners($el, node.props);
  node.children.map(createElement).forEach($el.appendChild.bind($el));
  return $el;
}
复制代码

对比差别更新UI

若是彻底的对比两个树形结构的差别,时间复杂度是O(n^3)的.为了必定的性能优化,能够有如下的假设:

  1. 节点的类型变动,两个DOM的结构就是不一样的.这种状况能够直接进行替换操做.
  2. 不多存在跨层级的节点移动
  3. 同一类型的节点的DOM结构是相同的

经过对上面假设的分析,在更新Virtual DOM的时候,主要有如下几种状况

  1. 对比两个node的类型不一样,直接替换
  2. 最新的node中没有元素和属性,须要删除对应的节点的属性
  3. 最新的node中增长了元素和属性,须要添加对应的属性和节点
  4. 节点类型相同,对子节点实现1 2 3的操做

更新节点

// 判断两个node是不是同一个节点
function changed(node1, node2) {
  return typeof node1 !== typeof node2 || typeof node1 === 'string' && node1 !== node2 || node1.type !== node2.type;
}

function updateElement($parent, newNode, oldNode, index = 0) {
  // index是子元素的位置
  if(!oldNode) {
    // 若是不存在oldNode进行添加操做
    $parent.appendChild(createElement(newNode));
  } else if(!newNode) {
    // 在新的节点中不村存在移除
    $parent.removeChild($parent.childNodes[index]);
  } else if(changed(newNode, oldNode)) {
    // 若是节点类型改变进行替换
    $parent.replaceChild(
      createElement(newNode), $parent.childNodes[index]
    )
  } else if(newNode.type) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    // 更新子节点
    for(let i = 0; i < newLength || i < oldLength; i++) {
      updateElement($parent.childNodes[index], newNode.children[i], oldNode.children[i], i)
    }
  }
}
复制代码

更新属性

在更新属性的时候跟更新节点的步骤相似

// 删除布尔属性
function removeBooleanProp($target, name) {
  $target.removeAttribute(name);
  $target[name] = false;
}
// 移除属性
function removeProp($target, name, value) {
  if(isCustomProp(name)) {
    return;
  } else if(name === 'className') {
    $target.removeAttribute('class');
  } else if(typeof value === 'boolean') {
    removeBooleanProp($target, name);
  } else {
    $target.removeAttribute(name);
  }
}
// 当不存在newVal的时候,remove对应的属性.其余状况进行覆盖
function updateProp($target, name, newVal, oldVal) {
  if(!newVal) {
    removeProp($target, name, oldVal);
  } else {
    setProp($target, name, newVal);
  }
}
function updateProps($target, newProps, oldProps = {}) {
  const props = Object.assign({}, newProps, oldProps);
  Object.keys(props).forEach(name => {
    updateProp($target, name, newProps[name], oldProps[name]);
  });
}
function updateElement($parent, newNode, oldNode, index = 0) {
  if(!oldNode) {
    // 若是不存在oldNode进行添加操做
    $parent.appendChild(createElement(newNode));
  } else if(!newNode) {
    // 在新的节点中不村存在移除
    $parent.removeChild($parent.childNodes[index]);
  } else if(changed(newNode, oldNode)) {
    // 若是节点类型改变进行替换
    $parent.replaceChild(
      createElement(newNode), $parent.childNodes[index]
    )
  } else if(newNode.type) {
    // 增长属性的更新
    updateProps($parent.childNodes[index], newNode.props, oldNode.props)
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    // 更新子节点
    for(let i = 0; i < newLength || i < oldLength; i++) {
      updateElement($parent.childNodes[index], newNode.children[i], oldNode.children[i], i)
    }
  }
}
复制代码

更新事件

函数是很差判断是否有变化的,能够经过一些参数来完成事件的更新(触发从新更新,经过节点替换来完成事件的更新这样很差)

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
      typeof node1 === ‘string’ && node1 !== node2 ||
      node1.type !== node2.type ||
      node1.props.forceUpdate;
}
function isCustomProp(name) {
  return isEventProp(name) || name === ‘forceUpdate’;
}
复制代码

参考

How to write your own Virtual DOM

Write your Virtual DOM 2: Props & Events
深度剖析:如何实现一个 Virtual DOM 算法

                                       前端小板凳
                                       欢迎你们关注个人微信公众号,一块儿学习
相关文章
相关标签/搜索