React 实现原理

115 阅读6分钟

跟着大佬做大佬,本文仅用于个人学习。如有错误,欢迎指出。

基础概念

    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) 领域特定语言、组件

  1. 定义: vdom 用JS对象 表示 最终渲染的 dom. (大家熟悉的描述 dom 的方式是 html )

  2. dsl 产生背景

    让开发直接去写这样的 vdom(JS对象) 并用渲染器  把它渲染出来 太麻烦,从而引入 dsl 。
    
    所以,前端框架都会设计一套 dsl. 然后编译成 render function, 执行后产生 vdom.
    
    例子:html、css 是 web 领域的 dsl。        
    
  3. dsl 的设计

     背景: 大家熟悉的描述 dom 的方式是 html,所以 dsl 也设计成那样。
     
     例子:  vue 的 template  (vue的 template compiler 是自己实现的);
     
             react 的 jsx (react的jsx的编译器是babel实现的)。
    
  4. 组件

     本质上是 对 产生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 这种数据结构了。

image.png

学习链接 原文大佬的React实现原理链接