实现简易版的virtual Dom | 刷题打卡

782 阅读2分钟

题目描述

HTML可以被当作某种意义上的 序列化→浏览器解释(反序列化)HTML文本→ 然后构建DOM tree。
除了基于XML的方案,我们也可以试试JSON。如果打印出React中对element的表示,像这样:

const el = <div>
 <h1> this is </h1>
 <p className="paragraph"> a <button> button </button> from <a href="https://bfe.dev"><b>BFE</b>.dev</a>
 </p>
</div>;

console.log(el)

得到如下数据结构:

{
  type: 'div',
  props: {
    children: [
      {
        type: 'h1',
        props: {
          children: ' this is '
        }
      },
      {
        type: 'p',
        props: {
          className: 'paragraph',
          children: [
            ' a ',
            {
              type: 'button',
              props: {
                children: ' button '
              }
            },
            ' from',
            {
              type: 'a',
              props: {
                href: 'https://bfe.dev',
                children: [
                  {
                    type: 'b',
                    props: {
                      children: 'BFE'
                    }
                  },
                  '.dev'
                ]
              }
            }
          ]
        }
      }
    ]
  }
}

题目链接

思路分析

要实现jsx <=> virtual dom,上来不太好判断难度,从处理数据生成html的流程更符合平时开发逻辑,就从 vdom -> jsx 开始吧!
最直观的规律,virtual dom 是一棵树,我们需要确定父子关系

  • 一个 type 对应 *个children
  • children 包含字符串节点和带标签的节点
  • 一个 type 对应 一个 props
  • props 中除了 children 其余都是元素上的属性

以上4条理顺了,就可以开始写代码了

function render(json) {
  // textNode
  if (typeof json === 'string') {
    return document.createTextNode(json);
  }
  
  // element
  const {type, props: {children, ...attrs}} = json;
  // 出现 type 就创建一个节点
  const element = document.createElement(type);
  
  // 给节点加上属性
  Object.entries(attrs).forEach(([attr, value]) => element[attr] = value);
  
  // 把 children 添加到节点内
  const childrenArr = Array.isArray(children) ? children : [children];
  childrenArr.forEach(child => element.append(render(child)));
  
  return element;
}

然后需要完成jsx => vdom,第一反应是 vdom 数据结构(长相)和 jsx 是一样的,是对数据结构进行扩展
大致应该是这样

function virtualize(element) {
  const result = {
    type: element.tagName,
    props: {}
  };
  
  // 先添加属性
  element.attributes.forEach... props[k] = v;
  
  // 再添加 children,循环节点递归(添加属性和children)
  element.childNodes.forEach... props.children = newChildren
}

顺着这个思路把代码补全:

function virtualize(element) {
  const result = {
    type: element.tagName.toLowerCase(),
    props: {}
  }

  // attrs
  element.attributes.forEach(attr => {
    const name = attr.name === 'class' ? 'className' : attr.name;
    result.props[name] = attr.value;
  });
  
  // children
  const children = [];
  element.childNodes.forEach(node => {
    if (node.nodeType === 3) {
      children.push(node.textContent);
    } else {
      children.push(virtualize(node));
    }
  });
  
  result.props.children = children.length === 1 ? children[0] : children;

  return result;
}

AC代码

function render(json) {
  // textNode
  if (typeof json === 'string') {
    return document.createTextNode(json);
  }
  
  // element
  const {type, props: {children, ...attrs}} = json;
  // 出现 type 就创建一个节点
  const element = document.createElement(type);
  
  // 给节点加上属性
  Object.entries(attrs).forEach(([attr, value]) => element[attr] = value);
  
  // 把 children 添加到节点内
  const childrenArr = Array.isArray(children) ? children : [children];
  childrenArr.forEach(child => element.append(render(child)));
  
  return element;
}

function virtualize(element) {
  const result = {
    type: element.tagName.toLowerCase(),
    props: {}
  }

  // attrs
  element.attributes.forEach(attr => {
    const name = attr.name === 'class' ? 'className' : attr.name;
    result.props[name] = attr.value;
  });
  
  // children
  const children = [];
  element.childNodes.forEach(node => {
    if (node.nodeType === 3) {
      children.push(node.textContent);
    } else {
      children.push(virtualize(node));
    }
  });
  
  result.props.children = children.length === 1 ? children[0] : children;

  return result;
}

总结

完成了 vdomjsx 的简单转换,在此基础上我们可以尝试写一个简易版的React.createElement 题目链接
写一个 renderh 函数,分别负责生成 jsxvdom

render(h(
  'div',
  {},
  h('h1', {}, ' this is '),
  h(
    'p',
    { className: 'paragraph' },
    ' a ',
    h('button', {}, ' button '),
    ' from ',
    h('a', 
      { href: 'https://bfe.dev' }, 
      h('b', {}, 'BFE'),
      '.dev')
  )
))

h 函数对应上文的 vdom

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

render 函数如上文

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情