基于JSX 的动态数据绑定
基于JSX 的动态数据绑定
笔者在
JSX 解析与DOM 元素构建
元素构建
笔者在 createElement
调用,这里需要在项目的
{
"plugins": [
"transform-decorators-legacy",
"async-to-promises",
[
"transform-react-jsx",
{
"pragma": "createElement"
}
]
]
}
这里的
/**
* Description 从 JSX 中构建虚拟 DOM
* @param tagName 标签名
* @param props 属性
* @param childrenArgs 子元素列表
*/
export function createElement(
tagName: string,
props: propsType,
...childrenArgs: [any]
) {}
该函数包含三个参数,分别指定标签名、属性对象与子元素列表;实际上经过
// ...
(0, _createElement.createElement)(
'section',
null,
(0, _createElement.createElement)(
'section',
null,
(0, _createElement.createElement)(
'button',
{ className: 'link', onClick: handleClick },
'Custom DOM JSX'
),
(0, _createElement.createElement)('input', {
type: 'text',
onChange: function onChange(e) {
console.log(e);
}
})
)
),
// ...
在获取到元素标签之后,我们首先要做的就是创建元素;创建元素 createElementByTag 过程中我们需要注意区分普通元素与
export const createElementByTag = (tagName: string) => {
if (isSVG(tagName)) {
return document.createElementNS('http://www.w3.org/2000/svg', tagName);
}
return document.createElement(tagName);
};
属性处理
在创建了新的元素对象之后,我们需要对
const children = flatten(childrenArgs).map(child => {
// 如果子元素同样为 Element,则创建该子元素的副本
if (child instanceof HTMLElement) {
return child;
}
if (typeof child === 'boolean' || child === null) {
child = '';
}
return document.createTextNode(child);
});
这里可以看出,对
...// 同时支持 class 与 className 设置
const className = props.class || props.className;
// 如果存在样式类,则设置
if (className) {
setAttribute(tagName, el, 'class', classNames(className));
}
// 解析行内样式
getStyleProps(props).forEach(prop => {
el.style.setProperty(prop.name, prop.value);
});
// 解析其他 HTML 属性
getHTMLProps(props).forEach(prop => {
setAttribute(tagName, el, prop.name, prop.value);
});
// 设置事件监听,这里为了解决部分浏览器中异步问题因此采用同步写法
let events = getEventListeners(props);
for (let event of events) {
el[event.name] = event.listener;
}...
if (setHTML && setHTML.__html) {
el.innerHTML = setHTML.__html;
} else {
children.forEach(child => {
el.appendChild(child);
});
}
到这里我们就完成了针对
简单使用
这里我们依旧使用 create-webpack-app
// App.js
import { createElement } from "../../../src/dom/jsx/createElement";
// 页面内状态
const state = {
count: 0,
};
/**
* Description 点击事件处理
* @param e
*/
const handleClick = (e) => {
state.count++;
document.querySelector("#count").innerText = state.count;
};
export default (
<div className="header">
{" "}
<section>
{" "}
<section>
{" "}
<button className="link" onClick={handleClick}>
Custom DOM JSX{" "}
</button>
<input
type="text"
onChange={(e) => {
console.log(e);
}}
/>
{" "}
</section>{" "}
</section>
<svg>
<circle cx="64" cy="64" r="64" style="fill: #00ccff;" />
{" "}
</svg>
<br />
{" "}
<span id="count" style={{ color: "red" }}>
{state.count} {" "}
</span>{" "}
</div>
);
// client.js
// @flow
import App from "./component/Count";
document.querySelector("#root").appendChild(App);
数据绑定
当我们使用
然后在本页面的
<script>
var ele = document.querySelector("#inline-jsx");
Ueact.observeDOM(
ele,
{
state: {
count: 0,
delta: 1,
items: [1, 2, 3],
},
methods: {
handleClick: function () {
this.state.count += this.state.delta;
this.state.items.push(this.state.count);
},
handleChange: function (e) {
let value = parseInt(e.target.value);
if (!Number.isNaN(value)) {
this.state.delta = value;
}
},
},
hooks: {
mounted: function () {
console.log("mounted");
},
updated: function () {
console.log("updated");
},
},
},
Babel
);
</script>
这里我们调用Ueact.observeDOM
outerHTML
属性,然后通过
let input = html2JSX(ele.outerHTML);
let output = Babel.transform(input, {
presets: ["es2015"],
plugins: [
[
"transform-react-jsx",
{
pragma: "Ueact.createElement",
},
],
],
}).code;
值得一提的是,因为
- 自闭合标签处理,即
<input > => <input />
- 去除输入的
HTML 中的事件监听的引号,即onclick="{methods.handleClick}"
=> onclick={methods.handleClick}
- 移除
value 值额外的引号,即value="{state.a}"
=> value={state.a}
到这里我们得到了经过eval
函数,不过因为该函数直接暴露在了全局作用域下,因此并不被建议使用;我们使用动态构造
/**
* Description 从输入的 JSX 函数字符串中完成构建
* @param innerContext
*/
function renderFromStr(innerContext) {
let func = new Function(
"innerContext",
`
let { state, methods, hooks } = innerContext;
let ele = ${innerContext.rawJSX}
return ele;
`
).bind(innerContext); // 构建新节点
let newEle: Element = func(innerContext); // 使用指定元素的父节点替换自身
innerContext.root.parentNode.replaceChild(newEle, innerContext.root); // 替换完毕之后删除旧节点的引用,触发 GC
innerContext.root = newEle;
}
innerContext
即包含了我们定义的
变化监听与重渲染
笔者在
import { observe } from "../../dist/observer-x";
const obj = observe(
{},
{
recursive: true,
}
);
obj.property = {};
obj.property.listen((changes) => {
console.log(changes);
console.log("changes in obj");
});
obj.property.name = 1;
obj.property.arr = [];
obj.property.arr.listen((changes) => {
// console.log('changes in obj.arr');
});
// changes in the single event loop will be print out
setTimeout(() => {
obj.property.arr.push(1);
obj.property.arr.push(2);
obj.property.arr.splice(0, 0, 3);
}, 500);
核心即是当某个对象的属性发生变化
// ...
// 将内部状态转化为可观测变量
let state = observe(innerContext.state); // ...
state.listen((changes) => {
renderFromStr(innerContext);
innerContext.hooks.updated && innerContext.hooks.updated();
}); // ...
完整的在线