• 豌豆资源网
  • 开引网企业服务
  • 服务外包网
  • 如何编写自己的虚拟DOM

    要构建自己的虚拟DOM,需要知道两件事。你甚至不需要深入 react 的源代码或者深入任何其他虚拟DOM实现的源代码,因为它们是如此庞大和复杂——但实际上,虚拟DOM的主要部分只需不到50行代码。

    有两个概念:

    • Virtual DOM 是真实DOM的映射
    • 当虚拟 DOM 树中的某些节点改变时,会得到一个新的虚拟树。算法对这两棵树(新树和旧树)进行比较,找出差异,然后只需要在真实的 DOM 上做出相应的改变。

     

    用js对象模拟DOM树

    首先,我们需要以某种方式将 DOM 树存储在内存中。可以使用普通的 js 对象来做。假设我们有这样一棵树:

    <ul class=”list”>
      <li>item 1</li>
      <li>item 2</li>
    </ul>
    

    看起来很简单,对吧? 如何用JS对象来表示呢?

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

    这里有两件事需要注意:

    • 用如下对象表示DOM元素
    { type: ‘…’, props: { … }, children: [ … ] }
    
    
    • 用普通 JS 字符串表示 DOM 文本节点

    但是用这种方式表示内容很多的 Dom 树是相当困难的。这里来写一个辅助函数,这样更容易理解:

    function h(type, props, …children) {
      return { type, props, children };
    }
    

    用这个方法重新整理一开始代码:

    h(‘ul’, { ‘class’: ‘list’ },
      h(‘li’, {}, ‘item 1’),
      h(‘li’, {}, ‘item 2’),
    );
    
    

    这样看起来简洁多了,还可以更进一步。这里使用 JSX,如下:

    <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’),
    );
    
    

    是不是看起来有点熟悉?如果能够用我们刚定义的 h(...) 函数代替 React.createElement(…),那么我们也能使用JSX 语法。其实,只需要在源文件头部加上这么一句注释:

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

    它实际上告诉 Babel ' 嘿,小老弟帮我编译 JSX 语法,用 h(...) 函数代替 React.createElement(…),然后 Babel 就开始编译。'

    综上所述,我们将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’] }
      ] }
    );

     

    从Virtual DOM 映射到真实 DOM

    好了,现在我们有了 DOM 树,用普通的 JS 对象表示,还有我们自己的结构。这很酷,但我们需要从它创建一个真正的DOM。

    首先让我们做一些假设并声明一些术语:

    • 使用以' $ '开头的变量表示真正的DOM节点(元素,文本节点),因此 $parent 将会是一个真实的DOM元素
    • 虚拟 DOM 使用名为 node 的变量表示

    * 就像在 React 中一样,只能有一个根节点——所有其他节点都在其中

    那么,来编写一个函数 createElement(…),它将获取一个虚拟 DOM 节点并返回一个真实的 DOM 节点。这里先不考虑 props 和 children 属性:

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

    上述方法我也可以创建有两种节点分别是文本节点和 Dom 元素节点,它们是类型为的 JS 对象:

    { type: ‘…’, props: { … }, children: [ … ] }
    

    因此,可以在函数 createElement 传入虚拟文本节点和虚拟元素节点——这是可行的。

    现在让我们考虑子节点——它们中的每一个都是文本节点或元素。所以它们也可以用 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的基本概念,因为它们会增加复杂性。

    完整代码如下:

    /** @jsx h */
    
    function h(type, props, ...children) {
      return { type, props, 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 a = (
      <ul class="list">
        <li>item 1</li>
        <li>item 2</li>
      </ul>
    );
    
    const $root = document.getElementById('root');
    $root.appendChild(createElement(a));

     

    比较两棵虚拟DOM树的差异

    现在我们可以将虚拟 DOM 转换为真实的 DOM,这就需要考虑比较两棵 DOM 树的差异。基本的,我们需要一个算法来比较新的树和旧的树,它能够让我们知道什么地方改变了,然后相应的去改变真实的 DOM。

    怎么比较 DOM 树?需要处理下面的情况:

    • 添加新节点,使用 appendChild(…) 方法添加节点

    • 移除老节点,使用 removeChild(…) 方法移除老的节点

    • 节点的替换,使用 replaceChild(…) 方法

    如果节点相同的——就需要需要深度比较子节点

    编写一个名为 updateElement(…) 的函数,它接受三个参数—— $parentnewNode 和 oldNode,其中 $parent 是虚拟节点的一个实际 DOM 元素的父元素。现在来看看如何处理上面描述的所有情况。

     

    添加新节点

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

     

    移除老节点

    这里遇到了一个问题——如果在新虚拟树的当前位置没有节点——我们应该从实际的 DOM 中删除它—— 这要如何做呢?

    如果我们已知父元素(通过参数传递),我们就能调用 $parent.removeChild(…) 方法把变化映射到真实的 DOM 上。但前提是我们得知道我们的节点在父元素上的索引,我们才能通过 $parent.childNodes[index] 得到该节点的引用。

    好的,让我们假设这个索引将被传递给 updateElement 函数(它确实会被传递——稍后将看到)。代码如下:

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

     

    节点的替换

    首先,需要编写一个函数来比较两个节点(旧节点和新节点),并告诉节点是否真的发生了变化。还有需要考虑这个节点可以是元素或是文本节点:

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

    现在,当前的节点有了 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]
        );
      }
    }

    广州vi设计公司http://www.maiqicn.com 办公资源网站大全 https://www.wode007.com

    比较子节点

    最后,但并非最不重要的是——我们应该遍历这两个节点的每一个子节点并比较它们——实际上为每个节点调用updateElement(…)方法,同样需要用到递归。

    • 当节点是 DOM 元素时我们才需要比较( 文本节点没有子节点 )
    • 我们需要传递当前的节点的引用作为父节点
    • 我们应该一个一个的比较所有的子节点,即使它是 undefined 也没有关系,我们的函数也会正确处理它。
    • 最后是 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 (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
          );
        }
      }
    }
    

     

    完整的代码

    Babel+JSX
    /* @jsx h /

    function h(type, props, ...children) {
      return { type, props, 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;
    }
    
    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) {
      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
          );
        }
      }
    }
    
    // ---------------------------------------------------------------------
    
    const a = (
      <ul>
        <li>item 1</li>
        <li>item 2</li>
      </ul>
    );
    
    const b = (
      <ul>
        <li>item 1</li>
        <li>hello!</li>
      </ul>
    );
    
    const $root = document.getElementById('root');
    const $reload = document.getElementById('reload');
    
    updateElement($root, a);
    $reload.addEventListener('click', () => {
      updateElement($root, b, a);
    });
    
    

    html

    <button id="reload">RELOAD</button>
    <div id="root"></div>
    

    css

    #root {
      border: 1px solid black;
      padding: 10px;
      margin: 30px 0 0 0;
    }
    
    

    打开开发者工具,并观察当按下“Reload”按钮时应用的更改。

     

    总结

    现在我们已经编写了虚拟 DOM 实现及了解它的工作原理。作者希望,在阅读了本文之后,对理解虚拟 DOM 如何工作的基本概念以及在幕后如何进行响应有一定的了解。

    然而,这里有一些东西没有突出显示(将在以后的文章中介绍它们):

    • 设置元素属性(props)并进行 diffing/updating
    • 处理事件——向元素中添加事件监听
    • 让虚拟 DOM 与组件一起工作,比如React
    • 获取对实际DOM节点的引用
    • 使用带有库的虚拟 DOM,这些库可以直接改变真实的 DOM,比如 jQuery 及其插件
    posted @ 2020-09-16 09:53  前端一点红  阅读(549)  评论(0编辑  收藏  举报
  • 乐游资源网
  • 热爱资源网
  • 灵活用工代发薪平台
  • 企服知识
  • 355软件知识