vue2组件化(2)- 组件的挂载过程

lxf2023-12-19 17:20:01

通过一个例子来分析

  <body>
    <div id="app"></div>
    <script src="./vue.js"></script>
    <script>
      const App = {
        data() {
          return {
            message: 'app数据'
          }
        },
        render(h) {
          return h(
            'div',
            {
              attrs: {
                class: 'app1'
              }
            },
            this.message
          )
        }
      }

    const vm = new Vue({
        el: '#app',
        components: {
          App
        },
        render(h) {
          return h(App)
        }
      })
    </script>
  </body>

接着上一篇,通过 vm._render生成了组件vnode,就该通过vm._update来插入到dom中,vm._update中调用了vm.__patch__,然后调用 createPatchFunction 中返回的patch,在patch中会调用createElm

createElm中组件的插入和普通元素就不一样了,

src/core/vdom/patch.ts

  function createElm(
    vnode,
    insertedVnodeQueue,
    parentElm?: any,
    refElm?: any,
    nested?: any,
    ownerArray?: any,
    index?: any
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested // for transition enter check
    //createComponent 方法目的是尝试创建子组件
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
  }

进入 createComponent尝试创建组件,

src/core/vdom/patch.ts

  function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef((i = i.hook)) && isDef((i = i.init))) {
        i(vnode, false /* hydrating */)
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

在上一篇,生成组件vnode的时候,有一个方法 installComponentHooks(data) 会把组件的钩子初始化放在vnode的data的hook里面,然后就会执行i(vnode, false),先进入init方法看一下

src/core/vdom/create-component.ts

  init(vnode: VNodeWithData, hydrating: boolean): boolean | void {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = (vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      ))
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

if中是keep-alive的逻辑进不去,看else里面,通过createComponentInstanceForVnode创建组件实例,参数是vnode(也就是例子App组件的vnode),activeInstance当前活跃的组件实例一会再说,在这里此时是最外层new Vue()生成的这个实例

src/core/vdom/create-component.ts

export function createComponentInstanceForVnode(
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent?: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}

参数vnode是组件APP的vnode,parent就是例子里的vue实例vm;给_parentVnode赋值组件App的vnode, 最后new vnode.componentOptions.Ctor(options),还记得上一篇中,src/core/vdom/create-component.ts的方法createComponent里面,生成组件Appd的时候,new Vnode的第五个参数componentOptions 传入了 { Ctor, propsData, listeners, tag, children }Ctor也是通过 Ctor = baseCtor.extend(Ctor as typeof Component)继承了Vue构造函数而生成的App的构造函数; 最后是返回App的组件实例;

在执行new vnode.componentOptions.Ctor(options),会执行到Vue.extend中的

const Sub = function VueComponent(this: any, options: any) {
      this._init(options)
    }

这个this就是组件App的实例,然后又回到了Vue.prototype._init这个方法

  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options as any)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    
    initLifecycle(vm)
    
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

这个参数options是

options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode, //组件App的vnode
    parent // Vue实例vm
  }

此时options._isComponent为true,不再是undefined,进入initInternalComponent,

export function initInternalComponent(
  vm: Component,
  options: InternalComponentOptions
) {
  const opts = (vm.$options = Object.create((vm.constructor as any).options))
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode
  
   ... 
}

options._parentVnode 是组件App的vnode,options.parent 是Vue实例vm ,所以在App组件实例的 $options上的 $options.parent是Vue实例vm $options._parentVnode是 App的vnode,

Vue.prototype._init中,再看一下 initLifecycle(vm)

src/core/instance/lifecycle.ts

export let activeInstance: any = null

export function initLifecycle(vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

}

export function lifecycleMixin(Vue: typeof Component) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
  }
  
 export function setActiveInstance(vm: Component) {
  const prevActiveInstance = activeInstance
  activeInstance = vm
  return () => {
    activeInstance = prevActiveInstance
  }
}

options.parent 实际就是 Vue实例vm ,当前活跃的组件实例,activeInstance是全局变量,在_update中会对activeInstance做操作,

在patch前,执行setActiveInstance(vm)activeInstance = vmprevActiveInstance = activeInstance

在patch后,activeInstance = prevActiveInstance

以上很大一部分都是建立父子组件之间的关系,在当前的组件实例的vnode在patch过程中,把当前组件实例作为父组件实例,传给子组件

然后回到child.$mount(hydrating ? vnode.elm : undefined, hydrating),手动调用了 $mount 来挂载,也就是执行之前的 Vue.prototype.$mount 和 mountComponent 方法,然后mountComponent里面实例化Watcher来执行vm._update(vm._render(), hydrating),回到了之前的逻辑

总结:组件的patch过程

  1. 在patch中调用createElm,createElm通过createComponent尝试创建组件,
  2. createComponent中调用了组件的钩子init,在init中,获得组件实例child,并且手动调用组件实例的$mount
  3. 然后又会走到mountComponentvm._update(vm._render(), hydrating)
  4. 回到createElm,当createComponent(vnode, insertedVnodeQueue, parentElm, refElm)不满足时,就往下走,就跟之前普通元素挂载一样,因为js是单线程,都是子组件先插入,而后才是父组件,最后是外面哪个div#app插到Dom里面
本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!