过去的历史,和一点点未来 UI 框架的幻想

1,961 阅读11分钟

回顾

jQuery

记得 13 年刚开始接触前端的时候,最兴奋的是用原生 JS 手写了一个左右漂浮的广告,那时候 jQuery 大行其道,现在回过头看,这或许是通过 JS 间接操作 UI 的起点,jQuery 的核心部分也是对 Dom 对象的包装,通过正则匹配来处理 CSS HTML,网上到处都是可随意 copy 的 jQuery 效果代码,和各种 jQuery Plugin,那时候的前端并不需要太多的专业计算机知识,理论上复制粘贴一样能完成大部分“诡异”的需求,如果要类比下,jQuery 的年代其实还挺超前,比如至今搁浅的 webComponent 和 jQuery Plugin 其实很像,都是通过网络来分享的包裹了 JS CSS HTML 的一个 Widget,当然内部原理相差甚远。

jQuery 不算是个 UI 框架,但是它开启了对 Dom 的间接操作,这是现代 UI 框架的基础理念,影响深远

Handlebar

随着需求的日益复杂,越来越多的需求需要通过响应后端数据来渲染网页,通过 jQuery 来响应后端数据操作 Dom 变得越来越低效和难以维护,于是 Handlebar 横空出世,很多年轻的前端开发们可能没有听过这个模板框架,但在 14 年的时候它可是大名鼎鼎,通过 Handlebar 你可以提前书写需要更新的 Dom 模板,大大提高了操作 Dom 的效率

Angular 1.0

不得不说 angular 是个跨时代的产物,从某种意义上来讲算是第一个 UI 框架也不为过,相对于开发 SPA 需要使用 Dom 操作库,业务逻辑处理框架比如 Backbone,加上一个好用的模板处理框架比如 Handlebar,angular 第一次把这些特性整合到一起,还不是杂糅的那种而是非常完整的整合成一个新东西,这是集 UI框架/模块依赖/打包工程于一体,所以可想而知,出来就大火,不过从 2020 往回看 angular 1.0 的大一统思想过于大包大揽,加上作者自己都觉得丧心病狂的脏检查机制,以至于后期 API 更新乏力,随着 React 的崛起,逐渐淡出。

React

15年的时候,React 突然就火了起来,最核心要属 virtualdom,借助 virtualdom,在渲染性能上的出色表现,React 开始崭露头角,而且其函数式的编程哲学也非常吸引人,那一段时间非常多的前端爱好者都致力于将函数式编程引入 JavaScript 的世界,加上 JavaScript 本身极强的可塑性,函数一等公民的底子,让一向在业务开发领域偏门的函数式编程走入大众视野,而后的 Redux 的出现,那一场时间旅行的精彩演讲更是将 immutable,thuck 等函数式概念推广的深入人心,以至于那一段时间不管 Redux 写起来有多繁琐,但是我都致力于把所有逻辑都往上面搬,仿佛如果不这么做,代码就不好维护了一样

事实上,直到 Hooks 出现之前,React 的函数式编程哲学都有点不伦不类,基于 class component 构建的复杂应用,绑上 Redux 这个函数式状态管理库,怎么看怎么别扭,加上 HOC 冗长的调用链,写起来还真是有点考验耐心,所以才会有 Dva 等基于 Redux 的 platform 来降低其编写代码的成本,但本质上也是将 Redux 通过一顿包装猛如虎,从函数式变成面向对象的方式,引入 module 等概念来矫正这种别扭

但 React 是以函数式而生的,包括 HOC 都是为了将 class component 变得更函数化一点儿而提出来的,直到开发团队设计了 Hooks,这个以去掉命令式中条件判断,内部采用闭包锁定每一次的作用域,去变量化等方式来让 React 彻底函数化,面对新的 React,此时前所未有的接近一切皆函数的概念,但同时也让这种“别扭”达到了顶峰,社区大量的人吐槽无法好好的写代码,无论是 useMemo 还是 useCallback 都让习惯了面向对象风格和命令式代码的开发者无所适从,如果说以前还只是用 immutable 来避免对象的可变性带来的麻烦,但大部分时候我们还是在使用 this,通过对象引用来共享数据,而现在就变成了我们得忘记面向对象,忘记命令式,如果你想绕过 Hooks 的常量性,你得用 ref 来维持对一个可变对象的引用,一切似乎都倒过来了,我们需要重新学习如何编写代码,Hooks 带来的心智成本几乎是颠覆性的,但说实话我本人还是很喜欢函数式的,也非常赞叹这种巧妙的设计,不过以 React 这种设计方式,估计对喜欢面向对象风格的人来说怎么看都是别扭吧。

Vue

我写过一点 Vue,网上总说 Vue 抄袭 React,然后尤雨溪忙着四处灭火,我想持此看法的人应该不少,不过说句公道话,开源技术本身就是彼此借鉴来发展的,只不过 Vue 的名气大所以树大招风,从技术层面讲,我觉得 Vue 和 React 是完全两种不同的设计初衷,即便互相有彼此的借鉴但背后的设计哲学是完全不一样的,和 React 的函数式编程哲学不同的是 Vue 我觉得是沿袭了 angular 1.0 中的设计,其实是比较正统的面向对象风格,包括响应式数据驱动,通过操作数据来控制 UI,这些在 angular 1.0 中其实都有体现,模板的指令化也是命令式的风格而不是函数式,你看 React 就从来没内置 IF 啥的组件,所以 Vue 能够拥有如此高的人气,也是其面向对象的风格更容易被广大开发所接受,毕竟我们都是学 C 出身的,大学里也受过面向对象编程的教育,底子在那里,确实更容易接受这种编程风格,其实对于这两种编程风格,我倒是觉得正好是个太极,彼之长乃吾之短也

举个例子,假如我们要开发一个用户的 profile,从面相对象的角度来思考,我们可能会这么写

class UserProfile extends Widget {
    async constructor(){
        const profile = await axios.get('user/profile')
        this.email = profile.email
        this.name = profile.name
        this.phone = profile.phone
        // other propotype
    }
    get name(){
        return 'username:' + this.name
    }
    get email(){
        return emailHandler(this.email)
    }
    set name(localNameString){
        this.name = nameValid(localNameString)
    }
    @axios('pose','user/profile/upload')
    upload(){
        return {
            name:this.name,
            email:this.email,
            phone:this.phone
        }
    }
    render(){
        return {
            `<div class="name">{{name}}</div>
             <div class="email">{{email}}</div>
             <div class="phone">{{phone}}</div>
            `
        }
    } 
}

把目前能用的特性都用上, 面向对象的核心是一切皆对象, 对象是最小单位, 通过继承和混入以对象为核心来构建应用, 因为在设计上最小代码单位是 class, 所以就不会出现将属于类的逻辑和属于函数的逻辑混用的情况, 在 JavaScript 这种灵活性极强的语言中, 我们很容易将业务逻辑书写在 class 以外的地方, 而不是像上面的示例一样将一切封装到类里, 事实上在 Hooks 出现之前 React 就是这种情况, 基于 class 的 component 和一堆工具函数或者函数级别的代码混用, 最小单位不一致, 混合风格编程导致的逻辑复用性很难得到保障, HOC 算是一种折中方案, 但是和 render props 一样随着业务代码的膨胀, 非常容易陷入深层嵌套, 越来越难以维护, 函数式的 React 急需一种函数式的解决方案将逻辑函数化, 彻底摆脱面向对象带来的困扰, 于是 Hooks 来了, 从某种意义上讲, Hooks 像个英雄.

有了 Hooks, 我们应该试着忘记面向对象, 对你来说, 函数是最小单位, 没有属性, 也没有方法, 一切都是函数链运算的结果, 没有变量也没有条件运行, 循环只能用递归, 没有 for 循环, 在这些约束下, 上面那个例子如果用 Hooks 加函数式的思维来写的话

function upload(profile){
    axios.post('user/profile', profile)
}
function useSetProfile(profile){
    const [profile, setProfile] = useState([{
        name:profile.name,
        email:profile.email,
        phone:profile.phone
    }])
    return setProfile
}
asnyc function useSyncUserProfile(callback,deps){
    useEffect(async ()=>{
        const profile = await axios.get('user/profile/upload')
        callback(profile)
    },deps)
}
function userProfileWidget(profile){
    const setProfile = useSetProfile({
        name:'jaka',
        email:'1123@qq.com',
        phone:'1123123'
    })
    useSyncUserProfile(setProfile,[profile])
    function onBtnClick(){
        setProFile(profile)
        upload()
    }
    return render(
        `<div class="name">{{name}}</div>
         <div class="email">{{email}}</div>
         <div class="phone">{{phone}}</div>
         <button onClick={() => onBtnClick()}></button>
        `
    )
}

相比第一个类的例子, 第二个例子中, 我们的思考单位聚焦在"函数"上, 没有对象五脏俱全的完整性, 对于一个函数来说只有入参/出参, 这样的好处自然显而易见的是更细的粒度, 更容易测试, 不过缺点也同样明显, 对于一个 function component 来说要具备自说明性, 在函数命名上需要制定相应的规范, 不像面向对象类/属性/方法天然的协同概念, 函数这个概念对于描述真实世界来说缺失太单调了些.

我们再看看 Vue3.0 给出的一种编程风格, 事实上在 Vue3.0 柔和了函数式, 面向对象两种风格, 并且还加入了响应式来自动触发 getter/setter, 带一点流式编程的味道

<template>
  <div class="name">{{name}}</div>
  <div class="email">{{email}}</div>
  <div class="phone">{{phone}}</div>
  <button @click="onBtnClick">
  </button>
</template>
<script>
  import { reactive, computed } from 'vue'
  function uplaod(profile){
      axios.post('user/profile/upload', profile)
  }
  function useSyncUserProfile(state){
      watchEffect(async ()=>{
        const profile = await axios.get('user/profile')
        state.name = profile.name
        state.email = profile.email
        phone = profile.phone
      })
  }
  export default {
    setup() {
      const state = reactive({
        name: 'jaka',
        email:'1123@qq.com',
        phone: 1833333,
      })
      onMounted(){
        useSyncUserProfile(state)
      }
      function onBtnClick() {
        upload(state)
      }

      return {
        state,
        onBtnClick,
      }
    },
  }
</script>

Vue 在引入函数式 API 的基础上并没有增加类似 React 那样的强约束, 依然保有生命周期, 从大结构上来看像一个函数组合成的对象, 可以算是对象函数吧...

所以 Vue 不像 React 那样设计的非常彻底, 糅合了面向对象可描述性和函数式的细粒度组合性, 不过 JavaScript 本身并不是响应式的, 所以 Vue 也有它自身的问题, 在 2.0 是无法内部自动劫持数组, 无法对对象新增属性自动劫持, 3.0 的 proxy 解决了这个问题, 不过带来的新问题就是一旦响应式对象被解构一切就 over 了, 从这个角度看, Vue 核心依然是面向对象的, 和 React 完全不同, 他的 Hooks 也是建立在 JavaScript 函数即对象的基础上, 而不是像 React 那样使用了函数闭包的特性.

不过无论是 React 还是 Vue, 我觉得函数式 API 的引入其实都增加了框架的上手门槛, 这个上手是指能好好写....你得了解许多新的底层细节, 遵循一些约定, 而不像向过去那样只要把一切交给框架, 框架就会帮你做好代码级别的约束

虽然这些都还是刚刚出炉的热乎概念, 但都是过去了, 本文到这里不免要开始做些 YY 的幻想, 未来几年 UI 框架会变成什么样子呢?

幻想

将函数坚持到底的 React

目前 React 的 Concurrent 已经在实验室里了, 始终坚持函数式的回报就是, 一切皆函数非常有利于将渲染颗粒度控制到堆栈级, 毕竟 JavaScript 堆栈就是函数堆栈, 利用浏览器的一些魔法 API 比如 requestidlecallback, 充分利用 cpu 时间片的间隙见缝插针的往里面塞可执行函数, 将 60fps 渲染变得更简单, 当然这不是幻想这只是展望, 毕竟已经在路上了, 如果再往后想一想, 我们都知道在最近火热的 ServerLess 经常会提到 函数即服务 的概念, 微服务被函数所替代, 所以后端都函数即服务了, 如果前端实现了函数即渲染...感觉这是前后函数大统一呀, 所以未来的应用是可以通过一堆函数自动串联在一起跑的么? 🤔 相比 webComponent 那不太靠谱的包装方式, 如果引入 Deno 那种模块网络化的思想, 把函数组件分发到网络上, 因为粒度的一致性和函数天然容易被机器理解的特性, 这不就实现了 webComponent 的愿景么, 可能以后我们可以这么写代码

import dataPicker from 'https://function/ui/component/data-picker'
import useWeiBoUserData from 'https://function/hooks/use-webbo-user-data'

async function app(){
    const weiboUserData = await useWeiBoUserData()
    render(){
        return {
            ``` 
                <dataPicker></dataPicker>
                <div>{weiboUserData.toJSON()}</div>
            ```
        }
    }
}

// HTML
<div app="react"></div>

不需要繁重的打包编译, 浏览器内置 ui 框架, 然后通过网络就能共享函数组件, 函数逻辑, 函数服务...感觉要进入新时代了

如果你还有什么脑洞可以开一开, 不妨留在评论里, 或许若干牛后回来还能吹个水, 昔日我就预言....