欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

vue组件初始化过程

程序员文章站 2022-04-10 13:57:16
之前文章有写到vue构造函数的实例化过程,只是对vue实例做了个粗略的描述,并没有说明vue组件实例化的过程。本文主要对vue组件的实例化过程做一些简要的描述。 组件的实例化与vue构造函数的实例化,大部分是类似的,vue的实例可以当做一个根组件,普通组件的实例化可以当做子组件。真实的DOM是一个树 ......

  之前文章有写到vue构造函数的实例化过程,只是对vue实例做了个粗略的描述,并没有说明vue组件实例化的过程。本文主要对vue组件的实例化过程做一些简要的描述。

  组件的实例化与vue构造函数的实例化,大部分是类似的,vue的实例可以当做一个根组件,普通组件的实例化可以当做子组件。真实的dom是一个树形结构,虚拟dom本质只是真实dom的抽象,也是一个树形结构。简单来说,整个vue工程的实例化过程如下:

  vue组件初始化过程

   如上图所示,在调用render函数时,会依次调用createelement方法,createelement方法的代码如下,主要作用就是生成vnode。

export function _createelement (
  context: component,
  tag?: string | class<component> | function | object,
  data?: vnodedata,
  children?: any,
  normalizationtype?: number
): vnode | array<vnode> {
  if (isdef(data) && isdef((data: any).__ob__)) {
    process.env.node_env !== 'production' && warn(
      `avoid using observed data object as vnode data: ${json.stringify(data)}\n` +
      'always create fresh vnode data objects in each render!',
      context
    )
    return createemptyvnode()
  }
  // object syntax in v-bind
  if (isdef(data) && isdef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createemptyvnode()
  }
  // warn against non-primitive key
  if (process.env.node_env !== 'production' &&
    isdef(data) && isdef(data.key) && !isprimitive(data.key)
  ) {
    if (!__weex__ || !('@binding' in data.key)) {
      warn(
        'avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (array.isarray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedslots = { default: children[0] }
    children.length = 0
  }
  // 组件格式化
  if (normalizationtype === always_normalize) {
    children = normalizechildren(children)
  } else if (normalizationtype === simple_normalize) {
    children = simplenormalizechildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let ctor
    ns = (context.$vnode && context.$vnode.ns) || config.gettagnamespace(tag)
    // 普通的html标签
    if (config.isreservedtag(tag)) {
      // platform built-in elements
      if (process.env.node_env !== 'production' && isdef(data) && isdef(data.nativeon)) {
        warn(
          `the .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      // 创建一个普通的dom节点
      vnode = new vnode(
        config.parseplatformtagname(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isdef(ctor = resolveasset(context.$options, 'components', tag))) {
      // component
      // 创建组件
      vnode = createcomponent(ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new vnode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createcomponent(tag, data, context, children)
  }
  if (array.isarray(vnode)) {
    return vnode
  } else if (isdef(vnode)) {
    if (isdef(ns)) applyns(vnode, ns)
    if (isdef(data)) registerdeepbindings(data)
    return vnode
  } else {
    return createemptyvnode()
  }
}
  从上述代码中可以看出,如果存在tag且tag的类型为string,会走一些判断逻辑,主要就是判断两类,一类是html标签,通过config.isreservedtag判断是否是html标签,另外一类就是在当前实例作用域options中的component中查找,是否存在对该类标签的声明,存在,即使组件,详细流程图如下图所示:
vue组件初始化过程

   如上图所示,主流程与实例化vue类似,只是在实例化vue的过程中,额外走了一个创建组件的分支,其中createcomponent方法实现如下:

export function createcomponent (
  ctor: class<component> | function | object | void,
  data: ?vnodedata,
  context: component,
  children: ?array<vnode>,
  tag?: string
): vnode | array<vnode> | void {
  if (isundef(ctor)) {
    return
  }
  // 获取vue基础构造函数,在initglobal中,将vue基础构造方法赋值给_base属性
  const basector = context.$options._base

  // plain options object: turn it into a constructor
  if (isobject(ctor)) {
    // 将组件的配置,合并到构造方法中,extend是定义在vue构造方法中的
    ctor = basector.extend(ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof ctor !== 'function') {
    if (process.env.node_env !== 'production') {
      warn(`invalid component definition: ${string(ctor)}`, context)
    }
    return
  }

  // async component
  let asyncfactory
  if (isundef(ctor.cid)) {
    asyncfactory = ctor
    ctor = resolveasynccomponent(asyncfactory, basector)
    if (ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createasyncplaceholder(
        asyncfactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveconstructoroptions(ctor)

  // transform component v-model data into props & events
  if (isdef(data.model)) {
    transformmodel(ctor.options, data)
  }

  // extract props
  const propsdata = extractpropsfromvnodedata(data, ctor, tag)

  // functional component
  if (istrue(ctor.options.functional)) {
    return createfunctionalcomponent(ctor, propsdata, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of dom listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeon

  if (istrue(ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  // 初始化组件的钩子函数
  installcomponenthooks(data)

  // return a placeholder vnode
  // 体现了组件名称在这里面的作用
  const name = ctor.options.name || tag
  // 创建vnode
  const vnode = new vnode(
    `vue-component-${ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { ctor, propsdata, listeners, tag, children },
    asyncfactory
  )

  // weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__weex__ && isrecyclablecomponent(vnode)) {
    return renderrecyclablecomponenttemplate(vnode)
  }

  return vnode
}

   从上述代码中可以看出,createcomponent主要作用就是返回一个vnode,中间的流程主要作用有两点,一是组装组件的构造方法,用于实例化组件,另外一点就是调用installcomponenthooks,初始化组件的生命周期入口。组件的声明周期钩子虽然与vue根实例一致,但是调用的位置还是有一定的差别,具体有以下几点:

  1. vue构造方法是在src\core\instance\index.js中,而组件的构造方法是基于vue根构造方法,在上述createcomponet中调用vue.extend方法进行组装而成,本质上都是调用vue实例上的_init方法,但是组件的构造方法vuecomponent声明了一些属于自己的自定义属性,具体实现代码如下:

 vue.extend = function (extendoptions: object): function {
    extendoptions = extendoptions || {}
    const super = this
    // 父级实例cid
    const superid = super.cid
    const cachedctors = extendoptions._ctor || (extendoptions._ctor = {})
    if (cachedctors[superid]) {
      return cachedctors[superid]
    }

    const name = extendoptions.name || super.options.name
    if (process.env.node_env !== 'production' && name) {
      validatecomponentname(name)
    }
    // 定义vue初始化方法,和实例化vue走同一个路线
    const sub = function vuecomponent (options) {
      this._init(options)
    }
    // super -> this -> vue 继承vue构造方法中的属性
    sub.prototype = object.create(super.prototype)
    // 指定子组件的构造方法为sub -> vuecomponent
    sub.prototype.constructor = sub
    sub.cid = cid++
    // 合并组件属性
    sub.options = mergeoptions(
      super.options,
      extendoptions
    )
    // 定义父级作用域
    sub['super'] = super

    // for props and computed properties, we define the proxy getters on
    // the vue instances at extension time, on the extended prototype. this
    // avoids object.defineproperty calls for each instance created.
    if (sub.options.props) {
      initprops(sub)
    }
    if (sub.options.computed) {
      initcomputed(sub)
    }

    // allow further extension/mixin/plugin usage
    // 子组件的实例,保持对vue构造方法的引用
    sub.extend = super.extend
    sub.mixin = super.mixin
    sub.use = super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    asset_types.foreach(function (type) {
      sub[type] = super[type]
    })
    // enable recursive self-lookup
    if (name) {
      sub.options.components[name] = sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if super's options have
    // been updated.
    sub.superoptions = super.options
    sub.extendoptions = extendoptions
    sub.sealedoptions = extend({}, sub.options)

    // cache constructor
    cachedctors[superid] = sub
    return sub
  }
}

  2. vue根实例的模板解析与dom挂载入口不一致,在_init方法中,提供了对根实例的模板解析与dom挂载,而组件没有。在创建组件时,调用了installcomponenthooks,componet hooks主要包含init、prepatch、insert、destory,init在实例化组件时调用,insert是插入dom时调用,destory是在销毁组件时调用,而prepatch是在更新组件时调用,具体如下:

const componentvnodehooks = {
  // 组件初始化方法
  init (vnode: vnodewithdata, hydrating: boolean): ?boolean {
    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)
    }
  },

  prepatch (oldvnode: mountedcomponentvnode, vnode: mountedcomponentvnode) {
    const options = vnode.componentoptions
    const child = vnode.componentinstance = oldvnode.componentinstance
    updatechildcomponent(
      child,
      options.propsdata, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

  insert (vnode: mountedcomponentvnode) {
    const { context, componentinstance } = vnode
    if (!componentinstance._ismounted) {
      componentinstance._ismounted = true
      callhook(componentinstance, 'mounted')
    }
    if (vnode.data.keepalive) {
      if (context._ismounted) {
        // vue-router#1212
        // during updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueactivatedcomponent(componentinstance)
      } else {
        activatechildcomponent(componentinstance, true /* direct */)
      }
    }
  },

  destroy (vnode: mountedcomponentvnode) {
    const { componentinstance } = vnode
    if (!componentinstance._isdestroyed) {
      if (!vnode.data.keepalive) {
        componentinstance.$destroy()
      } else {
        deactivatechildcomponent(componentinstance, true /* direct */)
      }
    }
  }
}

  如上述代码所示,实例化组件调用的是createcomponentinstanceforvnode,createcomponentinstanceforvnode代码如下,调用在vue.extend中组装的组件构造方法vuecomponent,初始化调用的还是vue原型上的_init方法,大致流程与vue初始化类似,只是解析模板有所区别,组件解析模板调用的是child.$mount。

// 创建组件的作用域,执行组件的_init方法,同vue实例化过程
export function createcomponentinstanceforvnode (
  vnode: any, // we know it's mountedcomponentvnode but flow doesn't
  parent: any, // activeinstance in lifecycle state
): 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)
}

  在installcomponenthooks中,在vnode的data属性中初始化了hooks,后面在_patch__中,会调用patch.js中声明的createcomponent -> init -> 实例化组件。组件实例化完成后,会将真实dom元素,插入到上一级元素。patch.js中的createcomponent方法如下:

  // 创建组件,如果节点类型是组件,则直接走创建组件的方法
  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)) {
        // 执行component的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.
      // 组件的vnode对象中存在当前组件的作用域
      if (isdef(vnode.componentinstance)) {
        initcomponent(vnode, insertedvnodequeue)
        // 将子组件插入到父节点中
        insert(parentelm, vnode.elm, refelm)
        if (istrue(isreactivated)) {
          reactivatecomponent(vnode, insertedvnodequeue, parentelm, refelm)
        }
        return true
      }
    }
  }

  在实例化完成后,会将生成的真实dom元素插入到上级元素中,vue在获取真实dom时,是从低往上,一级级添加,最终将渲染的元素添加到dom body中,__patch__主流程如下:

function patch (oldvnode, vnode, hydrating, removeonly) {
    if (isundef(vnode)) {
      if (isdef(oldvnode)) invokedestroyhook(oldvnode)
      return
    }

    let isinitialpatch = false
    const insertedvnodequeue = []

    if (isundef(oldvnode)) {
      // empty mount (likely as component), create new root element
      isinitialpatch = true
      createelm(vnode, insertedvnodequeue)
    } else {
      const isrealelement = isdef(oldvnode.nodetype)
      if (!isrealelement && samevnode(oldvnode, vnode)) {
        // patch existing root node
        patchvnode(oldvnode, vnode, insertedvnodequeue, null, null, removeonly)
      } else {
        if (isrealelement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          // nodetype 1 元素 3 文字
          if (oldvnode.nodetype === 1 && oldvnode.hasattribute(ssr_attr)) {
            oldvnode.removeattribute(ssr_attr)
            hydrating = true
          }
          if (istrue(hydrating)) {
            if (hydrate(oldvnode, vnode, insertedvnodequeue)) {
              invokeinserthook(vnode, insertedvnodequeue, true)
              return oldvnode
            } else if (process.env.node_env !== 'production') {
              warn(
                'the client-side rendered virtual dom tree is not matching ' +
                'server-rendered content. this is likely caused by incorrect ' +
                'html markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldvnode = emptynodeat(oldvnode)
        }

        // replacing existing element
        // 获取老旧节点
        const oldelm = oldvnode.elm
        // 获取老旧节点的父节点
        const parentelm = nodeops.parentnode(oldelm)

        // create new node
        // 将虚拟dom转换成真实dom
        // 传入父级节点,一级级添加
        createelm(
          vnode,
          insertedvnodequeue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. only happens when combining transition +
          // keep-alive + hocs. (#4590)
          oldelm._leavecb ? null : parentelm,
          nodeops.nextsibling(oldelm)
        )

        // update parent placeholder node element, recursively
        if (isdef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = ispatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptynode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerref(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        // 移除老旧节点
        if (isdef(parentelm)) {
          removevnodes([oldvnode], 0, 0)
        } else if (isdef(oldvnode.tag)) {
          invokedestroyhook(oldvnode)
        }
      }
    }

    invokeinserthook(vnode, insertedvnodequeue, isinitialpatch)
    return vnode.elm
  }

  模板的解析,是先把模板解析成html,然后再讲老旧节点移除。