跟着大佬做大佬,本文仅用于个人学习。如有错误,欢迎指出。
基础概念
1) 编译过程中的词法分析会生成4种树:DOM(HTML),CSSOM(CSS),AST(JS),可访问障碍树。
编译器 负责 词法分析及代码生成。
2) JavaScript引擎 负责 整个JavaScript程序的编译及执行过程。
3) [词法分析] 将代码分解为AST代码块 --> [JavaScript编译器] 解析AST代码块,生成AST语法树 -->
[JavaScrpit引擎]转换 AST语法树 为 可执行代码
注:javaScript代码无法直接执行,需JavaScript编译器编译后,才可识别,然后通过JavaScript引擎运行。
单文件组件,简称 SFC (single-file components) (Vue3)
1.Vdom 、dsl (domain specific language) 领域特定语言、组件
-
定义: vdom 用JS对象 表示 最终渲染的 dom. (大家熟悉的描述 dom 的方式是 html ) -
dsl 产生背景:让开发直接去写这样的 vdom(JS对象) 并用渲染器 把它渲染出来 太麻烦,从而引入 dsl 。 所以,前端框架都会设计一套 dsl. 然后编译成 render function, 执行后产生 vdom. 例子:html、css 是 web 领域的 dsl。 -
dsl 的设计背景: 大家熟悉的描述 dom 的方式是 html,所以 dsl 也设计成那样。 例子: vue 的 template (vue的 template compiler 是自己实现的); react 的 jsx (react的jsx的编译器是babel实现的)。 -
组件:本质上是 对 产生vdom的 逻辑 的封装。 函数的形式、class 的形式 、 option对象(vue)的形式都可以。 (函数式组件、类组件...)
2. react 架构的演变
背景:vue 和 react 最大的区别就是 状态管理方式 。 由此导致了后面 架构的不同演变方向。
react 通过 setState 的 api 触发状态更新,更新以后就重新渲染整个的 vdom. vdom庞大,计算量庞大无可避免。
但浏览器里 js 计算时间太长会阻塞渲染,会占用每一帧的动画、重绘重排的时间,从而导致动画卡顿。
==》新思路:可否拆分计算量,每一帧计算一部分,不要阻塞动画渲染?
引入 fiber。
fiber 架构:目标是可打断的计算。 --- children、 parent、 sibling、 return
关键字:render (reconcile + schedule) + commit ----- reconcile (effectTag 增删改标记, effectList队列)
原来react是 递归渲染,是不能被打断的。
原因有二:
1) 渲染就直接操作dom, 打断后,已更新到dom部分如何处理?
2) 现在是直接渲染vdom, vdom中只有 children 的信息,打断后, 如何找到其父节点?
由此 react 创建了 fiber 的数据结构:
除了原有的 children 信息,又添加了必需的 parent、sibling的信息。
react 会先把 vdom 转换成 fiber, 再去进行 reconcile, 这样就是可以打断的了。
解决:把渲染流程分为两部分:render (reconcile + schedule) + commit.
render: (reconcile + schedule)
reconcile: render阶段 找到vdom中变化的部分 创建dom, 打上增删改的标记,==> 此操作叫 reconcile (调和);
reconcile 的过程就是vdom 转 fiber 的过程。可以理解为树状结构转为链表结构。
==> 1) vdom 转 fiber; 2) 找到fiber中变化部分,创建dom; 3) 打增删改Tag
==> 就是 reconcile 的时候做 diff 的
schedule: reconcile是可以被打断的,由schedule调度。
reconcile 可以打断的原理:
在每次循环处理 fiber 节点的 reconcile 之前,都要先调用下shouldYield 方法 去 判断待处理的
任务队列有没有优先级更高的任务, 有的话就先处理优先级更高的 fiber。这边打断先暂停一下。
一句话:通过 fiber 的数据结构,加上循环处理前每次判断下是否打断来实现的。
commit: 全部计算完成之后,就一次性更新到 dom, ==> 此操作叫 commit.
commit有3个阶段:
before mutation: 异步调度useEffect.
mutation: 遍历 effectList 来更新 dom.
layout: 同步调度 useLayoutEffect, 且此阶段可拿到新的dom节点,还会更新下ref.
总结:fiber 既是一种(链表的)数据结构,也代表 render + commit 的渲染流程。fiber解决了react 递归的渲染期间 不能被打断的问题,从而解决到了动画卡顿的问题。函数组件和 class 组件的 reconcile 和之前讲的一样,就是调用 render 拿到 vdom, 然后继续处理渲染出的 vdom 。
3. 文章精华总结
React 和 vue 都是基于 vdom 的前端框架。
用vdom 的好处:不仅可以精准的对比关心的属性,还可以跨平台渲染。
开发人员不会去直接写 vdom, 而是通过 jsx 这种接近 html语法的 DSL,编译产生render function,
且在执行render function后产生 vdom.
vdom的渲染 就是根据 不同的类型 来用不同的dom api 来操作dom.
渲染组件的时候,函数组件:执行它拿到 vdom;
class组件:创建实例,然后调用 render方法拿到 vdom;
vue-option对象:调用 render方法拿到 vdom.
React 和 vue 最大区别在状态管理上。vue通过响应式,react是通过 setState的 api。
这个最大的区别导致了后面 react架构 的变更。
react 的 setState 的方式,导致它不知道哪些 组件 变了,必需渲染整个 vdom才行.
但这样,计算量庞大,会阻塞渲染,导致动画卡顿。
所以react后来改造成了 fiber架构,目标是可打断的计算。
为了这个目标,不能再对比改变更新 dom了,因此把渲染分为了 render 和 commit 两个阶段。
render阶段 通过 schedule调度来进行 reconcile, 也就是找到变化的部分,创建 dom,打上增删改的 Tag。
等全部计算完成之后,commit阶段一次性更新到 dom。
打断之后要找到父节点、兄弟节点,所以 vdom也被改造成了 fiber的数据结构,从而有了 parent、sibling的信息。
所以 fiber 既指这种链表的数据结构,又指这个 render、commitd的流程。
reconcile 阶段每次处理一个 fiber节点,处理前会判断下 shouldYield,如果有更高优先级的任务,那就执行它。
commit 阶段不用再次遍历 fiber树, 为了优化,react把 effectTag的 fiber都放到了 effectList队列中,遍历更新即可。
在 dom 操作前,会异步调用 useEffect的回调函数, 异步是因为不能阻塞渲染。
在 dom 操作之后,会同步调用 useLayoutEffect的回调函数,并更新 ref.
commit 阶段分成了 before mutation、mutation、 layout 三个阶段。
理解react 原理:要理解 vdom、jsx、组件本质、fiber、render(reconcile + schedule) + commit(三个阶段)的渲染流程。
4. 小结
(1) 虽然 before mutation 时候会触发异步的 useEffect 调用,layout 阶段会同步执行useLayoutEffect 但实际顺序上useLayoutEffect 是先于useEffect 的。(异步执行在同步执行后面)
(2) react 18 已经不存在effectlist 这种数据结构了。
学习链接 原文大佬的React实现原理链接