51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 3817|回复: 0
打印 上一主题 下一主题

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

[复制链接]
  • TA的每日心情
    无聊
    昨天 09:05
  • 签到天数: 1050 天

    连续签到: 1 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2020-10-13 10:04:58 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    先来看一个单测。

    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,呈现视图。

    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-11-22 09:24 , Processed in 0.062746 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表