ChelesteWang / FE-Review

前端知识复盘与整理
Apache License 2.0
31 stars 8 forks source link

React 原理学习 #51

Open ChelesteWang opened 2 years ago

ChelesteWang commented 2 years ago

React 原理学习

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

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

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

调和阶段(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

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部分个功能结合起来:

ChelesteWang commented 2 years ago

1)背景

2)实现原理

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

const fiber = {
    stateNode,    // 节点实例
    child,        // 子节点
    sibling,      // 兄弟节点
    return,       // 父节点
}
ChelesteWang commented 2 years ago

在处理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 实际上有点过于严格,并且[执行频次](https://github.com/facebook/react/issues/13206#issuecomment-418923831)

不足以实现流畅的UI渲染,因此React团队必须实现[自己的版本](https://github.com/facebook/react/blob/eeb817785c771362416fd87ea7d2a1a32dde9842/packages/scheduler/src/Scheduler.js#L212-L222)[](https://user-images.githubusercontent.com/1249423/53719626-94dde800-3e99-11e9-8108-1679cbf06185.png)

现在,如果我们将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

](https://github.com/facebook/react/issues/7942)

概括了该算法的要点。

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

在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 commented 2 years ago

React 18 与 Concurrent Mode

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

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

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

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

IO的瓶颈则交给Suspense解决。

ChelesteWang commented 2 years ago

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

ChelesteWang commented 2 years ago

最精简版 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 commented 2 years ago

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