一步一步带你实现virtual dom(一)

一步一步带你实现virtual dom(一)
一步一步带你实现virtual dom(二)--Props和事件html

要写你本身的虚拟DOM,有两件事你必须知道。你甚至都不用翻看React的源代码,或者其余的基于虚拟DOM的代码。他们代码量都太大,太复杂。然而要实现一个虚拟DOM的主要部分只须要大约50行的代码。50行代码!!node

下面就是那两个你要知道的事情:react

  • 虚拟DOM和真实DOM的有某种对应关系
  • 咱们在虚拟DOM树的更改会生成另一个虚拟DOM树。咱们会用一种算法来比较两个树有哪些不一样,而后对真实的DOM作最小的更改。

下面咱们就来看看这两条是如何实现的。web

生成虚拟DOM树

首先咱们须要在内存里存储咱们的DOM树。只要使用js就能够达到这个目的。假设咱们有这样的一个树:算法

<ul class="list">
  <li>item 1</li>
  <li>item 2</li>
</ur>

看起来很是简单对吧。咱们怎么用js的对象来对应到这个树呢?babel

{ type: 'ul', props: {'class': 'list}, children: [
  {type: 'li', props: {}, children: ['item 1']},
  {type: 'li', props: {}, children: ['item 2']}
]}

这里咱们会注意到两件事:app

  • 咱们使用这样的对象来对应到真实的DOM上:{type: '...', props: {...}, children: [...]}
  • DOM的文本节点会对应到js的字符串上。
    可是若是用这个方法来对应到巨大的DOM树的话那将是很是困难的。因此咱们来写一个helper方法,这样结构上也就容易理解一些:
function h(type, props, ...children) {
  return {type, props, children};
}

如今咱们能够这样生成一个虚拟DOM树:dom

h('ul', {'class': 'list'},
  h('li', {}, 'item 1'),
  h('li', {}, 'item 2'),
)

这样看起来就清晰了不少。可是咱们还能够作的更好。你应该据说过JSX对吧。是的,咱们也要用那种方式。可是,这个应该如何下手呢?spa

若是你读过Babel的JSX文档的话,你就会知道这些都是Babel的功劳。Babel会把下面的代码转码:.net

<ul className="list">
  <li>item 1</li>
  <li>item 2</li>
</ul>

转码为:

React.createElement('ul', {className: 'list'}),
 React.createElement('li', {}, 'item 1'),
 React.createElement('li', {}, 'item 2')
);

你注意到多类似了吗?若是把React.createElement(...)体换成咱们本身的h方法的话,那咱们也已使用相似于JSX的语法。咱们只须要在咱们的文件最顶端加这么一句话:

/** @jsx h */
<ul className="list">
  <li>item 1</li> 
  <li>item 2</li>
</ul>

这一行/** @jsx h */就是在告诉Babel“大兄弟,按照jsx的方式转码,可是不要用React.createElement, 使用h。你可使用任意的东西来代替h。

那么把上面咱们说的总结一下,咱们会这样写咱们的虚拟DOM:

/** @jsx h */
const a = {
  <ul className="list">
    <li>item 1</li>
    <li>item 2</li>
  </ul>
};

而后Babel就会转码成这样:

const a = {
  h('ul', {className: 'list'},
    h('li', {}, 'item 1'),
    h('li', {}, 'item 2'),
  )
};

当方法h执行的时候,它就会返回js的对象--咱们的虚拟DOM树。

const a = (
  { type: ‘ul’, props: { className: ‘list’ }, children: [
    { type: ‘li’, props: {}, children: [‘item 1’] },
    { type: ‘li’, props: {}, children: [‘item 2’] }
  ] }
);

JSFiddle里运行一下试试

应用咱们的DOM展现

如今咱们的DOM树用纯的JS对象来表明了。很酷了。可是咱们须要根据这些建立实际的DOM。由于咱们不能只是把虚拟节点转换后直接加载DOM里。

首先咱们来定义一些假设和一些术语:

  • 实际的DOM都会使用$开头的变量来表示。因此$parent是一个实际的DOM。
  • 虚拟DOM使用node变量表示
  • 和React同样,你只能够有一个根节点。其余的节点都在某个根节点里。

咱们来写一个方法:createElement(),这个方法能够接收一个虚拟节点以后返回一个真实的DOM节点。先不考虑propschildren,这个以后会有介绍。

function createElement(node) {
  if(typeof node === 'string') {
    return document.createTextNode(node);
  }
  return document.createElement(node.type);
}

由于咱们不只须要处理文本节点(js的字符串),还要处理各类元素(element)。这些元素都是想js的对象同样的:

{ type: '-', props: {...}, children: [...]}

咱们能够用这个结构来处理文本节点和各类element了。

那么子节点如何处理呢,他们也基本是文本节点或者各类元素。这些子节点也能够用createElement()方法来处理。父节点和子节点都使用这个方法,看到了么?其实这就是递归处理了。咱们能够调用createElement方法来建立子节点,而后用appendChild方法来把他们添加到根节点上。

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

看起来还不错,咱们先不考虑节点的props。要理解虚拟节点的概念并不须要这些东西却会增长不少的复杂度。

处理修改

咱们能够把虚拟节点转化为真实的DOM了。如今该考虑比较咱们的虚拟树了。基本上咱们须要写一点算法了。虚拟树的比较须要用到这个算法,比较以后只作必要的修改。

如何比较树的不一样?

  • 若是新节点的子节点增长了,那么咱们就须要调用appendChild方法来添加。
//new 
<ul>
  <li>item 1</li>
  <li>item 2</li>
</ul>
 
//old
<ul>
  <li>item 1</li>
</ul>
  • 新节点比旧节点的子节点少,那么就须要调用removeChild方法来删除掉多余的子节点。
//new
<ul>
  <li>item 1</li>
</ul>

//old
<ul>
  <li>item 1</li>
  <li>item 2</li>  // 这个要被删掉
</ul>
  • 新旧节点的某个子节点不一样,也就是某个节点上发生了修改。那么,咱们就调用replaceChild方法。
//new
<div>
  <p>hi there!</p>
  <p>hello</p>
</div>


//old
<div>
  <p>hi there!</p>
  <button>click it</button>   //发生了修改,变成了new里的<p />节点
</div>
  • 各节点都同样。那么咱们就须要作进一步的比较
//new
<ul>
  <li>item 1</li>
  <li> //*
    <span>hello</span>
    <span>hi!</span> 
  </li>
</ul>

//old
<ul>
  <li>item 1</li>
  <li> //*
    <span>hello</span>
    <div>hi!</div>
  </li>
</ul>

加醒的两个节点能够看到都是<li>,是相等的。可是它的子节点里面却有不一样的节点。

咱们来写一个方法updateElement,它接收三个参数:$parentnewNodeoldNode$parent是真的DOM元素。它是咱们虚拟节点的父节点。如今咱们来看看如何处理上面提到的所有问题。

没有旧节点

这个问题很简单:

function updateElement($parent, newNode, oldNode) {
  if(!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  }
}

没有新节点

若是当前没有新的虚拟节点,咱们就应该把它从真的DOM里删除掉。可是,如何作到呢?咱们知道父节点(做为参数传入了方法),那么咱们就能够调用$parent.removeChild方法,并传入真DOM的引用。可是咱们没法获得它,若是咱们知道的节点在父节点的位置,就能够用$parent.childNodes[index]来获取它的引用。index就是节点的位置。

假设index也做为参数传入了咱们的方法,咱们的方法就能够这么写:

function updateElement($parent, newNode, oldNode, index = 0) {
  if(!oldNode) {
    $parent.appendChild(
      createElement(newNode);
    );
  } else if(!newNode) {
    $parent.removeChild(
      $parent.childNodes[index];
    );
  }
}

节点改变

首先写一个方法来比较两个节点(新的和旧的)来区分节点是否发生了改变。要记住,节点能够是文本节点,也能够是元素(element):

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
    typeof node1 === 'string' && node1 !== node2 ||
    node1.type !== node2.type;
}

如今有了当前节点的index了,index就是当前节点在父节点的位置。这样能够很容易用新建立的节点来代替当前节点了。

function updateElement($parent, newNode, oldNode, index = 0) {
  if(!oldNode) {
    $parent.appendChild(
      createElement(newNode);
    );
  } else if(!newNode) {
    $parent.removeChild(
      $parent.childNOdes[index];
    );
  } else if(chianged(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  }
}

对比子节点的不一样

最后,须要遍历新旧节点的子节点,并比较他们。能够在每一个节点上都使用updateElement方法。是的,递归。

可是在开始代码以前须要考虑一些问题:

  • 只有在节点是一个元素(element)的时候再去比较子节点(文本节点不可能有子节点)。
  • 当前节点做为父节点传入方法中。
  • 咱们要一个一个的比较子节点,即便会遇到undefined的状况。没有关系,咱们的方法能够处理。
  • index,当前节点在直接父节点中的位置。
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) {
    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里看看代码把!

结语

祝贺你!咱们搞定了。咱们写出了虚拟节点的实现。从上面的例子中你已经能够理解虚拟节点的概念了,也大致能够知道React是如何运做的了。

当时还有不少须要讲述的内容,其中包括:

  • 设置节点的属性(props)和比较、更新他们
  • 处理事件,在元素上添加事件监听器
  • 让咱们的节点像React的Component那样运做
  • 获取实际DOM的引用
  • 虚拟节点和其余的库一块儿使用来修改真实的DOM,这些库有jQuery等其余的相似的库。
  • 更多。。

原文地址:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060

相关文章
相关标签/搜索