React フレームワーク、React Fiber アーキテクチャ、Fiber Diff アルゴリズムを 0 から実現

React フレームワークは、現在最も人気のあるフロントエンド フレームワークの 1 つで、特に多くの大規模な工場でより広く使用されています。一部の mvvm フレームワークと比較して、React を開始するには一定の技術的基礎が必要ですが、それをマスターした後のコーディング エクスペリエンスとパフォーマンスは非常に優れています。React の全体的な考え方は、コードの重複を大幅に削減し、同時プログラミングを容易にする関数型プログラミングです。React は、さまざまな実行タスクに優先順位を付けるための一連のスケジューリング メカニズムを内部的に実装し、ループ レンダリング (ファイバー) を高いものから低いものに設定し、A セットを導入します。 Fiber Diff アルゴリズムを使用して、Web ページの滑らかさを最大化し、パフォーマンスを最適化します。

React のソース コードは反復を繰り返しても比較的理解しにくいため、どうすれば React の基本的な動作原理を迅速に理解して習得できるでしょうか? 最近、従業員にトレーニングを実施し、このアイデアに基づいて React の簡易版を設計および実装しました。この記事では、レイヤーごとに深く掘り下げて、Fibre 構造、Fiber Diff アルゴリズム、useState フックの実装を含む、react の簡易版を 0 から実装します。

さらに詳しい内容については、公式アカウント「フロントエンド 361」をご覧ください。

概要

1. React の基本バージョンを実装する

2. ファイバーデータ構造の実装

3. ファイバー差分アルゴリズムの実装

4、useStateの実装

1. React の基本バージョンを実現する

まず反応コードの一部を見てみましょう

import React from "react"
import ReactDOM from "reaect-dom"

const container = document.getElementById("root")

ReactDOM.render(<div className="test" onClick={() => {console.log(123)}}>
    <p style={
   
   {background: 'red'}}>123</p>
</div>, container)

アイデア分析:

1. 上記のコードは React および ReactDOM オブジェクトを参照しているため、自分で実装する場合は React および ReactDOM オブジェクトをカスタマイズする必要があります

const React = {}
const ReactDOM = {}

2. React jsx コードを実行すると、@babel/plugin-transform-react-jsx によって次の形式に変換されます。

jsx変換

上記のコードからわかるように、jsx コードは babel でエスケープされた後に React.createElement を呼び出すため、カスタム React オブジェクトに createElement メソッドを追加する必要があります。createElement で受け取るパラメーターはラベルのタイプ (type) と要素です。プロパティ (props)。要素のすべての子要素 ​​(...children)

React.createElement = function(type, props, ...children){
    return {
        type,
        props: {
            ...props,
            children: children.map(child => {
                return child
            })
        }
    }
}

テキストノードが存在し、ラベルタイプがTEXTであるため、テキストノードを個別に抽出し、createTextElementメソッドを追加します。

React.createElement = function(type, props, ...children){
    // console.log(children)
    return {
        type,
        props: {
            ...props,
            children: children.map(child => {
                return typeof child === 'object' ? child : React.createTextElement(child)

                // 添加组件判断
            })
        }
    }
}

React.createTextElement = function(text){
  return {
    type: 'TEXT',
    props: {
      nodeValue: text,
      children: []
    }
  }
}

3.  React は mvvm フレームワークです。基本原理は、Vdom をリアル dom に変換し、ドキュメント フローに追加することです。そのため、ReactDOM.render メソッドを実装します。レンダリング内で、vDom がリアル dom に変換され、属性が追加されます。 dom を作成し、コンテナに追加します。

ReactDOM.render = function(vDom, container){
    container.appendChild(initElement(vDom))
}
function initElement(vDom){ //生成真实dom结构
  if(!vDom){
    return
  }
  let dom
    if(vDom.type === 'TEXT'){
        dom = document.createTextNode("")
    } else {
        dom = document.createElement(vDom.type)
        setAtr(dom, vDom.props)
    }
    if(vDom.props.children.length > 0){
        vDom.props.children.map((child) => {
            dom.appendChild(initElement(child))
        })
    } 
   setAtr(dom)
    return dom
}
function  setAtr(dom, props){
  if(!props){
    return
  }
  for(let [key, value] of Object.entries(props)){
      if(key !== 'children'){
        // 添加样式
        if(key === 'style'){
            if(value && typeof value === 'object'){
                for(let i in value){
                    dom.style[i] = value[i]
                }
            } else {
                dom.removeAttribute(key) // 样式对象不存在或者样式格式不对
            }
        } else if(/on\w+/.test(key)){  // 绑定事件 
            //\W+:匹配一个或多个非字母进行切割,匹配到的非字母不缓存;
            key = key.toLowerCase()
            if(value){
                dom[key] = value
            } else {
                dom.removeAttribute(key)
            }

        } else if(key === 'nodeValue'){
            dom.nodeValue=value
        } else { // 其他属性
            if(value){
                dom.setAttribute(key, value)
            } else {
                dom.removeAttribute(key)
            }

        }
      }
  }
}

これまでのところ、react の簡易版が実装されています (コンポーネントを除く。コンポーネントの種類は以下で紹介します)。

第二に、ファイバーデータ構造の実装

上記の再帰的な子ノードは中断できません. js はシングルスレッドなので、この時点で他の高度なタスクのレンダリングがあるとページがフリーズします. この問題に基づいて、react は双方向リンク リスト構造 (Fiber) を設計しました。変換再帰 循環トラバーサルの場合、高レベルのタスクが関係する場合、ループは一時停止され、高レベルのタスクの実行後にトラバーサルが継続されます。ループ トラバーサルは主にブラウザの特性を使用し、主に requestIdleCallback を使用して各フレームにアイドル時間があるかどうかを判断し、アイドル時間を使用してトラバースしてファイバー ツリーを生成します。

アイデア:

トラバーサル ファイバーは一時停止されているため、最後に一時停止した位置に戻って続行できます。そのため、現在通過しているノードを保存するグローバル変数 nextUnitOfWork を定義する必要があります。また、任意のファイバー ノードでツリー全体のトラバースを続行できます。ファイバー ノードでは、その親ノード、兄弟ノード、および子ノードを検索し、fiber.return を親ノードを指すように設定し、fiber.child を子ノードを指すように設定し、fiber.sibling を設定して兄弟ノードを実行できます。ファイバー ツリーは二重リンク リスト構造です。

ファイバーを横断するプロセスで、ファイバーノードに対応するリアル dom を同期的に作成します

1. 各走査を作業単位として扱い、現在の作業単位 (performUnitOfWork) を実行した後、次の作業単位に戻ります。

PerformUnitOfWork 関数は次の関数を実装します。

  • ファイバーノードのdomを作成する
  • ファイバーノードの子ノードをファイバー構造として作成します
  • ファイバーノードの次の作業単位 (実行するノード) を返します。
function createDom(fiber){
  let dom = fiber.type === 'TEXT' ? document.createTextNode("") : document.createElement(fiber.type)
  setAtr(dom, fiber.props)
  return dom
}
let nextUnitOfWork = null

// performUnitOfWork执行工作单元时,将当前节点的子元素都创建为fiber结构
// 遍历vDom和创建fiber节点是同步的
// fiber.props.children可以访问到子节点,根据这个将vDom遍历完毕
function performUnitOfWork(fiber){
  if(!fiber.dom){ 
    fiber.dom = createDom(fiber)
  }
  // 为每个子节点创建dom
  let elements = fiber.props.children 

  let index = 0

  let prevSibling = null

  // 将当前子节点虚拟dom对象创建为fiber节点
  while(index < elements.length){
      const element = elements[index]
      const newFiber = {
          type: element.type,
          props: element.props,
          return: fiber,
          dom: null
      }

      if(index === 0){ //如果是第一个元素,就设置为子节点
        fiber.child = newFiber
      } else {
        prevSibling.sibling = newFiber // 如果不是第一个元素,则指向上一个元素的兄弟节点
      }

      prevSibling = newFiber //每次循环,缓存当前节点
      index++

  }

  // 上面创建fiber节点,下面遍历fiber节点,将其子节点再次传入performUnitOfWork
  if(fiber.child){
    return fiber.child
  }

  let nextFiber = fiber 
  while(nextFiber){
    if(nextFiber.sibling){  
        return nextFiber.sibling
    }
    nextFiber = nextFiber.return
  }

}

2. ループ関数workLoopを設定しますブラウザは各フレームの空き時間にworkLoopを実行し、nextUnitOfWorkがnullになるまでworkLoop内でperformUnitOfWorkを実行しますワークユニットがあればブラウザにworkLoopを割り当ててworkLoopを実行しますブラウザの空き時間に。

function wookLoop(deadline){  
    let shouldYield = false //是否有剩余时间

    // 有空余时间的情况下,继续渲染
    while(nextUnitOfWork && !shouldYield) { // 链表结构没有渲染完毕并且有空余时间
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
        shouldYield = deadline.timeRemaining < 1
    }
    
    // 没有空余时间,交给浏览器等待执行
    if(nextUnitOfWork){
        requestIdleCallback(wookLoop)
    }
}

3. リンクリストのルートノードを格納するグローバル変数wipRootを設定します ReactDOM.render時に最初のファイバーノードを初期化してwipRootに割り当て、nextUnitOfWorkに割り当てます

let wipRoot = null
ReactDOM.render = function(vDom, container){
    wipRoot = {
        dom: container,
        props: {
            children: [vDom]
        }
    }
    nextUnitOfWork = wipRoot
    // 触发workLoop
    requestIdleCallback(wookLoop)
}

4. nextUnitOfWork が存在しない場合、fiberTree が生成されたことが証明され、この時点でレンダリング全体を送信する必要があります。

function wookLoop(deadline){  
    let shouldYield = false //是否有剩余时间

    // 有空余时间的情况下,继续渲染
    while(nextUnitOfWork && !shouldYield) { // 链表结构没有渲染完毕并且有空余时间
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
        shouldYield = deadline.timeRemaining < 1
    }
    
    // 没有空余时间,交给浏览器等待执行
    if(nextUnitOfWork){
        requestIdleCallback(wookLoop)
    }

    // nextUnitOfWork不存在,也就是fiber已经全部创建
    if(!nextUnitOfWork && wipRoot){
        // 执行完成,提交渲染
        commitRoot()
    }
}

5. commitRoot はファイバー ツリーをトラバースし、ドキュメント ストリームに dom を追加するために使用されます。

/**
 * 转化为双向链表结构后,开始提交和渲染真实dom
 * 提交整个fiber树(双向链表)
 */
function commitRoot(){
    commitWork(wipRoot.child)
    wipRoot = null
}

/**
 * 节点真实dom提交到父级dom中
 */
function commitWork(fiber){
    if(!fiber){
        return
    }
    
    const parentDom = fiber.return.dom;
    parentDom.appendChild(fiber.dom)
    commitWork(fiber.child)
    commitWork(fiber.sibling)

}

上記はファイバーデータ構造とファイバー二重リンクリストツリーを実装しています。

概要: ファイバー アーキテクチャは、元の再帰実行権限をブラウザに放棄することと同等であり、ブラウザがアイドル状態で再帰を実行できるようになります。これは、vDom をループして操作を中断することと同等です。

3、ファイバー差分アルゴリズムの実装

データ構造の比較を行うには、最初に最後のファイバー ツリーをキャッシュし、比較後に古いツリー上の不要なノードを削除する必要があります。

1. ファイバー ツリーを保存するグローバル変数を設定し、削除するノードを収集します。reactDOM.render が実行されると、ファイバーは最後のファイバー ツリーを指す代替属性を追加し、削除をリセットします。

let currentRoot = null //上一次提交的fiber树
let deletions = []

ReactDOM.render = function(vDom, container){
    wipRoot = {
        dom: container,
        props: {
            children: [vDom]
        },
        alternate: currentRoot, // 上一次提交的fiber
    }

    nextUnitOfWork = wipRoot

    deletions = [] // 渲染开始前将deletions重置为空

    // 触发workLoop
    requestIdleCallback(wookLoop)
}

2. ルート ノードから開始し、深さ優先トラバーサルを使用し、古いファイバー ツリーと比較して、新しいファイバー ツリーを生成します。

function performUnitOfWork(fiber){
   if(!fiber.dom){ //如果当前节点上没有真实节点,创建fiber对应的真实节点
        fiber.dom = createDom(fiber)
    }
   elements = fiber.props.children

  // 加入diff算法,对比新老节点
  reconcileChildren(fiber, elements) //说明 在遍历当前节点时,会遍历其所有子节点,并给子节点和子节点的兄弟节点设置alternate, 设置过之后,在下次递归中,刚好可以对比新老节点

  if(fiber.child){
    return fiber.child
  }

  let nextFiber = fiber //缓存当前fiber
  while(nextFiber){
    if(nextFiber.sibling){  //返回当前节点的兄弟节点
        return nextFiber.sibling
    }
    
    // 如果不存在兄弟节点,就返回父级节点,进而去查找父级节点的兄弟节点,如果没有也没有兄弟节点,则继续向上返回
    nextFiber = nextFiber.return
  }

}

3. ファイバーノードを実行する際、作成したファイバーノードにalternate属性を追加し、対応するoldFiberを指すようにすることで、新旧ノードを比較する際に、同じレベル、同じ位置の比較が可能となります。

代替通信

古いノードを比較して新しいファイバーを同時に生成し、新しいファイバーに異なるeffectTagsを設定し、コミット段階で異なる操作を実行します。

削除するファイバーを削除に入れます

ここでもう 1 つの特別な点があり、古いノードと新しいノードの種類が異なる場合は、古いノードを削除する必要がありますが、新しいノードを追加する場合は、古いノードの場所を見つける必要があります。新しく挿入されたノードに対応する子要素のシリアル番号を記録するために必要です。

/**
 * 对比新老节点,并为节点创建fiber结构
 * @param {*} wipFiber 当前节点
 * @param {*} elements 当前节点的子节点
 * 遍历当前节点时,设置其子元素和子元素兄弟元素的alternate,当遍历到子元素和兄弟元素时,刚好可以访问到alternate
 * 遍历同时,先设置alternate,后面再对比新老节点
 */
function reconcileChildren(wipFiber, elements){

    let index = 0;
    let prevSibling = null;

    // 获取老节点
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child

    while(index < elements.length || oldFiber != null){ // 子元素存在 或者 当前节点的旧节点存在
        const element = elements[index]
        let sameType = element && oldFiber && element.type === oldFiber.type
        let newFiber
        console.error('sameType', element.type, sameType)
        if(sameType){ //节点类型相同,比如均为div
            newFiber = {
                return: wipFiber,
                props: element.props,
                type: element.type,
                dom: oldFiber.dom,
                alternate: oldFiber, //这里将和当前节点对应的旧的fiber赋值记录下来,当比对当前节点子节点时,用来获得对应的旧的节点对象
                effectTag: 'UPDATE'
            }
        }
        
        if(element && !sameType){// 节点类型不同,比如新节点是span, 老节点是div
            newFiber = {
                return: wipFiber,
                props: element.props,
                type: element.type,
                dom: null, //重新创建dom
                alternate: null,
                effectTag: 'ADD',
                childIndex: index,// 用来记录当前元素是第几个子元素,在新节点是新类型时,根据index找到dom插入位置
                siblingNum: elements.length
            }
        }

        if(oldFiber && !sameType){ 
            newFiber.effectTag = "PLACEMENT" // 新老节点都存在时,类型不同,直接替换为新节点,删除老节点
            oldFiber.effectTag="DELETION";
            deletions.push(oldFiber) // 收集要删除的节点
        }

        // 每次while循环结束后, oldFiber指向其兄弟节点
        if(oldFiber){
            oldFiber = oldFiber.sibling
        }

        if(index === 0){
            wipFiber.child = newFiber
        } else {
            prevSibling.sibling = newFiber
        }
        prevSibling = newFiber 

        index++
    }
}

4. CommitRoot 変換、送信前に不要な古いノードを削除、送信後に currentRoot をキャッシュする

/**
 * 转化为双向链表结构后,开始提交和渲染真实dom
 * 提交整个fiber树(双向链表)
 */
function commitRoot(){
    //处理要删除的节点
    deletions.forEach(commitWork)
    commitWork(wipRoot.child)
    currentRoot = wipRoot // 缓存上次提交的fiber树
    wipRoot = null
}

/**
 * 节点真实dom提交到父级dom中
 */
function commitWork(fiber){
    if(!fiber){
        return
    }
    
    const parentDom = fiber.return.dom; 
    if(fiber.effectTag === 'UPDATE' && fiber.dom != null){
        updateDom(fiber.dom, fiber.alternate.props, fiber.props) //更新节点属性
    } else if(fiber.effectTag === 'DELETION'){
        domParent.removeChild(fiber.dom)
    } else if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null){
        if(fiber.childIndex < fiber.siblingNum){ // 中间元素
            parentDom.insertBefore(fiber.dom, fiber.sibling.dom)
        } else {
            parentDom.appendChild(fiber.dom)
        }
    } else if(fiber.effectTag === 'ADD' && fiber.dom != null){
        parentDom.appendChild(fiber.dom)
    }
    
    
    // 处理子节点和兄弟节点
    commitWork(fiber.child)
    commitWork(fiber.sibling)

}

5. updateDom は dom の属性を更新します この型では dom を再利用しているので、props の比較だけで済みます 簡単な実装は以下の通りで、createDom も updateDom を使用します

function createDom(fiber){
  let dom = fiber.type === 'TEXT' ? document.createTextNode('') : document.createElement(fiber.type)
  // 添加属性,文本节点除外
  updateDom(dom, {}, fiber.props)
  return dom
}
// 前缀是 on 返回是 true
const isEvent = (key) => key.startsWith("on");
// 去掉 children 和 on 开头的
const isProperty = (key) => key !== "children" && !isEvent(key);
// 前一次 和 本次 不同
const isNew = (prev, next) => (key) => prev[key] !== next[key];
// 过滤 匹配 下一次 中没有的值
const isGone = (prev, next) => (key) => !(key in next);

function updateDom(dom, prevProps, nextProps){
    // 清空 旧 事件
    Object.keys(prevProps)
    .filter(isEvent)
    .filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key))
    .forEach((name) => {
    const eventType = name.toLowerCase().substring(2);
        dom.removeEventListener(eventType, prevProps[name]);
    });

    // 清空 旧 的值
    Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach((name) => {
        dom[name] = "";
    });

    // 设置 新 事件
    Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach((name) => {
        const eventType = name.toLowerCase().substring(2);
        dom.addEventListener(eventType, nextProps[name]);
    });

    // 设置 新 的值
    Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach((name) => {
        if(dom instanceof Object &&  dom.setAttribute){
            // 设置样式
            if(name === 'style'){
                const value = nextProps[name]
                // console.log('style value', value)
                if(value && typeof value === 'object'){
                    for(let i in value){
                        dom.style[i] = value[i]
                    }
                } else {
                    dom.removeAttribute(name) // 样式对象不存在或者样式格式不对
                }
            } else if(name === 'nodeValue'){
                dom.nodeValue=nextProps[name]
            } else {
                dom.setAttribute(name, nextProps[name]);
            }
            
        }else{
            // console.log('设置样式', name)
            dom[name] = nextProps[name];
        }
    });

}

上記は Fiber の差分アルゴリズムを実装しています

概要: 新しいファイバー ノードを作成するとき、属性は古いファイバーを指します。新しいファイバー ツリーと古いファイバー ツリーを比較するとき、それらは同じレベルおよび同じ場所で比較されるため、計算効率が向上します。状況は 3 つあります。アルゴリズムの比較:

1. 新しいノードを追加します

2. 新しいノードに置き換え、古いノードを削除します

3. ノードのプロパティを変更する

4 番目、useState の実装

1. useStateは主に機能コンポーネントで使用されるため、useStateを実装する前に、ノードがコンポーネントであるシーンと互換性がある必要があります

分析します:

コンポーネントが babel によってエスケープされた後、取得される型 type は関数であるため、関数内の dom を取得するには、最初に関数を実行し、次の PerformUnitOfWork の変換を実行する必要があります。

function performUnitOfWork(fiber){
    let elements;
  // 判断fiber是不是组件
  if(typeof fiber.type === 'function'){
    // 判断是否是类组件
    if(fiber.type.prototype.render){
        elements = [fiber.type.prototype.render(fiber.props)] //兼容类组件
    } else {
        elements = [fiber.type(fiber.props)]
    }
  } else {
    if(!fiber.dom){ //如果当前节点上没有真实节点,创建fiber对应的真实节点
        fiber.dom = createDom(fiber)
    }
    elements = fiber.props.children
  }

  // 加入diff算法,对比新老节点
  reconcileChildren(fiber, elements) //说明 在遍历当前节点时,会遍历其所有子节点,并给子节点和子节点的兄弟节点设置alternate, 设置过之后,在下次递归中,刚好可以对比新老节点
  if(fiber.child){
    return fiber.child
  }

  let nextFiber = fiber //缓存当前fiber
  while(nextFiber){
    if(nextFiber.sibling){  //返回当前节点的兄弟节点
        return nextFiber.sibling
    }
    nextFiber = nextFiber.return
  }

}

2. コンポーネント自体は dom 構造を持たないため、dom を更新する際に、ノードの親がコンポーネントであるかどうかを判断し、コンポーネントである場合は上方向に検索を続ける必要があります。

不要な dom 要素を削除する場合、現在のファイバー ノードがコンポーネントであるかどうかを判断する必要があり、コンポーネントである場合は、その内部の実 dom を削除する必要があります。

commitRoot、updateDomは次の変更を加えます

function commitRoot(){
    //处理要删除的节点
    deletions.forEach(commitWork)
    commitWork(wipRoot.child)
    currentRoot = wipRoot // 缓存上次提交的fiber树
    wipRoot = null
}

function commitWork(fiber){
    if(!fiber){
        return
    }
    const parentDom = findParentDom(fiber) //函数式组件的dom为null, 这里要再向上查找,直到找到存在dom的父级

    if(fiber.effectTag === 'UPDATE' && fiber.dom != null){
        updateDom(fiber.dom, fiber.alternate.props, fiber.props) //更新节点属性
    } else if(fiber.effectTag === 'DELETION'){
        commitDeletion(fiber, parentDom) // 存在组件后,fiber.dom有可能为null
    } else if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null){
        if(fiber.childIndex < fiber.siblingNum){ // 中间元素
            parentDom.insertBefore(fiber.dom, fiber.sibling.dom)
        } else {
            parentDom.appendChild(fiber.dom)
        }
    } else if(fiber.effectTag === 'ADD' && fiber.dom != null){
        parentDom.appendChild(fiber.dom)
    }
    
    
    // 处理子节点和兄弟节点
    commitWork(fiber.child)
    commitWork(fiber.sibling)

}

function commitDeletion(fiber, domParent){
    if(fiber.dom){
      domParent.removeChild(fiber.dom)
    }else{
      commitDeletion(fiber.child, domParent) // 移除真实dom
    }
}

function findParentDom(fiber){
   if(fiber.return.dom){
        return fiber.return.dom
   } else {
        return findParentDom(fiber.return)
   }
}

3、useStateの実装

分析します:

機能コンポーネントでは実行のたびにuseStateが呼び出されるため、useStateを呼び出す際に既に状態が存在するかどうかを判断する必要があるため、各コンポーネントには状態を格納する変数が必要です

useState は複数の状態を生成するために複数回呼び出すことができ、状態を格納する変数は複数の状態を格納する配列キューと見なすことができます。

状態変数フック = [] を設定します。フックが 1 つだけになるようにするため、対応するコンポーネント ファイバー ノードにバインドします。

実装は次のとおりです

useState を呼び出すときは、現在のコンポーネント ファイバー ノードにアクセスする必要があります。performUnitOfWork が現在のノードを実行するときに、グローバル変数 wipFiber を設定し、現在のファイバーを wipFiber に割り当て、wipFiber.hooks=[] を設定します。

状態インデックスに対応するグローバルhookIndexを定義します。初期値は0で、useStateが呼び出されるたびにhookIndex+1となります。

PerformUnitOfWork がノードを実行するときに、hookIndex を 0 にリセットします。

let wipFiber = null;//本次操作的节点,在组件中赋值
let hookIndex = 0; //状态索引初始值

function performUnitOfWork(fiber){
    let elements;
  // 判断fiber是不是组件
  if(typeof fiber.type === 'function'){
      wipFiber = fiber
      hookIndex = 0 // 当前状态对应的是哪个hooks,状态索引
      wipFiber.hooks = [] //在组件上定义一个队列,用于存储状态,一个组件会有多个状态
    // 判断是否是类组件
    if(fiber.type.prototype.render){
        elements = [fiber.type.prototype.render(fiber.props)]
    } else {
        elements = [fiber.type(fiber.props)]
    }
  } else {
    if(!fiber.dom){ //如果当前节点上没有真实节点,创建fiber对应的真实节点
        fiber.dom = createDom(fiber)
    }
    elements = fiber.props.children
  }
  reconcileChildren(fiber, elements) //说明 在遍历当前节点时,会遍历其所有子节点,并给子节点和子节点的兄弟节点设置alternate, 设置过之后,在下次递归中,刚好可以对比新老节点
  if(fiber.child){
    return fiber.child
  }

  let nextFiber = fiber //缓存当前fiber
  while(nextFiber){
    if(nextFiber.sibling){  //返回当前节点的兄弟节点
        return nextFiber.sibling
    }
    nextFiber = nextFiber.return
  }

}

useStateの簡易バージョンを実装する

function useState(initial){
    // 获取老的状态对象
    const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]
    //当前状态值创建
    const hook = {
        state: oldHook ? oldHook : initial,
    }

    wipFiber.hooks.push(hook)
    hookIndex ++;

    return [hook.state]
}

4. useStateに変更状態関数を追加

useState が呼び出されると、setState 関数を返して、対応する状態を変更します。

setStateは単一の状態に対応するため、状態ごとにイベントキューキューを設定し、動作状態の関数を格納できます

setState が実行された後、操作を対応するキューに入れ、workLoop を開始してコンポーネントを再レンダリングします。

コンポーネントが再レンダリングされると、再度 useState が呼び出されますが、このとき、ステートに戻る前に、ステートに対応するキュー関数が最初に呼び出され、変更された関数が返されます。

function useState(initial){
    // 获取老的状态对象
    const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]
    //当前状态值创建
    const hook = {
        state: oldHook ? oldHook : initial,
        queue:[] //用来存储操作状态的动作
    }

    const actions = oldHook ? oldHook.queue : []
    // 状态是在下一次渲染时,更新上次修改的状态
    actions.forEach(action => {
        if( typeof action === 'function'){
            hook.state = action(hook.state)
        } else {
            hook.state = action
        }
        
    })
    
    // 定义一个收集action的函数,setState
    const setState = action => {
        hook.queue.push(action)
        
        // 触发更新
        wipRoot = {
            dom: currentRoot.dom,
            props: currentRoot.props,
            alternate: currentRoot,
        }

        nextUnitOfWork = wipRoot
        deletions = []
        requestIdleCallback(wookLoop)

    }

    wipFiber.hooks.push(hook)
    hookIndex ++;

    return [hook.state, setState]
}

例証します:

setState は実行直後に状態を変更するのではなく、操作を最初に保存し、次の更新の前に状態を変更します。

コンポーネント上の状態の保存はキュー形式になっており、その都度hookIndexに従って取得するため、判定条件には使用できません

上記は useState を実装します

五、例

function App(props){
    const [count, setCount] = useState(0)
    return <div className = "test-com" onClick={() => {console.log("test")}}>
        <div>测试</div>
        {
            count == 0 ? <div>当前count是0</div> : <div style={
   
   {color:'red'}}>当前大于0,count值为{count}</div>
        }
        <button onClick={() => {setCount(count+1)}}>点击增加</button>
        <Page></Page> // 组件嵌套
    </div>
}

function Page(){
    return <div>页面demo</div>
}

ReactDOM.render(<App></App>, container)

要約:

1. ファイバー アーキテクチャは、vDom ツリーをトラバースしながらファイバー ノードを作成し、ブラウザを使用して再帰的実行権限を制御します。ファイバー diff アルゴリズムが新しいファイバーを生成するとき、古いノードを指すように代替属性を各ファイバー ノードに追加します。比較すると同じレベルと同じ位置が得られます。

2. ファイバー アーキテクチャは、レンダリングを 2 つの段階に分割します。ラベル付きファイバー ツリーの生成 (再結合)、ファイバー ツリーの送信とレンダリング (コミット) です。

3. useState は関数型プログラミングを採用し、状態と操作を保存するために関数の副作用を導入します。

さらに詳しい内容については、公式アカウント「フロントエンド 361」をご覧ください。

おすすめ

転載: blog.csdn.net/qdmoment/article/details/124863776