51Testing软件测试论坛

标题: 从一个单元测试出发,梳理vue3的渲染过程 [打印本页]

作者: lsekfe    时间: 2020-10-13 10:04
标题: 从一个单元测试出发,梳理vue3的渲染过程
先来看一个单测。

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

  2.     transformVNodeArgs((args, instance) => {

  3.         if (instance) {

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

  5.         } else {

  6.           return args

  7.         }

  8.     })

  9.     const App = {

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

  11.         name: 'Root Component',

  12.         render() {

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

  14.         }

  15.     }

  16.     const root = nodeOps.createElement('div')

  17.     createApp(App).mount(root)

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

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

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

  3.       {

  4.           injectNativeTagCheck(app);

  5.       }

  6.       const { mount } = app;

  7.       app.mount = (containerOrSelector) => {

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

  9.       };

  10.       return app;

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


  1. function ensureRenderer() {  

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

  3. }
复制代码
即 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)


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

  2.     return function createApp(rootComponent, rootProps = null) {   

  3.       //...

  4.       //app实例上下文context对象

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

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

  7.       const context = createAppContext()   

  8.       const installedPlugins = new Set()   

  9.       let isMounted = false

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

  11.          _component: rootComponent as Component,

  12.          _props: rootProps,

  13.          _container: null,

  14.          _context: context,

  15.          version,

  16.          get config() {

  17.              return context.config

  18.          },      

  19.          set config(v) {        

  20.             if (__DEV__) {         

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

  22.             }

  23.          },

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

  25.         mixin(mixin: ComponentOptions) {},

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

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

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

  29.         unmount() {}

  30.         provide(key, value) {}

  31.     }   

  32.     return app

  33.   }

  34. }
复制代码
mount方法就是在第二步中的主要逻辑。

  1. function injectNativeTagCheck(app: App) {

  2.     // Inject `isNativeTag`

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

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

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

  6.         writable: false  

  7.     })

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


  1. const { mount } = app

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

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

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

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

  6.         component.template = container.innerHTML

  7.     }

  8.     // clear content before mounting

  9.     container.innerHTML = ''
  10.    

  11.     const proxy = mount(container)

  12.     container.removeAttribute('v-cloak')

  13.     return proxy

  14. }
复制代码
mount的过程:

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

  2.     if (!isMounted) {

  3.         const vnode = createVNode(rootComponent as Component, rootProps)

  4.         // store app context on the root VNode.

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

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

  7.         // HMR root reload

  8.         if (__DEV__) {

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

  10.                 render(cloneVNode(vnode), rootContainer)

  11.             }

  12.         }

  13.         if (isHydrate && hydrate) {

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

  15.         } else {

  16.             render(vnode, rootContainer)

  17.         }

  18.         

  19.         isMounted = true

  20.         app._container = rootContainer

  21.         

  22.         return vnode.component!.proxy

  23.    }

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


  1. const render: RootRenderFunction = (vnode, container) => {

  2.     if (vnode == null) {

  3.         if (container._vnode) {

  4.             unmount(container._vnode, null, null, true)   // 卸载

  5.         }

  6.     } else {

  7.         patch(container._vnode || null, vnode, container)

  8.     }

  9.     flushPostFlushCbs()    // check递归次数

  10.     container._vnode = vnode

  11. }
复制代码
patch(null, vnode, container)打补丁。

  1. const patch: PatchFn = (

  2.     n1,

  3.     n2,

  4.     container,

  5.     anchor = null,

  6.     parentComponent = null,

  7.     parentSuspense = null,

  8.     isSVG = false,

  9.     optimized = false  ) => {

  10.     // patching & not same type, unmount old tree

  11.     if (n1 && !isSameVNodeType(n1, n2)) {

  12.         anchor = getNextHostNode(n1)

  13.         unmount(n1, parentComponent, parentSuspense, true)

  14.         n1 = null

  15.     }

  16.     if (n2.patchFlag === PatchFlags.BAIL) {

  17.         optimized = false

  18.         n2.dynamicChildren = null

  19.     }

  20.     const { type, ref, shapeFlag } = n2

  21.     switch (type) {

  22.         case Text:

  23.             processText(n1, n2, container, anchor)

  24.             break

  25.        case Comment:

  26.             processCommentNode(n1, n2, container, anchor)

  27.             break

  28.        case Static:

  29.             if (n1 == null) {

  30.                 mountStaticNode(n2, container, anchor, isSVG)

  31.             } else if (__DEV__) {

  32.                 patchStaticNode(n1, n2, container, isSVG)

  33.             }

  34.             break

  35.        case Fragment:

  36.             processFragment(/*参数还是那些参数*/)

  37.             break

  38.        default:

  39.             if (shapeFlag & ShapeFlags.ELEMENT) {

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

  41.             } else if (shapeFlag & ShapeFlags.COMPONENT) {

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

  43.             } else if (shapeFlag & ShapeFlags.TELEPORT) {

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

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

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

  47.            } else if (__DEV__) {

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

  49.            }

  50.       }

  51.       // set ref

  52.       if (ref != null && parentComponent) {

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

  54.       }

  55. }
复制代码
在这个[url=]单元测试[/url]的情况下,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软件测试论坛 (http://bbs.51testing.com/) Powered by Discuz! X3.2