前端框架小解


前端框架流行程度

具体查询网址

框架 npm下载量( 2023-01-01 and 2023-10-10
react 788,921,076
vue 153,082,192
Angular 20,187,119
svelte 32,586,777
solid-js 3,752,099

流行度调研

React

详情可以查看本博客中的

《React》(上)、(中)、(下)

《React 拓展》

《React Hook解析》

《React Fiber》

Inferno作者对React评价:

我认为 React 已经改变了 Web UI 社区的游戏规则。它摒弃了我们在服务器上(例如 MVC)多年来一直在做事的旧方法,并提供了一种激发许多不同模式和设计讨论的方法;例如,单向数据流、模板上的组件、JSX、虚拟 DOM。

Svelte

Rich Harris 是Svelte的作者,也是rollup的作者,他把rollup关于代码打包策略的造诣

Svelte 采取了独特的方法。它在构建时编译您的应用程序,这样您就可以交付最轻量级的代码

Svelte不需在运行时解释应用程序代码,您的应用程序在构建时被转换成理想的JavaScript。

(通过静态编译减少框架运行时的代码量,并且直接转译为高效的命令式代码)

这意味着你不需要支付框架抽象的性能成本,或者当你的应用程序第一次加载时,受到惩罚。

基于编译的响应式系统:

  • Svelte
  • Vue Reactivity Transform
  • solid-babels

说以上是真正的响应式,是因为比如说React,实际上是根据状态变化,更改虚拟dom,重新render(有可能是父组件更新),对比起来solidjs、svelte响应单独针对的是数据粒度,React响应的体量是组件粒度

关于svelte的响应式系统:https://svelte.dev/blog/svelte-3-rethinking-reactivity

  • 基于反应式细粒度变化传播
  • bundle够小
  • 无虚拟dom
  • 语法简洁

语法(Svelte3)

使用 xx.svelte 文件,在vscode上装好拓展程序

<script>
  //状态
  export let title;
    let count = 0;

  // 这将在“title”的prop属性更改时
  // 更新“document.title”
  $: document.title = title;

  //副作用
  $: {
    console.log(`multiple statements can be combined`);
    console.log(`the current title is ${title}`);
  }

  //状态更新
  count++;
</script>

可以看到代码是如此简洁

但是Svelte简洁的代价在

  • 只能在Svelte组件内使用
  • 组件外需要不同API
  • 只能在顶层作用于使用,不可以在函数体中使用

Svelte3 组件编译逻辑也是React hooks启发而来的(作者本人说的),因为他们认为这不是他们想要发展的逻辑

That all changed with the advent of hooks, which handle state in a very different fashion. Many frameworks started experimenting with their own implementations of hooks, but we quickly concluded it wasn’t a direction we wanted to go in. Hooks have some intriguing properties, but they also involve some unnatural code and create unnecessary work for the garbage collector. For a framework that’s used in embedded devices as well as animation-heavy interactives, that’s no good.

这一切都随着钩子的出现而改变,钩子以非常不同的方式处理状态。许多框架开始尝试自己的钩子实现,但我们很快得出结论,这不是我们想要的方向。钩子有一些有趣的属性,但它们也涉及一些不自然的代码,并为垃圾回收器创造了不必要的工作。对于用于嵌入式设备以及动画密集型交互的框架,这是不好的。

So we took a step back and asked ourselves what kind of API would work for us… and realised that the best API is no API at all. We can just use the language. Updating some count value — and all the things that depend on it — should be as simple as this:

所以我们退后一步,问自己什么样的 API 对我们有用……并意识到最好的 API 根本不是 API。我们可以只使用语言。更新一些 count 值 - 以及依赖于它的所有事情 - 应该像这样简单:

count += 1;

Since we’re a compiler, we can do that by instrumenting assignments behind the scenes:
由于我们是编译器,因此可以通过在后台检测赋值来做到这一点:

count += 1;
$$invalidate('count', count); //const $$invalidate: (name: string, value: number) => undefined

Importantly, we can do all this without the overhead and complexity of using proxies or accessors. It’s just a variable.
重要的是,我们可以完成所有这些工作,而无需使用代理或访问器的开销和复杂性。它只是一个变量。

image-20231024095501111

预编译后

1111

solidjs的作者评价

它确实精简了实现代码,但性能改进可以忽略不计。我确实从实现中敲掉了另一千字节,因此它确实符合这些要求。这就是 Svelte 3 在语法上的巨大变化,但最终提供了出色的性能和极小的捆绑包大小。这意味着在实践中,它通常会生成比 Preact 或您最喜欢的“仅 2kb”库更小的捆绑包,并且可能性能不佳。

Vue作者评价

image-20231024105011819

知乎某评价

Solidjs

  • I’m going to look at each of Rich’s claims and contrast each library’s approach with hopes of separating the facts from the hyperbole.

注:Rich Harris 是Svelte的作者,也是rollup的作者,所以看起来solidjs的作者Ryan Carniato初衷是往抛开现代框架体系,走一条精简的(体积更小),当然性能也得快的前端框架之路

  • 颗粒度响应:Solid 响应性原理大致上是将任何响应性计算封装在函数中,并在其依赖关系更新时重新运行该函数;所以你可以看到他的state在引用是通过函数调用的形式引用的。

  • 不用在意hook顺序,自动依赖收集,并且自动提升createSignal到当前作用域的最前面

他依赖的是一个作者自创的库:dom-expressions, 一个渲染运行时,用于执行细粒度更改检测的反应式库。这些库依赖于可观察量和信号等概念,而不是生命周期函数和虚拟 DOM

是也是作者为支持任何细粒度反应式库而开发的 开源的 渲染器。

并且吐槽

我创建了 dom-expressions,希望让人们能够采用通用运行时,并带来他们的反应式库和意见,以创建他们完美的 UI 开发人员体验。那是一个幼稚的梦。人们没有使用我的库,他们只是抓住代码将其包装到他们的库中,并开始按照自己的方式行事。六个月前,我从未想过有人会以削减几千字节的名义获取我的代码,创建一个仅提供相同功能子集的库,并在 Github 上拥有近 300 颗星。这太神奇了。这就是开源的意义所在。

语法上和hooks相似,实现上和vue的composition api更相似(尤大说是几乎一样的),比如Solid的createEffect对应Vue的watchEffect

语法

function App() {
  const [count, setCount] = createSignal(0);
  const [data, { mutate, refetch }] = createResource(fetchData);

  createEffect((prev) => {
    console.log('do something')
  });

  onCleanup(() => {
    if (timer) {
      clearTimeout(timer);
    }
  });

  return (
    <div class="x-three-year" onClick={() => setCount((pre) => pre + 1)}>
      <div class="no-open">你有个蛋糕店待开业</div>
      <div class="no-open">{count()}</div>
    </div>
  );
}

可以看到和react是十分相似,它既保留了react class的生命周期,沿用了react hook的useEffect、useHook写法,但是比如createEffect就是会自动收集依赖

列表渲染

<For each={state.list} fallback={<div>Loading...</div>}>
  {(item) => <div>{item}</div>}
</For>

如何指定键位优化dom操作?How to specify key in <For> each

image-20231024113344819

Solid’s For is keyed by default by reference to the data

跑不掉的列表diff算法

image-20231024111543530

关于solid的节语(SolidJS: The Tesla of JavaScript UI Frameworks?):

Leo Horie, author of Mithril, to someone on Reddit wanting to get the TL;DR described Solid as:
《秘银》的作者Leo Horie对Reddit上想要获得TL;DR的人将Solid描述为:

Svelte is to Vue as Solid is to React
Svelte 之于 Vue,就像 Solid 之于 React 一样

That’s a pretty interesting perspective, and one that I find helps a lot of people first encountering Solid. Both Svelte and Solid are compiler driven variants of their counterparts. But what does that actually mean?
这是一个非常有趣的观点,我发现它帮助了很多人第一次接触 Solid。Svelte和Solid都是编译器驱动的变体。但这实际上意味着什么?

I’ve seen it positioned to the difference between being simple and being easy. Solid while being reactive is clearly on the React side of things where Svelte has inherited Vue’s easiness.
我已经看到它定位于简单和简单之间的区别。在反应的同时Solid显然是在 React 方面,Svelte 继承了 Vue 的易用性。

Solid和其他框架对比

Svelte

Svelte 开创了 Solid 在一定程度上也采用的预编译消失型框架。这两个库都是真正的响应式,可以生成非常小的执行代码包。

原文链接:Comparing Svelte and Solid

虽然世人评价Svelte 之于 Vue,就像 Solid 之于 React 一样,但是我们可以在前端框架结果公示表中看到Svelte只能排中游水平,但是Solid可以排在上游(前十左右),Ryan Carniato解释其原因在于

  • 优化 DOM 列表对账

    Svelte 确实有一个键控列表协调器(keyed list reconciler),但它使用了一个更原始的实现。Svelte的调和器要小得多,但Solid的调和器要复杂得多。Svelte 的方法并不像许多库那样幼稚(navite),可以正确处理交换行,而不会在两者之间移动每一行,但它并没有针对单个项目更改操作进行优化。这是处理列表时的大部分性能差异。

  • 模板克隆

    Solid 使用 node.cloneNode 而不是 document.createElement 来创建节点。这减少了临时内存使用量。它确实需要使用注释节点作为占位符,但对其中至少有 3 个节点的重复列表项的总体影响是显而易见的。

  • Solid 在驼峰大小写事件处理程序上执行隐式事件委托( implicit event delegation)。

    这比让 DOM 处理每行添加 2 个处理程序的性能更高。显然可以执行显式事件委派(explicit event delegation),但它需要更多的代码来执行显式查找/树遍历,以将事件类型和行数据绑定在一起。以前版本的 Svelte 实现没有这样做,所以我没有添加它,我目前正在等待 Svelte 社区或 Rich 本人来权衡“性能”与“编写更少代码”的相对重要性。

  • 同步更新批处理

    当您不断使用更新来冲击 DOM 时,使用动画帧甚至承诺来延迟它们是性能的必要条件。但是,如果只进行一组更改,则延迟更新的时间实际上是有代价的。库通常要么逐个同步运行所有更新,要么在延迟的微任务中批处理它们。你知道整个状态在 React 中的 setState 调用后没有更新。Solid 为批处理操作提供了显式语法,其 setState 帮助程序可以获取同时应用的所有更改的列表。

  • 显式反应性和计算

    Svelte的编译器可以自动识别您何时连接到值(这个和vue一样),并自动设置处理其值更改所需的一切。但是它很难判断您是否打算更改该值。此外,使用普通的JavaScript语法很难知道处理反应原子( reactive atom)的意图或它所持有的值。这可能会导致在组件等边界上进行额外的同步。Svelte 的存储机制使用不同的语法来处理这个缺点。计算的存在是响应式库最繁重的代码之一,因此能够不包含它们的创建会大大降低性能。有时自动并不是更好。

当然作者自己也说了:鉴于 Solid 的性能非常接近痛苦的手动优化的 Vanilla JavaScript (原生js)代码,可以肯定地说,原始性能不是这种方法的瓶颈。事实上,性能可能是预编译的最大优势之一。在未来的版本中,我们可能会看到Svelte更好的性能。

Vue

Vue 的细粒度依赖检测只是提供给一个细粒度的虚拟 DOM 和组件系统,而 Solid 将其粒度保持在它的直接 DOM 更新上。

Knockout

这个库的存在归功于 Knockout。将其模型现代化以进行细粒度依赖检测是该项目的动机。(对数据状态管理的的发布订阅模式,并及时修改数据)

Knockout 的绑定只是在运行时遍历的 HTML 中的字符串。它们取决于克隆上下文($parent 等…)。而 Solid 使用 JSX 或 JavaScript API 的标签模板字面量来模板化。

最大的区别可能是 Solid 的批处理更改方法可确保同步性,而 Knockout 具有使用延迟微任务队列的 deferUpdates。

Solid的几个版本

Solid 更加像是一个 “渲染库” 而不是一个框架,所以solid有几个版本都可以用,我们也可以从官方提供的案例看到其模版编译得到的颗粒度响应(其实就是直接编译成一个立即执行函数,对dom模版进行状态插入)

  1. solid — This is the stock ES2015 proxy version with succinct setter syntax on top of fine-grained change detection feeding into DOM Template Node cloning. It accomplishes this with precompiled JSX templates. (Code)
    solid

    这是 ES2015 代理版本,在细粒度更改的基础上具有简洁的 setter 语法,并馈送到 DOM 模板节点克隆中。它通过预编译的 JSX 模板实现此目的。(代码)

  2. solid-signals — This version is the same as above but eschews the convenience of proxies for raw Signals. It makes the implementation heavier, but it leaves a smaller bundle and greater performance. (Code)
    固体信号— 此版本与上述相同,但避开了原始信号代理的便利性。它使实现更重,但它留下了更小的捆绑包和更高的性能。(代码)

  3. solid-lit — This version eschews JSX precompilation for Just in Time Tagged Template Literal runtime compilation to achieve DOM Template Node cloning. (Code)
    solid-lit — 此版本避开了 JSX 预编译,用于实时标记模板文本运行时编译,以实现 DOM 模板节点克隆。(代码)

  4. solid-h — This version uses HyperScript to translate to document.createElement on the fly. But otherwise uses the same Solid implementation as the others. (Code)
    solid-h — 此版本使用HyperScript即时转换为 document.createElement 。但除此之外,使用与其他实现相同的 Solid 实现。(代码)

Solid源码

整理来看,翻阅solidjs源码比较麻烦,毕竟作者把一些编译和渲染的流程放在dom-expressions这个库,需要轮流翻看

第二个麻烦的点是作者喜欢用

export * from "./xxx/index.js";

的形式进行导出,比较难定位..

createSignal

主要看下 createSignal 的状态管理,很多文章会以为solid用的是基于Proxy的响应式,实则不然,还是用的老Knockout那一套发布订阅的数据响应。

首先我们得先知道2个重要的角色类型: SignalStateComputation

信号主要通过一个对象存储,类型为type SignalState

  • value:当前的值
  • observers:观察者数组, 类型为 type Computation
  • observerSlots:观察者对象在数组的位置
  • comparator:比较器,通过比较则更改value,默认false,浅比较
export function createSignal<T>(
  value?: T,
  options?: SignalOptions<T | undefined>
): Signal<T | undefined> {
  options = options ? Object.assign({}, signalOptions, options) : signalOptions;

  const s: SignalState<T | undefined> = {
    value,
    observers: null,
    observerSlots: null,
    comparator: options.equals || undefined
  };

  if ("_SOLID_DEV_" && !options.internal) {
    if (options.name) s.name = options.name;
    registerGraph(s);
  }

  const setter: Setter<T | undefined> = (value?: unknown) => {
    if (typeof value === "function") {
      if (Transition && Transition.running && Transition.sources.has(s)) value = value(s.tValue);
      else value = value(s.value);
    }
    return writeSignal(s, value);
  };

  return [readSignal.bind(s), setter];
}
export interface SignalState<T> extends SourceMapValue {
  value: T;
  observers: Computation<any>[] | null;
  observerSlots: number[] | null;
  tValue?: T;
  comparator?: (prev: T, next: T) => boolean;
}

我们可以看到在创建状态时,实际上就是创建了一个SignalState,通过 readSignalwriteSignal 分别读取和改写 SignalState

在全局下还有一个Listener,用于暂存一个computation类型的观察者,在组件渲染(createRenderEffect),或者在调用createEffect时,会通过一个叫 updateComputation 的方法对全局的 Listener 进行赋值,为后续的依赖追踪铺垫

let Listener: Computation<any> | null = null;
export interface Computation<Init, Next extends Init = Init> extends Owner {
  fn: EffectFunction<Init, Next>;
  state: ComputationState;
  tState?: ComputationState;
  sources: SignalState<Next>[] | null;
  sourceSlots: number[] | null;
  value?: Init;
  updatedAt: number | null;
  pure: boolean;
  user?: boolean;
  suspense?: SuspenseContextType;
}
function updateComputation(node: Computation<any>) {
  if (!node.fn) return;
  cleanNode(node);
  const owner = Owner,
    listener = Listener,
    time = ExecCount;
  Listener = Owner = node;
  runComputation(
    node,
    Transition && Transition.running && Transition.sources.has(node as Memo<any>)
      ? (node as Memo<any>).tValue
      : node.value,
    time
  );
//...
  Listener = listener;
  Owner = owner;
}

由于对signal的读取,是通过函数调用的形式进行数据读取

 <div class="no-open" style={{ color: 'blue' }}>{`当前count: ${count()}`}</div>

所以在任何一个角落读取SignalState时,都会调用 readSignal 函数,并且把当前全局下被暂存的“观察者”Listener,也就是引用到SignalState的地方,放入自身的observers(观察者数组)中,并且把观察者源(source)指向当前signal,实现数据绑定,并且返回对应的 SignalState

export function readSignal(this: SignalState<any> | Memo<any>) {
  //这里Transition可以先不用管,它用于 `useTransition`  ,批量异步更新延迟提交使用的
  const runningTransition = Transition && Transition.running;
  if (
    (this as Memo<any>).sources &&
    (runningTransition ? (this as Memo<any>).tState : (this as Memo<any>).state)
  ) {
    if ((runningTransition ? (this as Memo<any>).tState : (this as Memo<any>).state) === STALE)
      updateComputation(this as Memo<any>);
    else {
      const updates = Updates;
      Updates = null;
      runUpdates(() => lookUpstream(this as Memo<any>), false);
      Updates = updates;
    }
  }
  //添加观察者,绑定数据
  if (Listener) {
    const sSlot = this.observers ? this.observers.length : 0;
    if (!Listener.sources) {
      Listener.sources = [this];
      Listener.sourceSlots = [sSlot];
    } else {
      Listener.sources.push(this);
      Listener.sourceSlots!.push(sSlot);
    }
    if (!this.observers) {
      this.observers = [Listener];
      this.observerSlots = [Listener.sources.length - 1];
    } else {
      this.observers.push(Listener);
      this.observerSlots!.push(Listener.sources.length - 1);
    }
  }
  if (runningTransition && Transition!.sources.has(this)) return this.tValue;
  return this.value;
}

对于信号的写入,则调用 writeSignal 函数,在闭包内改变当前SignalState后,遍历在在readSignal阶段被收集的观察者数组,于当前Effect执行列表中推入观察者

export function writeSignal(node: SignalState<any> | Memo<any>, value: any, isComp?: boolean) {
  let current =
    Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value;
  if (!node.comparator || !node.comparator(current, value)) {
    if (Transition) {
      const TransitionRunning = Transition.running;
      if (TransitionRunning || (!isComp && Transition.sources.has(node))) {
        Transition.sources.add(node);

        .tValue = value;
      }
      if (!TransitionRunning) node.value = value;
    } else node.value = value;
    if (node.observers && node.observers.length) {
      runUpdates(() => {
        for (let i = 0; i < node.observers!.length; i += 1) {
          const o = node.observers![i];
          const TransitionRunning = Transition && Transition.running;
          if (TransitionRunning && Transition!.disposed.has(o)) continue;
          if (TransitionRunning ? !o.tState : !o.state) {
            if (o.pure) Updates!.push(o);
            else Effects!.push(o);
            if ((o as Memo<any>).observers) markDownstream(o as Memo<any>);
          }
          if (!TransitionRunning) o.state = STALE;
          else o.tState = STALE;
        }
        if (Updates!.length > 10e5) {
          Updates = [];
          if ("_SOLID_DEV_") throw new Error("Potential Infinite Loop Detected.");
          throw new Error();
        }
      }, false);
    }
  }
  return value;
}

此时我们的Effect列表就保存了当时的观察者们,然后遍历执行 runEffects,进行消息的重新分发,然后在对应的节点(Computation)重新执行 readSignal 函数,此时我们就可以得到最新的数据结果了。

没有任何依赖时就正常修改该SignalState,且observers、observerSlots为null

const { createSignal } = require("../../solid/dist/solid.cjs");

{
  const [count, setCount] = createSignal(0);

  setCount(1);
  setCount((pre) => pre + 2);


  console.log(count());
}

createEffect

而像createEffect这种自动追踪依赖的实现时调用时直接创建一个computation对象(createComputation),也就是一个观察者,随后被添加到Effects执行数组中。并且随后会和之前的流程一样,执行 runEffects -> updateComputation -> 去执行createEffect内部的代码逻辑

function createEffect<Next, Init>(
  fn: EffectFunction<Init | Next, Next>,
  value?: Init,
  options?: EffectOptions & { render?: boolean }
): void {
  runEffects = runUserEffects;
  const c = createComputation(fn, value!, false, STALE, "_SOLID_DEV_" ? options : undefined),
    s = SuspenseContext && lookup(Owner, SuspenseContext.id);
  if (s) c.suspense = s;
  if (!options || !options.render) c.user = true;
  Effects ? Effects.push(c) : updateComputation(c);
}

通过 updateComputation ,如上面所说 对 Computation 的介绍所说的,在 updateComputation时,在对全局的Listener进行赋值。

组件的更新

组件的更新和createEffect同理,只不过组件的引用是走 createRenderEffect -> updateComputation

function App() {
    const [count, setCount] = createSignal(0);

    return (
        
setCount((pre) => pre + 1)}>
你有个蛋糕店待开业
{count()}
); }

在点击事件发生后,和我们上面所描述的writeSignal 行为一致,触发updateComputation,走到对SignalState的获取readSignal,整体调用栈如下

image-20231020202011674

渲染jsx走的是dom-expression这个库

export * from "dom-expressions/src/client"; // render
import { render } from 'solid-js/web';
render(() => <App />, root!);
缺点

缺点1:

由于 Solid 不能使用 rest 和 spread 语法来拆分和合并 props,也就是不能直接对props响应式解构(一个直接传一个signal则可以),因为通过解构的形式(因为解构赋值属于浅拷贝),拷贝当时获取的值,会切断signal的更新,脱离追踪范围而失去响应。

正因如此,请时刻记住不能直接解构它们,这会导致被解构的值脱离追踪范围从而失去响应性。通常,在 Solid 的 primitive 或 JSX 之外访问 props 对象上的属性可能会失去响应性。除了解构,像是扩展运算以及 Object.assign 这样的函数也会导致失去响应性。

比如

//不行
function Other({count}) {
    return (
    
{count}
); } //可以 function Other(props) { return (
{props.count}
); } function App() { const [count, setCount] = createSignal(0); return (
setCount((pre: any) => pre + 1)}>
你有个蛋糕店待开业
{count()}
); }
//可以
function Other({count}) {
    return (
    
{count()}
); } function App() { const [count, setCount] = createSignal(0); return (
setCount((pre: any) => pre + 1)}>
你有个蛋糕店待开业
{count()}
); }

所以solid关于一些props响应式功能,用的是proxy实施数据拦截,比如splitPropsmergeProps

mergeProps

一个合并响应性对象的方法。用于为组件设置默认 props,以防调用者不提供这些属性值。或者克隆包含响应属性的 props 对象。

此方法通过使用代理并以相反的顺序解析属性来工作。这可以对首次合并 props 对象时不存在的属性进行动态跟踪。

// 默认 props
props = mergeProps({ name: "Smith" }, props);

// 克隆 props
newProps = mergeProps(props);

// 合并 props
props = mergeProps(props, otherProps);

splitProps

按照提供的 keys 参数来拆分响应对象。

它需要一个响应对象和任意数量的数组。它将返回数组中指定的那些响应对象,返回的数组中最后一个响应对象将拥有原始对象的所有剩余属性。

如果您想使用 props 的 children,并将剩余属性传递给 Child 组件,这将很有用,如下所示:

function MyComponent(props) {
  const [local, others] = splitProps(props, ["children"]);

  return (
    <>
      <div>{local.children}</div>
      <Child {...others} />
    </>
  );
}

缺点2:这种响应式方法只能进行同步跟踪。如果你使用 setTimeout 或在的 Effect 中使用异步函数,那么 Solid 并不会跟踪异步执行的代码。

image-20231023113629967

以下不行🙅,因为此时走readSignal函数读取Listener的时候,基本流程已经走完,数据已经被清空(Listener = null Owner= null),所以在读取时无法对该SignalState进行追踪

image-20231023113629967

Vue的Vapor模式

很多其他框架已经引入了与 Vue 组合式 API 中的 ref 类似的响应性基础类型,并称之为“信号”:

从根本上说,信号是与 Vue 中的 ref 相同的响应性基础类型。它是一个在访问时跟踪依赖、在变更时触发副作用的值容器。这种基于响应性基础类型的范式在前端领域并不是一个特别新的概念:它可以追溯到十多年前的 Knockout observablesMeteor Tracker 等实现。Vue 的选项式 API 和 React 的状态管理库 MobX 也是基于同样的原则,只不过将基础类型这部分隐藏在了对象属性背后。

虽然这并不是信号的必要特征,但如今这个概念经常与细粒度订阅和更新的渲染模型一起讨论。由于使用了虚拟 DOM,Vue 目前依靠编译器来实现类似的优化。然而,我们也在探索一种新的受 Solid 启发的编译策略 (Vapor Mode),它不依赖于虚拟 DOM,而是更多地利用 Vue 的内置响应性系统。

Vapor Mode

在2022年稀土掘金开发者大会上,尤雨溪《2022 前端生态趋势》在演讲中便提及到对 “无虚拟dom”的探索 —— Vue vapor模式。

因为模版是一个编译源,所以其实可以编译成不同的(非虚拟dom)的输出,也可以理解为模版的静态结构,然后再搜寻对应的动态节点,使动态节点和状态进行响应式的一个绑定(其实也就是solidjs采用的一个策略)

目前正实验进行中,并且希望是渐进式的功能,能让Vapor开启后继续使用虚拟dom的组件库

API 设计权衡

Preact 和 Qwik 的信号设计与 Vue 的 shallowRef 非常相似:三者都通过 .value 属性提供了一个更改接口。我们将重点讨论 Solid 和 Angular 的信号。

Solid Signals

Solid 的 createSignal() API 设计强调了读/写隔离。信号通过一个只读的 getter 和另一个单独的 setter 暴露:

const [count, setCount] = createSignal(0)

count() // 访问值
setCount(1) // 更新值

注意到 count 信号在没有 setter 的情况也能传递。这就保证了除非 setter 也被明确暴露,否则状态永远不会被改变。这种更冗长的语法带来的安全保证的合理性取决于项目的要求和个人品味——但如果你喜欢这种 API 风格,可以轻易地在 Vue 中复制它:

import { shallowRef, triggerRef } from 'vue'

export function createSignal(value, options) {
  const r = shallowRef(value)
  const get = () => r.value
  const set = (v) => {
    r.value = typeof v === 'function' ? v(r.value) : v
    if (options?.equals === false) triggerRef(r)
  }
  return [get, set]
}

Inferno

官网地址,语法和react很相似(方便react深度用户迁移),区别可以在官网处查看

一个巨快无比的ui框架,曾无数次卫冕执行速度最快的宝座(The quickest of the React clones and one of the fastest Virtual DOM libraries.)最开始的作者是多米尼克·甘纳韦—现在,作为React团队的一员,Inferno最初的设计目的是为了证明JavaScript框架可以在移动设备上运行良好。

  • 类似 React 的 API、概念和组件生命周期事件。使用Inferno兼容轻松切换。

    (Inferno的不同之处在于它提供了一些React或Preact没有的附加功能(以牺牲一些文件大小为代价))

  • 用于在 DOM 中渲染 UI 的最快前端框架之一,使移动设备上的 60 FPS 成为可能。(官网的基准)

  • 客户端和服务器上的同构渲染,以及从服务器端渲染快速启动。

Inferno通过利用现代 JavaScript 引擎提供的几个优化来加快这个过程;以及改进“接触”DOM 所需的大量试错

作者本人解释其优化经验涵盖了几个点

  • 试图确保尽可能多的对象属性调用站点是单态的(链接

    function f(o) {
      return o.x
    }
    
    //单态
    f({ x: 1 })
    f({ x: 2 })
    
    //多态
    f({ x: 3 })
    // o.x cache is still monomorphic here
    f({ x: 3, y: 1 })
    // what about now?
    
    //巨态
    f({ x: 4, y: 1 }) // polymorphic, degree 2
    f({ x: 5, z: 1 }) // polymorphic, degree 3
    f({ x: 6, a: 1 }) // polymorphic, degree 4
    f({ x: 7, b: 1 }) // megamorphic

    巨态的存在是为了防止多态缓存不受控制的增长,这意味着“我在这里看到了太多的形状,我放弃了跟踪它们”。在V8中,超形态IC仍然可以继续缓存东西,但不是在本地进行,而是将想要缓存的内容放入全局哈希表中。此哈希表具有固定大小,条目只是在冲突时被覆盖。

  • Inferno avoids using prototype objects with constructors and instead favours object literals with minimal properties.

    (避免将原型对象与构造函数一起使用,而是倾向于使用具有最小属性的对象文本)

    Inferno 使用实用程序/帮助程序函数来改变/访问这些对象,而不是将方法添加到对象本身。这在非 JIT(实时)编译或内存不足的移动设备上产生了明显的影响。

  • Inferno 不是将虚拟 DOM 与真实 DOM “差异”,而是将虚拟 DOM 与最后一个创建的虚拟 DOM 进行比较。这也对性能产生了明显的影响。

  • Inferno 尝试尽可能重用属性、对象和 DOM 节点。创建太多对象可能会在内存、GC(垃圾回收器)和整体性能上产生高昂的成本。例如,Inferno 不是在对象上 VNode 创建额外的属性,而是重用以前的属性(即使属性名称不再真正与其中放置的内容对齐)。此外,DOM 节点也被存储和回收,从而降低了重新创建大型 DOM 树和再次计算所有内部可视化计算的成本。

  • Inferno尽可能避免经常接触DOM。相反,它选择只接触 DOM 节点上的一小部分属性/方法( firstChild lastChild parentNode nextSibling createElement removeChild insertBeforereplaceChild )。Inferno避免使用 childNodesinnerHTML 因为这些方法往往非常昂贵。清除 DOM 内容的一个很好的优化技巧是使用 textContent('') .

  • Inferno 更喜欢使用所有由 JIT 编译器内联的辅助函数 - 例如,而不是执行 ,执行 foo === null isNull(foo) 。我们发现这确实有助于提高捆绑包大小,在某些情况下,它还提高了 JIT 性能(当内联的内联预算尚未完全用完时)。

  • 进行了广泛的检查,以确保将去优化(“deopts”,即 JIT 编译器无法编译某些内容)保持在绝对最低限度。Inferno团队使用基准测试,分析工具(IRHydra 2,Chrome Dev Tools)和代码库各个部分的大量峰值重写,以不断寻找删除deopts的方法。

  • Inferno 对某些事件使用自己的事件系统,这允许它根据事件类型选择委托事件或内联事件。在 cetain 用例中,与非委托事件相比,委托事件可以提供显着的性能和内存改进。

  • Inferno的键控子排序算法非常高效,并且产生从A到B的最小可能的DOM突变。

知乎上的评价:虽然有一些小瑕疵(例如 Inferno 只会对新旧两个 vdom 进行对比,拿 vdom 和真实 dom 对比的是 preact),但指出的方向没有错:Inferno 快的原因主要在数据结构、算法和 API。链接,不过这个有点老了,18年的评论

solidjs官方对Inferno的评价:Svelte很小,但性能并不优于Inferno等最快的VDOM库

虚拟dom的争议

虚拟dom的诞生间隔点主要在jquery到react这段时期,当时jquery的语法还是停留在那种命令式dom操作之中

//传统,前面绑定事件,后面创建的标签没有绑定该事件
$("ol li").click(function() {})
let li = $("<li>xxx</li>");
$("ol").append(li);

而虚拟dom诞生之初,也是为了把写法专注于声明式之上,需要dom操作这种“行为”,交给框架处理,因此引发了

  1. 既然dom操作集中交给框架了,那框架岂不是可以去“批处理”dom操作,更好的减少开销?
  2. 既然开始写声明式了,那如何让数据和dom关联起来?如果每次数据发生变化,该如何监听数据源?

诞生之初

虚拟dom写法

在上古流行的字符串拼接时代,jQuery一家独大,当时jquery的语法还是停留在那种命令式dom操作之中,而在2013年,Facebook的Jordan Walke提出来了,打算把2010年FaceBook做出来的 XHP 的拓展功能迁移到 JS 中,形成以JSX作为拓展的新编码形式

const fn = () => {};
const Component = (
  <ul>
    {data.map(item => <MyItem data={item} onClick={fn}/>)}
  </ul>
);
虚拟dom优化

一直以来我们都信奉一个道理:

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决. ——–David Wheeler

而当时虚拟dom,也就是在代码和实际dom操作,由框架做了一层中间层,从而实现 代码 -> 虚拟dom树 -> 真实dom树

这个概念是由 React 率先开拓,随后被许多不同的框架采用

并且当时有一本书《高性能的javascript》,里面有个观点就是“dom操作是很慢的,dom操作比较消耗性能”、

而前react核心团队Pete Hunt也在2013年时,对react的宣传演讲中疯狂吐槽重复性dom操作的“巨大开销” 《重新思考典范实例的意义》

所以当时提出这套虚拟dom在于

  • 打开函数式 UI 编程的大门,组件抽象化,使得代码更易维护

  • 跨平台,虚拟DOM不仅可以变成DOM,还可以变成小程序、ios应用、安卓应用,因为虚拟DOM本质上只是一个JS对象

  • 数据绑定,更新视图时,减少DOM操作:可以将多次DOM操作合并为一次操作,比如添加100个节点原来是一个一个添加,现在是一次性添加,减少浏览器回流(比如1000个节点的dom操作,合并为1次,进行批处理)

    类似这样批处理:

    const fragment = document.createDocumentFragment();
    
    for(let i = 0; i < 1000; i++) {
      const div = document.createElement('div');
      fragment.appendChild(div);
    }
    
    // 将文档片段一次性插入到目标容器中
    const container = document.getElementById('container');
    container.appendChild(fragment);
  • 用轻量级的js进行diff,避免大量查询和复杂的真实dom的存储(包含大量属性)

所以当时提出这套虚拟dom的diff算法在于

  • 虚拟DOM借助DOM diff 可以把多余的操作省略掉,减少页面reflow、repaint。
  • 缓存dom,更新dom时保存节点状态。
Vue2引入虚拟dom

Vue 的理念问题

尤大:React 的 vdom 其实性能不怎么样。Vue 2.0 引入 vdom 的主要原因是 vdom 把渲染过程抽象化了,从而使得组件的抽象能力也得到提升,并且可以适配 DOM 以外的渲染目标。这一点是借鉴 React 毫无争议

尤大:至于有人觉得门槛低是 low 的表现,我只能说欢迎你用汇编去写前端,没人拦着你。¯_(ツ)_/¯

虚拟dom现状

为什么现在有部分框架开始摒弃虚拟dom?

上方Pete Hunt在口嗨之后马上做出了澄清:

React不是魔法。就像你可以使用 C 进入汇编程序并击败 C 编译器一样,如果你愿意,你可以进入原始 DOM 操作和 DOM API 调用并击败 React。但是,使用 C 或 Java 或 JavaScript 是性能的一个数量级改进,因为您不必担心……关于平台的细节。使用 React,您可以构建应用程序,甚至不考虑性能,默认状态很快。

我们要先直视虚拟dom的缺点

  • 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。
  • 虚拟 DOM 需要在内存中的维护一份 DOM 的副本。
  • 如果虚拟 DOM 大量更改,这是合适的。但是单一的,频繁的更新的话,虚拟 DOM 将会花费更多的时间处理计算的工作。所以,如果你有一个 DOM 节点相对较少页面,用虚拟 DOM,它实际上有可能会更慢。但对于大多数单页面应用,这应该都会更快。

并且特别是当项目大起来,节点多起来之后,在进行对比和计算时,生成虚拟一颗dom树的开销是很大的,尽管现代框架对此进行了优化,但仍然有一定的性能开销。

当然有些企业,比如说Uber,通过广泛使用shouldComponentUpdate 来最大限度地减少对渲染的调用,详情可以看这里

react 16后面甚至推出了react fiber来避免对主进程的阻塞

而尤雨溪在《Vue3的设计》也提及到了致力于寻找对虚拟dom瓶颈的突破,打破这种看起来比较野蛮的算法比较模式

The framework figures out which parts of the actual DOM to update by recursively walking two virtual DOM trees and comparing every property on every node. This somewhat brute-force algorithm is generally pretty quick, thanks to the advanced optimizations performed by modern JavaScript engines, but updates still involve a lot of unnecessary CPU work.

我们需要清楚的是,虚拟 DOM 和 Diff 算法的出现是为了解决由命令式编程转变为声明式编程(让你的代码更容易维护)、数据驱动后所带来的性能问题的。换句话说,直接操作 DOM 的性能并不会低于虚拟 DOM 和 Diff 算法,甚至还会优于。

现阶段框架:

  • Vue3依然保留了虚拟dom,着重于对虚拟dom的diff算法优化
  • Inferno使用Virtual DOM
  • svelte无Virtual DOM
  • solidjs无Virtual DOM

Vue的带编译时信息的虚拟 DOM

虚拟 DOM 在 React 和大多数其他实现中都是纯运行时的:更新算法无法预知新的虚拟 DOM 树会是怎样,因此它总是需要遍历整棵树、比较每个 vnode 上 props 的区别来确保正确性。另外,即使一棵树的某个部分从未改变,还是会在每次重渲染时创建新的 vnode,带来了大量不必要的内存压力。这也是虚拟 DOM 最受诟病的地方之一:这种有点暴力的更新过程通过牺牲效率来换取声明式的写法和最终的正确性。

但实际上我们并不需要这样。在 Vue 中,框架同时控制着编译器和运行时。这使得我们可以为紧密耦合的模板渲染器应用许多编译时优化。编译器可以静态分析模板并在生成的代码中留下标记,使得运行时尽可能地走捷径。与此同时,我们仍旧保留了边界情况时用户想要使用底层渲染函数的能力。我们称这种混合解决方案为带编译时信息的虚拟 DOM

  • 静态提升,其实就是编译时,根据element是否带有“状态”,来生成vnode,如果是纯静态元素,就是生成静态 vnode

    <div>
      <div>foo</div> <!-- 需提升 -->
      <div>bar</div> <!-- 需提升 -->
      <div>{{ dynamic }}</div>
    </div>
  • 更新类型标记,其实也就是编译时会将元素标签携带信息带入vnode,每个vnode会有自己的一个一个patch Flag,然后将携带信息(比如class、id、value等)通过更新类型标记和信息进行位运算来判断更新

  • 树结构打平,针对普通的节点,比如不带v-if、v-for这种,可以被称为一个稳定区块,此时会被标记成一个

    export function render() {
      return (_openBlock(), _createElementBlock(_Fragment, null, [
        /* children */
      ], 64 /* STABLE_FRAGMENT */))
    }

    编译的结果会被打平为一个数组,仅包含所有动态的后代节点,当这个组件需要重渲染时,只需要遍历这个打平的树而非整棵树

  • 对 SSR 激活的影响

    更新类型标记和树结构打平都大大提升了 Vue SSR 激活的性能表现:

    • 单个元素的激活可以基于相应 vnode 的更新类型标记走更快的捷径。
    • 在激活时只有区块节点和其动态子节点需要被遍历,这在模板层面上实现更高效的部分激活。

大佬们对虚拟dom的评价

尤大堆虚拟dom的评价

无虚拟 DOM 版 Vue 即将到来

尤雨溪前端趋势2022 的主题演讲

State of Vue 2022 - 尤雨溪

一般来说:

初始渲染:Virtual DOM > 脏检查 >= 依赖收集

小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化

大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化)>> MVVM 无优化

除了dom diff检查更新,还有其他驱动数据更新的形式

比如Angular的脏检查,Vue的自动依赖收集

更新粒度

应用级:有状态改变,就更新整个应用,生成新的虚拟Dom树,与旧树进行Diff(代表作:React,当然了,现在它的虚拟Dom已升级为了Fiber)。

组件级:与上方类似,只不过粒度小了一个等级(代表作:vuev2及之后的版本)。

节点级:状态更新直接与具体的更新节点的操作绑定(代表作vue1.xSvelteSolidJS)。

solidjs作者对虚拟dom评价

里面参杂了solidjs相对客观的,将市面上最快的几个虚拟dom库和自己的solidjs系列产品进行对比

针对响应式前端框架,虚拟dom一定慢吗?当然也不是

这边Ryan Carniato针对他自己的对比结果,给了几个建议(根据模版,作者在官网也介绍了三种渲染模式:Solid 支持 JSX、标签模板字面量 和 Solid HyperScript 变体这 3 种模板形式)

如果您需要或喜欢在非编译环境中使用 Solid,例如纯 HTML 文件、https://codepen.io 等,您可以在普通的 JavaScript 中使用 `html``` Tagged Template LiteralsHyperScript h() functions,而不是 Solid 的编译时优化的 JSX 语法。

  1. HyperScript情况(inferno, ivi, solid-h对比)

    HyperScript 是一种将视图表示为函数组合的方式(通常是 h 或 React.createElement),一般是是虚拟 DOM 库拥有的类别。例如:

    h('div', {id: 'my-element'}, [
      h('span', 'Hello'),
      h('span', 'John')
    ])

    细粒度方案(Fine-Grained )可以在更新中表现更好,但是创建反应式( reactive graph)开销也会对应产生,针对良好的diff算法,比如Inferno,虚拟dom性能也能表现得很好,甚至更好

    所以如果使用HyperScript,最好用虚拟dom

  2. 字符串模版情况(非虚拟dom进行渲染)(domc, lit-html, solid-lit 对比)

    DomC 和 lit-html 做自上而下的差异类似于虚拟 DOM,而 Solid 使用细粒度的反应式图(reactive graph),其结果是虚拟dom库和solid-lit也相差无几

  3. jsx情况(solid, solid-signals, surplus)

    solid可能在此略胜一筹

结论
像往常一样,这些比较的结果永远不会是决定性的。重要的是旅程以及我们在此过程中学到的东西。在这种情况下,我们看到 DOM 本身是性能前沿的最大瓶颈。如此之多,以至于没有明确的最佳技术。

我承认,正是 React 对虚拟 DOM 性能的言论首先将我带入了这个领域。对意见的无知令人愤怒。

同样,最近合唱的“虚拟DOM很慢”也同样信息不足。与不这样做相比,渲染虚拟 DOM 树并对其进行差异将是纯粹的开销,但不这样做会扩展吗?如果您必须处理数据快照怎么办?

svelte对虚拟dom的评价

Virtual DOM is pure overhead ———-RICH HARRIS

他里面也提到了,即使不用虚拟dom,也能实现函数式编程

In many frameworks, you build an app by creating render() functions, like this simple React component:

function HelloMessage(props) {
    return <div className="greeting">Hello {props.name}</div>;
}

You can do the same thing without JSX…

function HelloMessage(props) {
    return React.createElement('div', { className: 'greeting' }, 'Hello ', props.name);
}

并且 哈里斯 在 *So… is the virtual DOM *slow? ** 也提及了几个情况,就是组件内触发渲染频率较高的写法和问题(通过传入的props,来决定渲染内部的列表ui),更多的重新渲染带来的是更多的diff累积的性能问题

并且表示虚拟dom只是为了当初React想要以状态驱动ui开发的一种手法而已

现代js框架对比

js-framework-benchmark

结果公示表

JavaScript Frameworks, Performance Comparison

该对比以作者本身设备作为测试基准:

  • MacBook Pro (Retina, 15-inch, Mid 2015)
  • Processor: 2.2 GHz Intel Core i7
  • Memory: 16 GB 1600 MHz DDR3
  • Graphics: Intel Iris Pro 1536 MB
  • Browser: Google Chrome, Version 69.0.3497.100

并且主要以以下3个维度进行pk

  1. DOM Manipulation(dom操作时间)
  2. Startup Time(启动时间)
  3. Memory Allocation(内存占用)

测试结果一览

初赛:

框架1 框架2 DOM Manipulation Startup Time Memory Allocation Score Winner
Elm0.19 Angular6.1 Angular6.1 Elm0.19 Elm0.19 2:1 Elm0.19
Choo6.13 AngularJS1.74 AngularJS1.74 Choo6.13 Choo6.13 2:1 Choo6.13
Mithril1.1 Marionette4.0 Marionette4.0 Mithril1.1 Marionette4.0 1:2 Marionette4.0
Ember3.3 Aurelia1.3 Aurelia1.3 Aurelia1.3 Aurelia1.3 0:3 Aurelia1.3
库1 库2 DOM Manipulation Startup Time Memory Allocation Score
React16 Vue2.6 Vue2.6 Vue2.6 Vue2.6 0:3
Preact8.3 Inferno5.6 Inferno5.6 Preact8.3 Inferno5.6 1:2
Svelte2.13 Redom3.13 Redom3.13 Redom3.13 Svelte2.13 1:2
Maquette3.3 Bobril8.11 Bobril8.11 Maquette3.3 Maquette3.3 2:1

晋级赛:

框架1 框架2 DOM Manipulation Startup Time Memory Allocation Score
Elm0.19 Choo6.13 Elm0.19 Elm0.19 Choo6.13 2:1
Aurelia1.3 Marionette4.0 Marionette4.0 Marionette4.0 Marionette4.0 0:3
Marionette4.0 Elm0.19 Marionette4.0 Elm0.19 Marionette4.0 2:1
框架1 框架2 DOM Manipulation Startup Time Memory Allocation Score
Vue2.6 Inferno5.6 Inferno5.6 Inferno5.6 Inferno5.6 0:3
Redom3.13 Maquette3.3 Redom3.13 Redom3.13 Maquette3.3 2:1
Inferno5.6 Redom3.13 Inferno5.6 Redom3.13 Redom3.13 1:2

决战:

框架1 框架2 DOM Manipulation Startup Time Memory Allocation Score
Redom3.13 Marionette4.0 Redom3.13 Redom3.13 Redom3.13 3:0

最后是和大魔王的对比

还是得VanillaJS(也就是原生js)

  • DOM操作性能方面只有Inferno可以与之匹敌
  • 在启动时间,Svelte,Redom,Preact,Elm和Inferno可以与之相匹配
  • 内存占用全部挂掉

Angular

angular在框架对比中经常会与React、Vue区分开,因为前者是frameworks,后者是UI library,其实看代码就可以看出区别,比如Angular的代码

并且这里有 Angular和AngularJS的区分

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',                //组件的 CSS 元素选择器
  standalone: true,
  imports: [],
  template: `<h1>Hello world!</h1>`,  //组件模板 模版本身 / 模版文件的位置路径,比如'./heroes.component.html'
  styleUrls: ['./app.component.css'], //组件私有 CSS 样式表文件的位置。
})
export class AppComponent {
  title = 'default';
}

并且还可以在工作区输入一些快捷指令创建组件,像这样

# 创建heroes组件
ng generate component heroes

脏检查:

在代码层面,Angular根本不监听数据的变动,而是在恰当的时机从$rootScope开始遍历所有$scope(每个组件创建的更改检测器),检查它们上面的属性值是否有变化,如果有变化,就用一个变量dirty记录为true,再次进行遍历,如此往复,直到某一个遍历完成时,这些$scope的属性值都没有变化时,结束遍历。

这个时机在于

  • 任何浏览器事件(单击,键入等)
  • setInterval() and setTimeout()
  • HTTP 请求

最后它将在 UI 中重新呈现值。由于使用了一个dirty变量作为记录,因此被称为脏检查机制。

但是缺点在于,由于数据模型现在没有任何内置探测器可以告诉框架有关更改的信息,因此框架无法深入了解它是否以及在何处发生。这意味着需要检查模型的外部更改,而这正是 Angular 所做的:每次发生任何事情时,都会运行所有观察器。单击处理程序、HTTP 响应处理器和超时都会触发摘要,这是负责运行观察程序的进程。

当然Angular的脏检查也有他自己的优化策略,比如Change Detection Strategies 变更检测策略,变化检测包括脏值检测、OnPush变化检测策略、手动触发变化检测等

详情可以自己去查看了解

不过后面Angular 正在经历一些底层的变化,它放弃了脏检查,并引入了自己的响应性基础类型实现。

Marionette

基于Backbone的前端框架

Backbone

起步于2005年的jQuery仅仅对DOM操作进行了基础性的封装,提供了可链式调用的写法、更加友好的Ajax函数、屏蔽了浏览器兼容性的各类选择器,但是并没有解决前端开发中选择器滥用、作用域相互污染、代码复用度低冗余度高、数据和事件绑定烦琐等痛点。

为此,2009年横空出世的Angular提供了一揽子解决方案,对浏览器原生事件机制进行深度封装的同时,提供了路由、双向绑定、指令等现代化前端框架的特性,但是也正是由于其封装的抽象程度太深,学习曲线相对陡峭,而对于controller$scope的过度倚重,以及照搬Java的MVC分层思想试图通过service来完成页面逻辑的复用,并未彻底解决前端开发过程中的上述痛点。

诞生于2010的Backbone则另辟蹊径,通过与UndersocreRequireHandlebar的整合,为那个年代的开发人员提供了Angular之外,一个更加轻量和友好的前端开发解决方案,其诸多设计思想对于后续的现代化前端框架发展起到了举足轻重的作用。

Backbone底层依赖underscore/lodash、jQuery/Zepto

它是第一个通过实现MVC模式将更多结构引入前端应用程序的框架之一,像mvc模式被改成这样

比如

  • 从视图组件化这方面:

Backbone视图对象:

/* Backbone视图对象 */
Backbone.View.extend({
  id: "app",
  template: '...',
  events: {
    "click .icon":          "open",
    "click .button.edit":   "openEditDialog",
    "click .button.delete": "destroy"
  },
  initialize: function() {
    this.listenTo(this.model, "change", this.render);
  },
  render: function() {
    this.$el.html(this.template());
    return this;
  }
});

Vue组件对象:

/* Vue组件对象 */
import Vue from 'vue';
new Vue({
  template: '<div>模板字符串<div>',
  data: {
    // 组件绑定的数据
  },

  methods: {
    myEvent() {
      // 组件自定义事件
    },
  },
});

React组件对象:

/* React组件对象 */
import React from 'react';
import ReactDOM from 'react-dom';
class MyComponent extends React.Component {
  constructor(props) {
    // 组件构造函数
  }
  myEvent(event) {
    event.preventDefault();
  }
  render() {
    return (
      // JSX
    );
  }
};

Angular2组件对象:

/* Angular2组件对象 */
import { Component, Input } from '@angular/core';
import { Demo } from './demo';
@Component({
  selector: 'demo-detail',
  template: `
    <div>模板字符串</div>
  `
})
export class DemoDetailComponent {
  @Input() demo: Demo;
}

构建单页面应用

Backbone出现的年代,Web单页面应用开发方式还未能普及,基于JSP或PHP等服务器标签的前后端耦合式开发还是主流,因此Backbone对构建单页面应用的支持还较为薄弱,也造成嵌套视图僵尸视图两大问题长期困扰着继往开来的Backbone开发人员们。伴随移动互联网的快速崛起,对单页面应用交互的需求量越来越大,许多开发人员在实际开发实践过程中,逐步对Backbone.Router进行增强,其间诞生了backbone.routefilterbackbone.subroute两款优秀的第3方Backbone路由插件,基本解决了僵尸视图卸载的痛点。但是伴随Web前端交互逻辑愈加复杂,嵌套视图的问题又开始逐步凸显,而嵌套视图依然与路由机制密切相关。因此,MarionetteThorax两款基于Backbone的单页面前端框架应运而生。

Marionette背景

背景:Backbone 为我们的 JavaScript 应用程序提供了一组很棒的构建块。它为我们提供了构建小型应用程序、组织 jQuery DOM 事件或创建支持移动设备和大规模企业需求的单页应用程序所需的核心构造。但Backbone并不是一个完整的框架。它是一组构建块。它将大部分应用程序设计、体系结构和可伸缩性留给开发人员,包括内存管理、视图管理等。

Marionette为Backbone带来了应用程序架构,以及内置的视图管理和内存管理。它被设计为一个轻量级且灵活的工具库,位于 Backbone 之上,为构建可扩展的应用程序提供框架。

Redom

RE:DOM 是一个由 Juha Lindstedt 和贡献者设计的小型 (2 KB) UI 库,它添加了一些有用的帮助程序来创建 DOM 元素并使它们与数据保持同步。

因为 RE:DOM 非常接近金属并且不使用虚拟 dom,所以它实际上比几乎所有基于 Virtual dom 的库(包括 React(基准测试))更快,使用更少的内存。

使用 RE:DOM 创建可重用组件也很容易。

另一个很大的好处是,你可以只使用纯JavaScript,所以没有复杂的模板语言需要学习和麻烦。

创建动态列表

import { el, list, mount } from "redom";

class Li {
    constructor() {
        this.el = el("li");
    }
    update(data) {
        this.el.textContent = "Item " + data;
    }
}

const ul = list("ul", Li);

mount(document.body, ul);

ul.update([1, 2, 3]);
ul.update([2, 2, 4]);

VanillaJS

大家有必要认识下他,他时常出现在各框架对比的榜首位置,这是他的官网

PlainJS a.k.a VanillaJS

没错,所以实际上它就是前端框架恶趣味:原生js

参考

OSCON - React Architecture

从 React 历史的长河来聊如何理解虚拟 DOM

知乎:网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?(尤雨溪答案)

对Inferno作者专访

为什么认为Backbone是现代前端框架的基石

尤雨溪前端趋势2022 的主题演讲


文章作者: Hello
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Hello !
 上一篇
前端设计模式 前端设计模式
前端设计模式MVC 模式顾名思义即 Model-View-Controller 模式 早期的mvc仅限于服务端: 在web2.0之后,后面开始走前端mvc架构 MVC的通信方式时单向的,view -> controller ->
2022-03-05
下一篇 
BOM BOM
1.BOM概述BOM是浏览器对象模型,他提供独立于内容而与浏览器窗口进行交互的对象,其核心对象是window BOM缺乏标准,Javascript语法的标准化组织是ECMA,DOM标准化组织是W3C,BOM最初是Netscape浏览器标准的
2022-03-05
  目录