当前位置: 首页 > 科技观察

说说我这些年对前端框架的理解

时间:2023-03-19 20:11:08 科技观察

最早的时候,页面是由服务端渲染的,也就是PHP和JSP技术。服务端通过模板引擎填充数据,返回生成的html,渲染给浏览器。届时会同步提交表单,服务器会返回结果页的html。后来浏览器有了ajax技术,可以异步请求,服务器返回xml或者json。Ajax最初是基于xml的,这也是它名字的由来。因为xml有很多不需要的标签,内容也比较多,所以后来流行了json。网页和服务器之间的数据交互变成了异步的。服务端可以返回json数据,浏览器可以拼接html然后渲染(浏览器生成dom相当于渲染)。基本不需要刷新页面,所以单页面应用SPA(singlepageapplication)逐渐演变而来。早期的页面开发是基于浏览器的domapi来操作dom进行渲染和交互,但是domapi比较冗长,而且当时浏览器的兼容性也很麻烦,不同的浏览器有不同的写作方法。为了简化dom操作,使其更兼容各种浏览器,jquery出现并迅速流行起来。那个时候jquery如火如荼。我一直习惯把网页分为物理层和逻辑层。即使dom是物理层,jquery也是操作dom的一系列工具函数,也是作用于物理层的。网页所做的基本上就是获取数据渲染dom,数据变化后更新dom。这个过程是普遍的。后来逐渐出现了mvvm框架,自动将数据变化映射到dom上,不再需要手动操作dom。即vue、react等现代前端框架。我称这一层为逻辑层。前端框架除了提供数据驱动视图变化的功能外,还支持dom的逻辑划分,可以将一部分dom封装成组件,组件相互组合形成整个界面。物理层还是dom,但是实现数据到dom的自动映射后,我们只需要在逻辑层编写组件即可。现在前端入门将不再学习jquery在物理层操作dom,而是直接从vue、react等逻辑层的前端框架入手。但是并不代表完全不需要jquery。前端框架主要解决数据与dom的绑定,可以在发生变化后自动更新dom。如果不需要更新,直接操作dom即可,比如各种活动页面,没有数据更新,用jquery操作dom还是很方便的。前端框架是UI=f(state)的声明式思想。你只需要声明组件的视图,组件的状态数据,以及组件之间的依赖关系,然后状态会自动更新dom。直接操作dom的jquery的工具函数库势在必行。对于view的描述,react和vue采用了不同的方案。React将jsx的语法扩展为js,通过babel实现。描述view的时候可以直接用js写逻辑,没有新的语法。而vue实现了一套模板DSL,引入了插值、指令、过滤器等模板语法,相比jsx更加简洁,模板编译器由vue实现。vuetemplate是有限制的,只能访问data,prop,method,可以静态分析优化,而react的jsx是直接基于js的语法,动态逻辑比较多,所以不能静态分析优化。但是vue模板也不是什么都好,因为它脱离了js上下文,更难引入typescript进行类型推导。prop、method、data的所有类型都需要单独声明。React的jsx本来就和js处于同一个语境中,自然而然的就和typescript结合起来了。所以vuetemplate和reactjsx各有优缺点。前端框架都是数据驱动的视图变化,数据分散在各个组件中。数据变化后如何更新dom?检测数据变化的方式基本上只有三种:watch、dirtycheck、nocheck。Vue是一个基于数据的手表。组件层通过Object.defineProperty监听对象属性的变化,重写数组的API监听数组元素的变化,进而更新dom。Angular基于脏检查。在每一个可能改变数据的逻辑之后,它都会比较数据是否改变了,如果改变了,就更新dom。React不检查,不检查,难道每次都渲染所有的dom?不是,它不检查是因为它没有直接渲染到dom,而是在中间加了一层虚拟dom,每次渲染这个虚拟dom。然后diff下渲染的虚拟dom是否发生了变化,如果发生变化则更新对应的dom。以上就是前端框架数据驱动视图变化的三个思路。Vue是一个组件级的数据监视。当组件内部监控数据变化的地方太多时,一次更新可能需要特别大的计算量。如果计算量大,可能会造成丢帧,即渲染卡顿。所以vue的优化方式就是把大组件拆成小组件,这样每个数据不会有太多的watcher。React不监听数据变化,而是渲染整个虚拟dom,然后diff。基于该方案的优化方法是,对于不需要重新生成vdom的组件,通过shouldComponentUpdate跳过渲染。但是,当应用程序的组件树非常大时,如果只是shouldComponentUpdate跳过部分组件的渲染,还是有可能计算量很大的。大量的计算也可能导致渲染延迟。我应该怎么办?遍历树有两种方式,深度优先和广度优先。组件树的渲染是深度优先的,一般是通过递归,但是如果你能记录路径,就可以变成循环。如果变成一个循环,那么就可以按照时间片进行分段,这样vdom的生成就不会再阻塞页面渲染,就像操作系统对多个进程的分时调度一样。这种通过将组件树改为链表来将vdom的生成从递归改为循环的功能就是reactfiber。与之前的组件节点相比,fiber节点没有parent和children的属性,而是多了child、sibling、return的属性。通过纤程链表树,优化了渲染的性能。可见vue的性能优化与react不同:vue是一个组件级的数据监控解决方案。当一个属性的watcher过多时可能会出现该问题,所以优化思路是将大组件拆分成小组件,保证每个属性不会出现过多的watcher。React不监视或检查数据更改。它每次渲染并生成vdom,然后比较vdom。然后优化的思路是用shouldComponentUpdate跳过部分组件的渲染,react内部也做了组件树的链表(fiber),把递归改成可中断渲染,按照时间片逐步生成整个vdom.组件之间必然要有逻辑复用。React和Vue有不同的解决方案:Vue的组件是option对象的形式,所以逻辑复用的方式自然可以想到通过对象属性的mixin,而vue2的组件内部的逻辑复用方案是mixin,但是它是mixin很难区分混入的属性和方法的来源,杂乱无章,不易维护。但是没有更好的解决办法。React一开始也支持mixin,后来放弃了。react的组件都是类和函数的形式,所以高阶组件(highordercomponents)类似高阶函数的方式更自然,即组件集合组件,执行父类中的部分逻辑组件,然后渲染子组件。没有逻辑的部分除了HOC的方式多加一层组件外,可以直接将那部分jsx作为props传给另一个组件复用,即renderprops。HOC和renderprops是React的类组件支持的两种逻辑复用方案。原始功能组件没有状态,仅作为类组件渲染的辅助存在。但是HOC的逻辑复用方式最终导致组件嵌套太深,类内部生命周期多,逻辑放在一起,导致组件比较大。如何解决类组件嵌套深、组件大的问题?而且,不能引入破坏性更新,否则下场可能会很惨。所以react团队看了一下函数组件,是不是也可以在函数组件中支持state,通过扩展一些API来支持,不是破坏性的更新。功能组件需要支持状态,那么状态存在于何处呢?类组件节点是有状态的,成为纤程节点后仍然存在。功能组件没有状态,因此纤程节点中也没有状态。给函数组件的fiber节点加state不就可以了吗?所以react在函数组件的fiber节点中添加了memorizedState属性来存储数据,然后通过API在函数组件中使用这些数据。这些API称为hooksapi。因为使用了fiber节点上的数据,所以api命名为useXxx。每个hooksAPI都必须有自己的位置来存储数据。如何组织呢?有两种方案,一种是map,一种是array。如果使用map,需要在hooksapi中指定一个key,根据key访问fiber节点中的数据。使用数组时不能改变顺序,所以hooksapi不能出现在if等逻辑块中,只能出现在最顶层。为了简化使用,hooks最终使用了数组的方式。当然,实现使用链表。每个hooksapi都去对应的fiber.memoriedState中取数据使用。hooksapi可以分为3类:第一类是数据类:useState:将数据存储在fiber.memoriedState的对应元素中useMemo:将数据存储在fiber.memoriedState的对应元素中,值为缓存函数的结果计算,状态改变后重新计算值useCallback:将数据存入对应的fiber.memoriedState元素中,值是一个函数,状态改变后重新执行该函数,是useMemo在where场景中的简化apivalue是一个函数,比如useCallback(fn,[a,b])等价于useMemo(()=>fn,[a,b])useReducer:将数据存储在fiber.memoriedState的对应元素中,value是reducer返回的结果,可以通过actionuseRef触发值的变化:将数据存储在fiber.memoriedState的对应元素中,值的形式为{current:specificvalue}。因为对象保持不变,只是current属性发生了变化,所以不会被修改。useState是最简单的存储值的方法。useMemo根据state执行函数,并缓存结果,相当于vue的getter。useCallback是对值是函数的情况的简化。useReducer通过action触发对值的修改。useRef包裹了一层对象,每次比较都是一样的,所以可以放一些不变的数据。不管是哪种形式,这些hooks的api的作用都是返回一个值。第二类逻辑上:useEffect:异步执行函数,依赖状态改变后会再次执行,组件销毁时调用返回的清理函数useLayoutEffect:渲染完成后同步执行函数,以及你可以获取dom中所有的hooksapi来执行逻辑,不需要等待渲染的逻辑可以放到useEffect中。第三类专门用于ref转发:可以通过各种scheme共享数据,但是dom元素必须通过ref转发。所谓ref转发就是在父组件中创建一个ref,然后将元素传递给子组件。如果你想在传递过来之前做一些改变,你可以使用useImperativeHandle来做改变。通过这三类hooksapi,以及后面会加入更多的hooksapi,函数组件也可以存储状态,在某些阶段执行一段逻辑,是一种可以替代类组件的方案。而且更重要的是,hooksapi是函数调用的形式,传递参数,hooksapi可以进一步封装成更强大的功能,即自定义hooks。这样就可以做到跨组件的逻辑复用。回顾一开始要解决的类组件嵌套太深、组件太大的问题,可以使用hooks来解决:逻辑扩展不需要嵌套hoc,多调用一个自定义hooks即可,逻辑组件的不需要都写在类里,可以分开成不同的hooks。React通过函数组件的hooksapi解决类组件的逻辑复用问题。(Fiber解决的是性能问题,而hooks解决的是逻辑复用问题。)在vue2中,通过mixin实现逻辑复用,同样存在组件过大的问题。在vue3中,可以用类似的思路来解决。为了让体验更接近原汁原味,这些基本上都是不刷新页面的单页应用,都是浏览器渲染(csr)方案,从服务器获取数据,驱动dom变化。但是对于一些低端机来说,还是需要服务器端渲染(SSR)的方案。但是你不能再回到jsp和php时代的模板引擎服务端渲染,而是要渲染成基于同一个组件树的字符串。服务端渲染和浏览器渲染都使用相同的组件代码,属于同构方案。从科技的出现到相关周边生态的改善,是一次轮回。从一开始的服务端渲染,到后来的客户端渲染,再到逻辑层的组件方案,最后到基于组件方案重新实现服务端渲染。其实物理层的东西并没有变,只是逻辑层一层一层的加了进去。目的是提高生产效率,降低开发成本,保证质量。这也是技术发展的趋势。