用 ObjectComponent 重新定义 React 组件

吐槽君 分类:javascript

前言

今天在公司内部完成了应用框架的第一个版本, 然后又开始回顾之前写的 structured-react-hook 这个库, 其实关于名字我一直很纠结, 我始终在思考, 这种定义 react 组件的方式和 classComponent 以及 functionComponent 有何区别, 直到我今天灵光一闪, 我突然意识到, 这其实就是用对象来定义 react 组件的一种方式 → objectComponent, 这可能是一种全新的 React 组件定义方式. 这篇文章将详细分析现有 React 生态流行的组件定义方式和 objectComponent 之间的区别, 同时也将指出我认为 React 官方团队可能存在的意识上的偏差.

正文

官方的未必是对的

众所周知, React 团队在发布 React 之后的若干年, 在组件定义方式上一直都摇摆不定, 从最早期的 classComponent 到中间提出的 纯函数组件以及最近 hook 发布后的带有状态的函数组件.

但经过这么多年的实践, 我们知道无论是 classComponent 还是 functionComponent 他们都有各自的问题, classComponent 过于笨重, functionComponent 虽然灵活粒度更小, 但是抽象程度很低, 业务表达力远不如 classComponent.

以最简单的 Button 举例我们来看看 classComponent 和 functionComponent 的表达力


class Button extends React.Component{
  constructor(props){
    super(props)
    this.state = {
      text:'按钮'
    }
  }
  onClick = ()=>{
    this.setState({
      text:'点击了按钮'
    })
  }
  render(){
    return(){
      <button onClick={this.onClick}>{this.state.text}</button>
    }
  }
}


function Button(props){
  const [text, setText] = useState('按钮')
  const onClick = ()=>{
    this.setState({
      text:'点击了按钮'
    })
  }
  return(
    <button onClick={onClick}>{text}</button>
  )
}

 

functionComponent 将状态, 视图, 事件处理逻辑等代码堆砌到一个函数内, 相比 classComponent 显得杂乱无章, 这还是一个很小的例子, 如果你编写的是一个更复杂的组件, 你可能会看到这样的代码

const [a,setA] = useState
const [a,setA] = useState
const [a,setA] = useState
const [a,setA] = useState
const [a,setA] = useState
const [a,setA] = useState
const [a,setA] = useState
...

 

这还不算那些 函数内的函数, 各种交互事件.

但是 classComponent 就没有问题么?

其实 classComponent 仅仅比 functionComponent 在管理 state 上略强, 另外可以区分 onClick 这样的事件处理函数, 但面对复杂组件的时间, classComponent 一样会遭遇困境.

class ..{

renderA(){}
onClickA(){}
transform(){}
render(){}
...
}
 

你几乎无法在一个 200 多行的 classComponent 内有效组织你的函数代码. 通常随着时间推移, 他们会越来越混乱和难以管理

所以有更好的方法么? 当然有, 不过在说明 objectComponent 如何改善这些问题之前, 让我们来进一步分析下为何, classComponent 和 functionComponent 在组织大型代码上表现乏力的根本原因.

JavaScript 的类型缺陷

如果你了解 class 只是 JavaScript 利用某些魔法实现的语法糖, 你就不会把它和 Java 中提到的类混为一谈.

我们经常称呼 JavaScript 是一种混合的编程范式语言, 包括面向结构, 面向对象, 函数式等等. 你几乎能用 JavaScript 来部分模拟这些编程范式.

注意这个词, 部分 我在之前的文章中提到过, 被称呼为大前端的前端体系更像是一种无法彻底取代相邻技术领域的嘲讽, 在移动端无论我们怎么鼓吹和挣扎, 依然是原生语言的天下, 至于服务端, nodejs 从来没成为过主流. 全栈也不过是一个流行的小众文化

JavaScript 中的类, 真的除了 类, 一无所有, 和 Java 相比, 在类型编程上, JavaScript 简直连玩具都算不上, 如果你参照 Java 的编程指南, 然后试图在 JavaScript 中写出一样能够有效组织的代码, 那一定是疯了.

这意味着, 使用类来组织 JavaScript 代码, 你会遇到尴尬的困境, 无法扩展.

看看 React 之前为了解决这个问题干了啥?

早期的 mixin 和 后期的 HOC

其实我觉得 HOC 还不如 mixin, 只是 React 实现方式有问题.

HOC 本质是高阶函数, 利用了 JavaScript 类 = 函数这种奇异的特性实现了一个非常诡异的扩展方式. 高阶类...

和继承相比, 我觉得 HOC 实在是别出心裁.

这种俄罗斯套娃一样的逻辑扩展方式经过实践之后被证明是不可行的. 因为 HOC 将一个组件的扩展变成了两个组件的问题, 没加一层扩展就需要多套一个组件. 这根本无法阻止中国产品经理疯狂的脑洞嘛.

React 会提出 HOC, 我一度觉得是因为 FB 的产品经理不给力, 应该派两个中国的产品经理过去, 可能他们就懂了.

只能说 React 团队对函数式的痴迷, 中毒已深, 高阶类这么奇葩的东西行不通, 那就干脆直接上函数吧. 于是有了 hook

函数式编程就是个坑

虽然函数在 JavaScript 是一等公民, 但我们都知道, 这是披着对象皮的函数, 对象才是一等公民. 这么多年我也一度被函数式编程给洗脑过, 现在想想应该是函数式编程在其他语言那里找不到突破口, 终于在 JavaScript 这个没爹妈的孩子这里找到了春天.

在此我们不讨论 JavaScript 和函数式编程的渊源, 我只想指出一点, 函数式编程如果能广泛用于工业软件开发, 还用得着来前端这里找机会?

难道指望 函数式 node 去干翻 Java 么?

JavaScript 早就给出了答案, 自己的才是最好的

早年间我们一直称呼 JavaScript 是玩具, 但这么多年下来, 我才发现, JavaScript 早已超越了函数式和基于类型的面向对象.

JavaScript 比上述两种编程范式拥有更强大能力, 不然如何模拟他们?

JavaScript 拥有天然定义和操作对象的能力, 而且这个对象还能直接序列化 → JSON.

强大的动态性可以灵活调整对象的作用域, 甚至是对象自己一部分的作用域

const object = {
  method(){
    console.log(this)
  }
}
const other = {}
object.method.call({})
 

一开始那些老古板们将这种方式称为禁忌, 主流认为 JavaScript 的 this 简直是万恶之源.
但是在框架作者眼里, 这种可以控制作用域的能力并不是万恶之源, 恰恰相反, 这是无上至宝.

因为 JavaScript 这一特性可以让赋予组件生命力. 让前端组件具有极强的扩展能力. 这种扩展能力到极致 → 就是定制

传统组件是不具备定制能力的, 通常你只能通过 api 来进行一些适当的扩展, 以满足组件定义者的设想.

但定制从来就不是预设的, 对于定制, 你可以理解就是甲方爸爸随时想到的一个 idea. 就问你能不能吧.

而实现的关键就是用对象定义组件, ObjectComponent, 在 React 里, 它是这样的.

const Button = createComponent({
  name:'Button',
  initState:{
    text:'按钮'
  },
  controller:{
    onClick(){
      this.rc.setState({
        text:'你点击了按钮'
      })
    }
  },
  view:{
    render(){
      return(){
        <button onClick={this.controller.onClick}>{this.state.text}</button>
      }
    }
  }
})

 

当对象即组件, 组件即对象, 很多事情就变简单了.

动态创建组件

如果你曾经开发过动态渲染的组件容器, 一定用过 createElement, 但这要求你通常先定义一个 classComponent 或者 functionComponent, 但我们知道组件内的逻辑是无法更改和提取的, 除了传递 props, 而 props 定义是一个静态的过程, 因为你需要为传入的 props 提前写好代码, 这个过程是非动态的, 因此动态创建组件, 并不能动态定制组件.

但如果组件本身就是对象, 这意味着我们不需要通过 props 并为其编写静态的消费代码, 你可以直接上.

const Button = {
  name:'Button',
  initState:{
    text:'按钮'
  },
  controller:{
    onClick(){
      this.rc.setState({
        text:'你点击了按钮'
      })
    }
  },
  view:{
    render(){
      return(){
        <button onClick={this.controller.onClick}>{this.state.text}</button>
      }
    }
  }
}

const NewButton = createComponent(Button, {name:'NewButton',initState:{text:'新按钮'}})

 

你甚至可以修改组件的视图部分, 而不用像 props 那样担心对原有视图造成影响, 因为基于对象, 被修改视图后的对象组件是一个独立的对象实例.

const Button = {
  name:'Button',
  initState:{
    text:'按钮'
  },
  controller:{
    onClick(){
      this.rc.setState({
        text:'你点击了按钮'
      })
    }
  },
  view:{
    render(){
      return(){
        <button onClick={this.controller.onClick}>{this.state.text}</button>
      }
    }
  }
}

const TwoButton = createComponent(Button, {name:'TwoButton',initState:{text:'两个按钮'},view:{
  render(){
    <div>
      <button onClick={this.controller.onClick}>{this.state.text}</button>
      <button onClick={this.controller.onClick}>{this.state.text}</button>
    </div>
  }
}})

 

还有其他玩法么, 我觉得有, 而且应该很丰富, 毕竟 JavaScript 对象这么灵活, 你怎么操作对象就可以试着怎么操作组件, 发挥想象力吧.

如果你想试试这种定义方式, 可以

yarn add structured-react-hook
 

然后

import {createComponent} from 'structured-react-hook'
 

如果你想关注这个项目, 可以去 github.com/kinop112365… Star 下

另外 createComponent 的细节并没有在库文档中更新, 不过我再 README 里更新了最初的实例。

回复

我来回复
  • 暂无回复内容