Интерпретация исходного кода Vue3 (2)-mount

Vue.js

предисловие

Как упоминалось ранее в Vue3mountДелай внутри дела, где первый шагmountКак объяснялось ранее, эта статья начнется с функции монтирования файла apiCreateApp в папке runtime-core на втором этапе, а остальные шаги будут объяснены по очереди. Интерпретируйте код ядра в соответствии с основным процессом, скачок может быть относительно большим, но какой файл будет указан в процессе объяснения.

текст

Здесь начинается текст, здесь начинается крепление. Давайте сначала взглянем на исходный код. Исходный код упомянутой ранее части dom здесь упоминаться не будет. Начнем с основной части:

packages/runtime-core/src/apiCreateApp.ts

mount(rootContainer: HostElement, isHydrate?: boolean): any {
  if (!isMounted) {
    const vnode = createVNode(
      rootComponent as ConcreteComponent,
      rootProps
    )
    vnode.appContext = context

    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      render(vnode, rootContainer)
    }
    isMounted = true
    app._container = rootContainer
    ;(rootContainer as any).__vue_app__ = app
    
    return vnode.component!.proxy
  }
}

Исходный код здесь на самом деле довольно прост.

  • Вызовите createVNode, чтобы получить vnode, rootComponent — это данные конфигурации, передаваемые при вызове createApp(config), rootProps — это реквизиты root, которые будут проверены, как упоминалось ранее, обычно во время использования, rootProps равен нулю;
  • Сохраните контекст на узле-последователе;
  • Вызвать функцию рендеринга, здесь объясняется только рендеринг;
  • isMounted имеет значение true;
  • _container экземпляра сохраняется как текущий rootContainer;
  • rootContainer добавляет атрибут __vue_app__ и устанавливает его как текущий экземпляр приложения;
  • Возвращает прокси для vnode.component.

Основной код рендеринга — это функция рендеринга.

render

Роль функции рендеринга в Vue2 и Vue3 совершенно разная.

  • Функция рендеринга в Vue2 выполняет определенную работу и представляет собой настоящую операцию рендеринга.Возвращаемый результат — vnode, который можно просмотреть здесь.Интерпретация исходного кода Vue2 (семь) — крепление;
  • Функция рендеринга в Vue3 предназначена для распространения, что эквивалентно маршрутизатору, две строки, размонтировать и исправить, и никаких результатов не возвращается.

Взгляните на исходный код рендера:

packages/runtime-core/src/renderer.ts

const render: RootRenderFunction = (vnode, container) => {
    if (vnode == null) {
        if (container._vnode) {
            unmount(container._vnode, null, null, true)
        }
    } else {
        patch(container._vnode || null, vnode, container)
    }
    flushPostFlushCbs()
    container._vnode = vnode
}

Приведенный выше код является исходным кодом функции рендеринга:

  • Параметр 1: vnode — обновляемый vnode на странице, полученный с помощью createVNode выше, container — отображаемый контейнер;
  • Сначала оценивается vnode, если он пустой, а container._vnode имеет значение, то есть есть предыдущая отрисовка DOM, выполняется операция размонтирования;
  • Если vnode не пуст, выполните операцию исправления, dom diff и рендеринг;
  • Выполните функцию flushPostFlushCbs, вызовите планировщик и реализуйте его с помощью Promise. Отличие от Vue2 в том, что Vue2 обрабатывается макро-задачами или микро-задачами.
  • Сохраните _vnode контейнера в качестве текущего vnode, чтобы упростить операцию сравнения dom позже, которая аналогична Vue2.

Поскольку это рендеринг, vnode не будет пустым, и он обязательно перейдет в часть функции исправления.Давайте посмотрим на код в части исправления:

packages/runtime-core/src/renderer.ts

const patch: PatchFn = (
    n1, // old
    n2, // new
    container, // 容器
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
) => {
    // 如果type不相同,则把n1直接卸载掉
    if (n1 && !isSameVNodeType(n1, n2)) {
        anchor = getNextHostNode(n1)
        unmount(n1, parentComponent, parentSuspense, true)
        n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
        optimized = false
        n2.dynamicChildren = null
    }

    const {type, ref, shapeFlag} = n2
    switch (type) {
        case Text:
            processText(n1, n2, container, anchor)
            break
        case Comment:
            processCommentNode(n1, n2, container, anchor)
            break
        case Static:
            if (n1 == null) {
                mountStaticNode(n2, container, anchor, isSVG)
            } else if (__DEV__) {
                patchStaticNode(n1, n2, container, isSVG)
            }
            break
        case Fragment:
            processFragment(
                n1,
                n2,
                container,
                anchor,
                parentComponent,
                parentSuspense,
                isSVG,
                optimized
            )
            break
        default:
            if (shapeFlag & ShapeFlags.ELEMENT) {
                processElement(
                    n1,
                    n2,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    optimized
                )
            } else if (shapeFlag & ShapeFlags.COMPONENT) {
                processComponent(
                    n1,
                    n2,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    optimized
                )
            } else if (shapeFlag & ShapeFlags.TELEPORT) {
                ;(type as typeof TeleportImpl).process(
                    n1 as TeleportVNode,
                    n2 as TeleportVNode,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    optimized,
                    internals
                )
            } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
                ;(type as typeof SuspenseImpl).process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals)
            } else if (__DEV__) {
                warn('Invalid VNode type:', type, `(${typeof type})`)
            }
    }

    if (ref != null && parentComponent) {
        setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
    }
}

Функция патча от двери до двери была прочесана и проанализирована для входа, и был получен следующий рисунок, в котором есть несколько часто используемых линий:

  • processFragment: функция для обработки фрагментов (массивов dom);
  • processElement: функция, которая обрабатывает элемент;
  • ProcessComponent: Компоненты обработки;

Далее мы изучим пример, в котором будут задействованы processFragment и processElement, и выполним операцию diff diff;

пример рендеринга

Теперь мы рассмотрим пример от начала до конца и шаг за шагом пройдемся по используемым функциям от начала до конца. Предположим, теперь у нас есть список:

packages/vue/examples/classic/hello.js

const app = Vue.createApp({
    data() {
        return {
            list: ['a', 'b', 'c', 'd']
        }
    }
});
app.mount('#demo')

packages/vue/examples/classic/hello.html

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport"  content="initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,
    user-scalable=no,target-densitydpi=medium-dpi,viewport-fit=cover"/>
    <title>Vue3.js hello example</title>
    <script src="../../dist/vue.global.js"></script>
</head>
<body>
<div id="demo">
    <ul>
        <li v-for="item in list" :key="item">
            {{item}}
        </li>
    </ul>
</div>
<script src="./hello.js"></script>
</body>
</html>

Мы создаем два новых файла, hello.js и hello.html, в корневом каталоге исходного кода Vue3, в соответствующем каталоге, копируем приведенный выше код в объектный файл, а затем запускаем его в корневом каталоге.npm run dev, ну вот сейчас проект запущен. Затем откройте браузер и введите URL-адрес: файл:///Users/draven/mywork/vue-3.0.0/packages/vue/examples/classic/hello.html ; вы можете увидеть рендеринг страницы:

На данный момент это успешно. Далее мы изучим, как работает эффект на странице, начиная с функции рендеринга выше.

  • 1. Начать работу: позвонитьrender(vnode, rootContainer), функция работает вpackages/runtime-core/src/apiCreateApp.ts, объявление функции рендеринга находится вpackages/runtime-core/src/renderer.ts;Параметр vnode генерируется вызовом createVNode выше, а параметр rootContainer — это элемент с идентификатором demo, переданным выше;
  • 2. Следующая записьpackages/runtime-core/src/renderer.tsфайл, большинство следующих функций находятся в этом файле, если есть особые обстоятельства, это будет объяснено.
  • 3. Следующий запуск: вызов внутри функции рендерингаpatch(container._vnode || null, vnode, container)
    • 3.1 Первый параметр — это старый vnode, потому что это первая визуализация, старый vnode не существует, поэтому он равен нулю; второй параметр — прозрачно переданный vnode; третий параметр — прозрачно переданный контейнер (#demo ) ;
    • 3.2 Функция patch принимает и другие параметры, но мы пока не можем ее использовать:patch(n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false);n1 имеет значение null, n2 – обновляемый vnode, а контейнер – прозрачный transfer#demo;
    • 3.3 В это время n1 равно null, а n2 все еще является объектом:

Суждение в это время будет соответствоватьshapeFlag & ShapeFlags.COMPONENT, идти кprocessComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)функция; в настоящее время значение параметра: n1 равно null, n2 показано на рисунке, а контейнер — #demo;

  • 4. Функция processComponent будет судить n1.Если n1 не равно нулю, это оказывается операцией обновления и вызовом updateComponent, в это время мы делаем рендеринг в первый раз, поэтому мы не будем проходить операцию обновления и пройдем другая логика; если это тип компонента keepAlive, используйте логику активации; на данный момент мы не за компонент keepalive, поэтому используйте функцию mountComponent,mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized), параметр n2 — это изображение выше, контейнер — #demo, а другие параметры по-прежнему имеют значение null (false) по умолчанию;
  • 5. Функция mountComponent сначала вызовет createComponentInstance для создания экземпляра текущего n2, затем вызовет setupComponent для инициализации свойств и слотов и т. д. и, наконец, вызоветsetupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ), экземпляр параметра — это экземпляр, сгенерированный выше, initialVNode по-прежнему имеет значение n2 на приведенном выше рисунке, контейнер — это #demo, а остальные — значения по умолчанию;
  • 6. Функция setupRenderEffect — очень важная функция. Эта функция монтирует метод обновления для текущего экземпляра. Метод обновления генерируется эффектом. Эффект эффекта в Vue3 эквивалентен наблюдаемому в Vue2; после обновления генерируется, перед монтированием сначала будет запущен сгенерированный метод эффекта, и, наконец, текущий метод эффекта будет возвращен для обновления; запуск функции эффекта эквивалентенЗвонки Watcher попадают в Vue2Process.effect принимает два параметра, первый параметр — это функция componentEffect, которая должна отслеживать изменения и вызывать эту функцию, как упоминалось выше, сначала запустите сгенерированный метод эффекта, а функция componentEffect будет вызываться внутри сгенерированного метода эффекта;
  • 7. В функции componentEffect есть две логики, чтобы определить, отрендерился ли он: instance.isMounted, если отрендерился, то переходим к логике обновления, если еще не отрендерили, переходим к неотрендеренному, возьмем посмотрите на исходный код этой части.
    function componentEffect() {
      if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const {el, props} = initialVNode
        const {bm, m, parent} = instance
    
        // beforeMount hook
        if (bm) {
            invokeArrayFns(bm)
        }
        // onVnodeBeforeMount
        if ((vnodeHook = props && props.onVnodeBeforeMount)) {
            invokeVNodeHook(vnodeHook, parent, initialVNode)
        }
        const subTree = (instance.subTree = renderComponentRoot(instance))
    
        if (el && hydrateNode) {
            hydrateNode(
                initialVNode.el as Node,
                subTree,
                instance,
                parentSuspense
            )
        } else {
            patch(
                null,
                subTree,
                container,
                anchor,
                instance,
                parentSuspense,
                isSVG
            )
            initialVNode.el = subTree.el
        }
        if (m) {
            queuePostRenderEffect(m, parentSuspense)
        }
        if ((vnodeHook = props && props.onVnodeMounted)) {
            queuePostRenderEffect(() => {
                invokeVNodeHook(vnodeHook!, parent, initialVNode)
            }, parentSuspense)
        }
        const {a} = instance
        if (
            a &&
            initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
        ) {
            queuePostRenderEffect(a, parentSuspense)
        }
        instance.isMounted = true
      } else {
        // no first render
      }
    }
    

Выше показан первый рендеринг после завершения.componentEffectисходный код функции;

    • 7.1 Сначала вызывается хук-функция beforeMount текущего экземпляра;
    • 7.2. Вызвать функцию ловушки BeforeMount родительского класса n2;
    • 7.3 Вызвать функцию renderComponentRoot для рендеринга корневого элемента компонента;
    • 7.4. Исправление вызова:patch(null, subTree, container, anchor, instance, parentSuspense, isSVG); В настоящее время значение поддерева:;container — #demo; привязка — null, instance — текущий экземпляр, parentSuspense — null, isSVG — false;
    • 7.5. Вызвать встроенную функцию ловушки текущего экземпляра; вызвать встроенную функцию ловушки родительского класса n2; вызвать активированную функцию ловушки текущего экземпляра; вместо прямого вызоваВместо этого он вызывается путем помещения его в очередь через queuePostRenderEffect;
    • 7.6 Наконец, установите isMounted экземпляра в true;
  • 8. ВышеВызов patch в функции componentEffect — это начало официального рендеринга., большинство предыдущих эквивалентны сопоставлению данных:
    • 8.1. Передайте рабочие параметры функции componentEffect, указанной выше, функции patch:patch(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized); В настоящее время n1 имеет значение null, n2 — это изображение (поддерево) выше, контейнер все еще #demo, привязка имеет значение null, parentComponent — это экземпляр выше, parentSuspense — null, isSVG — false, optimized — false;
    • 8.2 Код выполняется последовательно Как видно из рисунка выше, тип поддерева экземпляра, полученного компонентом, — Fragment, и он попадет в функцию processFragment;
    • 8.3. Параметры, принимаемые processFragment, такие же, как и у функции patch, или приведенные выше значения, без изменений. Давайте посмотрим на исходный код:
        const processFragment = (
            n1: VNode | null,
            n2: VNode,
            container: RendererElement,
            anchor: RendererNode | null,
            parentComponent: ComponentInternalInstance | null,
            parentSuspense: SuspenseBoundary | null,
            isSVG: boolean,
            optimized: boolean
        ) => {
            const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
            const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
            let {patchFlag, dynamicChildren} = n2
            if (patchFlag > 0) {
                optimized = true
            }
            if (n1 == null) {
                hostInsert(fragmentStartAnchor, container, anchor)
                hostInsert(fragmentEndAnchor, container, anchor)
                mountChildren(
                    n2.children as VNodeArrayChildren,
                    container,
                    fragmentEndAnchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    optimized
                )
            } else {
            	 // 其他逻辑
            }
        }
      
      По параметрам можно узнать, что будет достигнута текущая логика if, и скелет будет вставлен первым, затем выполнитьmountChildren, n2.children может узнать через поддерево выше, что значение является массивом, и в массиве есть 1 элемент, который является ul, который мы хотим отобразить;
      const mountChildren: MountChildrenFn = (
          children,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized,
          start = 0
      ) => {
          for (let i = start; i < children.length; i++) {
              const child = (children[i] = optimized
                  ? cloneIfMounted(children[i] as VNode)
                  : normalizeVNode(children[i]))
              patch(
                  null,
                  child,
                  container,
                  anchor,
                  parentComponent,
                  parentSuspense,
                  isSVG,
                  optimized
              )
          }
      }
      
      Вы можете видеть, что n2.children будет пройден, а n2.children имеет только один элемент, который является ul
    • 8.4 Используя приведенные выше параметры среды выполнения, вызовитеpatch(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG = false, optimized);параметры: n1 равно null; дочерний элемент – указанный выше ul; container – #demo, привязка – fragmentEndAnchor в приведенной выше функции processFragment; parentComponent – экземпляр экземпляра; parentSuspense – null; isSVG – false; optimized – истина, потому что в приведенном выше в нем были внесены изменения processFragment;
    • 8.5.Из приведенных выше параметров видно, что тип ul - ul.В это время он пойдет в функцию processElement.Параметры функции processElement такие же, как и у функции patch.Осуществляется прозрачная передача , Посмотрите на исходный код:
      const processElement = (
          n1: VNode | null,
          n2: VNode,
          container: RendererElement,
          anchor: RendererNode | null,
          parentComponent: ComponentInternalInstance | null,
          parentSuspense: SuspenseBoundary | null,
          isSVG: boolean,
          optimized: boolean
      ) => {
          isSVG = isSVG || (n2.type as string) === 'svg'
          if (n1 == null) {
              mountElement(
                  n2,
                  container,
                  anchor,
                  parentComponent,
                  parentSuspense,
                  isSVG,
                  optimized
              )
          } else {
              patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
          }
      }
      
      По параметру n1 равно null, можно знать, что он пойдет по логике mountElement, и параметр не изменится. В процессе выполнения mountElement будут обнаружены дочерние элементы ul, и при наличии значения под дочерними элементами ul будет вызвана функция mountChildren:
      mountChildren(
          vnode.children as VNodeArrayChildren,
          el,
          null,
          parentComponent,
          parentSuspense,
          isSVG && type !== 'foreignObject',
          optimized || !!vnode.dynamicChildren
      )
      
      На данный момент vnode.children — это массив, состоящий из 4 li, el — это ul, якорь — null, parentComponent — экземпляр экземпляра, parentSuspense — null, isSVG — false, optimized — true, повторите вышеописанную функцию mountChildren, а затем вернитесь к ней.
    • 8.6 Когда цикл for выполняется в функции mountChildren, тип li — li, он продолжит обрабатывать элемент, повторит вышеуказанные шаги и последовательно завершит выполнение;
  • 9. Все вышеперечисленные шаги выполнены, и теперь данные отображаются на странице.
  • 10. В это время в основном все делается, что равносильно бездействию основной очереди, вызовflushPostFlushCbs()Начать выполнение функции в очереди;
  • 11. Наконец, укажите атрибут _vnode контейнера на текущий vnode, это удобно для dom diff в следующий раз.
  • 12. Первый запуск рендеринга завершен.

Эпилог

Эта глава посвящена процессу рендеринга первого рендера, а следующая глава будет объединена в соответствии с ритмом этой главы.Интерпретация исходного кода Vue2 (семь)Раздел dom diff в Vue3 объясняет часть патча в Vue3 и медленно переваривает их все.