【框架源码学习】JSX 如何解析成 DOM 节点

前言

在配置 omi-vite 脚手架中,发现需要在 vite.config.js 中配置 h() 和 Fragment ,才能成功启动,否则就是默认将 JSX 转换为 React.createElement() ,本文为作者在学习探索过程中的一些总结,有些的错误的地方,还请批评指教。

什么是 JSX

JSX 全称 JavaScript XML , 本质就是 React.createElement() 语法糖. 可以通过 Babel 或者是 TypeScript 编译成 js

JSX 使用流程

以 React 为例

let root = document.getElementById('root')
function App(){
    return(<h1>Hello, world!</h1>)
}
ReactDOM.render(<App/>,root);
复制代码

通过 Babel 对 jsx 转译

"use strict";
let root = document.getElementById('root');
function App() {
  return /*#__PURE__*/React.createElement("h1", null, "Hello, world!");
}
ReactDOM.render( /*#__PURE__*/React.createElement(App, null), root);
复制代码

不难看出解析器将 JSX 解析成了 React.createElement , React.createElement 创建了 Virtual dom 并通过 ReactDOM.render 挂载成真正的 DOM 节点。

VDOM

Virtual dom, 即虚拟DOM节点。它通过JS的Object对象模拟DOM中的节点,然后再通过特定的render方法将其渲染成真实的DOM节点。通过diff算法解析虚拟 dom 的变化触发重新渲染。
虚拟 DOM 本身就是一个普通的 JavaScript 对象,包含了 tag、props、children 三个属性。传统的 DOM 对象包含了太多的 attributes ,因此操作起来较费性能。

实现一个自己的 VDOM 生成器 h()

首先要定义下转换的代码中调用的 React.createElement() 函数,也就是h()。

大部分框架中中定义的 createElement 函数的别名为 h()

function h(nodeName, attributes, ...args) {
      let children = args.length ? [].concat(...args) : null;
      return { nodeName, attributes, children };
}
复制代码

通过h()方法输出一个嵌套的树状对象,也就是所说的 VDOM 对象

{
  nodeName: "div",
  attributes: {
    "id": "hello"
  },
  children: ["Hello!"]
}
复制代码

同时还需要一个将 VDOM 转换为 DOM 节点中的 render()

function render(vnode) {
    // 如果 vnode 为字符串则创建文本节点
    if (typeof(vnode)=='string') return document.createTextNode(vnode);
    
    //根据 vnode 的名字创建节点
    let n = document.createElement(vnode.nodeName);
    
    // 为新的节点 setAttribute
    let a = vnode.attributes || {};
    Object.keys(a).forEach( k => n.setAttribute(k, a[k]) );
    
    // 将每个节点挂载到递归挂载在其父级上
    (vnode.children || []).forEach( c => n.appendChild(render(c)) );
    return n;
}
复制代码

最后调用 document.body.appendChild(render( <hello>"Hello!"</hello> )); 将转换后的 DOM 插入文档中。

来看一下其他框架的 h() 实现

Omi

Omi 是一个基于 webcomponent+JSX 的 前端跨框架框架

export function h(nodeName, attributes) {
  let children = [],
    lastSimple,
    child,
    simple,
    i
    
  for (i = arguments.length; i-- > 2; ) {
    stack.push(arguments[i])
  }
  if (attributes && attributes.children != null) {
    if (!stack.length) stack.push(attributes.children)
    delete attributes.children
  }
  
  while (stack.length) {
    if ((child = stack.pop()) && child.pop !== undefined) {
      for (i = child.length; i--; ) stack.push(child[i])
    } else {
      if (typeof child === 'boolean') child = null
      
      if ((simple = typeof nodeName !== 'function')) {
        if (child == null) child = ''
        else if (typeof child === 'number') child = String(child)
        else if (typeof child !== 'string') simple = false
      }

      if (simple && lastSimple) {
        children[children.length - 1] += child
      } else if (children.length === 0) {
        children = [child]
      } else {
        children.push(child)
      }

      lastSimple = simple
    }
  }

  if (nodeName === Fragment) {
    return children
  }

  const p = {
    nodeName,
    children,
    attributes: attributes == null ? undefined : attributes,
    key: attributes == null ? undefined : attributes.key
  }

  // if a "vnode hook" is defined, pass every created VNode to it
  if (options.vnode !== undefined) options.vnode(p)

  return p
}
复制代码

fre

Fre 是一个基于 Fiber 架构的前端框架,源码写的很精彩值得学习

export const h = (type, props: any, ...kids) => {
  props = props || {}
  
  const c = arrayfy(props.children || kids)
  
  kids = flat(c).filter((i) => i != null)
  
  if (kids.length) props.children = kids.length === 1 ? kids[0] : kids
  let key = props.key || null,
    ref = props.ref || null
  
  delete props.key
  delete props.ref
  return createVnode(type, props, key, ref)
}

export const arrayfy = (arr) => (!arr ? [] : isArr(arr) ? arr : [arr])

const flat = (arr) =>
  [].concat(
    ...arr.map((v) =>
      isArr(v) ? [].concat(flat(v)) : isStr(v) ? createText(v) : v
    )
  )

export const createVnode = (type, props, key, ref) => ({
  type,
  props,
  key,
  ref
})

export const createText = (vnode: any) =>
  ({ type: "", props: { nodeValue: vnode + "" } } as FreElement)

export function Fragment(props) {
  return props.children
}

export const isArr = Array.isArray
复制代码

Vue3

export function h(type: any, propsOrChildren?: any, children?: any): VNode {
  const l = arguments.length
  if (l === 2) {
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      if (isVNode(propsOrChildren)) {
        return createVNode(type, null, [propsOrChildren])
      }
      return createVNode(type, propsOrChildren)
    } else {
      return createVNode(type, null, propsOrChildren)
    }
  } else {
    if (l > 3) {
      children = Array.prototype.slice.call(arguments, 2)
    } else if (l === 3 && isVNode(children)) {
      children = [children]
    }
    return createVNode(type, propsOrChildren, children)
  }
}
复制代码

总结

整体而言,实现 h() 是使用 JSX 的一大重点,大部分框架都有自己不同的具体实现,但是宗旨相同,都是生成虚拟 DOM ,而大多数的框架核心不同在于虚拟 DOM 变化的跟踪,也就是 Diff 算法,本文为个人学习过程中的总结,如果有错误的地方烦请批评指正。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享