实现一个前端框架(UI库)

1. 目标

  • 仅依靠原生能力(js、css、html)。不引入工程化思想。
  • 只有函数式组件。
  • 符合直觉,足够简约。

2. yu

框架的中文名叫做。英文名也叫做yu

3. 分步实现

3.1 函数式组件

直觉上函数是很适合用于组件定义的。 函数接收参数返回值。组件接收属性返回视图。

							
								function(params) {
									return;
								}
							
						
							
								function Component(props) {
									const state:any;
	
									const el:HTMLElement;
	
									return el;
								}
							
						

虽然我们最终期望组件返回视图元素。但如果不在组件和元素之间建立一种数据结构,那更复杂的变换将无从下手。 所以yu定义了Node数据结构。组件的定义现在变成这样:

					
						interface Node {							
							typeof: Symbol;				
							type: Function | "string";							
							props: Object;							
							children: Node | Node[];							
						}

						function Component(props) {
							const state:any;

							const node:Node;

							return node;
						}
					
				

定义了Node数据结构,但我们不可能手动去定义整个应用程序的组件结构。为了书写方便,yu定义了一个高阶函数: h 。

					
						function h(Component) {
							return function WrapperComponent(_props, _children) {
							  const { props, children } = propsChildren(_props, _children);
						  
							  return {
									type: Component,
									props,
									children,
									typeof: Symbol.for("yu.node"),
							  };
							};
						}
					
				

所有自定义组件在导出前必须手动调用h。

					
						import { h } from "../yu/index.js";

						function TodoAdder({ onAdd }) {
							const useState = this.useState;

							const [value, setValue] = useState("");

							return div([
								input({
									value: value,
									oninput: (e) => {
											setValue(e.target.value);
									},
								}),
								button({
									textContent: "添加",
									onclick: () => {
										if (value) {
											onAdd(value);
										}
									},
								}),
							]);
						}

						export default h(TodoAdder);
					
				

3.2 html元素

无论什么前端ui库,最终的最小视图都是html元素。那如何高效地创建元素呢? 社区通常有两种做法:

但是它们都不符合yu设定的目标。yu把html元素也看作组件。暴露在window上。

					
						["div", "input", "label", "button", "fragment"].forEach((type) => {
							window[type] = function TagComponent(_props, _children) {
							  const { props, children } = propsChildren(_props, _children);
						  
							  return {
									type,
									props,
									children,
									typeof: "yu.node",
								};
							};
						});
					
				

3.3 state

state是组件实例的可变数据。yu是通过bind函数向useState注入组件实例的信息。

							
								import originalUseState from "./useState.js";

								
								function bind(component, keyPath) {
									const useState = originalUseState.bind({
										keyPath,
										hookIndex: 0,
									});

									return component.bind({ useState });
								}

								export default bind;
							
						
							
								if (isFunction(type)) {
									type = bind(type, keyPath);
								
									children = type(props);
									node.children = children;
								}
							
						
						
							import { update } from "../dom/render.js";

							const states = {};

							function useState(initialValue) {
							
								const key = `${this.keyPath.join(".")}.${this.hookIndex}`;
								this.hookIndex += 1;

								if (states[key] === undefined) {
									if (initialValue !== undefined) {
									states[key] = initialValue;
									}
								}

								const setState = (value) => {
									states[key] = value;

									update(this.keyPath);
								};

								return [states[key], setState];
							}

							export default useState;
						
					

问题:有没有发现目前做法的问题?提示词:children、key

3.4 diff算法

无论多么复杂的组件结构,人总是知道哪些被更改过。而程序却很难知道,或者要做很多工作才能像人一样知道。

										
						function diff(appNode, initKeyPath, callback) {
							//旧的节点
							const prevNode = initKeyPath.reduce((node, key) => node[key], appNode);

							//新的节点
							let nextNode = cloneNode(prevNode);
							expandNode(nextNode, initKeyPath);

							console.log(prevNode, nextNode, initKeyPath);
							//比较节点差异
							diffNode(prevNode, nextNode, prevNode.parent);

							//用新的节点替换旧的
							replace(appNode, initKeyPath, nextNode, callback);
						}
					
				

未完待续...