UI framework in 5 minutes


Some time ago I wondered why there are so many UI frameworks for the web? I have been in IT for a long time and I don’t remember that UI libraries on other platforms were born and died with the same speed as in WEB. Desktop OS libraries, such as: MFC, Qt, WPF, etc. - were monsters that have evolved over the years and did not have a large number of alternatives. On the Web, everything is wrong - the frameworks come out almost every week, the leaders change - why is this happening?


I think the main reason is that the complexity of writing UI libraries has dramatically decreased. Yes, in order to write a library that many will use - it still takes considerable time and expertise, but to write a prototype - which is wrapped in a convenient API - will be ready for use - it takes very little time. If you are interested in how this can be done - read on.


Why this article?


In due time on Habré there was a series of articles - to write X for 30 code lines on js.


I thought - is it possible to write a reagent for 30 lines? Yes, I did not succeed in 30 lines, but the final result is quite commensurate with this figure.


In general, the purpose of the article is purely educational. It can help to understand a bit more deeply how the UI framework works on a virtual home basis. In this article I want to show how quite simple it is to make another UI Framework based on a virtual house.


In the beginning I want to say what I understand by the UI framework - because many people have different opinions on this matter. For example, some believe that Angular and Ember are the UI framework and React is just a library that will make it easier to work with the view part of the application.


We define the UI framework as this is a library that helps to create / update / delete pages or individual page elements in this sense; a rather wide range of wrappers over the DOM API can be a UI framework, the question is only in the abstraction options (API) that this library provides for manipulating the DOM and in the effectiveness of these manipulations


In the proposed formulation - React is quite the UI framework.


Well, let's see how to write your React with blackjack and so on. It is known that React uses the concept of a virtual house. In a simplified form, it lies in the fact that the nodes (node) of the real DOM are built in strict accordance with the nodes of the previously built virtual DOM tree. Direct manipulation of the real DOM is not welcome; if changes are needed to the real DOM, changes are made to the virtual DOM, then the new version of the virtual DOM is compared with the old one, changes are going to be applied to the real DOM and they are applied in this way to minimize interaction with the real DOM - which makes the application work more optimal.


Since the virtual house tree is a regular java-script object — it is fairly easy for them to manipulate — change / compare its nodes, the word “easy” I understand here that the virtual assembly code is quite simple and can be partially generated by a preprocessor from a higher-level declarative language JSX.


Let's start with JSX


This is what the JSX code looks like.


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

we need to make sure that by calling the Component function we create such a virtual DOM


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

Of course, we will not write this conversion manually, we will use this plugin , the plugin is outdated, but it is simple enough to help us understand how everything works. It uses jsx-transform , which transforms JSX like this:


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

so, all we need is to implement the vdom node constructor h - a function that will recursively create virtual DOM nodes in the event of a reaction. This is what the React.createElement function does. Below is a primitive implementation of such a function.


 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 } 

Of course, recursion here complicates the code a bit, but I hope it is clear, now with the help of this function we can compile vdom


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

and so for nodes of any nesting


Excellent now our Component function - returns the vdom node.


Now there will be a part, we need to write the patch function which takes the root DOM element of the application, the old vdom, the new vdom, and updates the real DOM nodes according to the new vdom.


Perhaps you can write this code easier, but it turned out that I took the code from the picodom package as a basis.


 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 } 

This naive implementation, it is terribly not optimal, does not take into account the identifiers of the elements (key, id) - in order to correctly update the necessary elements in the lists, but in primitive cases it works as normal.


The implementation of the functions createElement updateElement removeElement I do not createElement updateElement removeElement here it is noticeable, anyone interested can see the source here .


There is a single nuance - when updating the value properties for input elements, the comparison needs to be done not with the old vnod but with the value attribute in the real house - this will prevent the active element from updating this property (since it is already updated there) and will prevent problems with the cursor and selection.


Well, that's all we have to do now is to put these pieces together and write the UI Framework.
We put in 5 lines .


  1. As in React to build the application, we need 3 parameters
    export function app(selector, view, initProps) {
    selector - the root selector dom to which the application will be mounted (default is 'body')
    view - the function that constructs the root vnode
    initProps - the initial properties of the application
  2. Take the root element in the DOM
    const rootElement = document.querySelector(selector || 'body')
  3. We collect vdom with initial properties
    let node = view(initProps)
  4. Mount the resulting vdom in the DOM as the old vdom. Take null
    patch(rootElement, null, node)
  5. We return the function to update the application with new properties.
    return props => patch(rootElement, node, (node = view(props)))

Framework is ready!


'Hello world' on this Framework will look like this:


 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') 

This library as well as React supports component composition, adding, deleting components at the time of execution, so that it can be considered a UI Framework. A little more complicated example of use can be found here ToDo example .


Of course, there is a lot of things in this library: life cycle events (although they are not difficult to tie, we ourselves manage the creation / update / deletion of nodes), a separate update of child nodes by the type of this.setState (for this you need to keep references to DOM elements for each the vdom node - this will complicate the logic a little), the patchElement code is terribly suboptimal, will work poorly on a large number of elements, does not track elements with an identifier, etc.


In any case, the library was developed for educational purposes - do not use it in production :)


PS: this article was inspired by the great Hyperapp library, part of the code was taken from there.


Good coding!

Source: https://habr.com/ru/post/415857/


All Articles