Vue源码学习9-组件化-createElement

  • 内容
  • 评论
  • 相关

前言

前面已经讲完了 Vue 的一个核心思想 响应式原理,Vue 的另一个核心思想就是组件化。什么是组件化?为啥要用组件化的方式开发?怎样做到组件化?这些问题网上随便一搜都能搜出一堆关于 组件化 的文章,文字上的描述可能略有不同,但其核心思想都是差不多的,不过我的文章是要讲源码,就不说太多这种概念性的东西了。

说说使用感受,平时使用Vue开发网页的时候,最直观的感受就像是在搭积木,各种功能不一的组件相互嵌套、堆叠,最后拼接出一个漂亮的网页出来,但是有时候过度抽取组件,把自己搞得脑壳疼,这就非常考验自己的组件抽象思维了。

在讲Vue组件化之前,要搞清楚一个问题,组件化和组件不是同一个东西。

组件化是一种思想,一种概念,是多年以来由开发人员总结出的一种最佳编程思想。

而组件,是代码编写规范,是 组件化思想 在代码上的体现。既然是代码体现,那么每个人的编写规范可能就会不一样,这个时候就需要框架了,以框架的规范统一开发人员的编码规范,省去很多不必要的麻烦。现阶段前端最流行、好用的框架就只有Vue、React、Angular(幸好只有三个),它们都是组件化思想的集大成者,虽然编码规范不一样

--以下所有代码来自Vue 2.6.11 版本

--以下的vm都代指vue实例

Vue组件化流程

Vue中组件的注册、命名规范这种问题就看看官方文档就行了。

首先说一点,组件在定义的时候就是一个Object对象,它的那些 options 和 new Vue 传入的 options 其实是一样的,不过还是有不同的地方,比如说 el data name 这些配置组件和 new Vue 的就不一样。我们在第一篇文章中就知道,Vue 本质上是个function,它可以通过new得到一个vm,但是我们注册传入的这个组件对象,最后也会变成一个个的vm,这是怎么做到的?看看源码就知道了。

前面在讲 mountComponent 的时候,提到了一个 vm._update 的方法,但是在执行 update 之前,还有一个叫_render(src/core/instance/render.js) 私有方法,事实上 vnode 正是在其中生成的,依赖收集 也是发生在这个过程中,我看看到下面的 _render 的源码,直接看到 render.call 那个地方,忽略前面的一些代码,这个 render 是从哪里来的?

createElement

1:看看Vue的文档,再看看示例代码,没错了,就是我们自己传入的这个render函数(在实际开发中我们可能和很少用这种写法,更多的是使用模板的方式,因为模板方式的可读性是最好的,但我们要知道,模板最终也是会被处理成这样的render函数,这是编译的内容)

2:看到  vm.$createElement 方法,一个入参,正是示例代码里面的 h 方法,它在render方法中被执行

/*示例代码*/
new Vue({
  el:'#app',
  render(h){
    return h('div'......);
  }
})
Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options
  if (_parentVnode) {
    vm.$scopedSlots = normalizeScopedSlots(
      _parentVnode.data.scopedSlots,
      vm.$slots,
      vm.$scopedSlots
    )
  }
  vm.$vnode = _parentVnode
  // render self
  let vnode
  try {
    currentRenderingInstance = vm
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    handleError(e, vm, `render`)
    if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
      try {
        vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
      } catch (e) {
        handleError(e, vm, `renderError`)
        vnode = vm._vnode
      }
    } else {
      vnode = vm._vnode
    }
  } finally {
    currentRenderingInstance = null
  }
  // if the returned array contains only a single node, allow it
  if (Array.isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]
  }
  // return empty vnode in case the render function errored out
  if (!(vnode instanceof VNode)) {
    if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
      warn(
        'Multiple root nodes returned from render function. Render function ' +
        'should return a single root node.',
        vm
      )
    }
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

3:$createElement 在哪里定义的?在第一篇文章中我们查看 _init 的时候,里面就有一个叫做 initRender(源码就不贴了) 的方法,就是在这里面赋的值,接着进入 createElement 方法,最终到达 _createElement(src/core/vdom/create-element.js) ,下面贴了源码

4:看到这段逻辑 typeof tag === 'string' ,tag 正是我们传入的标签字符串(上面示例代码传入的是div)

进入后遇到第一个 if 判断  config.isReservedTag(tag),这里是在检测标签是否是浏览器保留标签,如果是 div 这样的浏览器标签,就会创建一个普通的vnode;假设我们的 tag 名叫 Hello ,并且它成功注册为Vue的全局组件,那么就能顺利的进入第二个 if 逻辑--- createComponent

/*initRender*/
.......
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
.......
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)
    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))) {
      // 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()
  }
}

createComponent

在文章开始的时候我提过一个问题,组件是对象是怎么变成一个个的 vm?答案就在这个 createComponent 方法中(下面把源码贴出来了)

1:我们看到源码,_base是什么?它就是 Vue,它的赋值就发生在 最初的 _init 方法中,就是下面这段代码

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

extend 又是什么?它是 Vue 上的一个全局方法(src/core/global-api/extend.js,代码就不贴了,私下看看就行了),用来继承Vue的。我们注册的组件对象就是通过这个继承方法获取得了 Vue 的所有功能,并且被它会返回一个 function ,源码中的 Ctor 被处理之后就会变成一个 function,然后就它就可以执行 new 操作了(文章开始提的那个疑问到这里就搞定了)

2:接着看代码,跳过异步组件的那块内容,直接走到  installComponentHooks  安装组件钩子,这里是干什么的?先在这里留个悬念,后面的 patch 内容同会涉及这个东西的

3:紧接着,创建一个组件 vnode ,我们可以看到它的tag和普通的 vnode 是不一样的,也算是个区别于普通 vnode 身份标记吧。

4:至此,vnode生成完毕,我们的 createElement 或者说 vm._render 就告一段落了,后面还一些代码杂七杂八的代码在这也不用管了,影响不大

以下面示例代码为例:

此时 root-vm 中的 vnode 应该有一个 div-vnode,一个 span-vnode,以及一个 tag 为Hello 的组件vnode,还有一个依赖 name 的表达式,它们在代码中的表现就是和真实 dom 一样的树形结构

/*示例代码*/
new Vue({
  components:{
    Hello:{
      template:'<div>{{ name }}</div>'
      data(){
        return {
          name:'Tifa'
        }
      }
    }
  },
  el:'#app',
  data:{
    name:'Cloud'
  },
  template:'<div> <span> {{ name }} </span> <Hello/> </div>'
})
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

  // 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
  )

  // 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
}

总结

以上我们简单梳理了整个 vnode树 的生成过程,下篇文章开始我们梳理 patch 的过程,从图中可以看出,我们已经走到了最后一步了,组件实例化也发生在这一步中。

评论

0条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注