VUE3小实验 - 渲染 + 响应式数据
小实验 - 渲染 + 响应式数据
渲染
- 这里写一个编译时的小编译器。
前面说了HTML是树型结构的
<body>
<main></main>
<footer></footer>
</body>
而JS恰好拥有各类元素的创建结构(DOM为JS提供的控制文档树的结构)
document.createElement(); // div input a ...
所以我们可以使用一种树型的数据结构,并根据这个树型的数据结构在内存中把文档树生成出来,随后绑定在真实文档树上显示出来。
- 树型数据结构
- 几番考虑之下,这里选择如下的数据结构
type HTMLNode = {
tag: string,
id?: string,
children?: HTMLNode[], // 子
innerText?: string,
props?: [
{
propname: string,
value: string
}
],
methods?: [
{
funcName: string,
funcBody: (...args: any[]) => any
}
]
}
- 渲染过程也很好理解
- 先根据tag创建一个HTMLElement
- 随后查阅当前的各种属性是否存在,如果存在则主动添加到这个HTMLElement上
- 最终把这个HTMLElement返回回来
function render(node: HTMLNode): HTMLElement {
// 1. TAG to Element
let curElement = document.createElement(node.tag);
if (node.id) {
curElement.id = node.id;
}
if (node.innerText) {
console.log("节点的内部是", node.innerText);
curElement.innerText = node.innerText as string;
}
if (node.methods) {
for (let methods of node.methods) {
// @ts-ignore
curElement.addEventListener(methods.funcName, methods.funcBody);
}
}
if (node.props) {
for (let prop of node.props) {
curElement.setAttribute(prop.propname, prop.value); // 属性赋值
}
}
if (node.children) {
for (let child of node.children) {
curElement.appendChild(render(child));
}
}
return curElement;
}
- 如此我们就已经可以利用这个小编译器画一些东西出来
- 代码贴在最后
响应
- 创建代理的过程就好像是把数据的使用权交给某个新的数据
- 当代理人遇到修改数据的请求时,可以先自己斟酌,随后传递给数据的所有者。
- 具体代码
const 大聪明 = {
name: "大聪明",
age: 9999,
des: "身高八尺,力大无穷~",
告诉(消息) {
// 告诉大聪明
}
}
const 代理小伙 = new Proxy(大聪明, {
// 代理信息
见(大聪明, 请求) {
let 思考结果 = 深思熟虑(请求); // 好
大聪明.告诉(思考结果)
}
})
- 那么回到响应设计中来,当数据发生变化的时候,我们希望可以追踪这些数据的变化,并且及时反馈到屏幕上,具体表现为数据的响应式更新。
- 我们固然可以写一个setInterval(), 每当发现数据不一致就更新,但是性能不高。
- 代理就是我们的新选择。
- 我们把访问过这些数据的所有函数存在一个盒子里面(直接与页面展示有关的)
- 当数据发生更新的时候就通过js操作DOM修改页面中的数据。
为了使得所有函数都能装载进去,我们给传入的渲染函数重命名一下
let affectFunc: AnyFunc | undefined = undefined; // 未知或者是一个函数
const effect = (callFunc: AnyFunc) => {
affectFunc = callFunc; // 当前副作用函数赋值
console.log("当前affectFunc", affectFunc);
return callFunc(); // 内部可以包裹参数
}
这就是我们要的渲染函数盒子
随后写一下代理人工厂函数:
- get事件,获取这个数据中某个属性时触发,那么我们可以在此处添加当前的渲染函数到盒子(WeakMapBucket)
- set事件,当出现重新赋值的时候,说明数据发生了改变,那么应当对绑定在当前数据中的所有数据进行重新渲染。
function reactive<T extends object>(data: T) {
let curVariableFuncMap: VarBucket;
const dataProxy = new Proxy(data, {
// 对数据采用代理
get(target, key) {
console.log("Get 事件");
if (affectFunc) {
if (!WeakMapBucket.get(target)) {
WeakMapBucket.set(target, (curVariableFuncMap = new Map())); // 创建一个函数存储
}
let propertyFuncMap: Set<AnyFunc> | undefined = curVariableFuncMap.get(key);
if (!propertyFuncMap) {
// 当前键下未发现集合, 创建集合
curVariableFuncMap.set(key, (propertyFuncMap = new Set<AnyFunc>()));
}
propertyFuncMap.add(affectFunc);
}
// @ts-ignore
return target[key];
},
set(target, key, newVal) {
// 修改时
// @ts-ignore
target[key] = newVal;
let curVariableFuncMap = WeakMapBucket.get(target);
if (curVariableFuncMap) {
curVariableFuncMap.get(key)?.forEach(func => {
console.log(func);
func();
});
}
return true;
}
})
return dataProxy;
}
最后为了使得所有响应式数据都存在一起,我们使用闭包把这些数据封装在一起。
function cluster(): {
effect: AnyFunc,
reactive: AnyFunc
} {
let affectFunc: AnyFunc | undefined = undefined; // 未知或者是一个函数
const effect = (callFunc: AnyFunc) => {
affectFunc = callFunc; // 当前副作用函数赋值
console.log("当前affectFunc", affectFunc);
return callFunc(); // 内部可以包裹参数
}
const WeakMapBucket: FuncBucket = new WeakMap<FuncBucket>(); // 创造一个weakmap
function reactive<T extends object>(data: T) {
let curVariableFuncMap: VarBucket;
const dataProxy = new Proxy(data, {
// 对数据采用代理
get(target, key) {
console.log("Get 事件");
if (affectFunc) {
if (!WeakMapBucket.get(target)) {
WeakMapBucket.set(target, (curVariableFuncMap = new Map())); // 创建一个函数存储
}
let propertyFuncMap: Set<AnyFunc> | undefined = curVariableFuncMap.get(key);
if (!propertyFuncMap) {
// 当前键下未发现集合, 创建集合
curVariableFuncMap.set(key, (propertyFuncMap = new Set<AnyFunc>()));
}
propertyFuncMap.add(affectFunc);
}
// @ts-ignore
return target[key];
},
set(target, key, newVal) {
// 修改时
// @ts-ignore
target[key] = newVal;
let curVariableFuncMap = WeakMapBucket.get(target);
if (curVariableFuncMap) {
curVariableFuncMap.get(key)?.forEach(func => {
console.log(func);
func();
});
}
return true;
}
})
return dataProxy;
}
return {
effect,
reactive
};
}
- 最终导出effect 效果盒子, 以及 reactive 响应式数据构造函数
至此我们以及写好了一个简单的渲染 + 响应式的小引擎了。
小实验 做一个简单的小页面
import { HTMLNode } from "./types.js"; // 类型
import {render, effect, reactive, getElementProperty} from './utils/utils.js'; // 导入
const data = reactive({
username: "",
age: 0
}); // 创造响应式数数据
const header: HTMLNode = {
tag: 'p',
innerText: "记录",
props: [
{
propname: 'style',
value: "font-family: FangSong; font-size: 24px; color: #578689"
}
]
}
const inputName: HTMLNode = {
tag: 'input',
id: "username",
props: [
{
propname: "type",
value: "text"
}
],
methods: [
{
funcName: "input",
funcBody: () => {
data.username = getElementProperty('username', 'value'); // 拿到value数值绑定
}
}
]
}
const ageArea: HTMLNode = {
tag: 'input',
id: 'age',
props: [
{
propname: "type",
value: "number"
}
],
methods: [
{
funcName: "input",
funcBody: () => {
data.age = getElementProperty('age', 'value'); // 拿到value数值绑定
}
}
]
}
const showArea: HTMLNode = {
tag: 'div',
id: 'show',
}
const divArea: HTMLNode = {
tag: 'div',
props: [
{
propname: "style",
value: "background-color: #f3f3f3; display: flex; flex-direction: column; text-align: center"
}
],
children: [
header,
inputName,
ageArea,
showArea
]
}
document.body.appendChild(render(divArea));
effect(() => {
// 第一个效果
document.querySelector('#show')!.innerHTML = "我叫" + data.username + "今年" + data.age.toString() + "岁";
})
- 所有types
export type HTMLNode = {
id?: string,
tag: string,
children?: HTMLNode[], // 子
innerText?: string | ((...args: any[]) => string),
props?: [
{
propname: string,
value: string
}
],
methods?: [
{
funcName: string,
funcBody: (...args: any[]) => any
}
]
}
export type AnyFunc = (...args: any[]) => any
export type FuncBucket = WeakMap<object, Map<string | symbol, Set<AnyFunc>>>
export type VarBucket = Map<string | symbol, Set<AnyFunc>>;