React.js中声明式编程和命令式编程的区别是什么?

164

最近我一直在学习有关Facebook JavaScript库React.js的功能和使用方式。当谈到它与JavaScript世界中其他部分的不同之处时,通常会提到两种编程风格——声明式命令式

这两种风格有什么区别呢?


37
命令式编程:告诉“机器”如何做某事,结果会按照你的要求发生。 声明式编程:告诉“机器”你想要发生什么,让计算机自己想出如何实现。 - rickyduck
5
Tyler McGinnis写了一篇长文,其中包含一些很好的例子,详细介绍了命令式编程和声明式编程之间的区别。 - Ian Dunn
1
为什么要将长答案作为注释添加? - Alex
2
以上链接是正确的,但链接中包含尾部斜杠会导致404错误。latentflip.com/imperative-vs-declarative - James Yoo
13个回答

236

像React一样的声明式编程风格可以通过描述"它应该长成这个样子"来控制应用程序的流程和状态。而命令式编程则相反,通过描述"这是你应该做的事情"来控制应用程序。

声明式编程的好处在于,您不会陷入表示状态的实现细节中。您将代理保持应用程序视图一致性的组织部分,因此只需担心状态。

想象一下您有一个管家(类比于框架),您想制作晚餐。在命令式编程世界中,您将逐步告诉他们如何制作晚餐。您必须提供这些指令:

Go to the kitchen
Open fridge
Remove chicken from fridge
...
Bring food to the table
在一个声明式的世界里,你只需要简单地描述你想要什么。
I want dinner with chicken.

如果您的管家不知道如何烹制鸡肉,那么您无法以声明式风格操作。就像如果 Backbone 不知道如何突变自身以完成某个任务,您不能仅仅告诉它执行该任务。React 能够以声明式方式工作,因为它“知道如何烹制鸡肉”,例如。相比之下,Backbone 只知道如何与厨房进行交互。

能够描述状态可以大大减少错误的表面积,这是一个好处。另一方面,您可能在实现状态时委托或抽象了掉一些实现方式,因此在 如何 发生事情方面可能会有较小的灵活性。


感谢Nathan六年后的回答。非常简洁易懂的比喻。 - IamToobDude

126

想象一个简单的 UI 组件,比如一个“喜欢”按钮。当你点击它时,如果它之前是灰色的,那么它就会变成蓝色;如果它之前是蓝色的,那么它就会变成灰色。

这样做的命令式方式如下:

if( user.likes() ) {
        if( hasBlue() ) {
            removeBlue();
            addGrey();
        } else {
            removeGrey();
            addBlue();
        }
    }

基本上,您需要检查屏幕上当前的内容,并处理所有必要的更改以使用当前状态重新绘制它,包括撤消先前状态的更改。您可以想象在现实世界情况下这可能会有多么复杂。

相反,声明式方法则是:

return this.state.liked ? <blueLike /> : <greyLike />;

由于声明性方法将不同的关注点分离开来,因此它只需要处理UI在特定状态下的外观应该是什么样子,因此更加简单易懂。


49

最好比较React(声明式)和JQuery(命令式)以展示它们的差异。

在React中,你只需要在render()方法中描述你的UI的最终状态,而不必担心如何过渡从先前的UI状态到新的UI状态。例如,

render() {
  const { price, volume } = this.state;
  const totalPrice = price * volume;

  return (
    <div>
      <Label value={price} className={price > 100 ? 'expensive' : 'cheap'} ... />
      <Label value={volume} className={volume > 1000 ? 'high' : 'low'} ... />
      <Label value={totalPrice} ... />
      ...
    </div>
  )
}

另一方面,JQuery要求您以命令方式转换UI状态,例如,选择标签元素并更新它们的文本和CSS:

updatePrice(price) {
  $("#price-label").val(price);
  $("#price-label").toggleClass('expansive', price > 100);
  $("#price-label").toggleClass('cheap', price < 100);

  // also remember to update UI depending on price 
  updateTotalPrice();
  ... 
}

updateVolume(volume) {
  $("#volume-label").val(volume);
  $("#volume-label").toggleClass('high', volume > 1000);
  $("#volume-label").toggleClass('low', volume < 1000);
  
  // also remember to update UI depending on volume
  updateTotalPrice();
  ... 
}

updateTotalPrice() {
  const totalPrice = price * volume;
  $("#total-price-label").val(totalPrice);
  ...
}

在现实世界的情境下,需要更新的UI元素会更多,还包括它们的属性(例如CSS样式和事件监听器)等。如果使用JQuery来进行命令式编程,那么这将变得复杂而繁琐。容易忘记更新某些UI部分或忘记删除旧的事件处理程序(导致内存泄漏或处理程序多次触发),等等。这就是错误发生的地方,即UI状态和模型状态不同步。

React声明式的方法不会出现状态不同步的问题,因为我们只需要更新模型状态,React负责保持UI和模型状态同步。

  • 在幕后,React将使用命令式代码更新所有已更改的DOM元素。

您还可以阅读我对编程中声明式和命令式范例的区别是什么?的回答。

PS:从上述jQuery示例,您可能会想到,如果我们将所有DOM操作放入updateAll()方法中,并在模型状态更改时每次调用该方法,UI将永远不会失步。你是正确的,这实际上就是React所做的,唯一的区别在于jQuery的updateAll()会导致许多不必要的DOM操作,但React只会使用其虚拟DOM差异算法来更新已更改的DOM元素。


34

这是一个很好的类比:

*命令式响应:从停车场北出口出去,向左转。上I-15南行,直到到达Bangerter高速公路出口。像前往宜家一样右转离开出口。直走,在第一个红绿灯处右转。通过下一个红绿灯后,再左转。我的房子是#298。

声明式响应:我的地址是Utah州Draper市Immutable Alley西298号,邮编84020*

来源:https://tylermcginnis.com/imperative-vs-declarative-programming/


15

JavaScript的命令式代码会指示JavaScript执行每个步骤的方式。而声明式代码则是告诉JavaScript我们想要完成的任务,然后让JavaScript处理执行步骤。

React是声明式的,因为我们编写我们想要的代码,React负责使用我们声明的代码执行所有的JavaScript / DOM步骤,以达到我们想要的结果。


12
在命令式编程中,一个真实的例子是进入酒吧并对酒吧招待说以下指令:
--从架子上取下一杯
--将杯子放在饮料机前面
--拉动手柄直到玻璃杯满
--把玻璃杯递给我。
相反地,在声明式编程中,你只需要说:“请给我一杯啤酒。” 在声明式风格中,我们编写代码是描述应该是什么,而不是如何做的过程。
在声明式编程中,开发者只需描述其想要实现的目标,无需列出所有步骤以使其工作。这使得您的代码更简单,易读。然而,在命令式风格中,开发人员逐步定义任务。
例如,for循环是命令式风格,因为您需要声明起始点,结束点和间隔。然而,Array.map是一种声明式表达方式。
React提供声明式方法的事实使其易于使用,因此生成的代码简单,通常会导致更少的错误和更好的可维护性。
由于React遵循声明式范例,并且无需告诉它如何与DOM进行交互,您只需声明想要在屏幕上看到什么,React就可以为您完成工作。

7

声明式编程是一种编程风格,应用程序的结构优先描述应该发生什么,而不是定义它如何发生。

为了理解声明式编程,让我们将其与命令式编程进行比较(一种只关注如何使用代码实现结果的编程风格)。

例如:使字符串符合URL规范。通常,这可以通过将字符串中的所有空格替换为连字符来完成,因为空格不符合URL规范。首先,对于这个任务来说,一种命令式的方法是:

const string = "difference between declarative and imperative in react.js";
const urlFriendly = "";
for (var i = 0; i < string.length; i++) {
    if (string[i] === " ") {
      urlFriendly += "-";
    } else {
      urlFriendly += string[i];
    }
}
console.log(urlFriendly); // "difference-between-declarative-and-imperative-in-react-js"

在这个例子中,我们循环遍历字符串中的每个字符,并在出现空格时替换它们。这个程序的结构只关心如何实现这样的任务。我们使用for循环和if语句,并使用等号运算符设置值。仅仅看代码本身并不能告诉我们太多信息,因为命令式程序需要大量注释才能理解正在发生的事情。
现在让我们看一个相同问题的声明式方法:
const string = "Difference between declarative and imperative in React.js?";
const urlFriendly = string.replace(/ /g, "-");
console.log(urlFriendly);

在这里,我们使用string.replace和正则表达式一起替换所有空格为连字符。使用string.replace是描述应该发生什么的方法:应该替换字符串中的空格。如何处理空格的详细信息都在replace函数中抽象化了。

在声明式程序中,语法本身描述了应该发生什么,而事情如何发生的细节被抽象化了。

基本上,声明式编程产生的应用程序更容易推理,当应用程序更容易推理时,它就更容易扩展。有关声明式编程范例的其他详细信息可以在Declarative Programming wiki找到。

现在,让我们考虑构建文档对象模型的任务。一种命令式方法会关注DOM是如何构建的:

const target = document.getElementById("target");
const wrapper = document.createElement("div");
const headline = document.createElement("h1");
wrapper.id = "welcome";
headline.innerText = "Hello World";
wrapper.appendChild(headline);
target.appendChild(wrapper);

这段代码涉及到创建元素、设置元素并将它们添加到文档中。如果使用命令式方式构建DOM,那么要对一万行代码进行更改、添加功能或者扩展是非常困难的。

现在让我们看一下如何使用React组件以声明方式构建DOM:

const { render } = ReactDOM;
const Welcome = () => (
   <div id="welcome">
      <h1>Hello World</h1>
   </div>
);

render(<Welcome />, document.getElementById("target"));

React是一种声明式编程方式。在这里,Welcome组件描述了应该呈现的DOM。渲染函数使用组件中声明的指令来构建DOM,抽象掉了如何呈现DOM的细节。我们可以清楚地看到,我们希望将Welcome组件呈现到具有ID为target的元素中。

来源: 《现代React应用程序开发模式》


4

2
解释每一个步骤是一种必要的方法,例如,我们需要创建一个带有“Hello World!”文本的段落标签。
//Imperative 

const para = document.createElement('p');
para.innerText = 'Hello World !';
document.querySelector('#root').appendChild(para);

定义所需的目标状态,而不指定确切的过程。例如,使用带有文本的p标签,而不告诉createElement或innerText。
 //Declarative 

 import React from "react"; 
 import ReactDOM from "react-dom"; 
 
 const App = () =>{
  return(<p>Hello World !</p>);
 }

 ReactDOM.render(<App />, document.getElementById("root"));

1

那么,什么是声明式编程?

声明式编程是指更有经验的专业人员以一种方式编写代码,使其行为可以通过使用外部配置来改变,这些配置代表对象的导向图。例如,Jetpack ComposeFlutter Widgets

avalonia funcui

传统的解决方案使用了类似于XML的标记语言来表示UI对象树。例如,WPF中的XAML(请参见FuncUI)或Qt中的*.ui。它们仍然具有内部对象组合语法(如DOM API),但强烈建议使用标记语言以声明方式实现UI。

wpf-xaml

Facebook引入JSX之后,整个企业都迁移到Web上,因为它为开发人员提供了更多的设计功能和定制用户体验的可访问性,通过使用函数式编程(代码显着便宜)。 在这种情况下,JSX是声明性的到HTML(更正确的是DOM API -> React -> JSX

facebook-jsx

如果你学过计算机科学,你一定知道软件中的所有内容都是建立在抽象之上的。例如,在macOS中,硬件mach内核使用。Mach内核Core OS使用。Core OSQuickTime使用。QuickTimeGUI使用。正如你所看到的,关系的主语对关系的客体是陈述性的。

osx-architecture

这个想法很简单。如果C/C++语言是声明式的,那么JavaScript就是对C++的声明,而React则是对JavaScript的声明。通常情况下,开发人员正在设计一些东西,以减少使用React(React -> Plain JS objects -> JSX)时的应用程序开发成本。

react-declarative

社区提出了一个新术语来定义这种现象,它被称为低代码。您可以查看codesanbox中示例应用程序的源代码或浏览github组织以获取更多示例
import { Scaffold2, IScaffold2Group } from "react-declarative";

const options: IScaffold2Group[] = [
  {
    id: 'build',
    label: 'Build',
    children: [
      {
        id: 'authentication',
        label: 'Authentication',
        isVisible: async () => await ioc.authService.hasRole('unauthorized'),
        icon: PeopleIcon,
        tabs: [
          { id: 'tab1', label: 'Tab1 in header', },
          { id: 'tab2', label: 'Tab2 in header', },
        ],
        options: [
          { id: 'tab1', label: 'Tab1 in side menu' },
          { id: 'tab2', label: 'Tab2 in side menu' },
        ],
      },
      { id: 'Database', label: 'Label is optional (can be generated automatically from ID in snake case)', icon: DnsRoundedIcon, },
      { id: 'Storage', isDisabled: async () => await myAmazingGuard(), icon: PermMediaOutlinedIcon, },
      { id: 'Hosting', icon: PublicIcon, },

...

<Scaffold2
  options={options}
  ...

顺便提一下,声明式编程更SOLID,因为我们不是手动通过new operator创建GUI类实例(详见依赖反转原则)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接