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": {}// }
把你的注意力集中在这个对象的type 和props 属性上。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地反映出返回的对象,其中children 是props 对象的一部分。相反,当调用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>);