Home

动手写一个简单的 Virtual Dom(2)

分类:

上一篇文章其实就是介绍了怎么创建虚拟 DOM,真的只是创建 DOM 元素而已,因为没有设置属性的功能,事件绑定的功能也没有。 现在慢慢的一个个来实现吧。

设置属性

设置属性很简单的,还记得前面我们怎么表达 DOM 的吗? 我们把属性用纯 JS 对象存起来,比如:

<ul className=”list” style=”list-style: none;”></ul>

用 JS 对象来表达这个 DOM,就会是下面这个样子:

{ 
  type: ‘ul’, 
  props: { className: ‘list’, style: ’list-style: none;’ } 
  children: []
}

上面对象的每个字段就是属性名称,字段的值就是属性的值了。我们只要把这个对象映射到 DOM节点上去就可以啦。 可以利用 setAttribute() 方法来写一个设置属性的函数:

function setProp($target, name, value) {
  $target.setAttribute(name, value);
}

我们既然知道在 JS 里设置一个属性,那么可以通过遍历把对象里的每个字段设置成属性:

function setProps($target, props) {
  Object.keys(props).forEach(name => {
    setProp($target, name, props[name]);
  });
}

接下来我们要用到上篇文章提到的 createElement() 函数。在创建 DOM 后引用 setProps() 函数

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
}

到这里还没完,还有几个需要注意的小地方。首先 class 在 JS 关键字,需要用 ‘className’ 代替:

<nav className=”navbar light”>
  <ul></ul>
</nav>

但是因为 DOM 里面是没有 className 这个属性的,所以需要我们自己利用 setProp() 函数去设置。

还有一个注意的地方就是,我们经常在 DOM 里面要用到一些布尔属性,比如:

<input type=”checkbox” checked={false} />

前面写了一个设置元素标签属性的 setProp 函数,现在需要再写一个设置元素标签属性布尔值的函数:

function setBooleanProp($target, name, value) {
  if (value) {
    $target.setAttribute(name, value);
    $target[name] = true;
  } else {
    $target[name] = false;
  }
}

还有一个需要注意的就是我们有的时候可能会自己定义一个属性在 DOM 元素上,像这种情况我们需要检查如果是自定义的不存在的属性,我们 返回 false:

function isCustomProp(name) {
  return false;
}

现在我们把 setBooleanPropisCustomProp添加到 setProp 实现一个完整的设置属性的功能函数

function setProp($target, name, value) {
  if (isCustomProp(name)) {
    return;
  } else if (name === ‘className’) {
    $target.setAttribute(‘class’, value);
  } else if (typeof value === ‘boolean’) {
    setBooleanProp($target, name, value)
  } else {
    $target.setAttribute(name, value)
  }
}

上一篇讲创建 DOM 的时候有写到进行 DOM diff,其实现在这个属性也要进行 props diff。当然 diff 无非几种情况,删除属性,添加属性,更新属性这几种。

下面是删除属性的具体的函数:

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);
  }
}

我们还需要一个 updateProp 的函数来比较DOM标签上新旧属性的区别并且更新最后的 DOM。细想一下应该主要就是 下面几种情况:

  • 添加属性,指的是本来 DOM 标签上一个属性都没有,添加了一个属性
  • 把属性删除了,和上面想反
  • 删除属性值,可能一个属性里面本来有多个值,现在删了一个,比如本来 DOM class 属性有两个值的,现在删了一个
  • 添加属性值,和上面相反

对于一个属性的更新可以这么写:

function updateProp($target, name, newVal, oldVal) {
  if (!newVal) {
    removeProp($target, name, oldVal);
  } else if (!oldVal || newVal !== oldVal) {
    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]);
  });
}

好了,属性也更新了,最后就是更新DOM元素了,在第一篇文章里我们写了一个 updateElement函数,现在我们来更新一下这个函数:

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!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
      );
    }
  }
}

最后我把上面的代码整理一下在 jsfiddle 上: https://jsfiddle.net/gothic/a1c4qky5/