前端老司机带你排水渠过弯

277 阅读7分钟

左手原生右手框架

乍一看,vue 和 react 明显的区别可能是:

vue 是自动挡,而 react 是手动挡。

想要在这两框架间分出高下的人不在少数,且根据观察,开手动挡的鄙视开自动挡的大有人在,不信你可以去问问驾校教练。

再聊一下前端原生三剑客 html + js + css,原生和这两个框架相比应该有两个最基本的短板:

  • 第一点发生在渲染列表的时候,需要复用条目 dom ,以及更新 innerText 还有事件的挂载卸载等,原生得自己解决这个问题。框架不知是否有解决这个问题,但用 v-for 或者 for 循环的时候框架编译器会提示少了 key,不过这个解决的也可能只是虚拟 dom 树这一层的复用。

    当有海量条目的时候,还需要不断剥离滚动区域外的 dom ,插入到当前视图滚动方向的末尾,以做性能优化,这个原生还得自己解决,框架则可以方便地利用一些第三方 ui 库。

  • 第二点是在补足上面短板问题的前提下,在数据变化后,最小化地更新 dom ,这一点也是框架要解决的主要问题,为此不惜在内存创建了虚拟 dom 树,以及运用相应的算法策略。如果用原生的话,在具体的项目中,解决此类问题,可以做到精准而高效,但要构建成通用的优化策略,仍需做很多额外的工作,因此也算是一个短板。

精准而优雅

上面这一段看似想要抛砖引玉,实则是为了引出前端开发中的两个进阶问题,优雅的复用 dom 和数据的精准化更新。

首先说如何优雅的复用 dom, 优雅这个词还是有点抽象,其实可以先用一个具象化一点的词代替,简洁。

因为简洁和优雅总是被一块提及,简洁应算优雅的第一大特征。

对框架来说,天然就自带复用 dom 机制,一个组件被引入一次都只会产生一个 dom 实例。

简洁复用 dom 的前提在于,深刻理解组件使用的业务场景,比如某些弹窗,如果很确定不会产生同时出现两个弹窗的场景,则可将其做成全局单例的模式。

很多前端项目,存在大量的代码堆砌,有点像上学应付老师写的口水文,洋洋洒洒一大段,字数很多,但就是阅后无感,甚至有想骂街的冲动。

归根结底,一方面是写得少,另一方面是没有自己的思考,或借鉴别人的思考。

只有不断的提高练习时长,才能练就优雅的转身,动作一气呵成,在删减代码量的同时,提高可读性。

ok,练习时长够了,接下来离实现优雅还有另一个重要特征,灵活。

就像运球一样,肩膀的灵活性也必不可少。

代码上来讲,就是完成功能抽象的同时,还能和组件使用场景高度解耦。

比如之前说的全局单例组件,在封装完毕后,这时突然又出现了需要同时产生两个实例的场景,那么原有组件应该改造量极小,或者可以直接复用。

如果用原生来写组件,就像捏橡皮泥,可以捏成自己想要的形状,灵活性拉满了。

但工作中,作为团队一份子,我们还得基于框架去做灵活性实现。

那在使用框架的前提下,既要简洁,又要灵活,还能实现数据的精准化更新,看哪方面呢?

比较看框架的使用年限,即架龄。

排水渠过弯

下面仅限表达个人观点。

vue 和 react 经过多年内卷,形成了从类组件到函数组件,从面向对象到到组合式设计的转变过程,不断给社会的底层牛马输送着新鲜的养分。

年轻的牛马都是负重前行,老牛马就应该学点弯道超车的技术。

下面教大家如何在实践中,利用排水渠过弯,在秋名山一骑绝尘。

首先要做的,是尽量避免对模板语法的依赖,不再通过模板语法进行 props 传值,实践证明这种使用方式,看似合理,实际呆板低效。

其次,要尽量减少甚至杜绝传统 store 机制的使用,这种机制一般用来存储和更新全局响应状态,在项目上,我们就要放弃 pinia,redux,mobx 这类插件的引入。

那如何补充缺失的 store 机制呢?

vue3示例

对 vue3 来说,因为直接利用了现代浏览器提供的 Proxy 这一天然的排水渠,设计上不仅相比上代版本更加简洁,使用上也更加灵活了,下面是展示一个示例代码:

  <!DOCTYPE html>
  <html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
  </head>
  <body>
      <div id="app"></div>
      <script type="module">
          import { createApp, ref, h } from 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js';

          const ViewData = ref([1,2,3]);
          setTimeout(() => {
              ViewData.value = [4,5,6];
          }, 2000);

          const ViewToBeUpdatedForSure = { 
              render(){
                  // 非数组演示 
                  return h("div",{ innerHTML: ViewData.value.join("<br/>") })
              }
          }

          createApp({
              render:() => h("div",[
                  h("div","无聊的不会变化的title"),
                  ViewToBeUpdatedForSure.render()
              ])
          }).mount(app);

      </script>
  </body>
  </html>

通过使用 getter 来监听数据的访问,将数据和组件形成一对多的订阅定关系。

当一个数据的 setter 被触发,所有组件订阅的视图更新操作也会触发。

同时,其他未订阅相关数据的组件则完全不受影响。

项目中,ViewData 这些需要产生视图响应的数据可以放在一个单独的文件中保存,以代替原来的 store 机制,同时对外导出这些属性的 setter 方法。

react示例一

React 因为是手动挡框架,实现同样的的 store 机制,离不开离合器。

因为前文说了要尽量避免引入 redux 或者 mobx 这种预制离合器。

放弃 props 的前提下,意味着要使用 context,而精准更新,意味着要写一个订阅器,用来实现数据修改时,所有订阅的组件会自动触发更新。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
    <script>
        const h = React.createElement;
        const ctx = React.createContext(null);

        function useDataProxyToBindCom(data){

            let snapshotData = data;
            let updaters = [];

            function subscribe(updater){
                updaters.push(updater);
                return () => updaters.splice(updaters.indexOf(updater),1);
            }

            function getSnapshot(){
                return snapshotData;
            }

            function trigger(newData){
                snapshotData = newData;
                for(let updater of updaters){
                    updater();
                }
            }

            function dataProxyBinder(childs){
                const dataRefs = React.useSyncExternalStore(subscribe,getSnapshot);
                return h(ctx.Provider,{value:snapshotData},childs);
            }
            return [trigger, dataProxyBinder]
        }

        const listData = [1,2,3];
        const [setListData,dataProxyBinder] = useDataProxyToBindCom(listData);

        const TitleNoNeedToUpdate = h("div", null, "无聊的不会变化的title");
        const ComBindListData = h(function (){
            const list = React.useContext(ctx);
            return  h("div", null,!list ? "" : list.map(e => h("div",null,e)) );
        });
        const ViewToBeUpdatedForSure = h(function(){
            return dataProxyBinder(ComBindListData);
        })

        setTimeout(() => {
           setListData([4,5,6]);
        }, 2000);

        
        const App = h("div", null, [
                TitleNoNeedToUpdate,
                ViewToBeUpdatedForSure,
                // 内嵌
                h("div",null,ViewToBeUpdatedForSure)
            ])
        ReactDOM.render(App, app);
      </script>

</body>
</html>

这里使用了 React.useSyncExternalStore 来实现状态管理,当然也可以用官方提供的其他 hook 比如 useReducer 实现同样的效果。

显然,和 Vue3 相比还是不够简洁,这离合器还得改造。

那么可以效仿 Vue3 借助浏览器自带的 Proxy 来让使用变得更简洁。

react示例二

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.production.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
    <script>

        // 组件注册器,仅单一场景演示
        const data2ComRegister = {
            set(rEle){
              if(!this._proxyMap){
                    this._proxyMap = new Map();
                }
                this._proxyMap.set(rEle,this._tmpProxy);
            },
            get(rEle){
                return this._proxyMap.get(rEle);
            }
        };
        const h = function(...args) {
            const reactEle = React.createElement(...args);
            if(typeof args[0] === "function"){
                args[0]();
                data2ComRegister.set(reactEle);
                return React.createElement(function(){
                  return  data2ComRegister.get(reactEle).dataProxyBinder(args[0]());
                })
            }
            return reactEle;
        }

        function ref(data){

            // 仅单一场景演示
            let snapshotData = data;
            const handler = {
                set:function(obj,prop,value){
                    trigger(value)
                },
                get: function (obj, prop) {
                    if(prop === "value"){
                        // 仅单一场景演示
                        data2ComRegister._tmpProxy = dataProxy;
                        return snapshotData;
                    }else if(prop === "dataProxyBinder"){
                        return dataProxyBinder;
                    }
                }
            }
            const dataProxy = new Proxy({},handler)
            data2ComRegister._tmpProxy = dataProxy;
            let updaters = [];

            function subscribe(updater){
                updaters.push(updater);
                return () => updaters.splice(updaters.indexOf(updater),1);
            }

            function getSnapshot(){
                return snapshotData;
            }

            function trigger(newData){
                snapshotData = newData;
                for(let updater of updaters){
                    updater();
                }
            }

            function dataProxyBinder(childs){
                const dataRefs = React.useSyncExternalStore(subscribe,getSnapshot);
                return h("div",null,childs);
            }
            
            return dataProxy;
        }

        const listData = ref([1,2,3]);

        const TitleNoNeedToUpdate = h("div", null, "无聊的不会变化的title");
        const ViewToBeUpdatedForSure = h(function (){
            const list = listData.value;
            return  h("div", null,!list ? "" : list.map(e => h("div",null,e)) );
        });
        
        setTimeout(() => {
           listData.value = [4,5,6];
        }, 2000);

        
        const App = h("div", null, [
                TitleNoNeedToUpdate,
                ViewToBeUpdatedForSure,
                // 内嵌
                h("div",null,ViewToBeUpdatedForSure)
            ])
        ReactDOM.render(App, app);
      </script>

</body>
</html>

上面只是单一场景的简单示例,实际要管理的场景要更复杂。

但基本体现了函数式组件的核心绑定机制,即在 render 函数中实现数据到视图的绑定。

最后,道路千万条,优雅第一条,希望各位都能在前端领域找到自己的专属银弹。