前言
在配置 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 算法,本文为个人学习过程中的总结,如果有错误的地方烦请批评指正。