React 的问题和我在试验的 Respo

这几周没有作多少开发的任务了, 生活节奏慢下来一些, 思考的时间也多点了
不过单页面相关的事情仍是很头疼, 清算以前 React 问题仍是不少
今天下午在内部分享听了承玉(但愿名字不拼错)分享 ant.design 相关的东西获益不少
以前简聊作这方面的尝试然而实力以及投入都不足, 只是停在了中间
支付宝的进展以及推动的策略是足够我敬佩了.. 之后工做中多学习前端

按照贺老说新技术迭代必不可少并且也是好事, 繁荣的表象之一
然而 Redux 方面的节奏我实在不能满意, 新项目废弃项目节奏太快了
个人理解当中, 函数式必成半个世纪, 理论上大多问题已经定论了
工程当中虽说涂涂改改, 但是 React 社区的连基础工具链都改来改去
个人担忧是 React 掌舵的人太年轻, 咱们会跟着他兜很大的弯子
并非排斥兜圈子, 而是不该该一群人一块儿去消耗时间, 效率低, 我宁愿本身探索react

React 的不足

这里我尝试描述整理一下我认为的 React 的问题
而后, 我本身在尝试 Respo 方案, 中间有一些比以前写业务深一点的反思git

服务端渲染, 不能有效利用缓存

个人后端经验不足, 但后端遇到渲染性能问题, 经常使用的方案就是缓存
好比查询数据库热门信息慢, 好比渲染网站首页慢, 但这种信息不常更新能够设计缓存
React 如今很差作, 服务端接收到两次请求, 调用 React 渲染, 彻底是独立的
两次渲染之间不会有任何的优化, 如今社区有的只是基于 Stream 加快传输
即使尝试缓存, 当两个页面有任何细微的不一样, 缓存就失效了程序员

这里我认为问题在于, 缓存的粒度在哪里? React 并无替换细粒度的方案
好比前端当中组件会经过 shouldComponentUpdate 作两次渲染的缓存优化
说明 React 中粒度应该在组件层次, 而不是 renderToString 这样整个页面
个人观点基本上是: shouldComponentUpdate 不够, 须要组件级缓存
至少两次渲染页面当中, 好比某个 <div> 内容不变, 就可以经过缓存作
实际当中每秒就过百次渲染的话, 缓存的场景会很是多, 优化的空间是在的github

相似的方案在前端依然能发挥做用, 好比两个 tab 切换, 内容就是重复的
如今 React 经过 shouldComponentUpdate 只能判断两次渲染之间没有更新
若是能扩展到缓存, 那么屡次渲染也应该是可行的, 这也有很多性能优化的空间
memoization 是性能优化中惯用的办法, React 社区也有高阶组件在尝试
我相信不久也许能看到更广阔的可能能够优化 React 的渲染, 经过缓存
我猜测 React 代码中解耦会有障碍影响 memoization, 不能也许早用上了算法

组件 state 抽象方案弱, 代码重用差

应用开发用到组件私有属性的概念很正常, 因此 React 提供了方案很赞
然而长期使用认识到一些问题, 好比说数据和界面相似双向绑定的状况
固然 React 也提供了 mixin 进行双向绑定, 只是说, 这种抽象能力够吗?
好比 <input> onChange 绑定到一个 field 的值, 每次要单独写 setState
或者用 Function.prototype.bind 执行一次 partial evaluation 拿第一个参数
或者更复杂, 跨组件重用 state 相关代码, 那有只能用 mixin 来抽象了
对于 Store, 已经有了十多种 Flux 的改进, 而 State, 几乎仍是 mutable 的思路数据库

其次, Component.state 是一个 Object, 可是为何?
简单场景不能一个 Boolean 解决吗? 复杂场景是一棵树那么为何又套一个 Object?
即使用了不可变数据, this.state.tree.getIn(['a', 'b']), state 仍是可变数据
一样的问题也在 props 上存在 this.props.tree.get('a', 'b') 中 props 也是可变数据
固然 props 仅仅是用于传参, 语法层面换成怎样不影响大局
state 的影响相对严重, 限制了更多的复用代码的可能性express

组件 state 影响热替换

Redux 出名以后提到这个比较多, 组件有 state, 热替换时会丢失
react-hot-loader 做者想了很复杂的花招对方法作代理才解决掉
也有另外的说法, 认为 state 也能够存放到 Store 当中一块儿管理
固然立刻有其余的考虑, 树的 state 在 Store 中怎样管理才不会乱, 怎样设计?
另外铺平的状态, 往树当中继续传递, 原来担忧的多参数问题是否是更严重?
Redux 做者最近撰文说起热替换中的 state, 也认为是过于复杂了编程

在 Clojure 社区有 Om 各类实如今以前就引起过相似的争论
Om 当中就有用 Atom 存储数据, 一棵树, 其中嵌套存储数据, 经过 Cursor 修改
js 社区也有 Cursor 的实现, 好比 baobab, 达峰说有在生产环境有某个 Cursor 实现
Cursor 的思路是在组件传递数据时带上数据在树当中 path, 以便进行修改
而它的代价是传递的数据再也不是纯粹的数据, 而且须要额外的代码封装
...我没有弄清 Cursor 全部细节, 大体就是有方案, 也有一些学习成本使用成本
另外 Cursor 通常不包含管理私有状态, 因此对上面的 state 做用大小有待讨论json

这个问题我认为相似 Cursor 的思路是可以用来优化 state 并改善热替换的
只是目前的 React 在处理 state 的问题上并不完全, 遗留下来比较难办
我在 Respo 当中实际上尝试了一个方案, 作到了几乎同样的 state 管理写法
同时能知足, 热替换时 state 是全局的, 语法上像是私有的, 并且并不复杂
..固然, 个人作法的不足, 可能对性能优化有影响, 还有待探索
能明确的是 state 有方案能够改, 能很好兼顾热替换和写法, 只是如今还不成熟

框架的模块化

React 相对于 Angular 的好处在于 Store, Virtual DOM, DOM 三者之间相互解耦
于是 DOM 能够被替换成 Canvas 或者 Native View, Store 也能够切换
不过, 通常人去替换 DOM 很难对吧, React 有发布 DOM Diff 接口文档吗? 没看到
若是真的是 Git 那样的 Diff, 那么在一台机器 Diff, 另外一台机器 Patch, 理论上不难
我认为 React 在这一点上并无解耦清楚, 但我没有读源码也很差展开说

在 Respo 当中我基本实现了 Virtual DOM 和事件处理的逻辑(但放弃了支持冒泡)
Virtual DOM Diff 和 JSON Diff 有点类似, 加上不可变数据, 或者说 Immutable Data Diff
做为参考能够看 http://jsonpatch.com/https://github.com/intelie/immutable-js-...
那在 Respo 我已经作到了在服务端 Diff, 发送到客户端进行 Patch(网速缘由, 通常不实用)
这是对框架进行解耦以及模块化带来的好处, 能够带来更多的可能性
好比说 DOM Diff 放到 service worker 里作, DOM Patch 主线程, 性能能够提升
那么我认为 React 其实没有作到支持这样的可能性

组件当中书写逻辑的语法

我说是的 JSX 中写 if 和 switch 的场景, 相信不少人遇到过
我从前说得少, 由于在 CoffeeScript 和 ClojureScript 当中自然解决掉了
JSX 受到 js 自己的拖累, 两年多了依然偶尔有吐槽说 CoffeeScript 都行 JSX 怎么不行
缘由很明显, ES6 ES7 也改不了这个问题, 我只能说我偏好 everything is an expression

自己体积的影响

本来不算大问题, cdnjs 上压缩 89k, 加上 gzip 28k, 单页面动不动 500k 了
可是社区的实现, deku 就小不少了, 还有 react-lite 做者聊到的 preact 3kb
这让 React 自己的体积显得可疑了, 并且影响了在不少场景当中的应用
既然已经出现了小体积版本的兼容方案, 事情就这样了

工具链层叠太多, 没有及时压缩

应该说是 js 的问题, React 大量使用不可变数据和表达式, 这些是 js 支持的弱项
固然也还有热替换, 这种除了可变数据的影响, 还有的打包方面的支持
你只对比 js 和以往的 js 的大概以为 js 已经很努力了, 除非换一门语言吧
可是 WebAssembly 的出现, 简称 wasm 吧, 浏览器可不仅一门语言了
我相信这对于 js 来讲将会是很大的冲击, 虽然不致命, 老程序员嘛总不喜欢立刻换工具
可是大量其余语言开发者会来写代码编译到 wasm, 而 wasm 自己比 js 更优秀

我熟悉了 cljs, 能够拿来对比. React 须要不可变数据, 须要表达式, 须要热替换, 高阶函数对吧
这些直接经过 cljs 语言上默认支持, 并且热替换和压缩打包工具链很简单
我录过视频用 cljs 搭建热替换和打包的场景, 很是简单
http://www.tudou.com/programs/view/kWScM...
在 js 中完成这些须要整合至关多的工具, 得学吧, 得认得坑吧, 得升级吧, 很麻烦
虽然 cljs 并非实际项目的首选, 但直接能对比出来 js 生态有多么破碎
并且 js 是标准, 存在各类各样的方案相互兼容有大量的成本, 这套工具链并不让人舒服

并且 cljs 像咱们展现了一种可能性, 加入当初 js 就是按照 Scheme 设计的话会怎样?
对, 技术自己仍是很复杂, 可是复杂都复杂在语言内部, 怎么解析怎么编译怎么执行
如今 React 复杂在整个环境怎么搭建上, 作不到安装两个命令语言能跑所有搞定
不少人吐槽 React 使用成本高, 我认为就是由于中间大量直接直接暴露没有封装致使的

Redux 的复杂度, Store 的设计不够清晰, 站队不明确

Redux 引入了不少复杂的概念, 后来我本身作 actions-recorder 熟悉了一遍
整个原理原本很简单, 单个的复杂 Store 更新, 加上调试工具, 这样
在 Redux 当中引入了中间件, 接着大量的内容都以中间件的形式抽象
这个时候又引入了大量的新概念和新组件. 很是难跟进
函数式编程虽然啰嗦, 可是好歹各类论文堆了几十年才演化出来
Redux 才一两年工夫就弄出各类名词, 很让我怀疑是不成熟的方案混杂在里面

另外一方面, Redux 又像服务端的方案, 中间件, 单一数据, 相似服务端和数据库嘛
考虑到我浅薄的后端开发经验, 对这种模仿自己不作分析
可是前端造成一套数据方案以后, 至关于又一个数据库, 那么它和服务端数据库关系怎样?
按说两个数据库之间, 主从同步会是一种关系吧, 就像 Meteor 给的方案
或者 Falcor 也给出了方案能够智能处理数据的抓取, 减小开发成本
而 Redux 自己造成了复杂的生态, 却没有站出来讲清楚这个问题, 怎么回事?
我认为方案已经比要解决的问题还有复杂了, 对 Redux 打一个问号

尝试方案 Respo

这些点, 我认为 React 存在问题, 但并不认为是漏洞, 而仅仅是不足
个人意思是官方为了照顾更大的局没有对这些方面作优化, 于是不足
并且我也没有明确的方案能够把我认为的问题能够干掉
React 官方仓库空间兼容性, 稳定性, 性能, 跨平台, 复用, 各类因素很是多多须要考虑
我在 Respo 上作的尝试, 只是试着去解决某些问题, 下面细说

如今 GitHub 上 respo 有关的代码仓库包含这一些:

代码我用的是 ClojureScript 和 Cirru, 绕过了前面提到的 js 不少的问题
固然, 实际上也创造了更多问题, 好比 cljs 部署方案不完善, 学习成本, Cirru 问题等等
可是好处在于代码基本上经过 cljs 中的不可变数据作抽象和解耦
我贴 cljs 大概会吓跑不少人... 但为了作说明仍是要贴啊, 主要看注释吧

; removed code for namespace and styles

; 处理事件的函数, 其实须要的是里边一层函数
; 外一层函数只是为了传递数据, 由于 this 去掉了, 因此 props 和 state 须要改为手动传入
(defn handle-toggle [task state]
  (fn [simple-event dispatch mutate] (dispatch :toggle (:id task))))
; 经过 dispatch 发送 action, 不须要一层层传递, 直接能用
; mutate 相似于 set-state, 但功能不同, 看后边的 :get-state 和 :update-state

(defn handle-change [task state]
  (fn [simple-event dispatch mutate]
    (dispatch :update {:id (:id task), :text (:value simple-event)})))

(defn handle-remove [task state]
  (fn [simple-event dispatch mutate] (dispatch :rm (:id task))))

(def task-component
 {:name :task, ; 增长命名方便内部分析, 还有某些场合判断是否 DOM 换了

  ; 对于 hash-map 来讲更新简单是 merge, 这是函数, 能够自定义
  ; 本身定义的写法是 (fn [old-store param1 param2] (some-fn old-store))
  :update-state merge,

  ; 初始的 state 本身定义, 只要 update-state 能处理便可
  :get-state (fn [task] {:draft ""}),
  :render
  (fn [task] ; render 嵌套两层函数, 前面传递 props, 直接用函数参数的写法就行了
    (fn [state] [:div ; Clojure 当中表示 Virtual DOM 的数据, 写法奇怪, 我知道
                 {:style style-task}
                 [:div
                  {:on-click (handle-toggle task state), ; 前面的事件处理函数, 传入依赖, 返回函数
                   :style (style-toggle (:done? task))}]
                 [:input
                  {:placeholder "Describe the task",
                   :value (:text task),
                   :style style-input,
                   :on-change (handle-change task state)}]
                 [:div
                  {:on-click (handle-remove task state),
                   :style style-remove}]]))})
; removed some code of namespace and styles

(defn handle-change [props state]
  (fn [simple-event dispatch mutate]
    (mutate {:draft (:value simple-event)}))) ; mutate 能够传多个参数给 update-state

(defn handle-add [store state]
  (fn [simple-event dispatch mutate]
    (dispatch :add (:draft state))
    (mutate {:draft ""})))

(def todolist-component
 {:name :todolist,
  :get-state (fn [store] {:draft ""}),
  :render
  (fn [store]
    (fn [state]
      (let [tasks store]
        [:div
         {:style style-todolist}
         [:div
          {:style style-header}
          [:input
           {:placeholder "new task",
            :value (:draft state),
            :style style-input,
            :on-input (handle-change store state)}]
          [:span
           {:inner-text "Add",
            :on-click (handle-add store state),
            :style style-add}]]
         [:div
          {}
          (->>
            tasks
            (map (fn [task] [(:id task) [task-component task]])) ; 嵌套 task-component 组件
            (into (sorted-map)))]]))),
  :update-state merge})

前面整理的问题主要是在 Respo 一块儿思考的, 因此有尝试去解决:

服务端渲染, 不能有效利用缓存

没有实现, 可是预留了方案. 思路是对建立组件的函数作 memoization
cljs 中能直接判断参数数据是否一致, 深匹配, 有性能开销, 但 cljs 语言内估计有策略
我判断组件的全部 props 和 state 都相等, 就可以复用已有的 Virtual DOM
具体要有复杂的应用才能进行测试和改进

组件 state 抽象方案弱, 代码重用差

get-state update-state 两个方案能够本身定义, 必定程度上复用了代码
目前看来复用的代码并很少, 只是某些状况一个组件一个值就行了
我还有尝试下复杂的场景当中怎样利用这个优点进行抽象

组件 state 影响热替换

在 Respo 当中全局的 states 用例子写出来是下面这样
key 是数字组成的 vector, 值用 x 表示的地方是 get-state 返回的值:

{
  [0] x
  [0 1] x
  [0 1 1] x
  [0 1 1 1] x
  [0 2] x
}

states 是跟 Store 分开的, 但也是放在全局的, 而后隐式地传给每一个组件
热替换测试可用, 清除多余的 state 也是能作到的. 可行性已经完成.

框架的模块化

注意 respo-client, 我把 Diff 的工做放在 respo 中, 而把 Patch 拆出来了
也就是说能实现服务端 Diff, 前端 Patch 这样的行为
我还在考虑未来在 Canvas 上实现客户端的可能性
其实 Canvas 渲染能够粗暴实现, 捕捉事件加上定位很困难, 实际上我很难作出来

自己体积的影响

cljs 打包一块儿就有 29k, 毕竟编译了整个语言, 不过还好不可变数据在里边
对于前端来讲有些大, 带来新问题

组件当中书写逻辑的语法

cljs 解决了

工具链层叠太多, 没有及时压缩

cljs 自己做为一门编译到 js 的语言就有些问题, 只是没有 js 那样分裂的状况
因此是工具链层叠很少, 可是单层的语言编译有点复杂

Redux 的复杂度, Store 的设计不够清晰, 站队不明确

没有涉及. 我只是内置了 dispatch 方案方便了内嵌组件进行事件分发
另外的更新函数以及 Atom 更新后的通知, 直接用 cljs 语言完成了
Atom 类型数据有相似 Object.observe 的机制, 能直接监听数据变化调用操做的
但对于具体 Store 应该怎样, 还要继续思考一些方案

总结关于 Respo 目前状态

Respo 主要是个尝试项目, 并且 cljs 加上没有文档, 估计没有人想看
但仍是整理一下目前作的很很差的地方以便参考:

  • 模块化以后, 挂载应用涉及到不少代码, 因此当前 API 没有美化的状况下很复杂

  • mutate 函数没有封装组件信息, 致使子组件不能操做父组件

  • Diff 算法是经过 sorted-map 来简化的, 影响了写列表的体验, 有视频解释

  • 调试很难, cljs 没有实现 core.typed 这个 Clojure 已有的方案, 我也没作手工处理

  • DevTools 只是尝试, 还不能用

  • cljs 前面说了, 设计很好, 但并不主流, 生产环境不少细节处理远不如 Webpack 等

好消息是上面的 wanderlist 我用 Respo 实现并且使用了数日, 说明方案小应用能跑了
说真的, cljs 能自动检查变量, 虽然类型不能, 但已经比我写的 coffee 代码可靠多了
我后边还会基于 Respo 作一些小的 Toy App 同时添加一些语法糖方便使用
有兴趣 cljs 的同窗, 上边连接给过了, 我录了视频介绍怎么入门 cljs我也但愿对 cljs 有了解的同窗了解一下 Respo, 做为对 Reagent 的一个补充个人方案里热替换, inline CSS 也挺溜, 改界面效率不会 Webpack 差, 好歹用了这么多天最重要的是, 如今我若是对 React 有不满, 至少我能够想办法在 Respo 里作出来给人看

相关文章
相关标签/搜索