Vue实例的源码解析 (实例二)

实例二

该实例的模板如下:

    <div id="app">
        <mycom></mycom>
        <div></div>
    </div>
    <script>
          var vm = new Vue({
     
            el:'#app',
            data:{
     
                message:123
            },components:{
     
                "mycom":{
     
                    template:'<div><aa></aa></div>',
                    data(){
     
                        return {
     }
                    },
                    components:{
     
                        "aa":{
     
                            template:'<span></span>',
                            data(){
     
                                return {
     }
                            }
                        }
                    }
                }
            }
        })
    </script>

主要是组件实例的创建流程。

实例二基本介绍

对于我们的实例二,你可能因为实例一的组件渲染过程而对实例二心存忌惮,因为实例一只是一个根组件,但是却写了那么多的流程。其实你这样想是正常的,但是我们需要明白一点,那就是实例一的过程是创建一个组件,根组件是组件,那么子组件同样也是组件,也就是说,子组件也会走根组件创建时一样的流程。也就是说,在接下来的子组件嵌套的过程中,有绝大部分的代码是相同的,所以不用担心。为了流程的简洁,所以相同的部分我们会略讲,重点不同的地方我们会详细讲解。好了,开始我们的实例二的讲解吧。

首先我们来梳理一下我们将要创建的组件的结构:

<div id="app">
        <mycom></mycom>
        <div></div>
</div>
<mycom>组件:
<div>
    <aa></aa>
</div>
<aa>组件
<span></span>

以上是我们组件的基本结构。了解了基本结构之后我们正式的进入源码中去解析。

根组件实例的创建

首先通过new Vue构造函数创建根组件的实例对象,我们称为vm_0

根组件数据初始化

然后执行vm_0._init()函数,我们前面讲了,该函数的主要作用是对组件实例的数据初始化。我们进入该函数,主要调用:

  initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

获取render函数

当执行完这些后,根组件的数据初始化就基本完成了,当初始化完成之后就开始进行挂载部分。进行挂载调用的是:vm.$mount(vm.$options.el)函数,该函数主要判断vm_0实例对象是否有render函数,很明显是没有,然后就会通过解析模板来生成render函数,然后将其挂载到vm_0.render上。

在这里重新回顾一下对于render函数的获取流程。首先判断我们是否自己写的有render函数,如果没有开始解析我们是否定义了template配置,如果定义了那么就使用该配置项通过编译生成render函数,如果我们没有定义template,那么只能使用我们的根组件的id所对应的模板当作template。然后通过编译来生成render函数。然后将生成的render函数赋值给vm_0.render

当挂载完渲染函数之后,接着调用mount.call(this, el, hydrating)函数,进行下一部分的功能模块。mount函数内部调用mountComponent函数。我们来看mountComponent函数

创建watcher实例

mountComponent函数中,主要做了这么几件事。首先是判断render函数,然后执行beforeMount钩子函数,接着定义updateComponent函数,然后执行new Watcher

Watcher构造函数中,首先创建一个与vm_0实例对象相对应的watcher实例对象,用这个watcher来代表vm_0这个组件,当watcher创建完成之后调用watcher.get函数。

该函数首先将watcher压入target栈中,用来注明现在是vm_0组件正在进行渲染的流程。然后执行updateComponent。该函数是用来进行挂载的,我们来具体看看该函数内部的操作。

生成根组件的vnode

进入到updateComponent函数的内部:

 updateComponent = () => {
   
      //vm._update是在lifecycleMixin(Vue)中定义的
      //vm._render是在renderMixin中定义的。
      //hydrating:false
      //该函数的执行其实是在new Watcher()中执行的,我们暂时只关注它的执行,不去关注在什么地方触发。
      vm._update(vm._render(), hydrating)
    }

首先是调用vm_0._render函数,在实例一中我们讲过,该函数是用来生成组件的vnode。现在我们就进入到该函数中来粗略的过一下它的具体流程。进入到_render函数中,首先解析出render函数,然后对vm_0实例又挂载一些属性。然后就调用render.call函数。render函数的内部是这样的:

_c('div',{
   attrs:{
   "id":"app"}},[_c('mycom'),_v(" "),_c('div')],1)

关于_c函数的内部处理机制,我们在实例一中已经讲解过了。但是这里我还是要讲一下,因为这里涉及到一个特殊的节点,那就是组件节点。当调用_c函数的时候,它的内部调用的是createElement函数,而这个函数内部调用的是_createElement函数。我们来看这个函数内部的代码逻辑:

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)} 
      '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)
    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
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
   
      vnode = createComponent(Ctor, data, context, children, tag)
      console.log(vnode)
    } else {
   
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
   
    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()
  }
}

如果是正常的元素节点,那么毫无疑问的它会走:

 vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )

这个分支,然后创建元素节点的vnode

遇到组件节点

但是有趣的是该节点的子节点中,有一个_c('mycom')。也就是说它传入的tag并不是一个原生的元素标签,而是一个我们自定义的一个标签。我们来看它会走哪个分支。很显然,他会走:

else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
   
      // component
      debugger
      vnode = createComponent(Ctor, data, context, children, tag)
      console.log(vnode)
    }

也就是说会调用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
  }

  const baseCtor = context.$options._base//  Vue

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
   
    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
  
  const vnode = new VNode(
    `vue-component-${
     Ctor.cid}${
     name ? `-${
       name}` : ''}`,
    data, undefined, undefined, undefined, context,
    {
    Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

 //......

  return vnode
}

首先我们来解释一些参数:

Ctor:{
   template: 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答Vue2源码面试题,需要对Vue2的核心实现原理有较深入的理解。以下是一些可能的问题及回答: 1. Vue2如何实现响应式数据绑定? 答:Vue2通过Object.defineProperty()方法,对数据对象的属性进行劫持,当属性被读取或修改时,触发相应的getter和setter方法,在setter方法中进行依赖收集和派发更新。 2. Vue2的模板编译原理是什么? 答:Vue2的模板编译分为三个阶段:parse、optimize和generate。parse阶段将模板字符串解析为抽象语法树(AST),optimize阶段对AST进行静态节点标记、静态根节点标记和子树剪枝等优化,generate阶段将AST转为渲染函数。 3. Vue2的生命周期钩子函数有哪些?分别在哪个阶段执行? 答:Vue2的生命周期钩子函数包括:beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy和destroyed。它们分别在Vue实例的不同生命周期阶段执行,如beforeCreate在实例初始化之后,数据观测和事件配置之前执行。 4. Vue2的异步更新策略是什么? 答:Vue2的异步更新策略基于JavaScript的事件循环机制,通过nextTick方法将DOM更新推迟到下一个事件循环时执行。在同一个事件循环中,如果多次更新同一个数据,Vue2会将这些更新合并为一个更新,以提高性能。 总之,回答Vue2源码面试题需要具备深入的理解和实践经验,需要对Vue2的核心原理、优化策略和实现细节有较为透彻的掌握。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值