lsekfe 发表于 2020-10-13 10:04:58

从一个单元测试出发,梳理vue3的渲染过程

先来看一个单测。

test('receive component instance as 2nd arg', () => {

    transformVNodeArgs((args, instance) => {

      if (instance) {

          return ['h1', null, instance.type.name]

      } else {

          return args

      }

    })

    const App = {

      // this will be the name of the component in the h1

      name: 'Root Component',

      render() {

          return h('p') // this will be overwritten by the transform

      }

    }

    const root = nodeOps.createElement('div')

    createApp(App).mount(root)

})我们先从最熟悉的createApp(App).mount(root)这句入手,分两步看起。第一步 createApp(App)创建App实例,第二步mount(root)挂载。
  1. createApp
  packages\runtime-dom\src\index.ts

const createApp = ((...args) => {

      const app = ensureRenderer().createApp(...args);

      {

          injectNativeTagCheck(app);

      }

      const { mount } = app;

      app.mount = (containerOrSelector) => {

          // 调用解构生成的mount方法...

      };

      return app;

});该方法返回app实例,并在其上定义mount方法,即第二步的mount方法。
  此app实例是由ensureRenderer返回render方法调用后的实例,并调用其上的createApp方法生成的。


function ensureRenderer() {

    return renderer || (renderer = createRenderer(rendererOptions))

}即 ensureRenderer --> createRenderer --> baseCreateRenderer -->
  其中,rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps) ,作为操作DOM的方法。baseCreateRenderer定义了一系列操作的闭包方法,供渲染使用(packages\runtime-core\src\renderer.ts)。
  接下来就是createAppAPI(render, hydrate)(packages\runtime-core\src\apiCreateApp.ts)


export function createAppAPI<HostElement>(render: RootRenderFunction,hydrate?: RootHydrateFunction): CreateAppFunction<HostElement> {

    return function createApp(rootComponent, rootProps = null) {   

      //...

      //app实例上下文context对象

      //config: { isNativeTag: NO, devtools: true, performance: false, globalProperties: {}, optionMergeStrategies: {}, isCustomElement: NO, warnHandler: undefined },   

      // mixins: [], components: {}, directives: {}, provides: Object.create(null) }   

      const context = createAppContext()   

      const installedPlugins = new Set()   

      let isMounted = false

      const app: App = {      nder: RootRenderFunction,hydrate?: RootHydrateFunction): CreateAppFunction<HostElement> {

         _component: rootComponent as Component,

         _props: rootProps,

         _container: null,

         _context: context,

         version,

         get config() {

             return context.config

         },      

         set config(v) {      

            if (__DEV__) {         

                warn( `app.config cannot be replaced. Modify individual options instead.`)

            }

         },

      use(plugin: Plugin, ...options: any[]) {},

      mixin(mixin: ComponentOptions) {},

      component(name: string, component?: PublicAPIComponent): any {},

      directive(name: string, directive?: Directive) {},

      mount(rootContainer: HostElement, isHydrate?: boolean): any {}

      unmount() {}

      provide(key, value) {}

    }   

    return app

}

}mount方法就是在第二步中的主要逻辑。

function injectNativeTagCheck(app: App) {

    // Inject `isNativeTag`

    // this is used for component name validation (dev only)

    Object.defineProperty(app.config, 'isNativeTag', {

      value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag),

      writable: false

    })

}注入验证组件name的isNativeTag。至此,第一步告一段落。
  2. mount
  挂载:


const { mount } = app

app.mount = (containerOrSelector: Element | string): any => {

    const container = normalizeContainer(containerOrSelector)// document.querySelector(container)或者container

    if (!container) return const component = app._component   // App根组件

    if (!isFunction(component) && !component.render && !component.template) {

      component.template = container.innerHTML

    }

    // clear content before mounting

    container.innerHTML = ''
   

    const proxy = mount(container)

    container.removeAttribute('v-cloak')

    return proxy

}mount的过程:

function mount(rootContainer: HostElement, isHydrate?: boolean): any {

    if (!isMounted) {

      const vnode = createVNode(rootComponent as Component, rootProps)

      // store app context on the root VNode.

      // this will be set on the root instance on initial mount.

      vnode.appContext = context   // createApp时创建的app上下文

      // HMR root reload

      if (__DEV__) {

            context.reload = () => {       // 什么时候触发???

                render(cloneVNode(vnode), rootContainer)

            }

      }

      if (isHydrate && hydrate) {

            hydrate(vnode as VNode<Node, Element>, rootContainer as any)

      } else {

            render(vnode, rootContainer)

      }

      

      isMounted = true

      app._container = rootContainer

      

      return vnode.component!.proxy

   }

}isMounted为false,基于根组件创建vnode,绑定上下文,执行render函数完成页面渲染,isMounted置为true,app实例的_container绑定根DOM元素root。
  接下来就是老太太裹脚布般的render过程:


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()    // check递归次数

    container._vnode = vnode

}patch(null, vnode, container)打补丁。

const patch: PatchFn = (

    n1,

    n2,

    container,

    anchor = null,

    parentComponent = null,

    parentSuspense = null,

    isSVG = false,

    optimized = false) => {

    // patching & not same type, unmount old tree

    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(/*参数还是那些参数*/)

            break

       default:

            if (shapeFlag & ShapeFlags.ELEMENT) {

                processElement(/*参数还是那些参数*/)

            } else if (shapeFlag & ShapeFlags.COMPONENT) {

                processComponent(/*参数还是那些参数*/)

            } else if (shapeFlag & ShapeFlags.TELEPORT) {

                ;(type as typeof TeleportImpl).process( /*参数还是那些参数,*/ internals )

         } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {         

                ;(type as typeof SuspenseImpl).process( /*参数还是那些参数,*/ internals )

         } else if (__DEV__) {

                warn('Invalid VNode type:', type, `(${typeof type})`)

         }

      }

      // set ref

      if (ref != null && parentComponent) {

      setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)

      }

}在这个单元测试的情况下,type是App对象,即Object类型。ref为undefined,shapeFlag为4,所以来到了processComponent方法。
  processComponent
  --> mountComponent
  --> instance = createComponentInstance(vnode, parent = null, suspense = null)
  --> instance.ctx = createRenderContext(instance)
  --> setupComponent(instance)
  --> initProps(instance, props = null, isStateful = 4, isSSR = false)
  --> initSlots(instance, children = null)
  --> setupStatefulComponent(instance, isSSR)
  -->setupRenderEffect( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized )
  normalizePropsOptions函数可以看做类似扁平化,返回的是,是对props、extends、mixins中的props做递归,浅拷贝得到的props和驼峰命名的key的集合。
  initProps初始化给instance实例的props和attrs,并对props最外层数据做响应式。
  initSlot初始化给instance实例的slots为响应的vnode
  setupStatefulComponent先对组件名、子组件名以及指令进行预判,给instance添加给accessCache、proxy属性,执行setup方法,然后给instance添加render函数(与vue2相同,去组件的render函数或者编译template生成),最后是一些兼容2.x的操作。
  最后是setupRenderEffect方法,根据instance.isMounted属性判断是首次渲染还是更新,执行patch --> processElement --> mountElement,呈现视图。

页: [1]
查看完整版本: 从一个单元测试出发,梳理vue3的渲染过程