Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 原理学习 #51

Open
ChelesteWang opened this issue Apr 2, 2022 · 6 comments
Open

React 原理学习 #51

ChelesteWang opened this issue Apr 2, 2022 · 6 comments

Comments

@ChelesteWang
Copy link
Owner

ChelesteWang commented Apr 2, 2022

React 原理学习

  1. JSX -> CE (CreateElement)JSX parser TypeScript Babel React , 或者 React 17 中提出了新的React-JSX JSX-runtime

  2. React-Dom 将创建 DOM 节点递归挂载到 DOM 树上面

  3. 现在我们的 React 是没有状态存在的

  • 移除现有的 DOM 节点。

  • 重新渲染一切。

  • Patch 算法增量更新渲染

调和阶段(Reconciler): 官方解释。React 会自顶向下通过递归,遍历新数据生成新的 Virtual DOM,然后通过 Diff 算法,找到需要变更的元素(Patch),放到更新队列里面去。

渲染阶段(Renderer):遍历更新队列,通过调用宿主环境的API,实际更新渲染对应元素。宿主环境,比如 DOM、Native、WebGL 等。

什么是 Fiber

Fiber 是对 React element 的拷贝,是一份描述 Diff 的工作模拟了函数调用关系

Current 是当前 node , work in progress node 是 copy on write 体现了 immutable 拷贝修改后替换原始数据,计算阶段可中断,提交阶段不可中断。

Element 与渲染无关,只是页面的数据描述,描述组件信息与层级关系

fiber 与渲染有关,fiber 树描述 DIff 等工作调用关系即更新路径,有 react 组件的 render 处理顺序,类似于一棵树转二叉树,线索树指针指向父级,children 直接子节点 ,sibling 相邻节点

function FiberNode(...) {
  // 工作类型(Fiber类型)
  this.tag = tag;

  // ReactElement.key
  this.key = key;


  // ReactElement.type
  this.elementType = null;


  // Fiber
  // 执行完当前工作返回的Fiber
  this.return = null;

  // 当前Fiber的(最左侧)子Fiber
  this.child = null;

  // 当前Fiber的下一个同级Fiber
  this.sibling = null;

  ……
}

Fiber 是如何工作的

  1. ReactDOM.render()setState 的时候开始创建更新。
  2. 将创建的更新加入任务队列,等待调度。
  3. 在 requestIdleCallback 空闲时执行任务。
  4. 从根节点开始遍历 Fiber Node,并且构建 WokeInProgress Tree。
  5. 生成 effectList。
  6. 根据 EffectList 更新 DOM。

Render 是如何执行的

分解任务,加入任务队列,逐一修改,执行 commit 提交

https://zhuanlan.zhihu.com/p/137234573

如何进行 Dom diff

  • 对于类型相同的节点直接替换属性(保留节点,更新属性)
  • 对于不同类型的节点直接替换节点(放弃比较,删除新建)
  • Children 对比需要用 Key 帮助用于复用
function* domDIFF(vDOM1, vDOM2) {
    if(!vDOM1) {
        yield new InsertUpdate(vDOM1, vDOM2)
        return
    }


    if(vDOM1.type === vDOM2.type) {
        if(vDOM1.key === vDOM2.key) {            
          yield new AttributeUpdate(vDOM1, vDOM2)                
          yield * domDIFFArray(vDOM1.children, vDOM2.children)
        } else {
          yield new ReplaceUpdate(vDOM1, vDOM2)
        }
        return
    } else {
        yield new ReplaceUpdate(vDOM1, vDOM2)
    }

}



function toMap(arr) {
    const map = new Map()
    arr.forEach(item => {
        if(item.key)
          map.set(item.key, item)  
    })
    return map
}

function * domDiffArray(arr1, arr2) {
    if(!arr1 || !arr2) {
        yield new ReplaceUpdate(vDOM1, vDOM2)
        return
    }

    const m1 = toMap(arr1)
    const m2 = toMap(arr2)

    // 需要删除的VDOM
    const deletes = arr1.filter( (item, i) => {        
        return item.key ? 
            !m2.has(item.key)
                   : i >= arr2.length
    })

    for(let item of deletes){
        yield new ReplaceUpdate(item, null)
    }

    // 需要Replace的VDOM    
    for(let i = 0; i <arr1.length; i++) {
        const a = arr1[i]
        if(a.key ) {
            if(m2.has(a.key)) {
                yield * domDIFF(a, m2.get(a.key))
            }
        }
        else {
            if(i < arr2.length) {
                yield * domDIFF(a, arr2[i])
            }
        }
    }

    // 需要Insert的VDOM
    for(let i = 0; i <arr2.length; i++) {
        const b = arr2[i]

        if(b.key) {
            if(!m1.has(b.key)) {
                yield new InsertUpdate(i, b)
            }            
        }else {
          if(i >= arr1.length) {
            yield new InsertUpdate(i, arr[2])
          }
        }
    }


}

class InsertUpdate {    
    constructor(pos, to){
        this.pos = pos
        this.to = to
    }
}


class ReplaceUpdate {
    constructor(from, to){
        this.form = from 
        this.to = to
    }
}

什么是 Reconciler

一个调和器(Reconciler)将3部分个功能结合起来:

  • Fiber (VirtualDOM): 对组件层级结构的描述

  • Reconciliation: 计算DOM DIFF

  • HostConfig :对于React渲染需要的具体端(DOM/React Native等)的封装

    • 删除元素的能力
  • 改变属性的能力

  • 插入元素的能力

  • 替换元素的能力

@ChelesteWang
Copy link
Owner Author

1)背景

  • react在进行组件渲染时,从setState开始到渲染完成整个过程是同步的(“一气呵成”)。如果需要渲染的组件比较庞大,js执行会占据主线程时间较长,会导致页面响应度变差,使得react在动画、手势等应用中效果比较差。
  • 页面卡顿:Stack reconciler的工作流程很像函数的调用过程。父组件里调子组件,可以类比为函数的递归;对于特别庞大的vDOM树来说,reconciliation过程会很长(x00ms),超过16ms,在这期间,主线程是被js占用的,因此任何交互、布局、渲染都会停止,给用户的感觉就是页面被卡住了。

2)实现原理

旧版 React 通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。而Fiber实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的requestIdleCallback这一 API。
Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示:

const fiber = {
    stateNode,    // 节点实例
    child,        // 子节点
    sibling,      // 兄弟节点
    return,       // 父节点
}
  • react内部运转分三层:
    • Virtual DOM 层,描述页面长什么样。
    • Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。
    • Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。
  • 为了实现不卡顿,就需要有一个调度器 (Scheduler) 来进行任务分配。优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。任务的优先级有六种:
    • synchronous,与之前的Stack Reconciler操作一样,同步执行
    • task,在next tick之前执行
    • animation,下一帧之前执行
    • high,在不久的将来立即执行
    • low,稍微延迟执行也没关系
    • offscreen,下一次render时或scroll时才执行
  • Fiber Reconciler(react )执行阶段:
    • 阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。
    • 阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。
  • Fiber树:React 在 render 第一次渲染时,会通过 React.createElement 创建一颗 Element 树,可以称之为 Virtual DOM Tree,由于要记录上下文信息,加入了 Fiber,每一个 Element 会对应一个 Fiber Node,将 Fiber Node 链接起来的结构成为 Fiber Tree。Fiber Tree 一个重要的特点是链表结构,将递归遍历编程循环遍历,然后配合 requestIdleCallback API, 实现任务拆分、中断与恢复。
  • 从Stack Reconciler到Fiber Reconciler,源码层面其实就是干了一件递归改循环的事情

@ChelesteWang
Copy link
Owner Author

ChelesteWang commented Apr 3, 2022

在处理UI时,如果一次执行太多工作,可能会导致动画丢帧。

基本上,如果React要同步遍历整个组件树,并为每个组件执行任务,它可能会运行超过16毫秒,这将导致不顺畅的视觉效果。

较新的浏览器(和React Native)实现了有助于解决这个问题的API:requestIdleCallback,可用于对函数进行排队,这些函数会在浏览器空闲时被调用:

requestIdleCallback((deadline)=>{
    console.log(deadline.timeRemaining(), deadline.didTimeout)
});

如果我现在打开控制台并执行上面的代码,Chrome会打印49.9false,它基本上告诉我,我有49.9ms去做我需要做的任何工作,并且我还没有用完所有分配的时间,否则deadline.didTimeout将会是true

请记住timeRemaining可能在浏览器被分配某些工作后立即更改,因此应该不断检查。

requestIdleCallback 实际上有点过于严格,并且[执行频次](facebook/react#13206 (comment))

不足以实现流畅的UI渲染,因此React团队必须实现[自己的版本](https://github.com/facebook/react/blob/eeb817785c771362416fd87ea7d2a1a32dde9842/packages/scheduler/src/Scheduler.js#L212-L222)。

现在,如果我们将React对组件执行的所有活动放入函数performWork, 并使用requestIdleCallback来安排工作,我们的代码可能如下所示:

requestIdleCallback((deadline) => {
    while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) {
        nextComponent = performWork(nextComponent);
    }
});

我们对一个组件执行工作,然后返回要处理的下一个组件。

这是可以做到的,但前提是我们不能同步地处理整个组件树。

因此,我们需要一种方法将渲染工作分解为增量单元。

为了解决这个问题,React必须重新实现遍历树的算法,从依赖于内置堆栈的同步递归模型,变为具有链表和指针的异步模型

递归遍历

walk(a1);

function walk(instance) {
    doWork(instance);
    const children = instance.render();
    children.forEach(walk);
}

function doWork(o) {
    console.log(o.name);
}

递归方法直观,非常适合遍历树。

但是正如我们发现的,它有局限性:最大的一点就是我们无法分解工作为增量单元。

我们不能暂停特定组件的工作并在稍后恢复。

通过这种方法,React只能不断迭代直到它处理完所有组件,并且堆栈为空。

链表遍历

[Sebastian Markbåge](https://github.com/sebmarkbage)在[[Fiber Principles: Contributing To Fiber](facebook/react#7942)

](facebook/react#7942)

概括了该算法的要点。

要实现该算法,我们需要一个包含3个字段的数据结构:

  • child — 第一个子节点的引用
  • sibling — 第一个兄弟节点的引用
  • return — 父节点的引用

在React新的协调算法的上下文中,包含这些字段的数据结构称为Fiber。

下图展示了通过链表链接的对象的层级结构和它们之间的连接类型:

function walk(o) {
    let root = o;
    let current = o;

    while (true) {
        let child = doWork(current);

        if (child) {
            current = child;
            continue;
        }

        if (current === root) {
            return;
        }

        while (!current.sibling) {

            if (!current.return || current.return === root) {
                return;
            }

            current = current.return;
        }

        current = current.sibling;
    }
}

它看起来像浏览器中的一个调用堆栈。

我们现在通过保持对current节点(充当顶部堆栈帧)的引用来控制堆栈:

function walk(o) {
    let root = o;
    let current = o;

    while (true) {
            ...            current = child;
            ...            current = current.return;
            ...            current = current.sibling;
    }
}

我们可以随时停止遍历并稍后恢复。

@ChelesteWang
Copy link
Owner Author

React 18 与 Concurrent Mode

对于React来说,有两类瓶颈需要解决:

CPU的瓶颈,如大计算量的操作导致页面卡顿

IO的瓶颈,如请求服务端数据时的等待时间

其中CPU的瓶颈通过并发特性的优先级中断机制解决。

IO的瓶颈则交给Suspense解决。

@ChelesteWang
Copy link
Owner Author

react fiber 移除 Effect List 依赖 https://github.com/facebook/react/pull/19673/files

@ChelesteWang
Copy link
Owner Author

最精简版 react hooks 实现

let state = [];
let setters = [];
let firstRun = true;
let cursor = 0;

function createSetter(cursor) {
  return function setterWithCursor(newVal) {
    state[cursor] = newVal;
  };
}

// This is the pseudocode for the useState helper
export function useState(initVal) {
  if (firstRun) {
    state.push(initVal);
    setters.push(createSetter(cursor));
    firstRun = false;
  }

  const setter = setters[cursor];
  const value = state[cursor];

  cursor++;
  return [value, setter];
}

// Our component code that uses hooks
function RenderFunctionComponent() {
  const [firstName, setFirstName] = useState("Rudi"); // cursor: 0
  const [lastName, setLastName] = useState("Yardley"); // cursor: 1

  return (
    <div>
      <Button onClick={() => setFirstName("Richard")}>Richard</Button>
      <Button onClick={() => setFirstName("Fred")}>Fred</Button>
    </div>
  );
}

// This is sort of simulating Reacts rendering cycle
function MyComponent() {
  cursor = 0; // resetting the cursor
  return <RenderFunctionComponent />; // render
}

console.log(state); // Pre-render: []
MyComponent();
console.log(state); // First-render: ['Rudi', 'Yardley']
MyComponent();
console.log(state); // Subsequent-render: ['Rudi', 'Yardley']

// click the 'Fred' button

console.log(state); // After-click: ['Fred', 'Yardley']

@ChelesteWang
Copy link
Owner Author

react因为先天的不足——无法精确更新,所以需要react fiber把组件渲染工作切片;而vue基于数据劫持,更新粒度很小,没有这个压力;
react fiber这种数据结构使得节点可以回溯到其父节点,只要保留下中断的节点索引,就可以恢复之前的工作进度;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant