首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

自带异步渲染的前端框架: Crank

本文要点:

  • 主要的前端框架,如React,在不断增加特性的同时也变得越来越复杂。与这些框架一起使用的其他工具、语法和生态系统的复杂性也在增加。
  • 复杂性增加的一部分原因是,大型框架由于用户众多,需要保持高度的向后兼容性和稳定性。因此,它们就有理由不去重新考虑关键的设计选择。
  • Crank重新研究了类似React这样的框架的关键架构部分,该部分规定渲染函数必须为纯函数。相反,Crank利用异步生成器执行异步渲染,没有任何成本。异步生成器是JavaScript的一种标准语言特性,不用承担实现该功能的库的成本。
  • 借助语言自带的生成器和async/await语法,开发人员可以像处理同步任务一样自然地处理异步任务(获取远程数据、暂停和恢复渲染)。实现前端应用程序时需要掌握的、与这门语言无关的概念减少了。

Brian Kim是开源库Repeater.js的作者,他最近发布了一个新的用于创建Web应用程序的JavaScript库Crank。Crank的创新之处在于,它使用协程声明式地描述了应用程序的行为,这是用JavaScript及异步生成器实现的。

虽然Crank尚在测试阶段,还需要进一步的研究,但它支持的异步渲染可能可以处理类似React提供的Suspense功能这样的用例。

当生成器运行时,它可能会接收数据(初始的prop),返回一个迭代器,用于访问生成器闭包中保存的私有状态。迭代器在迭代方(Crank库函数)请求时计算并生成视图,而迭代方在其迭代请求中传递更新后的prop。Crank异步迭代器返回一个promise,使计算视图得以渲染,从而提供异步渲染能力。这样,Crank组件就自然地提供了局部state和effect支持,而不需要专门的语法。另外,Crank组件的生命周期就是生成器的生命周期:生成器在装载匹配的DOM元素时启动,在卸载匹配的DOM元素时停止。错误可以使用标准的JavaScript结构try... catch捕获。

还有其他框架用JavaScript生成器来创建Web应用程序。Concur UI从Haskell移植到JavaScript,PureScript和Python使用异步生成器组合组件。Turbine将自己描述为一个毫不妥协的纯函数Web框架,它利用生成器实现了FRP范式。

InfoQ采访了Brian Kim,内容涉及这个新的JavaScript框架的基本原理,以及他认为利用JavaScript生成器可以获得哪些好处。

InfoQ:您可以向我们的读者介绍下自己吗?

Brian Kim:我是一名独立的前端工程师。我整个编程生涯几乎都在使用React——你甚至可以在2013年的一篇React博文中看到我的名字。 我也是开源异步迭代器库Repeater.js的创建者和维护者。该库旨在成为创建安全的异步迭代器所缺少的构造函数。[…]我创建了repeaters,这是一个看起来很像Promise构造函数的实用工具类,它让你可以更轻松地将基于回调的API转换为异步迭代器。

InfoQ:您能快速地为我们介绍下repeaters的设计目标吗?

Kim:Repeaters采用了我多年来学到的许多好的异步迭代器设计实践,比如延迟执行、使用有界队列、处理反压以及以可预测的方式传播错误。本质上,它是一个精心设计的API,让开发人员可以顺利地使用异步迭代器,确保他们的事件处理程序总是得到清理,而瓶颈和死锁可以被迅速发现。

InfoQ:您最近发布了Crank.js。您将其描述为一个新的创建Web应用程序的Web框架。为什么要创建一个新的JavaScript框架?

Kim:我知道,感觉像是每周都有一个新的JavaScript框架发布,我在介绍Crank的博文中甚至为又创建了一个框架而道歉。我创建Crank是因为我对最新的React API(如Hooks和Suspense)感到失望,但我仍然希望使用靠着React流行起来的JSX和元素比较算法。我已经愉快地使用React超过五年了,所以我是用了很长时间后才说受够了,并编写了自己的框架。

InfoQ:是什么让您无法忍受了?

Kim:我想我的挫败感是从钩子开始的。我之前很兴奋,React团队致力于避免组件拥有状态,使其函数语法更有用,但是我担心“钩子的规则”,它们似乎很容易规避,这对其他框架是不公平的,因为它有权调用任何名字以use开头的函数。然后,当我开始在实践中学习更多关于钩子的知识,并看到了letconst发明以前在JavaScript中从未见过的新的stale closure缺陷时,我开始怀疑钩子是不是最好的方法。 但对我来说真正的转折点是Suspense项目。[…]

InfoQ:您能详细地说明下吗?

Kim:这时,我开始试用Suspense,因为我认为它将允许人们使用我编写的异步迭代器钩子,就好像它们是同步的一样。但是,我很快就发现,我实际上不可能使用Suspense,因为它对缓存有严格的要求,而且我也不清楚如何缓存以及重用我的钩子所依赖的异步迭代器。 Suspense以及React中异步数据获取的实现需要缓存,这让我有些震惊,因为到目前为止,我只是假设可以在React组件中获得类似于async/await这样的特性。[…]我非常担心,我必须使用key并失效每一个为了使用promises而进行的异步调用。 [我开始意识到]React在组件中使用componentDidWhat或钩子所做的每件事都可以封装成一个单一的异步生成器函数:

代码语言:javascript
复制
async function *MyComponent(props) 
  let state = componentWillMount(props);
  let ref = yield <MyElement />;
  state = componentDidMount(props, state, ref);
  try {
    for await (const nextProps of updates()) {
      if (shouldComponentUpdate(props, nextProps, state)) {
        state = componentWillUpdate(props, nextProps, state);
        ref = yield <MyElement />;
        state = componentDidUpdate(props, nextProps, state, ref);
      }
      props = nextProps;
    }
  } catch (err) {
    return componentDidCatch(err);
  } finally {
    componentWillUnmount(ref);
  }
}

[…] 通过生成JSX元素而不是返回它们,你可以在渲染之前和之后编写代码,类似于componentWillUpdatecomponentDidUpdate。State变成局部变量,而新的props可以通过框架提供的异步迭代器传入,甚至可以使用JavaScript控制流如try/catch/finally从子组件捕获错误并编写清理逻辑,所有这些都在相同的作用域内。

InfoQ:所以您决定使用异步生成器作为这个新框架的基础?

Kim:[…] React团队安排了大量的工程人才来构建一个“UI运行时”,我[意识到我]可以把最困难的部分如堆栈暂挂或调度委托给JavaScript运行时,它提供生成器、异步函数和一个微任务队列来完成这些工作。我觉得React团队所做的一切让人印象深刻,作为一名程序员,那是我所力不能及的,但是,他们把这些特性以原生JavaScript的形式提供了出来,我只需要弄清楚如何把这些拼图拼在一起就可以了。 组件不是只能用同步函数编写,还可以用异步函数,以及同步和异步生成器函数。我走了弯路,在此之前,我一直埋头于我的一个创业点子。在对这个想法进行了长达数月的调查研究之后,Crank诞生了。坦白地说,我希望回到编写应用程序而不是框架的工作中来,但是,JavaScript社区突如其来的兴趣让Crank成了一个愉快的意外。

InfoQ:您提到,Crank.js利用了基于JSX的组件和异步生成器。JSX组件在使用渲染函数的框架(某种程度上类似于React这样的框架或Vue)中非常常见。而生成器很少使用,异步生成器就更少用了。这些构造与开发Web应用程序有什么关系?

Kim:我绝对不是第一个试验生成器和异步生成器的人;我一直在GitHub上寻找新的想法,我看到在前端领域有很多人在用生成器做试验。 然而,也许是由于JavaScript中的生成器早期与async/await和promises存在关联,作为指定组件异步依赖项的一种方式,这些库中似乎有许多都使用生成器来生成promises而只返回JSX元素。我意识到,我们还可以简单地生成JSX元素,并基于虚拟DOM比较算法为异步组件找到一个单独的语义。 最后,我认为,JSX元素和生成器实际上是完美的搭配:你生成元素,框架渲染它们,渲染后的节点通过调用以某种响应模式传递回生成器。我认为,总的来说,很多人,特别是那些有函数编程背景的人,往往不急于采用迭代器和生成器,因为它们是有状态的数据结构,这些人认为,生成器的有状态性增加了推理难度。但实际上,我认为,这是生成器的一个很好的特性,至少在JavaScript中,有状态过程建模的最好方法是使用有状态抽象。 将组件生命周期建模为生成器,我们不仅能够在一个函数中捕获DOM的状态并建模,我们还能以非常透明的方式完成这项工作,因为每个组件实例只有一个生成器执行,其闭包会在两次渲染之间保留。在Crank中,同步生成器组件的恢复次数等于父组件更新它的次数加上组件本身更新的次数。这种可以推理组件执行准确次数的能力,人们基本不再期待React会提供,在实践中,这意味着我们可以借助Crank把副作用直接放到“渲染方法”中,因为这个框架不会在你不期望的时候不断地重新渲染你的组件。

InfoQ:您从开发人员那里收到了什么反馈吗?

Kim:我收到过这样的反馈:“我希望我们在Rust中也能有这样的东西。”我很高兴看到人们参照Crank的思想,然后用其他语言实现它们,这些语言可能会有像Rust futures这样更强大的抽象。

InfoQ:有哪些事情用Crank更简单,用其他框架更难?您能举个例子吗?

Kim:因为所有状态都是局部变量,所以我们可以在生成器组件中自由地组合来自React的概念,比如props、state和refs,这是其他框架无法做到的。例如,这个组件示例比较了新旧props,并根据它们是否匹配来渲染一些不同的东西,在Crank开发早期,我对此感到很震惊:

代码语言:javascript
复制
function *Greeting({name}) {
  yield <div>Hello {name}</div>;
  for (const {name: newName} of this) {
    if (name !== newName) {
      yield (
        <div>Goodbye {name} and hello {newName}</div>
      );
    } else {
      yield <div>Hello again {newName}</div>;
    }
    name = newName;
  }
}
renderer.render(<Greeting name="Alice" />, document.body);
console.log(document.body.innerHTML); // "<div>Hello Alice</div>"
renderer.render(<Greeting name="Alice" />, document.body);
console.log(document.body.innerHTML); // "<div>Hello again Alice</div>"
renderer.render(<Greeting name="Bob" />, document.body);
console.log(document.body.innerHTML); // "<div>Goodbye Alice and hello Bob</div>"
renderer.render(<Greeting name="Bob" />, document.body);
console.log(document.body.innerHTML); // "<div>Hello again Bob</div>"

我们不需要一个单独的生命周期或钩子来比较新旧props,我们只需要在同一个闭包中引用它们。简而言之,比较新旧props就像比较数组中的相邻元素一样简单。 此外,因为Crank将局部状态的概念与重新渲染解耦,我认为它解锁了许多在其他框架中不可能实现的高级渲染模式。例如,你可以想象这样一种架构:子组件具有局部状态,但不会重新渲染,而是由在requestAnimationFrame循环中渲染的单个父组件一次性渲染。有状态的组件不必在每次更新时都重新渲染,在Crank中这很容易实现,因为我们已经从渲染中解耦了状态。 举个例子,你可以看看我制作的这个快速演示,我在其中实现了一个3D立方体/球体演示,去年,React和Svelte的用户在Twitter上讨论过这个演示。我对Crank的性能上限感到兴奋,因为更新组件是通过生成器逐步完成的,当状态只是局部变量,而有状态性本身并没有与反应式系统紧密耦合(迫使每个有状态组件重新渲染,即使有一个祖先组件会重新渲染它)时,你还可以在用户空间做很多有趣的优化。虽然在Crank最初的版本中,我更关注的是正确性和API设计,而不是性能,但我目前正努力提升Crank的速度,而且结果看起来很有希望,尽管我还没有计划对Crank的性能提什么具体的要求。

InfoQ:反过来说,有什么事情使用其他框架更简单,而使用Crank更难?

Kim:我曾批评过Concurrent Mode和React的未来发展方向,但如果React团队能够完成,那么看到组件可以根据主线程拥挤程度而自动调度渲染,还是让人感觉很神奇的。关于如何在Crank中实现这类调度,我有一些想法,但我没有任何具体的解决方案。我的希望是,既然你可以直接在组件中await,那么我们就可以在用户空间中以一种透明、可选的方式直接实现调度。 此外,尽管我不喜欢React的钩子,但对于库作者如何将他们的整个API封装在一个或两个钩子中,我还是有一些话要说。有一件我应该料到但实际没料到的事情是,早期的采用者大力呼吁使用类似钩子这样的特性来将他们的库与Crank集成。我还不确定那会是什么样子,但我也有一些想法。

受访者介绍:

Brian Kim是一名独立的前端工程师。他是开源异步迭代器库Repeater.js的创建者和维护者。该库旨在成为创建安全的异步迭代器所缺少的构造函数。

原文链接:

Crank, a New Front-End Framework with Baked-In Asynchronous Rendering - Q&A with Brian Kim

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/xcmgpKSzaSSQWX5QVgWl
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券