惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
S
Schneier on Security
T
The Exploit Database - CXSecurity.com
Webroot Blog
Webroot Blog
AI
AI
P
Palo Alto Networks Blog
Attack and Defense Labs
Attack and Defense Labs
WordPress大学
WordPress大学
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
Spread Privacy
Spread Privacy
T
Tor Project blog
罗磊的独立博客
小众软件
小众软件
S
Security Affairs
酷 壳 – CoolShell
酷 壳 – CoolShell
量子位
Apple Machine Learning Research
Apple Machine Learning Research
T
Threatpost
NISL@THU
NISL@THU
博客园_首页
PCI Perspectives
PCI Perspectives
大猫的无限游戏
大猫的无限游戏
IT之家
IT之家
N
News and Events Feed by Topic
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Forbes - Security
Forbes - Security
博客园 - 叶小钗
D
Darknet – Hacking Tools, Hacker News & Cyber Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Last Week in AI
Last Week in AI
L
LINUX DO - 热门话题
T
Threat Research - Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
腾讯CDC
Security Latest
Security Latest
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Cloudflare Blog
A
About on SuperTechFans
爱范儿
爱范儿
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
TaoSecurity Blog
TaoSecurity Blog
宝玉的分享
宝玉的分享
G
GRAHAM CLULEY
雷峰网
雷峰网
F
Full Disclosure
I
Intezer
Cloudbric
Cloudbric
博客园 - 三生石上(FineUI控件)
U
Unit 42

Minko Gechev's blog

skillgrade Unit Tests for AI Agent Skills You Should Care About AI Generative Development LLM-first Web Framework Managing Angular Are LLMs going to replace us? Prefetching Heuristics Design Patterns in Open Source Projects - Part II Design Patterns in Open Source Projects - Part I What I learned doing 125 public talks - Part I Dynamic imports solve all the problems, right? 5 Angular CLI Features You Didn't Know About Angular quicklink Preloading Strategy Introducing Bazel Schematics for Angular CLI Building TypeScript Projects with Bazel Joining Google Playing Mortal Kombat with TensorFlow.js. Transfer learning and data augmentation Fast, extensible, configurable, and beautiful linter for Go Introducing Guess.js - a toolkit for enabling data-driven user-experiences on the Web Machine Learning-Driven Bundling. The Future of JavaScript Tooling. JavaScript Decorators for Declarative and Readable Code 3 Tricks For Using Redux and Immutable.js with TypeScript Follow Your Dream Career with Open Source. Personal Story. Redux Anti-Patterns - Part 1. State Management. Faster Angular Applications - Understanding Differs. Developing a Custom IterableDiffer Faster Angular Applications - Part 2. Pure Pipes, Pure Functions and Memoization Faster Angular Applications - Part 1. On Push Change Detection and Immutability Understanding Dynamic Scoping and TemplateRef Implementing a Simple Compiler on 25 Lines of JavaScript Developing Statically Typed Programming Language WebVR for a Gamified IDE 7 Angular Tools That You Should Consider Announcing ngrev - Reverse Engineering Tool for Angular Implementing Angular's Dependency Injection in React. Understanding Element Injectors. Distributing an Angular Library - The Brief Guide Angular in Production Ahead-of-Time Compilation in Angular 2.5X Smaller Angular 2 Applications with Google Closure Compiler Using Stripe with Angular (Deprecated) Building an Angular Application for Production Implementing the Missing "resolve" Feature of the Angular 2 Router Scalable Single-Page Application Architecture Managing ambient type definitions and dealing with the "Duplicate identifier" TypeScript error Static Code Analysis of Angular 2 and TypeScript Projects Enforcing Best Practices with Static Code Analysis of Angular 2 Projects ViewChildren and ContentChildren in Angular Dynamically Configuring the Angular's Router Angular 2 Hot Loader Lazy Loading of Route Components in Angular 2 Aspect-Oriented Programming in JavaScript Flux in Depth. Store and Network Communication. Using JSX with TypeScript Flux in Depth. Overview and Components. Even Faster AngularJS Data Structures Boost the Performance of an AngularJS Application Using Immutable Data - Part 2 Angular2 - First Impressions Build Your own Simplified AngularJS in 200 Lines of JavaScript Persistent State of ReactJS Component Boost the Performance of an AngularJS Application Using Immutable Data Processing Binary Protocols with Client-Side JavaScript Stream your Desktop to HTML5 Video Element Multi-User Video Conference with WebRTC Asynchronous calls with ES6 generators Binary Tree iterator with ES6 generators WebRTC chat with React.js AngularJS in Patterns (Part 3) AngularJS in Patterns (Part 2). Services. Using GitHub Pages with Jekyll! AngularJS in Patterns (Part 1). Overview of AngularJS Singleton in JavaScript Express over HTTPS What I get from the JavaScript MV* frameworks Remote Desktop Client with AngularJS and Yeoman The magic of $resource (or simply a client-side Active Record) AngularJS Inheritance Patterns AngularAOP v0.1.0 Advanced JavaScript at Sofia University AngularJS style guide Lazy prefetching of AngularJS partials VNC client on 200 lines of JavaScript Aspect-Oriented Programming with AngularJS CSS3 flipping effect Practical programming with JavaScript Why I should use publish/subscribe in JavaScript JavaScript, the weird parts Functional programming with JavaScript plainvm Looking for performance? Probably you should NOT use [].sort (V8) JavaScript image scaling ELang Caching CSS with localStorage Self-invoking functions in JavaScript (or Immediately Invoked Function Expressions) Asus N56VZ + Ubuntu 12.04 (en) Asus N56VZ + Ubuntu 12.04 Debian Squeeze + LXDE on Google Nexus S (or having some fun while suffering) HTML5 image editor Курсови проекти – ФМИ Carousel Gallery SofiaJS...
Reactive framework in ~200 lines of JavaScript
2025-01-09 · via Minko Gechev's blog

One of my current projects is converging Angular and Wiz into the same framework. This is a complex projects that involves a lot of work and many people. It also got me thinking about different client-side rendering models. In this blog post I’ll show a very simple library that enables you to develop components with fine-grained reactivity. To make it easier to talk about this library, I called it “revolt.”

You can find the implementation in my repo on GitHub.

This prototype is just meant to be a fun experiment and nothing more!

Component model

Each component in revolt is a function which returns a view. The view is represented by nested objects which correspond to DOM elements and text nodes that we’d render on the page. For each DOM element we can specify event listeners and attributes.

Here’s how a “Hello, world!” component in revolt:

const HelloWorld = () => {
  return 'Hello, world!';
};

Here’s how a timer would look like:

const Timer = () => {
  const timer = signal(0);
  setInterval(() => timer.set(timer() + 1), 1000);
  return () => `Timer: ${timer()}`;
};

And here’s an example of how you can compose components and pass properties:

const Avatar = (photo: () => string) => {
  return () => {
    name: 'img',
    attributes: {
      src: photo
    }
  };
};

const UserProfile = () => {
  const userProfile = signal(...);
  return [{
      name: 'h1',
      children: [
        () => `Profile of ${userProfile.name()}`
      ]
    },
    Avatar(userProfile.avatarUrl)
  ];
};

You can play with the grocery list app below that I built with revolt:

A couple of things to observe:

  • Each component is a function which returns a structure of nested objects that represents the view
  • We declare the state of each component with signals within the function body
  • There’s a way to get a reference to a particular DOM node that’s inspired by React’s ref
  • We have “fine-grained reactivity” in a sense that we can bind an attribute or a text node to a particular signal

This implementation uses functions for convenience. I know that many people feel strongly about the abstractions that enable component definition (e.g. classes vs functions vs sfc). As an Angular team member, I feel obligated to share that this prototype does not represent mine or the team's vision for the future of Angular's authoring.

We can render a component using:

render(Component(), document.body);

I decided to use nested objects instead of JSX or templating language to simplify the build and use fewer abstractions.

View model

Let’s look at the type definitions of the view:

export type Binding = string | ReadableSignal<string>;
export type EventListener = <K extends keyof GlobalEventHandlersEventMap>(event: GlobalEventHandlersEventMap[K]) => void;

export interface When {
  condition: ReadableSignal<boolean>;
  then: View;
  else?: View;
}

export interface For<T, I extends Iterable<T> = T[]> {
  collection: ReadableSignal<T>;
  items: (item: T, index: number) => ViewNode;
}

export interface ElementConfig {
  name: keyof HTMLElementTagNameMap;
  attributes?: Record<string, string|ReadableSignal<string|false>;
  children?: View;
  events?: {[key in keyof GlobalEventHandlersEventMap]?: EventListener};
  ref?: (node: Element) => void;
}

export type ViewNode = Binding | ElementConfig | When | For<any>;

export type View = ViewNode | View[];

When writing these types, it’s fun to see how similar they look to a grammar of a programming language. Templating languages are pretty much programming languages that render DOM.

Each view is a composition of nodes which could be:

  • Text or a text binding
  • DOM elements
  • Control flow (when, for)

It’s interesting to notice that revolt does not have the concept of a “host element” - a component can produce any number of root nodes including zero if we just render a text node.

Also notice that both When and For accept a readable signal and they rerender when the value of the signal changes. Similarly, we can get a sense how we implement fine-grained reactivity in text and attribute bindings - both could be either strings or ReadableSignals.

Signals

Our reactive framework will use a minimal signal implementation that I reused from the post “Building a Reactive Library from Scratch”. The library exports three abstractions ReadableSignal<T>, WritableSignal<T> and Effect:

export type ReadableSignal<T> = () => T;

export interface WritableSignal<T> extends ReadableSignal<T> {
  set(value: T): void;
}

export type Effect = () => void;

Here’s how you can use them:

const counter = signal(0);

effect(() => {
  console.log('Current value', counter());
});

counter.set(1);

The code above will output "Current value 0" and "Current value 1". If you’re interested in how the signals library works, have a look at its implementation or Ryan’s blog post.

Rendering

We already have the view and the signal library. The only thing left is the renderer! Let’s look at the render function:

export const render = (view: View, root: Element): Node | Node[] => {
  if (isConditional(view)) {
    return renderCondition(view, root);
  }
  if (isIterator(view)) {
    return renderIterator(view, root);
  }
  if (view instanceof Array) {
    return renderViewList(view, root);
  }
  if (typeof view === "string") {
    return renderTextNode(view, root);
  }
  if (typeof view === "function") {
    return renderDynamicText(view, root);
  }
  return renderElement(view, root);
};

Pretty straightforward and very similar to a parser. Now let’s look at the implementation of isIterator to see how we use signals:

const renderIterator = (view: For<any>, root: Element) => {
  let collection;
  let result: Node | Node[] | undefined;
  effect(() => {
    collection = view.collection();
    if (result) {
      destroy(result);
    }
    result = render(collection.map(view.items), root);
  });
  return result ?? [];
};

That’s it!

Inside an effect we read the signal representing the collection, after that we destroy the DOM from the previous value of the signal, and we render the new collection. Here we take advantage of the synchronous effect implementation.

Keep in mind that's an oversimplified implementation. All modern frameworks will have diffing logic that will rerender only the changed items to optimize the runtime.

In a similar way we implement other signal bindings:

const renderElement = (view: ElementConfig, root: Element) => {
  const element = document.createElement(view.name);
  for (const attribute in view.attributes) {
    const binding = view.attributes[attribute];
    if (!isDynamicBinding(binding)) {
      element.setAttribute(attribute, binding);
      continue;
    }
    effect(() => {
      const value = binding();
      if (value === false) {
        element.removeAttribute(attribute);
        return;
      }
      element.setAttribute(attribute, value);
    });
  }
  // ...
};

Here we create an effect for attribute bindings that are signals. Each time we receive a new value for the signal, we manually update the attribute.

You can see the entire implementation on GitHub.

Other approaches

Angular and React use very different approaches. Virtual DOM relies on pruning parts of the component tree that have not changed. Its elegance comes from functional programming, but could also cause extra rendering.

Similarly to the approach in revolt, Angular separates creation from update, but differently. The Angular compiler generates two rendering code paths:

  • One for initial render of the component
  • Another which contains only the dynamic parts of the view

Signals notify the framework that something in the dynamic part of the view has changed, which causes Angular to schedule change detection and update it.

That’s all folks

That was pretty much everything. Revolt is a tiny library that allows exploration of different concepts from web frameworks such as fine-grained reactivity, host elements, references, server-side rendering, etc.

In this write up we focused primarily on rendering and fine-grained reactivity, but I’ll be happy to drill more in other topics. Let me know what you’d be interested in learning about!