vue-source-code-analysic(8)

阅读Vue源码有感(8)

Posted by Mickey on July 19, 2018

上一篇博客中,我们介绍了在vue中非常重要VNode类,这是AST的基础,在这篇博客中,我们来介绍一下位于vnode目录下的create-element.js文件以及create-component.js两个文件,在这两个文件中,我们能够知道VNode节点是如何生成的

create-element.js文件

render.js中有如下的一段代码

export function initRender() {
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

createElement方法挂载到vm实例上,在render函数中相应的执行,下面我们就来看看该方法的源码

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode {
  /*兼容不传data的情况*/
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  /*如果alwaysNormalize为true,则normalizationType标记为ALWAYS_NORMALIZE*/
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  /*创建虚拟节点*/
  return _createElement(context, tag, data, children, normalizationType)
}

从上面的代码可以看到,createElement返回了一个_createElement函数,其实,核心的代码都是放在_createElement函数中,createElement函数做了一层兜底,兼容了不传data的情况(因为一个节点是v-pre的节点的子节点,那么el.plain=true,不会传VNodeData也就是data变量到createElement函数中)

下面看看核心的_createElement方法,简单来说,_createElement方法的作用是在不同的条件下渲染不同的VNode节点,例如:空节点,普通节点,组件节点等

/*创建VNode节点*/
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode {
  /*
    如果data未定义(undefined或者null)或者是data的__ob__已经定义(代表已经被observed,上面绑定了Oberver对象),
    https://cn.vuejs.org/v2/guide/render-function.html#约束
    那么创建一个空节点
  */
  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()
  }
  /*如果tag不存在也是创建一个空节点*/
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // 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
    /*获取tag的名字空间*/
    ns = config.getTagNamespace(tag)
    /*判断是否是保留的标签*/
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      /*如果是保留的标签则创建一个相应节点*/
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      /*从vm实例的option的components中寻找该tag,存在则就是一个组件,创建相应节点,Ctor为组件的构造类*/
      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
    /*tag不是字符串的时候则是组件的构造类*/
    vnode = createComponent(tag, data, context, children)
  }
  if (isDef(vnode)) {
    /*如果有名字空间,则递归所有子节点应用该名字空间*/
    if (ns) applyNS(vnode, ns)
    return vnode
  } else {
    /*如果vnode没有成功创建则创建空节点*/
    return createEmptyVNode()
  }
}

让我们细细来分析一下,首先如果传入_createElement函数的data存在__ob__属性,说明该data是被Observer注册过的$options.data,这种情况下,Vue会创建一个空的VNode节点

如果tag参数不存在的话,同样创建一个空的VNode节点,毕竟不知道tag,Vue也不知道给你生成啥节点啊,臣妾做不到啊,虽然template编译不会出现这些问题,但是还是要杜绝手写render可能引发的问题

然后程序判断tag参数是否为String类型,如果不为String类型的话,Vue会认为tag是一个组件的构造器,那么会调用create-component.js内的createComponent函数生成一个组件节点;如果为String类型的话,如果tag为reserveTag也就是div之类的平台保留节点,Vue会生成一个普通的VNode节点,如果tag存在于vm.$options.components中的话,同样生成组件节点(这是Vue引入模块的最常见方法),如果都不匹配的话,认为是未知的元素,在运行时检查,因为父组件可能在序列化子组件的时候分配一个名字空间

create-component.js

create-element.js中,我们可以看到当tag在components中注册过,或者tag是一个构造类的时候,调用了createComponent方法,下面我们就来看看定义了该方法的create-component.js文件

/*创建一个组件节点,返回Vnode节点*/
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data?: VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | void {
  /*没有传组件构造类直接返回*/
  if (isUndef(Ctor)) {
    return
  }

  /*_base存放了Vue,作为基类,可以在里面添加扩展*/
  const baseCtor = context.$options._base

  // 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.
  /*如果在该阶段Ctor依然不是一个构造函数或者是一个异步组件工厂则直接返回*/
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  /*处理异步组件*/
  if (isUndef(Ctor.cid)) {
    Ctor = resolveAsyncComponent(Ctor, baseCtor, context)
    if (Ctor === undefined) {
      // return nothing if this is indeed an async component
      // wait for the callback to trigger parent update.
      /*如果这是一个异步组件则会不会返回任何东西(undifiened),直接return掉,等待回调函数去触发父组件更新。s*/
      return
    }
  }

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  data = data || {}

  // 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
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners
    data = {}
  }

  // merge component management hooks onto the placeholder node
  mergeHooks(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 }
  )
  return vnode
}

createComponent方法如上所示,首先,让我们回忆一下,Vue有一个全局的Vue.extend的方法,用于创造一个构造器,在createComponent中,context.$options._base取出的就是该extend方法.

如果传入的Ctor是一个函数,那么不会掉用extend方法,也就意味着不会得到cid,那么就会调用resolveAsyncComponent 加载的就是一个异步组件,如果不了解Vue加载异步组件,可以先看看官方文档,下面代码中的注释写的比较详细,可以结合官方文档看看

function ensureCtor (comp, base) {
  return isObject(comp)
    ? base.extend(comp)
    : comp
}

export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>,
  context: Component
): Class<Component> | void {
  /*出错组件工厂返回出错组件*/
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

  /*resoved时候返回resolved组件*/
  if (isDef(factory.resolved)) {
    return factory.resolved
  }

  /*加载组件过程中,显示loading组件*/
  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

  if (isDef(factory.contexts)) {
    // already pending
    factory.contexts.push(context)
  } else {
    const contexts = factory.contexts = [context]
    let sync = true
	 
	 // 加载完成,异步更新context
    const forceRender = () => {
      for (let i = 0, l = contexts.length; i < l; i++) {
        contexts[i].$forceUpdate()
      }
    }

    const resolve = once((res: Object | Class<Component>) => {
      // cache resolved
      factory.resolved = ensureCtor(res, baseCtor)
      // invoke callbacks only if this is not a synchronous resolve
      // (async resolves are shimmed as synchronous during SSR)
      if (!sync) {
        forceRender()
      }
    })

    const reject = once(reason => {
      process.env.NODE_ENV !== 'production' && warn(
        `Failed to resolve async component: ${String(factory)}` +
        (reason ? `\nReason: ${reason}` : '')
      )
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender()
      }
    })

    const res = factory(resolve, reject)

    if (isObject(res)) {
    	// () => Promise类型
      if (typeof res.then === 'function') {
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      } else if (isDef(res.component) && typeof res.component.then === 'function') {
		  // const AsyncComponent = () => ({
			  // 需要加载的组件 (应该是一个 `Promise` 对象)
			  // component: import('./MyComponent.vue'),
			  // 异步组件加载时使用的组件
			  // loading: LoadingComponent,
			  // 加载失败时使用的组件
			  // error: ErrorComponent,
			  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
			  // delay: 200,
			  // 如果提供了超时时间且组件加载也超时了,
			  // 则使用加载失败时使用的组件。默认值是:`Infinity`
			  // timeout: 3000
		  })
        res.component.then(resolve, reject)

        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }

        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
          if (res.delay === 0) {
            factory.loading = true
          } else {
            setTimeout(() => {
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true
                forceRender()
              }
            }, res.delay || 200)
          }
        }

        if (isDef(res.timeout)) {
          setTimeout(() => {
            reject(
              process.env.NODE_ENV !== 'production'
                ? `timeout (${res.timeout}ms)`
                : null
            )
          }, res.timeout)
        }
      }
    }

    sync = false
    // return in case resolved synchronously
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}

当传入的Ctor是对象的时候,隐式的生成一个构造器,然后调用resolveConstructorOptions方法去获取最新的options,这个方法定义在core/instance/init.js

随后,Vue调用了transformModel方法,用于给v-model绑定处理函数,其实v-model就是一个语法糖,通过监听input事件,来响应式修改value数值,方法如下

function transformModel (options, data: any) {
  const prop = (options.model && options.model.prop) || 'value'
  const event = (options.model && options.model.event) || 'input'
  ;(data.props || (data.props = {}))[prop] = data.model.value
  const on = data.on || (data.on = {})
  if (isDef(on[event])) {
    on[event] = [data.model.callback].concat(on[event])
  } else {
    on[event] = data.model.callback
  }
}

紧接着,Vue调用了extractPropsFromVNodeData方法,这个方法是生成propsData对象的。我们知道,一个Vue组件,在props中可以定义组件接收的变量,而propsData则是父组件真实传入的变量,剩余的变量,可以通过default定义

Ctor.options.functional为true的时候,Vue认为这是一个函数式组件,会调用createFunctionalComponent函数,至于什么是函数式组件,我们可以把函数式组件想像成组件里的一个函数,入参是渲染上下文(render context),返回值是渲染好的HTML,函数式组件是没有this的,也就意味着组件本身是没有状态的,具体可以看看这篇掘金blog

在完成上述准备操作之后,Vue会执行new VNode(...params),得到组件对应的一个VNode节点(占位所用),不过该VNode在当前context中,没有elm,只有componentInstance,这也是ref的时候,获取的是vm,可以直接调用methods中方法的原因