从一个单元测试出发,梳理Vue3的渲染过程-数据与编程-热点资讯-野望文存-科技 
    欢迎来到野望文存-科技!
当前位置:野望文存-科技 > 热点资讯 > 数据与编程 >  从一个单元测试出发,梳理Vue3的渲染过程

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

发表时间:2021-01-11 17:31:00  来源:野望文存  浏览:次   【】【】【


先来看一个单测:

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)挂载。


createApp


packages untime-domsrcindex.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 -->。

return {        render,        hydrate,        createApp: createAppAPI(render, hydrate)  }

(左右滑动查看完整原文


其中,rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps) ,作为操作DOM的方法。baseCreateRenderer定义了一系列操作的闭包方法,供渲染使用(packages untime-coresrc enderer.ts)。


接下来就是createAppAPI(render, hydrate)(packages untime-coresrcapiCreateApp.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。至此,第一步告一段落。


mount


挂载:

const { mount } = appapp.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,呈现视图。


链接:https://juejin.cn/post/6880309692367634439

本文为51Testing经授权转载,转载文章所包含的文字来源于作者。如因内容或版权等问题,请联系51Testing进行删除

推荐阅读

点击阅读?

点击阅读?

点击阅读?

点击阅读?

点击阅读?

“阅读原文”一起来充电吧!

责任编辑:蔡学森