Vue3-one piece尝鲜:React Hooks VS Composition API

4,562 阅读6分钟

万众期待的Vue3在9月18号发布了正式版本,代号为One Piece,看过海贼的都知道One Piece是大秘宝,也就意味着这次从2.x到3.x的升级有很多的亮点和惊喜等着去发现。比如说全新的 Proxy 架构, Composition API ,对 TypeScript 的支持。而且这次升级学习了 react 时下最热的 hooks 特性,让用户在选择框架的时候也少了更多的心智负担。我们就来通过一个 TodoList 组件案例来看看 React Hooks 与 Composition API 之间的对比。

首先贴上 Hooks 和 Composition API 的文档,还没具体看过API的可以去了解一下:

react入口

vue3入口

开发体验

开始先来创建项目,这里我们试了下尤大之前提到的一个工具, vite ,这是一个由原生ESM驱动的Web开发构建工具,在开发环境下基于浏览器的原生 ES imports 进行开发,生成环境基于 Rollup 打包。

vite: github.com/vitejs/vite

具有以下特点:

  • 轻量级快速冷启动
  • 模块热更新
  • 真正的按需加载

vite.png 不得不说,这个工具的特点真的很 vue ,轻量,快读,易上手。

React 方面,我们选用了蚂蚁金服团队提供的 UmiJS ,一个插件化的 React 前端应用框架。

UmiJs: umijs.org/zh-CN/docs/…

Umi 内置了路由、构建、部署、测试等一系列的开发生产模块,仅需要一个依赖即可上手开发,结合 antd 使用起来也是相当优雅,可以说是一站式的 react 应用框架。

vs_pic.png 这边为了方便对比实际开发的体验,我们针对同一个业务场景(TodoList)做两套项目(Vue和React),并分别使用了 Composition API 和 React Hooks 。

从UI上来看其实是没有差别的,毕竟使用了相同的CSS,Vue使用的是 Template 方式,React使用的是 JSX 引入css的方式。我个人可能会更倾向于使用额外引入css的方式,管理起来会更加清晰,vue内也是支持的。

css_vs.png

功能代码

Hooks风格

function TodoList({
  dataSource,
  onChange
}: TProps) {
  const [itemName, setItemName] = useState('')

  const handleChangeItemName = (e: any) => {
    setItemName(e.target.value);
  }

  const handleAddItem = () => {
    const newList = [
      ...dataSource,
      {
        name: itemName,
        hasDo: false
      }
    ]
    setItemName('')
    onChange(newList)
  }

  const handleItemStatus = (value: boolean, index: number) => {
    const newList = dataSource.map((item, tIndex) => {
      if (tIndex === index) {
        return {
          ...item,
          hasDo: value
        }
      }
      return item
    })
    onChange(newList)
  }

  const handleRemove = (index: number) => {
    const newList = dataSource.filter((item, tIndex) => tIndex !== index)
    onChange(newList)
  }
  
  return (
    <div>
      <div className="input-line">
        ...内容输入
      </div>
      <div className="todo-list">
        ...列表渲染
      </div>
    </div>
  )
}

首先我们来看下 React 内function组件 Hooks 的写法,react组件的特点就是每次状态发生改变都会更新虚拟DOM,并重新渲染一遍,函数式组件内因为this指向的问题,所以在之前的版本里无法做到状态管理,这一次的 hooks 加入了 useState 进行状态管理,本质上就是利用了JS闭包的特性,将状态进行了持久化的保存。另外,改变状态也不能用之前的 this.setState ,只能使用useState结果中解构的第二个参数,进行状态更新。

Hooks 也是 react 未来发展的一个方向,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码给“钩”进去。同时,也建议把一些常用的 hooks 业务逻辑封装成自定义 hooks ,方便项目庞大后的统一业务管理,社区里也有很多 hooks 方面的沉淀,比如说 ahooks 就提供了常用的高质量的 hooks 库, react-table7.x 的使用就全面拥抱了 hooks 。

自定义Hooks:reactjs.org/docs/hooks-…

Composition API风格

export default {
  name: 'TodoList',
  props: {
    dataSource: Array,
    // onChange: Function
  },
  setup() {
    const itemName = ref('')

    const handleAddItem = (dataSource) => {
      dataSource.push({
        name: itemName.value,
        hasDo: false
      })
      itemName.value = ''
    }

    const handleItemStatus = (index, dataSource) => {
      dataSource[index].hasDo = !dataSource[index].hasDo
    }

    const handleRemove = (index, dataSource) => {
      dataSource.splice(index, 1)
    }

    return {
      itemName,
      handleAddItem,
      handleItemStatus,
      handleRemove
    }
  }
}

我们来看看Vue3.0提供的 Composition API ,用起来就是定义一个 setup 函数,这个函数相当于在 beforeCreated 和 created 这两个生命周期,然后将一些属性赋予成响应式监听的能力,从而返回输出至视图层,这样数据就有了双向绑定的能力。 其实对比 React , vue 在以下几个方面看起来会方便:

  1. 使用 v-model 双向绑定了以后,就不需要关心Input的 onChange 事件处理
  2. 在改变状态的时候,react是setState然后进行重新渲染,性能上要做很多的 useMemo 的优化,vue可以直接修改属性值: ref.value = '' ,而且并不会触发重新渲染。
  3. react为了保证 diff 算法的比较,修改一些数组的时候会比较麻烦,要用到rest风格,甚至用一些 immutable , immer 库来辅助,vue因为 proxy 的原因,可以直接做修改。

Composition API 针对之前的 Option API 风格上很大程度上借鉴了 Hooks 的设计理念,那就是业务驱动代码,把每一个小的业务模块进行集中化管理,并且可以使用自定义 hooks 的方式,进行业务逻辑的复用。

生命周期

function Todo () {
  const [todo, setTodo] = useState<Todo[]>([])

  useEffect(() => {
    async function fetchData() {
      const info = await request('/api/todolist')
      setTodo(info)
    }
    fetchData()
    return () => {
    	...// 做一些清理工作
    }
  }, [])

  return (
    <TodoList
      dataSource={todo}
      onChange={(val: Todo[]) => {
        setTodo(val)
      }}
    />
  )
}

我们在 React 的父组件里增加了一个初始化数据的逻辑,调用了 useEffect 方法,第二个参数传入[]就等同于class组件内的 componentDidMount 生命周期,当return一个函数出来的时候,这个函数会在组件销毁的时候执行。不过需要注意的一点是:hooks不能加载if判断中,这样会导致每次组件渲染的混乱。

export default {
  name: 'App',
  components: {
    TodoList
  },
  setup() {
    const dataSource = ref([])

    onMounted(() => {
      async function fetchData() {
        const info = await request('/api/todolist')
        dataSource.value = info
      }
      fetchData()
    })

    return {
      dataSource
    }
  }
}

同样的业务逻辑,我们看到 Composition API 就是在 onMounted 函数里传入一个包含处理逻辑的函数,进行生命周期的挂载,如果从语义化的角度来说, Composition API 会更加简单明了,从 Options API 迁移到Composition API也更加的平滑,风格上, hooks 和Composition API也是比较接近的。

Vue2Vue3
beforeCreatedsetup
createdsetup
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted
errorCapturedonErrorCaptured

这边我们也列出了Vue2和Vue3的生命周期函数对比。

性能比较

那么Vue3的性能怎么样呢,我这里做了一个小实验,创建了一个长度为10000的列表数据来进行初始化,然后再添加新的数据。

const list = []

for(let i = 0; i < 10000; i++) {
  list.push({
    name: `${i}--test plan`,
    hasDo: false
  })
}

performance.png 左边这个是用 Vue Composition API 的检测结果,右边是 React hooks 的结果,两边的代码其实都是没有进行优化过的,从指标上来看, Vue 占用的内存更少,CPU执行的占用也会比 React 更低,说明Vue全新的 Proxy 底层带来的性能提升还是比较显著的,当然 react 也能通过一些手段( Immutable )进行优化。

最后

react hooks 和 Composition API 各有特色,都是很好的前端解决方案,也说明了未来的前端发展更加规范化,通用化。(文中有任何错误,欢迎大家指正)