if you’re curious… it’s nice to know how it works.

函数组件和类组件有什么区别

从一个 demo (父组件传入 select 中的 value,子组件设置一个 3s 的延时打印这个 value,在 3s 中我们改变 select 中的 value 看看子组件最后打印了啥) 来看区别

两个组件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default class ClassButton extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};

handleClick = () => {
setTimeout(this.showMessage, 3000);
};

render() {
return <button onClick={this.handleClick}>Follow(class)</button>;
}
}
1
2
3
4
5
6
7
8
9
10
11
function FunctionButton(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};

const handleClick = () => {
setTimeout(showMessage, 3000);
};

return <button onClick={handleClick}>Follow(Function)</button>;
}

父组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class App extends React.Component {
state = {
user: ``
};
changeSelect = ({ target: { value } }) => {
this.setState({ user: value });
};
render() {
return (
<div className='App'>
<select onChange={this.changeSelect}>
<option value=''>Choose a name</option>
<option value='tiny'>tiny</option>
<option value='mini'>mini</option>
</select>
<ClassButton user={this.state.user} />
<FunctionButton user={this.state.user} />
</div>
);
}
}

最终结果:class 组件打印的是改变后的值,function 组件打印的是改变前的值。

说到改变就容易让人产生迷惑,props 不是不可变吗?

props 是不可变这点没错,但是 this 总是可变的,React 每次 re-render 都会让 this 变动,所以 class 组件中拿到的 user 永远都是最新的 props

但是如果想拿到当前props 应该怎么办呢,答案也很简单:在 render 函数中加上 const props = this.props,但是这样还是存在一个小问题的,showMessage 函数如果想拿到 props 中的 user 岂不是也要把函数写在 render 函数中吗,这样 class 的方法似乎就没有什么意义了。


再来看下 Function 组件带来的结果,是符合我们需求的,暂定这种特性叫做 Capture Value 吧。

Function components capture the rendered values.

函数式组件中没有用到 this ,而是传递的 props,那结果想必也一清二楚了。

在 Hooks 中也会有这样的特性, 这边就不贴代码了,Hooks 只是写法上有些不同。


但是如果想要 Function 组件规避 Capture Value 属性需要怎么做?

可以利用 useRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default function HooksButton() {
const latestMessage = useRef('');
const [message, setMessage] = useState('');

const showMessage = () => {
alert('You said: ' + latestMessage.current);
};

const handleSendClick = () => {
setTimeout(showMessage, 3000);
};

const handleMessageChange = e => {
setMessage(e.target.value);
latestMessage.current = e.target.value;
};

return (
<>
<input value={message} onChange={handleMessageChange} />
<button onClick={handleSendClick}>Send</button>
</>
);
}

如何区分开 function 和 class 组件

在 JavaScript 中,「如何区分 function 和 class」就是一个经典问题。

StackOverflow 上可以搜到区分 JS 中的 functionclass 的相关方法,其中最高票回答是

1
2
3
4
5
6
7
8
9
10
11
12
function isClass(func) {
return (
typeof func === 'function' &&
/^class\s/.test(Function.prototype.toString.call(func))
);
}

class A {}
function B() {}

Function.prototype.toString.call(A); // class A {}
Function.prototype.toString.call(B); // function B() {}

原因也很简单,因为在 ES6 的规范中, toString 也支持了 ClassDeclaration。


不过在 Babel 中,class 是转译成 function 的,也就是说 Babel 环境下, classfunction 只是名字不同的 function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class A {}

// Babel

('use strict');

function _instanceof(left, right) {
if (
right != null &&
typeof Symbol !== 'undefined' &&
right[Symbol.hasInstance]
) {
return !!right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}

function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError('Cannot call a class as a function');
}
}

var A = function A() {
_classCallCheck(this, A);
};

React 中是如何检查 class 组件的

1
2
3
4
5
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}

想必大家对 JS 中的原型链有所认识。

x instanceof Y 做的事就是沿着 x.__proto__ 寻找 Y.prototype

这也可以用来检查一个类是否拓展了另一个类。

1
Greeting.prototype instanceof React.Component;

不过这样在多副本的 React 中会失效


基于上面的一些原因,React 采用的是下面的方式来检查 class:

React.Component.prototype 上会挂着 isReactComponent

1
2
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent); // {}

参考: