vaakian / vaakian.github.io

some notes
https://vaakian.github.io
3 stars 0 forks source link

手撸VDOM——渲染篇 #33

Open vaakian opened 2 years ago

vaakian commented 2 years ago

1. 创建

定义Element类

每个vdom元素都是一个Element,其中tag表示标签名,props表示dom属性,children表示元素内的孩子节点。

function Element(tag, props, children = []) {
    this.tag = tag;
    this.props = props;
    this.children = children;
    if (props.key) {
      this.key = this.key;
    }
    let count = children.length;
    children.forEach((child, i) => {
      if (child instanceof Element) {
        // 是DOM节点,统计个数
        count += child.count;
      } else {
        // 否则直接转换为字符串,当作渲染内容
        children[i] = "" + child;
      }
    });
    this.count = count;
  }
// 创建一个vdom元素
let div = new Element('div', { class: 'container' }, children ['Hello, Vaakian!'])

通过new运算符比较繁杂,不太直观,所以再简写一个函数el

  const el = function () {
    return new Element(...arguments);
  };

创建vdom

那么vdom就可以像这样创建:

const ul = el("ul", { class: "nav" }, [
    el("li", { class: "nav-item" }, ["AAA"]),
    el("li", { class: "nav-item" }, ["BBB"]),
    el("li", { class: "nav-item" }, ["CCC"])
  ]);
  const img = el("img", {
    src: "https://avatars.githubusercontent.com/u/30516060?v=4"
  });
  const vdom = el("div", { class: "container" }, [
    el("p", {}, ["Hello, World"]),
    ul,
    img
  ]);

2. 渲染

以上通过自建vdom对象,描述出网页内dom的结构,那么下一步就是将vdom渲染成浏览器所能接受的真实dom元素。 主要是通过递归+前序遍历方式来创建,当然层序也能够完成,只是个人选择。

function renderVDOM(vdom) {
    let dom = document.createElement(vdom.tag);
    // 设置属性
    for (let [prop, value] of Object.entries(vdom.props)) {
      dom.setAttribute(prop, value);
    }
    // 渲染孩子节点
    for (let child of vdom.children) {
      // 如果是Element,递归创建dom
      if (child instanceof Element) dom.append(renderVDOM(child));
      // 否则是文本,直接插入
      else dom.append(child);
    }

    return dom;
  }

测试结果 image

挂载

通过renderVDOM函数,可以将vdom渲染成真实可以挂载的dom元素对象,然后还需要提供一个类似react/vuemount函数,也很简单:

// 挂载
  function mount(vdom, element) {
    element.append(renderVDOM(vdom));
  }

那么创建、渲染、挂载完整的代码如下:

<body>
   <div id="app"></div>
</body>
<script>
const ul = el("ul", { class: "nav" }, [
    el("li", { class: "nav-item" }, ["AAA"]),
    el("li", { class: "nav-item" }, ["BBB"]),
    el("li", { class: "nav-item" }, ["CCC"])
  ]);
  const img = el("img", {
    src: "https://avatars.githubusercontent.com/u/30516060?v=4"
  });
  const vdom = el("div", { class: "container" }, [
    el("p", {}, ["Hello, World"]),
    ul,
    img
  ]);

mount(renderVDOM(vdom), document.getElementById('app'))
</script>

在线预览: https://codesandbox.io/embed/vaakian-vdom-render-r7mgf?fontsize=14&hidenavigation=1&theme=dark

vaakian commented 2 years ago

差异更新

差异更新主要是指vdom元素变化,对应的真实dom也要变化。有两种解决方式,第一种就是最简单的把整颗vdom树全部重新渲染,替换原来的dom,这样显然是不可取的,有太多太多不必要的性能消耗。第二种就是差异更新,通过diff算法对比差异,然后对局部变更进行相应更新,使更新效率大大提升。