5分钟内的UI框架


前段时间,我想知道为什么会有这么多的Web UI框架? 我从事IT已有很长时间了,我不记得其他平台上的UI库的诞生和死亡与WEB一样快。 桌面操作系统的库,例如:MFC,Qt,WPF等。 -是多年来发展起来并且没有大量替代品的怪物。 网络上的一切都不同-框架几乎每周都会发布,领导者会不断变化-为什么会这样?


我认为主要原因是编写UI库的复杂性急剧下降。 是的,要编写一个可供许多人使用的库-仍然需要花费大量时间和专业知识,但是要编写一个原型-将其包装在方便的API中即可使用-只需很少的时间。 如果您对如何完成此操作感兴趣,请继续阅读。


为什么写这篇文章?


一次在Habré上有一系列文章-在js上为30行代码编写X。


我认为-可以在30行中写出反应吗? 是的,对于30条线,我没有成功,但最终结果与该数字相当。


一般而言,本文的目的纯粹是教育性的。 它可以帮助您更深入地了解基于虚拟房屋的UI框架的原理。 在本文中,我想展示基于虚拟家庭创建另一个UI框架有多么简单。


首先,我想说一下UI框架的意思-因为许多人对此有不同的看法。 例如,有人认为Angular和Ember是一个UI框架,而React只是一个库,这将使使用应用程序的视图部分更容易。


我们将UI框架定义如下:在这个意义上,这是一个有助于创建/更新/删除页面或单个页面元素的库,因此DOM API上相当广泛的包装可能是UI框架,唯一的问题是该库提供的用于处理DOM的抽象选项(API)以及这些操作的有效性


用建议的措辞-React是一个UI框架。


好吧,让我们看看如何用21点及更多内容编写您的React。 众所周知,React使用虚拟家庭的概念。 以简化的形式,它包括以下事实:真实DOM的节点严格按照先前构建的虚拟DOM树的节点来构建。 不欢迎直接操作真实DOM,如果需要对真实DOM进行更改,则对虚拟DOM进行更改,然后将虚拟DOM的新版本与旧版本进行比较,收集需要应用到真实DOM的更改,并以最小化与真实DOM的交互的方式进行应用DOM-使应用程序更优化。


由于虚拟房屋树是一个普通的Java脚本对象-相当容易操作-更改/比较其节点,因此在这里我很容易理解这个词,即我理解汇编代码是虚拟的但非常简单,并且可以由预处理器部分地从高级JSX的声明性语言生成。


让我们从JSX开始


这是JSX代码的示例


const Component = () => ( <div className="main"> <input /> <button onClick={() => console.log('yo')}> Submit </button> </div> ) export default Component 

我们在调用Component函数时需要制作一个这样的虚拟DOM


 const vdom = { type: 'div', props: { className: 'main' }, children: [ { type: 'input' }, { type: 'button', props: { onClick: () => console.log('yo') }, children: ['Submit'] } ] } 

当然,我们不会手动编写此转换,而是使用此插件 ,该插件已过时,但是它足够简单,可以帮助我们了解所有工作原理。 它使用jsx-transform ,它像这样转换JSX:


 jsx.fromString('<h1>Hello World</h1>', { factory: 'h' }); // => 'h("h1", null, ["Hello World"])' 

因此,我们要做的就是实现h节点的vdom构造函数,该函数将递归创建虚拟DOM节点,在有React的情况下,React.createElement函数将执行此操作。 以下是此类功能的原始实现


 export function h(type, props, ...stack) { const children = (stack || []).reduce(addChild, []) props = props || {} return typeof type === "string" ? { type, props, children } : type(props, children) } function addChild(acc, node) { if (Array.isArray(node)) { acc = node.reduce(addChild, acc) } else if (null == node || true === node || false === node) { } else { acc.push(typeof node === "number" ? node + "" : node) } return acc } 

当然,递归使这里的代码有些复杂,但是我希望这很清楚,现在有了这个功能,我们可以构建vdom


 'h("h1", null, ["Hello World"])' => { type: 'h1', props:null, children:['Hello World']} 

所以对于任何嵌套的节点


很好,现在我们的Component函数返回了vdom节点。


现在最部分是,我们需要编写一个patch函数,该函数需要应用程序的根DOM元素,旧的vdom,新的vdom,并根据新的vdom更新实际DOM的节点。


也许您可以更轻松地编写此代码,但事实证明,因此我以picodom软件包中的代码为基础


 export function patch(parent, oldNode, newNode) { return patchElement(parent, parent.children[0], oldNode, newNode) } function patchElement(parent, element, oldNode, node, isSVG, nextSibling) { if (oldNode == null) { element = parent.insertBefore(createElement(node, isSVG), element) } else if (node.type != oldNode.type) { const oldElement = element element = parent.insertBefore(createElement(node, isSVG), oldElement) removeElement(parent, oldElement, oldNode) } else { updateElement(element, oldNode.props, node.props) isSVG = isSVG || node.type === "svg" let childNodes = [] ; (element.childNodes || []).forEach(element => childNodes.push(element)) let oldNodeIdex = 0 if (node.children && node.children.length > 0) { for (var i = 0; i < node.children.length; i++) { if (oldNode.children && oldNodeIdex <= oldNode.children.length && (node.children[i].type && node.children[i].type === oldNode.children[oldNodeIdex].type || (!node.children[i].type && node.children[i] === oldNode.children[oldNodeIdex])) ) { patchElement(element, childNodes[oldNodeIdex], oldNode.children[oldNodeIdex], node.children[i], isSVG) oldNodeIdex++ } else { let newChild = element.insertBefore( createElement(node.children[i], isSVG), childNodes[oldNodeIdex] ) patchElement(element, newChild, {}, node.children[i], isSVG) } } } for (var i = oldNodeIdex; i < childNodes.length; i++) { removeElement(element, childNodes[i], oldNode.children ? oldNode.children[i] || {} : {}) } } return element } 

这种幼稚的实现方式并不是最佳选择,它没有考虑元素的标识符(键,ID)-正确更新列表中的必要元素,但是在原始情况下,它可以正常工作。


createElement updateElement removeElement我没有在这里介绍,值得注意的是,任何有兴趣的人都可以在这里查看源代码。


唯一需要注意的是-更新input元素的value属性时,不应与旧的vnode进行比较,而应与真实房屋中的value属性进行比较-这将防止活动元素更新此属性(因为该属性已在此处进行更新),并防止了游标出现问题和选择。


好了,仅此而已,我们只需要将这些部分放在一起并编写UI框架
我们保持在5行以内。


  1. 像在React中一样,要构建应用程序,我们需要3个参数
    export function app(selector, view, initProps) {
    选择器-将在其中安装应用程序的根dom选择器(默认为“ body”)
    view-构造根vnode的函数
    initProps-初始应用程序属性
  2. 在DOM中获取根元素
    const rootElement = document.querySelector(selector || 'body')
  3. 我们收集具有初始属性的vdom
    let node = view(initProps)
  4. 我们将接收到的vdom安装在DOM中,因为旧的vdom我们为null
    patch(rootElement, null, node)
  5. 我们返回具有新属性的应用程序更新功能
    return props => patch(rootElement, node, (node = view(props)))

框架已准备就绪!


该框架上的“ Hello world”将如下所示:


 import { h, app } from "../src/index" function view(state) { return ( <div> <h2>{`Hello ${state}`}</h2> <input value={state} oninput={e => render(e.target.value)} /> </div> ) } const render = app('body', view, 'world') 

该库与React一样,支持组件组成,在运行时添加或删除组件,因此可以将其视为 UI框架。 稍微复杂一点的用例可以在ToDo示例中找到。


当然,该库中有很多内容:生命周期事件(尽管并不难,但我们自己管理节点的创建/更新/删除),子节点(如this.setState)的单独更新(为此,您需要为每个节点保存指向DOM元素的链接vdom节点-这会使逻辑复杂一点),patchElement代码非常不理想,无法在大量元素上正常工作,无法使用标识符跟踪元素,等等。


无论如何,该库都是出于教育目的而开发的-请勿在生产中使用它:)


PS:这篇文章的灵感来自宏伟的Hyperapp库,部分代码是从那儿获取的。


好的编码!

Source: https://habr.com/ru/post/zh-CN415857/


All Articles