Eu vou te dizer o princípio de renderização do React

Este artigo analisa a versão do código-fonte como 17.0.1. Embora Reacttenha sido iterado para a 18+versão agora, acredito que a maioria dos projetos não foi atualizada, então, pessoalmente, acho que aprender a versão antiga não está desatualizado. Portanto, o conteúdo subsequente, a menos que especificado de outra forma, é padronizado para o Syncmodo.

Um, pavimentação

imagem.png

Primeiro, lembre-se, se você não usa nenhum framework, como criar uma árvore dom com js nativo deve ser tratado. Para reduzir a operação dom, vamos primeiro criar o elemento mais baixo e armazená-lo na variável, depois criar seus elementos pai por sua vez, até que o div mais alto seja criado e, finalmente, inserir o div mais alto no dom, o que evita múltiplos vezes. dom inserção. Este é realmente o caso do React. Vamos dar uma olhada em nossos métodos de entrada comumente usados.

ReactDOM.render(<App />, document.getElementById("root"));
复制代码

No React 17 e anteriores, usamos os métodos acima para registrar os componentes do React nas visualizações. Como animais avançados, podemos ver rapidamente qual elemento é o elemento mais inferior (nó folha) e, em seguida, criar elementos pai camada por camada até o elemento superior (nó raiz). Mas para um programa, não há como saber qual é o nó folha no início, então ele só pode percorrer o nó raiz fornecido pela entrada até que o nó folha seja encontrado e o elemento correspondente seja criado. Portanto, para o framework React, existem dois processos nos quais procurar elementos filhos de cima para baixo e criar elementos DOM de baixo para cima, que correspondem a dois processos de travessia respectivamente beginWork, e dois processos completeWorkserão analisados ​​em detalhes posteriormente. . Antes de entrar nesses dois processos, vamos ver qual processamento é feito na função de renderização.

2. Preparações

Acho que antes de entrar beginWorkno completeWorkprocesso, faça um trabalho preparatório para abrir caminho para esses dois processos.

(Para conveniência da descrição, vamos chamar o componente de nível superior de React 根组件e o elemento dom que precisa ser suspenso é chamado 根元素de código acima div#root)

Se você deseja percorrer Appo componente, deve marcar um ponto de partida para facilitar a inserção do dom quando o nó raiz for criado posteriormente. Então, como marcar o ponto de partida? Alguns colegas disseram: App"É isso! Imagine, se o componente raiz não for nomeado Appmas Rootou outro nome, ele não funcionará se você apenas Appo souber. Então React adiciona um "componente" HostRootpara marcar o início do componente ( não um componente, apenas um valor especial, usado como uma tag ao HostRootcriar o componente raiz )Fiber

( FiberÉ um tipo de classe, cada nó componente será criado como um objeto de fibra posteriormente, a estrutura aproximada é a seguinte)

imagem.png

Grave o componente raiz HostRoote também grave o elemento raiz div#root, pois depois que o componente dom é criado, ele precisa ser inserido div#rootsob, não bodysob.

Ao mesmo tempo, o evento também está registrado aqui, por que registrar o evento? Para eventos dom, sabemos na era do jquery, não vincular eventos em cada elemento, tente vincular ao elemento pai na forma de vinculação de proxy. Isso mesmo, o React é o mesmo, antes da versão 17, todos os eventos eram vinculados ao documento, e após a versão 17, eles eram vinculados ao elemento raiz do mount, que está aqui div#root. onClickQuando escrevemos código, embora eventos como , onFocus, etc. sejam vinculados a componentes React, onScrolla execução de eventos é acionada na forma de proxies. O sistema de eventos do React também é muito interessante, vou desenhar um artigo separado para analisá-lo mais tarde. Aqui, você só precisa saber que o evento está registrado no componente raiz antes de iniciar a travessia.

O fluxo de chamada de função principal de todo o processo é o seguinte

imagem.png

在实例化ReactDomBlockingRoot时又创建了根组件对应的fiber对象即上面所说tag为HostRootFiber我们称为RootFiber, 同时为了维护RootFiberdiv#root的关系创建了一个对象叫FiberRoot。 此外对于div#rootFiberRootRootFiber三个对象上都有字段指向彼此,这样在不同的场景下,都能很容易根据一方找到另外两个。实例化的主要函数流程调用如下图,可以看到通过调用listenToAllSupportedEventsdiv#root上注册了事件

imagem.png

三者之间的关系如下

imagem.png

准备工作做完开从根组件向下遍历查找子组件了

自上而下、自下而上遍历执行

在没遍历执行beginWork之前,react也不知道后续的组件结构会是啥样,所以在beginWork时每遇到一个组件时都要记录下来,同时要记录父组件和子组件、组件与组件间的关系,这样才能保证后续创建出来的dom树不会错乱掉。react内部对于每个组件都会创建成Fiber对象,通过Fiber记录组件间的关系,最后构成一个Fiber链表结构。 父组件parentFiber.child指向第一个子组件对应的fiber,子组件的fiber.return指向父组件,同时子组件的fiber.sibling指向其右边的相邻兄弟节点的fiber, 构成一个fiber树。如下图

imagem.png 还需要说明的是, beginWork的遍历并不是先查找完某一层所有的子元素再进行下一层的查找, 而是只查父元素的第一个子元素, 然后继续查找下一层的子元素, 如果没有子元素才会查找兄弟元素,兄弟元素查找完再查找父元素的兄弟元素, 类似于二叉树的前序遍历。所以对于上图的结构, 遍历顺序如下: App->Comp1->Comp3->div1->div2->div3->div4->Comp2->div5

beginWork

beginWork主要的功能就是遍历查找子组件,建立关系树。 那么怎么查找子组件呢,我们只分析class组件和函数式组件。 对于函数式组件,会执行组件对应的函数,注册hooks,同时拿到函数return的结果,即为该组件的child;对于class组件,会先实例化class,在这个阶段也会调用class的静态方法getDerivedStateFromProps以及实例的componentWillMount方法最后执行render方法拿到对应的child。 在mount阶段和update阶段, beginWork的执行逻辑也有区别的。 我们都知道为了减少重排和重绘,react帮助我们找出那些有变化的节点,只做这些节点的更新。 在mount阶段,因为在这之前没有创建节点,所以每个节点的fiber都是新建的;在update阶段, 会通过diff算法判断当前节点是否需要变更,如果需要变更会重新创建新的fiber对象并复用部分老的fiber对象属性,如果不需要变更则直接clone老的fiber对象;如果diff对比后老的fiber存在,新的fiber不存在,则会给fiber打上Deletion标签标示该元素需要删除; 如果老的fiber不存在,新的fiber存在说明是新创建的元素,则给fiber打上Placement标签。 beginWork大概流程如下

imagem.png

completeWork

completeWork阶段主要执行dom节点的创建或者标记变更。在mount阶段时,对于自定义组件比如class组件、函数式组件,其实不做什么特殊处理; 对于divpspan(这种组件在react内部定义为HostComponent),就会调用document.createElement方法创建dom元素存放到该节点fiber对象的stateNode字段上;对于父元素是HostComponent的情况,先创建父元素的dom节点parentInstance, 然后调用parentInstance.appendChild(child)方法将子元素挂在该节点上。 在update阶段,如果老的fiber存在则不会重新创建dom元素,而是给该元素打上Update标签;如果是新的元素和mount阶段一样创建新的dom元素。 大概流程如下

imagem.png

此外在react内部, beginWorkcompleteWork是交替进行,这是为什么呢? 试想一下, 如果不交替运行,beginWork执行完之后只记录了关系, 然后再想通过completeWork创建dom元素,是不是又得从根组件开始遍历一遍,这样就至少需要遍历两遍。react通过合适的时机切换执行beginWorkcompleteWork只需遍历一遍就可以完成所有操作了。那么在什么时机切换呢?还记得我们一开始说,用原生js创建dom时先创建最底层的元素, react也是,在遍历执行beginWork到最底层元素时即下图的div1,该元素已经没有子元素了, 开始执行completeWork创建dom节点, 执行完div1completeWork又切换成执行div2beginWorkdiv2也没有子节点,所以进而执行div2completeWorkdiv3也同样先执行beginWork再执行completeWork, 和div1div2不同的是, div3已经没有右边的兄弟元素了, 转向执行父元素Comp3的completeWork, 然后再执行div4beginWork。所以beginWorkcompleteWork的执行顺序是动态切换的

imagem.png

beginWorkcompleteWork时, 分别维护了一个指针workInProgresscompleteWork指向当前正在执行的work的节点, 执行完当前节点指针执行下一个节点, 通过判断workInProgress是否为null进行beginWork => completeWork的切换, 通过判断fiber.sibling是否为null进行completeWork => beginWork的切换。

整个遍历流程的主要函数调用如下

imagem.png

经过beginWorkcompleteWork, 每个组件节点的dom元素都创建完成或是被打上了对应的标签。在mount阶段,根组件下已经挂载了所有子元素节点的dom, 那么只需要将根组件dom节点插入到div#app下即可;update阶段组件fiber都被打上了标记,哪个元素需要删除,哪个需要更新都在下个阶段这些;这些操作在commit流程中进行。

Commit阶段

上面说了对于dom元素挂在到根标签div#root上以及一些元素的删除、更新等都是在commit阶段进行。 此外我们声明的一些useLayoutEffect、useEffect等hooks,以及组件的生命周期也会在该阶段运行。 commit又分为3个阶段分别为commitBeforeMutationEffectscommitMutationEffectscommitLayoutEffects

1. commitBeforeMutationEffects

个人认为该阶段主要是为后面两个阶段做一些准备工作

对于不同组件,处理逻辑不同。 对于HostRoot根组件,在mount时会清除根节点div#root已有的子元素,为了插入App的dom做准备; 对于函数式组件,在这个阶段会通过react-scheduler以普通优先级调用useEffect但是不会立刻执行,可简单认为在这里加了一个延时器执行useEffect; 对于class组件会调用静态方法getSnapshotBeforeUpdate, 即组件被提交到dom之前的方法

2. commitMutationEffects

Neste estágio, diferentes lógicas são executadas principalmente de acordo com os rótulos correspondentes nos componentes; por exemplo , neste estágio, mounto nó dom correspondente ao componente App ficará pendurado div#rootnele e a página poderá ver os elementos correspondentes update; , o correspondente , Update, Deletion, etc. Placement_useLayoutEffect

3. ComprometerEfeitos de Layout

Como o elemento DOM do componente foi pendurado na página no estágio anterior, esse estágio é principalmente para executar a mountfunção de ciclo de vida do componente, como componentes de função useLayoutEffect, componentDidMount;

Após a execução dos três estágios acima, se não houver tarefa de maior prioridade (como chamar setState no ciclo de vida didMount), a função que está atrasada no primeiro estágio será chamada useEffect; se houver, ela entrará no updateestágio e reexecutar beginWork, completeWork, commit. De fato, pode-se constatar que ainda existe uma diferença entre o tempo de execução useEffecte componentDidMounto tempo de execução.

O processo de chamada de função principal de todo o commit é o seguinte

imagem.png

Desta forma, o processo de renderização e atualização de todo o react está basicamente terminado.

escreva no final

Este artigo é um artigo depois de ler o reactcódigo-fonte e adicionar uma compreensão pessoal da saída. Se houver um erro, aponte.

おすすめ

転載: juejin.im/post/7134230942901600263