手把手教你写前端框架(一):渲染DOM元素

本系列文章是分享我自己一步一步编写整个框架的过程,有兴趣的xdm可以参考源代码阅读。git仓库:github.com/sullay/art-…

渲染DOM元素

本文作为本系列的第一篇文章,也是我们编写前端框架的第一步,我们先完成框架中最重要也是最基础的功能也就是渲染DOM元素。

下面是我们经常会见到的一段html代码。

<div style="margin: 100px auto; text-align: center;">
    <p style="color: red;">Hello World!</p>
</div>
复制代码

image.png

我们先考虑如何用js对象来抽象上述的dom结构,下面是我写给出的答案。

{
    type: "div",
    props: {style"margin: 100px auto; text-align: center;"},
    children:[{
        type: "p",
        props: {style'color:red;'},
        children:["Hello World!"]
    }]
}
复制代码

有了上面的js对象,我们可以开始反向思考如何把js对象渲染成页面需要dom元素。

我们需要编写一个render方法,这个方法接收两个参数,第一个参数element是我们上面构造的js对象,第二个参数parentDom是我们渲染好的元素对应的挂载点。

function render(element, parentDom) {
  let dom;
  if (typeof element === "string") {
    dom = document.createTextNode(element)
  } else {
    const { type, props, children } = element;
    // 根据元素类型创建对应的dom
    dom = document.createElement(type);
    // 设置属性
    for (let key in props) dom[key] = props[key];
    // 渲染子元素
    for (let child of children) render(child, dom);
  }
  // 绑定到父元素
  parentDom.appendChild(dom);
}
复制代码

此时我们已经编写好了render方法,在浏览器中测试一下,已经可以正常的将js对象渲染到页面上。 image.png

虚拟DOM与jsx

通过render方法,我们已经可以将js对象渲染成对应的dom元素了。但是通过html文本我们能够更清楚的了解页面的结构,但是仅仅通过js对象我们很难想象出页面该长什么样子,这时候我们就需要预编译将我们编写的类html文本处理成js对象。

jsx语法

考虑到esbuild、bable、ts都是支持jsx语法的,这里我们直接使用jsx语法,简化我们js对象的创建。 如果不清楚jsx语法的同学建议先了解一下,再阅读后续内容。

这里我们使用esbuild预处理jsx文件,命令如下:

esbuild xxx.jsx --bundle --jsx-factory=h --jsx-fragment=Fragment --outfile=xxx.out.js
复制代码

我们使用esbuild对下面的代码进行处理

// 处理前代码
render(
  <div style="margin: 100px auto; text-align: center;">
    <p style="color:red;">Hello World!</p>
  </div>,
  document.getElementById('root')
)

// 处理后代码
(() => {
  render(/* @__PURE__ */ h("div", {
    style: "margin: 100px auto; text-align: center;"
  }, /* @__PURE__ */ h("p", {
    style: "color:red;"
  }, "Hello World!")), document.getElementById("root"));
})();
复制代码

可以看到处理后的代码中多出了一个h方法,这个方法是jsx用于创建js对象的工厂函数,可以通过命令行终端jsx-factory参数修改函数名。

在开始编写我们的工厂函数h之前,我们可以重新整理一下我们的js对象。

function isEvent(key) {
  return key.startsWith('on');
}

function getEventName(key) {
  return key.toLowerCase().replace(/^on/, "");
} 

// 普通元素
class vNode {
  constructor(type = '', allProps = {}, children = []) {
    this.type = type;
    this.props = {};
    this.events = {};
    for (let prop in allProps) {
      if (isEvent(prop)) {
        this.events[getEventName(prop)] = allProps[prop];
      } else {
        this.props[prop] = allProps[prop];
      }
    }
    // 处理子元素中的文字类元素
    this.children = children.map(child => {
      return vNode.isVNode(child) ? child : new vTextNode(child);
    });
  }
  // 判断是否属于虚拟Dom元素
  static isVNode(node) {
    return node instanceof this;
  }
}

// 文字元素
class vTextNode extends vNode {
  constructor(text) {
    super(vTextNode.type, { nodeValue: text })
  }
  static type = Symbol('TEXT_ELEMENT');
}

// 创建元素
export function h(type, props, ...children) {
  return new vNode(type, props, children);
} 
复制代码

这里我们编写了两个类分别表示我们的普通元素与文字元素,并且将事件与其他属性进行了区分,然后我们编写了h方法帮我们创建虚拟DOM对象。

此时更新一下我们的render方法,新增了事件监听。

function render(element, parentDom) {
  if (!vNode.isVNode(element)) throw new Error("渲染元素类型有误");
  const { type, props, events, children } = element;
  // 根据元素类型创建对应的dom
  const dom = vTextNode.isVNode(element) ? document.createTextNode('') : document.createElement(type);
  // 设置属性
  for (let key in props) dom[key] = props[key];
  // 渲染子元素
  for (let child of children) render(child, dom);
  // 监听事件
  for (let event in events) dom.addEventListener(event, events[event])
  // 绑定到父元素
  parentDom.appendChild(dom);
}
复制代码

到此为止,我们的框架已经基本完成了dom渲染功能。

下一边文章中将主要介绍自定义组件的实现,感兴趣的xdm可以关注我后续的更新。

猜你喜欢

转载自juejin.im/post/7080566613614788644