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

发表于:2020-10-10 09:41

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:灰原尛哀    来源:掘金

  先来看一个单测。
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函数可以看做类似扁平化,返回的是[normalized, needCastKeys],是对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,呈现视图。

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理

《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号