美文网首页javascript
React 如何区别class和function?

React 如何区别class和function?

作者: 一蓑烟雨任平生_cui | 来源:发表于2020-05-28 15:36 被阅读0次

React定义组件的方式有两种,class和function。如下:

函数式定义:

function Button() {
  return <div>hello</div>
} 

class方式定义:

class Button extends React.Component {
  render() {
    return <div>>hello</div>
  }
}

当我们需要渲染Button组件的时候,直接使用即可,无需关心它是通过什么方式定义的。

<Button />

但是React内部会关心它是通过什么方式定义的。

如果是函数,React会直接调用。

// React 内部
const result = Button(props); // <div>Hello</div>

如果是class,React 需要先用 new 操作符将其实例化,然后调用刚才生成实例的 render 方法:

// React 内部
const instance = new Button(props) // Button {}
const result = instance.render() // <div>Hello</div>

无论哪种情况,React的目的是获取渲染后的Node(节点),具体获取方式取决于Button是如何定义的。

所以React是怎么区分class和function的?

答案是:在Component的原型上定义属性 isReactComponent = {}

// React 内部
class Component {}
Component.prototype.isReactComponent = {}

简单吧,但React为什么采用如此简单的方案呢?

以下通过JavaScript的诸多特性去逐步解释,这其中涉及 new this class 箭头函数 prototype __propto__ instanceof等方面。

首先理解 new 在JavaScript中是干什么的。

在之前,js是没有类这个概念的,但是可以通过常规函数来模拟。通过new操作符调用函数,意味着任何函数都可以作为构造函数使用。

// 只是一个函数
function Person(name) {
  this.name = name
}

var jack = new Person('Jack') // Person {name: 'Jack'}
var george = Person('George') // 没用的

如果直接调用函数,函数内部的this在非严格模式下指向全局对象window,在严格模式下为undefined。所以直接调用会操作window属性或者报错。

通过new操作符调用函数,函数内部所做的事情就是,首先创建一个 {},赋值给函数内部的this,初始化之后隐式的返回this。同时创建的实例可以共享Person.prototype上的属性和方法。

function Person(name) {
  this.name = name
}
Person.prototype.sayHi = function() {
  console.log('Hi, I am ' + this.name)
}
var jack = new Person('Jack')
jack.sayHi()

ES6 推出类以后,我们就可以通过class的方式实现上述效果:

class Person {
  constructor(name) {
    this.name = name
  }
  sayHi() {
    console.log('Hi, I am ' + this.name)
  }
}

let jack = new Person('Jack')
jack.sayHi()

类是不允许直接调用的,必须通过 new 操作符去调用,否则报错。

let jack = new Person('Jack')
// 如果 Person 是个函数:有效
// 如果 Person 是个类:依然有效

let george = Person('George') // 忘记使用 `new`
// 如果 Person 是个函数: 只是简单调用,可能会出错
// 如果 Person 是个类:立即失败

这样可以帮助我们在早期就捕获到错误,而不会出现类似上面this.name时this是window还是undefined这种潜在的错误。

所以React在调用类组件时,需要通过new的方式,而不是当作常规函数调用。

以下为调用时加不加 new 的差别:

new Person() Person()
class this 是一个 Person 实例 TypeError
function this 是一个 Person 实例 this 是 window 或 undefined

这就是 React 正确调用你的组件很重要的原因。如果组件被定义为一个类,React 需要使用 new 来调用它。

单纯的判断是函数还是类,还是比较容易的。事实上,在开发中,React都会经过babel将类等新语法编译成在可在低版本浏览器上运行的代码。所以class会被编译成经过特殊处理的函数。又该如何判断?

以下为class编译后的伪代码:

function Person(name) {
  if (!(this instanceof Person)) {
    throw new TypeError('Cannot call a class as a function')
  }
  this.name = name
}

new Person('Jack') // OK
Person('George') // 无法把类当做函数来调用

为什么不干脆都都通过new的方式调用呢?并不可以。

对于常规函数来说,勉强可取,但是函数内部生成的实例对我们并没有什么用。我们只是关心其返回的节点。

此外还有两个致命的问题:

第一:箭头函数。

箭头函数是不可以被当作构造函数通过new的方式调用的。因为箭头函数没有自己的this,其内部的this指向离它最近的常规函数所处的上下文。

而且箭头函数没有 prototype 属性。那我们是不是可以通过函数是否有prototype属性来判断直接调用还是通过new方式调用?

(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}

不可以!

万一箭头函数被babel编译过呢。

那干脆把class和箭头函数都通过babel编译成常规函数,然后都通过new的方式不行吗? 是的,不行!看第二个原因。

第二:不能都使用new的原因是,React支持组件返回字符串、数字等基本类型的值。

function Title() {
  return 'hello title'
}

Title() // 'hello title'
new Title() // {}

返回另一个对象的函数,在使用new调用时会覆盖其创建的实例。但是,如果一个函数的返回值不是一个对象,它会被 new 完全忽略。如果你返回了一个字符串或数字,就好像完全没有 return 一样。

function Answer() {
  return 42
}

Answer() //  42
new Answer() // Answer {}

当使用 new 调用函数时,是没办法读取基本类型的返回值的。因此如果 React 总是使用 new,就没办法增加对返回字符串的组件的支持!

至此清晰的一点是,React在使用类(包括Babel编译成函数)组件时使用new调用,使用常规函数或箭头函数(包括Babel编译)时直接调用。但还是没有完美的方式区分。

不要忘了,我们定义class组件时,需要继承自React.Component,以便可以共享其原型上的方法,比如 setState()、forceUpdate()、render()等。与其检查所有自定义的类,为什么不借助React.Component检查呢?

我们知道,x instanceof Y 所做的就是判断 Y.prototype 是否在x的原型链上。

比如:

class Person extends React.Component {
  render() {
    return <div>hello</div>
  }
}

const p = new Person()

p instanceof Person // true
  // p.__proto__ ---> Person.prototype

p instanceof React.Component // true
  // p.__proto__.__proto__ ---> React.Component.prototype

p instanceof Object // true
  // p.__proto__.__proto__.__proto__ ---> Object.prototype

p instanceof Array // false
  // p.__proto__.__proto__.__proto__.__proto__ ---> null (原型链顶端)

所以基类和父类之间存在如下关系:

class A {}
class B extends A {}

// 因为 
B.prototype.__proto__ === A.prototype // true
// 所以 
B.prototype instanceof A // true

所以通过这种方式可以判断是class还是function。但React还不是这么判断的。

因为 instanceof 解决方案还存在不足之处,当页面上有多个 React 副本,并且我们要检查的组件继承自 另一个 React 副本的 React.Component 时,这种方法是无效的。尽管在一个项目里混合多个 React 副本是不好的,但我们不能排除这种问题的存在性。

另一点启发是去检查原型链上的 render 方法。然而,当时还不确定组件的 API 会如何演化。每一次检查都有成本,所以我们不想再多加了。如果 render 被定义为一个实例方法,例如使用类属性语法,这个方法也会失效。

因此, React 通过为基类增加一个特别的标记。React 检查是否有这个标记,以此知道某样东西是否是一个 React 组件类。

最初这个标记是在 React.Component 这个基类自己身上:

// React 内部
class Component {}
Component.isReactClass = {}

// 可以这样检查
class Greeting extends Component {}
Greeting.isReactClass // ok

然而,某些 compile-to-js 的语言,在类的实现上有所不同,子类在继承父类的时候,会丢失静态属性,导致在子类上访问不到父类的 isReactComponent,所以保险起见,React 把标记移到了 React.Component.prototype上,选择把它作为实例属性,以确保子类能够正确继承。

// React 内部
class Component {}
Component.prototype.isReactComponent = {}

class Greeting extends Component {}
Greeting.prototype.isReactComponent // ok

到这里可能还会有疑惑,isReactComponent 为什么是一个对象,而不是boolean值。这是早期版本的Jest(facebook推出的一款测试框架)是默认开始自动模拟功能的,生成的模拟数据省略掉了原始类型属性,破坏了检查。

所以我们编写的class组件一定要继承自React.Component,否则 React 不会在原型上找到 isReactComponent,因此就不会把组件当做类处理。以至于抛出这种错误 Cannot call a class as a function

最终方案很简单,就是 Component.prototype.isReactComponent = {},但是用了大量的篇幅来解释为什么 React 最终选择了这套方案。

以下是原文作者的感想,我觉得说的特别好。

为了一个 API 能够简单易用,你经常需要考虑语义化(可能的话,为多种语言考虑,包括未来的发展方向)、运行时性能、有或没有编译时步骤的工程效能、生态的状态以及打包方案、早期的警告,以及很多其它问题。最终的结果未必总是最优雅的,但必须要是可用的。

如果最终的 API 成功的话,它的用户 永远不必思考这一过程。他们只需要专心创建应用就好了。

本文基本是翻译原文,一些基础有自己的理解亦有删节。

原文地址

相关文章

网友评论

    本文标题:React 如何区别class和function?

    本文链接:https://www.haomeiwen.com/subject/huraahtx.html