最早的时候页面是做事端渲染的,也便是 PHP、JSP 那些技能,做事端通过模版引擎填充数据,返复天生的 html,交给浏览器渲染。那时候表单会同步提交,做事端返回结果页面的 html。
后来浏览器有了 ajax 技能,可以异步的要求,做事端返回 xml 或者 json。ajax 最早是基于 xml 的,这也是它名字的由来。由于 xml 多了很多没必要的标签,内容比较多,所往后来 json 盛行开来。
网页和做事真个数据交互变成了异步的,可以做事端返回 json 数据,浏览器里拼接 html,之后渲染(浏览器里面天生 dom 就等同于渲染)。页面基本没啥刷新的必要了,于是后来就逐渐演化出了单页运用 SPA(single page application)。
早期开拓页面的时候便是基于浏览器的 dom api 操作 dom 来做渲染和交互,但是 dom api 比较啰嗦,而且当时浏览器的兼容性问题也比较麻烦,不同浏览器有不同的写法。为了简化 dom 操作和更方便的兼容各种浏览器,涌现了 jquery 并且迅速盛行开来,那个时期 jquery 是如日中天的。
我一贯习气把网页分为物理层和逻辑层,dom 就算是物理层,jquery 是操作 dom 的一系列工具函数,也是事情在物理层。
网页做的事情基本便是拿到数据渲染 dom,并且数据改变之后更新 dom,这个流程是通用的,后来逐渐涌现了 mvvm 框架,来自动把数据的变更映射到 dom,不再须要手动操作 dom。也便是 vue、react 等当代的前端框架。我把这一层叫做逻辑层。
前端框架除了供应了数据驱动视图变革的功能以外,还支持了 dom 的逻辑划分,可以把一部分 dom 封装成组件,组件和组件之间相互组合,构成全体界面。物理层依然是 dom,只是实现了数据到 dom 的自动映射之后,我们只须要在逻辑层写组件就可以了。
现在前端入门也不会再学物理层的操作 dom 的 jquery 了,而是直接从 vue、react 这种逻辑层的前端框架开始。
但是也不是说完备不须要 jquery,前端框架紧张办理的是数据到 dom 的绑定,可以变革往后自动更新 dom。如果不须要更新,那么直接操作 dom 即可,比如各种活动页,没啥数据更新,用 jquery 操作 dom 还是很方便。
前端框架是 UI = f(state) 这种声明式的思想,只须要声明组件的视图、组件的状态数据、组件之间的依赖关系,那么状态改变就会自动的更新 dom。而 jquery 那种直接操作 dom 的工具函数库则是命令式的。
对付视图的描述这件事 react 和 vue 用了不同的方案,react 是给 js 扩展了 jsx 的语法,由 babel 实现,可以在描述视图的时候直接用 js 来写逻辑,没啥新语法。而 vue 是实现了一套 template 的 DSL,引入了插值、指令、过滤器等模版语法,相对付 jsx 来说更简洁,template 的编译器由 vue 实现。
vue template 是受限定的,只能访问 data,prop、method,可以静态的剖析和优化,而 react 的 jsx 由于直接是 js 的语法,动态逻辑比较多,没法静态的做剖析和优化。
但是 vue template 也不全是好处,由于和 js 高下文割裂开来,引入 typescript 做类型推导的时候就比较困难,须要单独把所有 prop、method、data 的类型声明一遍才行。而 react 的 jsx 本来便是和 js 同一个高下文,结合 typescript 就很自然。
以是 vue template 和 react jsx 各有优缺陷。
前端框架都是数据驱动视图变革的,而这个数据分散在每个组件中,怎么在数据变革往后更新 dom 呢?
数据变革的检测基本只有三种办法:watch、脏检讨、不检讨。
vue 便是基于数据的 watch 的,组件级别通过 Object.defineProperty 监听工具属性的变革,重写数组的 api 监听数组元素的变革,之后进行 dom 的更新。
angular 则是基于脏检讨,在每个可能改变数据的逻辑之后都比拟下数据是否变了,变了的话就去更新 dom。
react 则是不检讨,不检讨难道每次都渲染全部的 dom 么?也不是,不检讨是由于不直接渲染到 dom,而是中间加了一层虚拟 dom,每次都渲染成这个虚拟 dom,然后 diff 下渲染出的虚拟 dom 是否变了,变了的话就去更新对应的 dom。
这便是前端框架的数据驱动视图变革的三种思路。
vue 是组件级别的数据 watch,当组件内部监听数据变革的地方特殊多的时候,一次更新可能打算量特殊大,打算量大了就可能会导致丢帧,也便是渲染的卡顿。以是 vue 的优化办法便是把大组件拆成小组件,这样每个数据就不会有太多的 watcher 了。
react 并不监听数据的变革,而是渲染出全体虚拟 dom,然后 diff。基于这种方案的优化办法便是对付不须要重新天生 vdom 的组件,通过 shouldComponentUpdate 来跳过渲染。
但是当运用的组件树特殊大的时候,只是 shouldComponentUpdate 跳过部分组件渲染,依然可能打算量特殊大。打算量大了同样可能导致渲染的卡顿,怎么办呢?
树的遍历有深度优先和广度优先两种办法,组件树的渲染便是深度优先的,一样平常是通过递归来做,但是如果能通过链表记录下路径,就可以变成循环。变成了循环,那么就可以按照韶光片分段,让 vdom 的天生不再壅塞页面渲染,这就像操作系统对多个进程的分时调度一样。
这个通过把组件树改成链表,把 vdom 的天生从递归改循环的功能便是 react fiber。
fiber 节点相对付之前的组件节点来说,没有了 parent、children 这种属性,多了 child、sibling、return 属性。
通过 fiber 链表树,优化了渲染的性能。
可以看到 vue 的性能优化和 react 的性能优化是不一样的:
vue 是组件级别的数据监听的方案,问题可能涌如今一个属性太多 watcher 的时候,以是优化思路便是大组件拆分成小组件,担保每个属性不要有太多 watcher。
react 不监听、不检讨数据变革,每次都渲染天生 vdom,然后进行 vdom 的比拟,那么优化的思路便是 shouldComponentUpdate 来跳过部分组件的 render,而且 react 内部也做了组件树的链表化(fiber)来把递归改成可打断的渲染,按照韶光片来逐渐天生全体 vdom。
组件之间难免要有逻辑的复用,react 和 vue 有不同的方案:
vue 的组件是 option 工具的办法,那么逻辑复用办法很自然可以想到通过工具属性的 mixin,vue2 的组件内逻辑复用方案便是 mixin,但是 mixin 很难区分混入的属性、方法的来源,比较乱,代码掩护性差。但也没有更好的方案。
react 刚开始也是支持 mixin 的,但后来废弃了。
react 的组件是 class 和 function 两种形式,那么类似高阶函数的高阶组件(high order component)的办法就比较自然,也便是组件套组件,在父组件里面实行一部分逻辑,然后渲染子组件。
除了多加一层组件的 HOC 办法以外,没有逻辑的部分可以直接把那部分 jsx 作为 props 传入另一个组件来复用,也便是 render props。
HOC 和 render props 是 react 的 class 组件支持的两种逻辑复用方案。
最开始的 function 组件是没有状态的,只是作为 class 组件渲染的赞助而存在。
但是 HOC 的逻辑复用办法终极导致了组件嵌套太深,而且 class 内部生命周期比较多,逻辑都放在一起导致了组件比较大。
怎么办理 class 组件嵌套深和组件大的问题呢?而且还不能引入毁坏性的更新,不然了局可能会很惨。
于是 react 团队就瞅准了 function 组件,能不能在 function 组件里面也支持 state,通过扩展一些 api 的办法来支持,也不是毁坏性的更新。
function 组件要支持 state,那 state 存在哪里呢?
class 组件节点有 state,变成 fiber 节点之后依然有,function 组件本来就没有 state,那么 fiber 节点中同样也没有。
那在 function 组件的 fiber 节点中加入 state 不就行了?
于是 react 就在 function 组件的 fiber 节点中加入了 memorizedState 属性用来存储数据,然后在 function 组件里面通过 api 来利用这些数据,这些 api 被叫做 hooks api。
由于是利用 fiber 节点上的数据,就把 api 命名为了 useXxx。
每个 hooks api 都要有自己存放数据的地方,怎么组织呢?有两种方案,一种是 map,一种是数组。
用 map 的话那么要 hooks api 要指定 key,按照 key 来存取 fiber 节点中的数据。
用数组的话顺序不能变,以是 hooks api 不能涌如今 if 等逻辑块中,只能在顶层。
为了简化利用, hooks 终极利用了数组的办法。当然,实现起来用的是链表。
每个 hooks api 取对应的 fiber.memoriedState 中的数据来用。
hooks api 可以分为 3 类:
第一类是数据类的:
useState:在 fiber.memoriedState 的对应元素中存放数据useMemo:在 fiber.memoriedState 的对应元素中存放数据,值是缓存的函数打算的结果,在 state 变革后重新打算值useCallback:在 fiber.memoriedState 的对应元素中存放数据,值是函数,在 state 变革后重新实行函数,是 useMemo 在值为函数的场景下的简化 api,比如 useCallback(fn, [a,b]) 相称于 useMemo(() => fn, [a, b])useReducer:在 fiber.memoriedState 的对应元素中存放数据,值为 reducer 返回的结果,可以通过 action 来触发值的变更useRef:在 fiber.memoriedState 的对应元素中存放数据,值为 {current: 详细值} 的形式,由于工具不变,只是 current 属性变了,以是不会修正。useState 是存储值最大略的办法,useMemo 是基于 state 实行函数并且缓存结果,相称于 vue 的 getter,useCallback 是一种针对值为函数的情形的简化,useReducer 是通过 action 来触发值的修正。useRef 包了一层工具,每次比拟都是同一个,以是可以放一些不变的数据。
不管形式怎么样,这些 hooks 的 api 的浸染都是返回值的。
第二类是逻辑类的:
useEffect:异步实行函数,当依赖 state 变革之后会再次实行,当组件销毁的时候会调用返回的清理函数useLayoutEffect:在渲染完成后同步实行函数,可以拿到 dom这两个 hooks api 都是用于实行逻辑的,不须要等渲染完的逻辑都可以放到 useEffect 里。
第三类是 ref 转发专用的:
数据可以通过各种方案共享,但是 dom 元素这种就得通过 ref 转发了,所谓的 ref 转发便是在父组件创建 ref,然后子组件把元素传过去。传过去之前想做一些修正,就可以用 useImperativeHandle 来改。
通过这 3 类 hooks api,以及之后会添加的更多 hooks api ,函数组件里面也能做 state 的存储,也能在一些阶段实行一段逻辑,是可以替代 class 组件的方案了。
而且更主要的是,hooks api 是通报参数的函数调用的形式,可以对 hooks api 进一步封装成功能更强大的函数,也便是自定义 hooks。通过这种办法就可以做跨组件的逻辑复用了。
再转头看一下最开始要办理的 class 组件嵌套过深和组件太大的问题,通过 hooks 都能办理:
逻辑扩展不须要嵌套 hoc 了,多调用一个自定义的 hooks 就行组件的逻辑也不用都写在 class 里了,完备可以抽离身分歧的 hooksreact 通过 function 组件的 hooks api 办理了 class 组件的逻辑复用方案的问题。(fiber 是办理性能问题的,而 hooks 是办理逻辑复用问题的)
vue2 中是通过 mixin 的办法来复用逻辑的,也有组件太大的问题,在 vue3 中也可以通过类似的思路来办理。
为了体验和原生更靠近,现在基本都是不刷新页面的单页运用,都是从做事端取数据然后驱动 dom 变革的 浏览器渲染(csr)方案。但对付一些低端机,仍旧须要做事端渲染(SSR)的方案。但不能回到 jsp、php 时期的那种模版引擎做事端渲染了,而是要基于同一个组件树,把它渲染成字符串。做事端渲染和浏览器渲染都用同样的组件代码,这便是同构的方案。
技能从涌现到完善到连带的周边生态的完善是一个循环,从最开始做事端渲染,到了后来的客户端渲染,然后涌现了逻辑层的组件方案,末了又要基于组件方案重新实现做事端渲染。其实物理层的东西一贯都没变,只是逻辑层不断的一层添加又一层,目的都是为了提高生产效率,降落开拓本钱,担保质量,这也是技能发展的趋势。