React元素与组件的详细指南

100 阅读6分钟

React元素、组件和实例是React中的不同术语,它们密切相关。本指南将引导你了解所有这三个术语,并逐步解释它们。我们将以下面的代码片断为例开始:

const App = () => {  return <p>Hello React</p>;};

React组件从字面上看就是一个组件的声明,正如我们在前面的代码片断中看到的那样。在我们的例子中,它是一个函数组件,但它也可以是任何其他类型的React组件(如React类组件)。

在函数组件的情况下,它被声明为一个JavaScript函数,返回React的JSX。虽然更复杂的JSX是HTML和JavaScript的混合体,但这里我们处理的是一个简单的例子,它只返回一个带有内部内容的HTML元素:

(props) => JSX

我们可以从另一个组件中提取一个组件,并以如下方式渲染它。只要我们在另一个组件中把这个组件作为带角括号的React元素(例如:<Greeting /> ),就会发生渲染组件的情况。

const Greeting = ({ text }) => {  return <p>{text}</p>;};
const App = () => {  return <Greeting text="Hello React" />;};

我们也可以将一个组件作为React元素多次渲染。每当一个组件被渲染成元素时,我们就会创建一个该组件的实例

const Greeting = ({ text }) => {  return <p>{text}</p>;};
const App = () => {  return (    <>      <Greeting text="Hello Instance 1 of Greeting" />      <Greeting text="Hello Instance 2 of Greeting" />    </>  );};

虽然一个React组件被声明一次,但它可以在JSX中作为一个React元素被多次使用。当它被使用时,它就成为该组件的一个实例,并生活在React的组件树中。基本上这就是对React组件、元素和实例的简明解释。然而,为了更深层次地理解一切,我们需要了解React是如何用JSX显示HTML的。

深入了解React元素

让我们退一步,再次从一个简单的例子开始:

const App = () => {  return <p>Hello React</p>;};

每当React组件被调用(渲染)时,React在内部调用其React.createElement() 方法,该方法返回以下对象:

console.log(App());
// {//   $$typeof: Symbol(react.element)//   "type": "p",//   "key": null,//   "ref": null,//   "props": {//     "children": "Hello React"//   },//   "_owner": null,//   "_store": {}// }

把你的注意力集中在这个对象的typeprops 属性上。type 代表实际的HTML元素,而props 则是传递给这个HTML元素的所有HTML属性(加上内部内容,读作:孩子)。

当看上面的段落HTML元素时,你可以看到没有属性被传递给它。然而,React将children 作为伪HTML属性,而children 代表HTML标签之间呈现的一切。当向段落的HTML元素添加属性时,这个事实会变得更清楚:

const App = () => {  return <p className="danger">Hello React</p>;};
console.log(App());
// {//   $$typeof: Symbol(react.element)//   "type": "p",//   "key": null,//   "ref": null,//   "props": {//     "children": "Hello React",//     "className": "danger"//   },//   "_owner": null,//   "_store": {}// }

本质上,React除了将所有HTML属性转化为React道具外,还将内部内容添加为children 属性。

如前所述,React的createElement() 方法是内部调用的。因此,我们可以用它来替代返回的JSX(为了学习)。React的createElement方法需要一个类型、props和children作为参数。我们将HTML标签'p' 作为第一个参数,将props 作为一个对象,将className 作为第二个参数,将children 作为第三个参数。

const App = () => {  // return <p className="danger">Hello React</p>;  return React.createElement(    'p',    { className: 'danger' },    'Hello React'  );};

请看该方法的调用并没有1:1地反映出返回的对象,其中childrenprops 对象的一部分。相反,当调用React的createElement() 方法时,孩子们被单独作为参数提供。然而,由于children 被视为道具,我们也可以在第二个参数中传递它们。

const App = () => {  // return <p className="danger">Hello React</p>;  return React.createElement(    'p',    {      className: 'danger',      children: 'Hello React'    }  );};

虽然默认情况下children 是作为第三个参数使用的。下面的例子显示了一个React组件是如何将HTML树渲染成JSX的,并通过React的createElement() 方法转换为React元素的。重要的行被突出显示。

const App = () => {  return (    <div className="container">      <p className="danger">Hello React</p>      <p className="info">You rock, React!</p>    </div>  );};
console.log(App());
// {//   $$typeof: Symbol(react.element)//   "type": "div",//   "key": null,//   "ref": null,//   "props": {//     "className": "container",//     "children": [//       {//         $$typeof: Symbol(react.element)//         "type": "p",//         "key": null,//         "ref": null,//         "props": {//           "className": "danger",//           "children": "Hello React"//         },//         "_owner": null,//         "_store": {}//       },//       {//         $$typeof: Symbol(react.element)//         "type": "p",//         "key": null,//         "ref": null,//         "props": {//           className: "info",//           children: "You rock, React!"//         },//         "_owner": null,//         "_store": {}//       }//     ]//   },//   "_owner": null,//   "_store": {}// }

同样在内部,所有的JSX都通过React的createElement() 方法进行翻译。虽然我们返回一个元素作为对象,但在这个例子中它有多个内部元素作为子元素。这在调用创建元素的方法时变得更加明显。

const App = () => {  // return (  //   <div className="container">  //     <p className="danger">Hello React</p>  //     <p className="info">You rock, React!</p>  //   </div>  // );
  return React.createElement(    'div',    {      className: 'container',    },    [      React.createElement(        'p',        { className: 'danger' },        'Hello React'      ),      React.createElement(        'p',        { className: 'info' },        'You rock, React!'      ),    ]  );};

使用多个组件并不能改变这种HTML元素的聚合。以下面的代码片段为例,我们将段落的HTML元素提取为独立的React组件。

const Text = ({ className, children }) => {  return <p className={className}>{children}</p>;};
const App = () => {  return (    <div className="container">      <Text className="danger">Hello React</Text>      <Text className="info">You rock, React!</Text>    </div>  );};

如果你自己遍历底层的HTML元素,你会发现它和以前没有变化。只是在React土地上,我们把它提取为可重用的组件。所以调用React的createElement() 方法会和以前一样。

作为一个额外的学习,我们也可以通过在React的createElement() 方法调用中使用提取的组件作为第一个参数来混合两个世界。

const Text = ({ className, children }) => {  return <p className={className}>{children}</p>;};
const App = () => {  // return (  //   <div className="container">  //     <Text className="danger">Hello React</Text>  //     <Text className="info">You rock, React!</Text>  //   </div>  // );
  return React.createElement(    'div',    {      className: 'container',    },    [      React.createElement(        Text,        { className: 'danger' },        'Hello React'      ),      React.createElement(        Text,        { className: 'info' },        'You rock, React!'      ),    ]  );};

为了使这个例子完整,我们也必须用React的createElement() 来替换子组件的JSX。

const Text = ({ className, children }) => {  return React.createElement('p', { className }, children);};
const App = () => {  return React.createElement(    'div',    {      className: 'container',    },    [      React.createElement(        Text,        { className: 'danger' },        'Hello React'      ),      React.createElement(        Text,        { className: 'info' },        'You rock, React!'      ),    ]  );};

这样一来,我们只是在使用React的createElement() 方法,而不是JSX,同时还能从彼此之间提取组件。但这绝对不是推荐的做法,它只是说明了React是如何从它的JSX中创建元素的。

我们在这一节学到的是,不仅<Text /><Greeting /> 是React元素,而且JSX中的所有其他HTML元素也在ReactcreateElement() 调用中得到翻译。本质上,在引擎盖下**,我们使用React元素来渲染所需的JSX**。因为我们想在React中使用声明式编程而不是命令式编程,所以我们使用JSX作为默认,而不是React的createElement() 方法。

调用React函数组件

调用React函数组件与将其作为React元素有什么实际区别?在前面的代码片段中,我们已经调用了函数组件,以便从React的createElement() 方法返回其输出。当把它作为React元素使用时,其输出有什么不同。

const App = () => {  return <p>Hello React</p>;};
console.log(App());// {//   $$typeof: Symbol(react.element),//   "type": "p",//   "key": null,//   "ref": null,//   "props": {//     "children": "Hello React"//   },//   "_owner": null,//   "_store": {}// }
console.log(<App />);// {//   $$typeof: Symbol(react.element),//   "key": null,//   "ref": null,//   "props": {},//   "type": () => {…},//   "_owner": null,//   "_store": {}// }

输出略有不同。当把React组件作为元素使用而不是调用它时,我们会得到一个type 函数,其中包含了所有函数组件的实现细节(例如,儿童、钩子)。props 是所有其他的HTML属性,它们被传递给组件。

console.log(<App className="danger" />);// {//   $$typeof: Symbol(react.element),//   "key": null,//   "ref": null,//   "props": {       "className": "danger"//   },//   "type": () => {…},//   "_owner": null,//   "_store": {}// }

对于一个真正的React应用来说,type 成为一个函数而不再是一个字符串,这意味着什么?让我们通过一个例子来看看这个问题,这个例子说明了为什么我们不应该调用React函数组件。首先,我们通过使用角括号来使用组件的意图。

const Counter = ({ initialCount }) => {  const [count, setCount] = React.useState(initialCount);
  return (    <div>      <button onClick={() => setCount(count + 1)}>+</button>      <button onClick={() => setCount(count - 1)}>-</button>
      <div>{count}</div>    </div>  );};
const App = () => {  return (    <div>      <Counter initialCount={42} />    </div>  );};

根据我们之前的学习,我们会认为调用一个函数组件而不是把它作为React元素来使用,应该是开箱即用。确实如此,正如我们接下来看到的那样。

const App = () => {  return (    <div>      {Counter({ initialCount: 42 })}    </div>  );};

但让我们探讨一下为什么我们不应该调用React函数组件。我们将对渲染的子组件使用条件性渲染,可以通过点击按钮来切换。

const App = () => {  const [isVisible, setVisible] = React.useState(true);
  return (    <div>      <button onClick={() => setVisible(!isVisible)}>Toggle</button>
      {isVisible ? Counter({ initialCount: 42 }) : null}    </div>  );};

当我们把子组件切换为不可见时,我们会得到一个错误提示。*"未发现的错误。渲染的钩子比预期的少"。*如果你以前使用过React钩子,你可能知道这应该是可能的,尽管如此,因为钩子被分配在子组件中(这里:Counter),这意味着如果这个组件取消挂载,因为它是有条件渲染的,钩子应该被移除而不会有任何错误。只有当一个挂载的组件改变了它的钩子数量(这里是:App),它才会崩溃。

继续阅读。React中的条件性钩子

但确实它崩溃了,因为一个安装的组件(这里:App)改变了它的钩子的数量。因为我们是以函数的形式调用子组件(这里是:Counter),React并没有把它当作React组件的一个实际实例。相反,它只是将子组件的所有实现细节(如钩子)直接放在其父组件中。因为钩子的实现由于条件渲染而在被安装的组件(这里:App)中消失了,所以React应用程序崩溃了。

从本质上讲,目前的代码与下面的相同,因为子组件没有被当作一个独立的组件实例。

const App = () => {  const [isVisible, setVisible] = React.useState(true);
  return (    <div>      <button onClick={() => setVisible(!isVisible)}>Toggle</button>
      {isVisible        ? (() => {            const [count, setCount] = React.useState(42);
            return (              <div>                <button onClick={() => setCount(count + 1)}>+</button>                <button onClick={() => setCount(count - 1)}>-</button>
                <div>{count}</div>              </div>            );          })()        : null}    </div>  );};

这违反了钩子的规则,因为React钩子不能在组件中被有条件地使用。

继续阅读。学习React Hooks

我们可以通过告诉React这个React组件来解决这个错误,作为回报,它被视为一个组件的实际实例。然后它可以在这个组件的实例中分配实现细节。当有条件的渲染开始时,该组件就会取消挂载,并随之取消其实现细节(如钩子)。

const App = () => {  const [isVisible, setVisible] = React.useState(true);
  return (    <div>      <button onClick={() => setVisible(!isVisible)}>Toggle</button>
      {isVisible ? <Counter initialCount={42} /> : null}    </div>  );};

在这里你可以看到为什么React组件的实例有意义。每个实例都会分配自己的实现细节,而不会泄露给其他组件。因此我们使用React元素,而不是在JSX中调用一个函数组件。总之,一个返回JJSX的函数可能不是一个组件。这取决于它是如何被使用的。

React元素与组件

让我们总结一下React元素和组件。虽然React Component是一个组件的一次性声明,但它可以作为JJSX中的React元素使用一次或多次。在JSX中,它可以使用角括号,然而,在引擎盖下React的createElement 方法启动,为每个HTML元素创建React元素作为JavaScript对象。

const Text = ({ children }) => {  console.log('I am calling as an instance of Text');
  return <p>{children}</p>;};
console.log('I am a component', Text);
const App = () => {  console.log('I am calling as an instance of App');
  const paragraphOne = <p>You rock, React!</p>;  const paragraphTwo = <Text>Bye!</Text>;
  console.log('I am an element:', paragraphOne);  console.log('I am an element too:', paragraphTwo);
  return (    <div>      <p>Hello React</p>      {paragraphOne}      {paragraphTwo}    </div>  );};
console.log('I am a component', App);console.log('I am an element', <App />);console.log('I am an element', <p>too</p>);