最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • vue3源码浅析:mount

    正文概述 掘金(单线程_01)   2021-02-02   524

    vue3源码浅析:createApp

    vue3源码浅析:mount

    一个简单的例子

    mount方法,将应用实例的根组件挂载在提供的 DOM 元素上。数据响应式;vnode获取,vnode diff,最后渲染成DOM,这些方法都是在mount执行中完成的。

    还是从一个简单的例子中看看其执行过程

    <div class="" id="app">
        <button @click='add'>
            <h3>{{num}}</h3>
        </button>
    </div>
    <script>
        const {
            createApp,ref
        } = Vue
        const app = createApp({
            setup(){
                let num = ref(0);
                const add = ()=>{
                    num.value++;
                }
                return{
                    num,add
                }
            }
        }).mount('#app')
    </script>
    

    mount的执行过程

    以下为本例中app.mount()的执行

    vue3源码浅析:mount

    下面针对每一个函数的执行过程做一个初步解析

    app.mount

    app.mount方法是在createApp中对mount方法的扩展,会将根元素传递给内部的mount方法

    执行过程如下:

    1. normalizeContainer通过document.querySelector获得DOM元素

      在本例中参数containerOrSelector为#app,得到的container为div#app及其子元素

    2. 得到createApp的配置对象,保存在component

      在本例中app._component为setup函数

    3. 判断配置对象component,得到template元素,并保存到component

      在本例中component.template为div#app的子元素

    4. 将根元素传递给内部的mount方法执行

    //**  \packages\runtime-dom\src\index.ts  */
    
      app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
        const container = normalizeContainer(containerOrSelector)
        if (!container) return
        const component = app._component
        if (!isFunction(component) && !component.render && !component.template) {
          component.template = container.innerHTML
        }
        // clear content before mounting
        container.innerHTML = ''
        const proxy = mount(container)
        if (container instanceof Element) {
          container.removeAttribute('v-cloak')
          container.setAttribute('data-v-app', '')
        }
        return proxy
      }
    

    mount

    mount方法是app实例的应用方法,就执行过程如下:(SSR暂不做讨论)

    1. 调用createVNode获得vnode,参数rootComponent为调用createApp(config)的时候传递进来的config对象和template

      本例中createVNode参数具体值如下

      rootComponentrootProps
      vue3源码浅析:mountnull
    2. context则是通过createAppContext()获得的,将context保存在根节点上

      context
      vue3源码浅析:mount
    3. 执行render将vnode转变成DOM元素并挂载到根元素上

      本例中render中参数如下

      vnoderootContainer
      vue3源码浅析:mountdiv#app
    //** \packages\runtime-core\src\apiCreateApp.ts */
      const context = createAppContext()
      mount(rootContainer: HostElement, isHydrate?: boolean): any {
        if (!isMounted) {
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          // store app context on the root VNode.
          // this will be set on the root instance on initial mount.
          vnode.appContext = context
    
          // 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
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app
    
    
          return vnode.component!.proxy
        } else if (__DEV__) {
          warn(...)
        }
      },
    

    render

    render方法执行步骤如下:

    1. 判断vnode是否存在,不存在且container._vnode有值,也就是有之前的dom渲染,则进行unmount操作

    2. 如果vnode不为空,则进行patch操作,dom diff和渲染

      本例中vnode存在执行patch方法

    3. 执行flushPostFlushCbs函数,回调调度器,使用Promise实现

    4. 把container的_vnode存储为当前vnode,方便后面进行dom diff操作

    //** \packages\runtime-core\src\renderer.ts */
    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()
        container._vnode = vnode
    }
    

    patch

    在下面思维导图中,n1代表前一次的vnode,n2代表本次的vnode

    path方法执行过程如下:

    1. 判断n1是否不为null,n1和n2的type是否相同,如果符合条件执行unmount

      本例中n1为null,不执行unmount

    2. 根据n2的vnode.type的类型选择执行方式

      本例中vnode.type为component,执行的是processComponent方法,最后调用mountComponent方法

    vue3源码浅析:mount

    //*packages\runtime-core\src\renderer.ts  */
    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(...)
            } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
              ;(type as typeof SuspenseImpl).process(...)
            } else if (__DEV__) {
              warn('Invalid VNode type:', type, `(${typeof type})`)
            }
        }
    
        // set ref
        if (ref != null && parentComponent) {
          setRef(ref, n1 && n1.ref, parentSuspense, n2)
        }
      }
    

    mountComponent

    执行过程如下:

    1. 调用createComponentInstance生成当前组件实例instance

      本例中instance实例如下

      vue3源码浅析:mount
    2. 调用setupComponent(instance)进行组件初始化,初始化props和slots等,并完成数据响应式

    3. 调用setupRenderEffect()安装渲染函数

    //** \packages\runtime-core\src\renderer.ts */
      const mountComponent: MountComponentFn = (
        initialVNode,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      ) => {
        const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
          initialVNode,
          parentComponent,
          parentSuspense
        ))
        
    	...
        
        setupComponent(instance)
          
    	...
        
        setupRenderEffect(
          instance,
          initialVNode,
          container,
          anchor,
          parentSuspense,
          isSVG,
          optimized
        )
    
      }
    

    setupComponent

    执行步骤如下:

    1. 执行initProps,初始化props
    2. 执行initSlots,初始化slots
    //*packages\runtime-core\src\component.ts*//
    export function setupComponent(
      instance: ComponentInternalInstance,
      isSSR = false
    ) {
      isInSSRComponentSetup = isSSR
    
      const { props, children, shapeFlag } = instance.vnode
      const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
      initProps(instance, props, isStateful, isSSR)
      initSlots(instance, children)
    
      const setupResult = isStateful
        ? setupStatefulComponent(instance, isSSR)
        : undefined
      isInSSRComponentSetup = false
      return setupResult
    }
    

    setupRenderEffect

    setupRenderEffect只有一步:

    1. 为当前实例挂载上update方法,update方法是通过effect生成的

    effect在Vue3中的作用就相当于Vue2中的observe,update生成后,挂载之前会先运行一下生成的effect方法,最后返回当前effect方法给update;运行effect函数就相当于Vue2中watcher调用get的过程.

    //** \packages\runtime-core\src\renderer.ts */ 
    const setupRenderEffect: SetupRenderEffectFn = (
        instance,
        initialVNode,
        container,
        anchor,
        parentSuspense,
        isSVG,
        optimized
      ) => {
        // create reactive effect for rendering
        instance.update = effect(function componentEffect() {
          if (!instance.isMounted){
              ...
          }else {
              ...
          }
        }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
      }        
    

    componentEffect

    componentEffect函数有两个逻辑,判断是否已经渲染:instance.isMounted;如果已经渲染,则走更新逻辑;未渲染,则走未渲染的逻辑

    该函数的执行位置在effect --> createReactiveEffect中,未渲染的执行过程如下:

    1. beforeMount生命周期如果存在,执行该生命函数

    2. 父类的BeforeMount生命周期如果存在,执行该生命函数

    3. 调用renderComponentRoot,进行渲染组件的根元素,得到subTree

      instance实例subTree
      vue3源码浅析:mountvue3源码浅析:mount
    4. 执行patch方法

      本例中第二次执行patch,vnode.type为element,执行的是processElement方法,最后调用mountElement方法

    5. 调用当前实例的mounted钩子函数;调用n2的父类的mounted钩子函数;调用当前实例的activated钩子函数;不是直接调用,而是通过queuePostRenderEffect放到队列中去调用

    6. 最终把实例的isMounted置为true

    //** \packages\runtime-core\src\renderer.ts */ 
    function componentEffect() {
          if (!instance.isMounted) {
            let vnodeHook: VNodeHook | null | undefined
            const { el, props } = initialVNode
            const { bm, m, parent } = instance
    
            // beforeMount hook
            if (bm) {
              invokeArrayFns(bm)
            }
            // onVnodeBeforeMount
            if ((vnodeHook = props && props.onVnodeBeforeMount)) {
              invokeVNodeHook(vnodeHook, parent, initialVNode)
            }
    
            // render
    		  ...
            const subTree = (instance.subTree = renderComponentRoot(instance))
    		  ...
    
            if (el && hydrateNode) {
    		  ...
            } else {
    		  ...
              patch(
                null,
                subTree,
                container,
                anchor,
                instance,
                parentSuspense,
                isSVG
              )
    			...
              initialVNode.el = subTree.el
            }
            // mounted hook
            if (m) {
              queuePostRenderEffect(m, parentSuspense)
            }
            // onVnodeMounted
            if ((vnodeHook = props && props.onVnodeMounted)) {
              const scopedInitialVNode = initialVNode
              queuePostRenderEffect(() => {
                invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode)
              }, parentSuspense)
            }
            // activated hook for keep-alive roots.
            // #1742 activated hook must be accessed after first render
            // since the hook may be injected by a child keep-alive
            const { a } = instance
            if (
              a &&
              initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
            ) {
              queuePostRenderEffect(a, parentSuspense)
            }
            instance.isMounted = true
    
            // #2458: deference mount-only object parameters to prevent memleaks
            initialVNode = container = anchor = null as any
          } else {
    		...
          }
    

    patch

    这次第二次执行patch,第一次patch的对象是单个组件,而这次是div#app内的元素,执行过程和第一次的相同

    mountElement

    mountElement作为元素渲染方法,其将vnode通过insert方法渲染为真实DOM,如果碰到含有子元素则调用mountChildren

    //** \packages\runtime-core\src\renderer.ts */  
    const mountElement = (
        vnode: VNode,
        container: RendererElement,
        anchor: RendererNode | null,
        parentComponent: ComponentInternalInstance | null,
        parentSuspense: SuspenseBoundary | null,
        isSVG: boolean,
        optimized: boolean
      ) => {
        let el: RendererElement
        let vnodeHook: VNodeHook | undefined | null
        const {
          type,
          props,
          shapeFlag,
          transition,
          scopeId,
          patchFlag,
          dirs
        } = vnode
        if (
          !__DEV__ &&
          vnode.el &&
          hostCloneNode !== undefined &&
          patchFlag === PatchFlags.HOISTED
        ) {
          // If a vnode has non-null el, it means it's being reused.
          // Only static vnodes can be reused, so its mounted DOM nodes should be
          // exactly the same, and we can simply do a clone here.
          // only do this in production since cloned trees cannot be HMR updated.
          el = vnode.el = hostCloneNode(vnode.el)
        } else {
          el = vnode.el = hostCreateElement(
            vnode.type as string,
            isSVG,
            props && props.is
          )
    
          // mount children first, since some props may rely on child content
          // being already rendered, e.g. `<select value>`
          if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
            hostSetElementText(el, vnode.children as string)
          } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
            mountChildren(...)
          }
    
          if (dirs) {
            invokeDirectiveHook(vnode, null, parentComponent, 'created')
          }
          // props
          if (props) {
            for (const key in props) {
              if (!isReservedProp(key)) {
                hostPatchProp(...)
              }
            }
            if ((vnodeHook = props.onVnodeBeforeMount)) {
              invokeVNodeHook(vnodeHook, parentComponent, vnode)
            }
          }
          // scopeId
          setScopeId(el, scopeId, vnode, parentComponent)
        }
        if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
          Object.defineProperty(el, '__vnode', {
            value: vnode,
            enumerable: false
          })
          Object.defineProperty(el, '__vueParentComponent', {
            value: parentComponent,
            enumerable: false
          })
        }
        if (dirs) {
          invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
        }
        // #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
        // #1689 For inside suspense + suspense resolved case, just call it
        const needCallTransitionHooks =
          (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
          transition &&
          !transition.persisted
        if (needCallTransitionHooks) {
          transition!.beforeEnter(el)
        }
        hostInsert(el, container, anchor)
        if (
          (vnodeHook = props && props.onVnodeMounted) ||
          needCallTransitionHooks ||
          dirs
        ) {
          queuePostRenderEffect(() => {
            vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
            needCallTransitionHooks && transition!.enter(el)
            dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
          }, parentSuspense)
        }
      }
    

    effect

    effect 作为 reactive 的核心,主要负责收集依赖,更新依赖

    effect保存了组件更新函数componentEffect,如果数据发生变化,重新执行组件更新函数

    effect函数有两个参数

    fnprodEffectOptions
    fn = ƒ componentEffect()vue3源码浅析:mount
    //* vue-next\packages\reactivity\src\effect.ts */
    export interface ReactiveEffectOptions {
      lazy?: boolean //  是否延迟触发 effect
      computed?: boolean // 是否为计算属性
      scheduler?: (job: ReactiveEffect) => void // 调度函数
      onTrack?: (event: DebuggerEvent) => void // 追踪时触发
      onTrigger?: (event: DebuggerEvent) => void // 触发回调时触发
      onStop?: () => void // 停止监听时触发
    }
    
    //* packages\reactivity\src\effect.ts */
    export function effect<T = any>(
      fn: () => T,
      options: ReactiveEffectOptions = EMPTY_OBJ
    ): ReactiveEffect<T> {
        // 如果已经是 `effect` 先重置为原始对象
      if (isEffect(fn)) {
        fn = fn.raw
      }
        // 创建`effect`
      const effect = createReactiveEffect(fn, options)
      // 如果没有传入 lazy 则直接执行一次 `effect`
      if (!options.lazy) {
        effect()
      }
      return effect
    }
    

    createReactiveEffect

    createReactiveEffecteffect创建函数,依赖收集过程如图所示

    三个函数作用如下:

    effect:将回调函数保存起来备用,立即执行一次回调函数触发它里面一些响应数据的getter

    track:getter中调用track,把前面存储的回调函数和当前target,key之间建立映射关系

    trigger:setter中调用trigger,把target,key对应的响应函数都执行一遍

    vue3源码浅析:mount

    //* packages\reactivity\src\effect.ts /
    function createReactiveEffect<T = any>(
      fn: (...args: any[]) => T,
      options: ReactiveEffectOptions
    ): ReactiveEffect<T> {
      const effect = function reactiveEffect(...args: unknown[]): unknown {
    
        // 没有激活,说明我们调用了effect stop 函数,
        if (!effect.active) {
          // 如果没有调度者,直接返回,否则直接执行fn
          return options.scheduler ? undefined : fn(...args)
        }
    
        // 判断effectStack中有没有effect, 如果在则不处理
        if (!effectStack.includes(effect)) {
          // 清除effect依赖,定义在下方
          cleanup(effect)
          try {
            // 开始重新收集依赖
            enableTracking()
            // 压入Stack
            effectStack.push(effect)
            // 将activeEffect当前effect 
            activeEffect = effect
            return fn(...args)
          } finally {
            // 完成后将effect弹出
            effectStack.pop()
            // 重置依赖
            resetTracking()
            // 重置activeEffect 
            activeEffect = effectStack[effectStack.length - 1]
          }
        }
      } as ReactiveEffect
      effect.id = uid++ // 自增id, effect唯一标识
      effect._isEffect = true  // 是否是effect
      effect.active = true // 是否激活 
      effect.raw = fn // 挂载原始对象
      effect.deps = []  // 当前 effect 的dep 数组
      effect.options = options // 传入的options,在effect有解释的那个字段
      return effect
    }
    
    const effectStack: ReactiveEffect[] = []
    
    // 每次 effect 运行都会重新收集依赖, deps 是 effect 的依赖数组, 需要全部清空
    function cleanup(effect: ReactiveEffect) {
      const { deps } = effect
      if (deps.length) {
        for (let i = 0; i < deps.length; i++) {
          deps[i].delete(effect)
        }
        deps.length = 0
      }
    }
    

    参考

    effect源码分析

    mount源码分析

    vue3初探


    起源地下载网 » vue3源码浅析:mount

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元