Blog

函数式组件

time: 2021-05-06 16:04:26
author: heyunjiang

普通组件和函数式组件都总结在这篇文章中了

1 函数式组件基本知识

  1. 组件所有的 attribute 都会被解析为 prop
  2. 声明方式:template + functional,或者 functional: true
  3. 使用 context 对象传递参数,包含了 props, children, slots, scopedSlots, data, parent, listeners, injections。其中,context.data 是所有 attribute 和事件的统一集合

2 问题

  1. 为什么能提升系统性能?没有 vue 实例,占用内存少
  2. 函数式子组件会受到影响吗?为什么?不会,渲染函数式组件时,其实就是渲染的自身 render 及内部子组件
  3. 函数式组件是如何渲染的呢?是将函数式组件 render 生成的 vnode 对象,在函数式的父组件环境中渲染
  4. 函数式组件是响应式的吗?是的,数据使用时收集的是父组件的 watcher 对象
  5. new Vue() 创建实例和通过 components 注册组件 createComponent 生成的实例有什么区别? new 方式的实例,在 createComponent 时,是没有 parentElm,需要手动调用 vm.$mount 来渲染; 而组件方式的渲染,是有 parentElm 的,在 patch.createComponent 时,会调用 componentVNodeHooks.init.$mount 来生成组件 vnode.elm, 然后再 insert dom tree。

3 函数式组件和普通组件的渲染流程

3.1 编译

对于如下组件,2者是如何编译的呢?

<hello /> // 普通组件
<world /> // 函数式组件

// 编译结果
[
  createElement('hello'),
  createElement('world')
]

// 待补充函数式组件的编译例子
export default {
  functional: true,
  render: function(h, context) {
    return (
      <div></div>
    )
  }
}

// 编译结果
var lib_vue_loader_options_srcvue_type_script_lang_js_ = ({
  functional: true,
  render: function render(h, context) {
    return h("div");
  }
});

3.2 render 生成 vnode

函数式组件的 jsx 或 template 编译结果还是生成 render 方法,此刻依然使用 createElement 代替了标签; createElement 在执行时,也是调用的 createComponent 方法

if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
  vnode = createComponent(Ctor, data, context, children, tag)
}

此刻已经获取到函数式组件的配置对象 Ctor 了,来看看 createComponent 方法

const baseCtor = context.$options._base
if (isObject(Ctor)) {
  // Vue.extend 表示合并新的配置,生成新的 Vue 函数对象,此刻未实例化
  Ctor = baseCtor.extend(Ctor)
}
if (isTrue(Ctor.options.functional)) {
  return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// 为普通组件注入 component hooks.init 等方法
installComponentHooks(data)
const vnode = new VNode(
  `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
  data, undefined, undefined, undefined, context,
  { Ctor, propsData, listeners, tag, children },
  asyncFactory
)
return vnode

如果 functional === true,内部调用 createFunctionalComponent 来生成函数式组件;普通组件则直接 new vnode

export function createFunctionalComponent (
  Ctor: Class<Component>,
  propsData: ?Object,
  data: VNodeData,
  contextVm: Component,
  children: ?Array<VNode>
) {
  const options = Ctor.options
  // 1. 生成 render context 对象
  const renderContext = new FunctionalRenderContext(
    data,
    props,
    children,
    contextVm,
    Ctor
  )
  // 2. 同样调用 render 生成 vnode 对象
  const vnode = options.render.call(null, renderContext._c, renderContext)
  if (vnode instanceof VNode) {
    // 3. 标记为函数式 vnode
    return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
  }
}

createComponent 函数式组件和普通组件处理对比

  1. 函数式组件会生成 render context 对象,提供给 render 函数的第二个参数,同时作为 vnode 的 fnContext 属性值
  2. 普通组件会直接 new Vnode,而函数式组件会调用 render 方法去生成 vnode,这就是2者在 createElement 之后最大的差异
  3. 函数式组件生成的 vnode,是自身 render 内部的顶层节点 vnode 对象,比如 div 就返回的 div 的 vnode
  4. 针对 extend 生成的 Ctor 对象,函数式组件只会读取它的 options,后续就无其他作用,普通组件的 Ctor 会作为普通组件的 vnode 属性

组件 slot 渲染请查阅 插槽渲染

问题:普通组件内部的 render 方法是在什么时候执行,新生成的 vnode 的 parent 是谁?
答:普通组件的 render 是在 mountComponent 中调用 vm._render 执行;新生成的 vnode 的 parent 是会在组件 initLifecycle 时绑定父 vnode

3.3 update 渲染

在调用 createElement 生成 vnode 之后,父组件 watcher 通过 vm._update 去渲染 vnode,来看看渲染对 普通组件 的处理
处理流程如下: patch -> createElm -> createComponent,而 createComponent 内部调用 vnode.data.hooks.init (是在 vdom/create-component/componentVNodeHooks 中定义的)来初始化组件对象

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 */)
    }
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

来看看 vdom/create-component/componentVNodeHooks

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive 组件更新
      const mountedNode: any = vnode 
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      // 所有组件初次渲染,实例化组件
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance // 表示当前渲染组件的环境
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
}
export function createComponentInstanceForVnode (
  vnode: any,
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  return new vnode.componentOptions.Ctor(options)
}

可以看到

  1. 普通组件在 patch 时,会调用 createComponent 方法来生成组件 vm 实例,并自身执行一次 $mount 挂载 ($mount 也就是调用 mountComponent)
  2. 此刻会再次走组件的完整渲染流程,内部初始化 watcher 对象,调用 vm._update(vm._render),具体可以看 vue深入 - 数据驱动 流程

而函数式组件的渲染,是在父组件 patch 中直接渲染了,而不会生成自己的 vm 实例再渲染

4 函数式组件和普通组件的详细对比

  1. 渲染流程不同:普通组件会走完整组件渲染流程,函数式组件不过是层壳子,还是在父组件环境中渲染的 vnode.context
  2. 函数式组件无组件实例:因为函数式组件在 createElement.createComponent 中,返回的自身 render 之后生成的普通 vnode,而普通组件会直接实例化一个组件化的 vnode

5 结果分析

函数式组件只是一层壳子,没有独立 vue 实例,也就是说没有生成额外的对象,占用内存少了,自然就性能更好。

函数式组件使用场景

  1. 简单组件,无状态、生命周期操作
  2. 类似高阶组件的包装使用

问题

  1. 函数式组件、slot、高阶组件差异?
  2. 既然函数式组件能提升性能,我们能不能使用它代替普通组件?不能,普通组件需要有局部状态、生命周期、事件等管理,业务是需要这样子的功能的

突发灵感

2021-05-06 19:32:48

vue 组件核心的是 createElement 生成 vnode,然后调用 patch 去渲染成真实 dom,而且这2块分别属于 src/core/vdom/create-elementsrc/core/vdom/patch 模块,再次归纳一下 vue2 的核心模块

  1. vdom 下包含了 createElement 和 patch 2大核心,涉及到 vnode 的生成和渲染真实 dom
  2. observer 模块实现了响应式系统,内部实现了 watcher, dep, scheduler 核心对象,也作为 vue 实例和 vdom 的衔接点
  3. 衔接 vm、vdom:在 src/core/instance/lifecycle/ 的 mountComponent 方法中,使用到了 watcher 对象。updateComponent 作为 watcher 的更新方法,又调用了 vm._update(vm._render()) 结构
  4. vm._render:直接调用 vm.render 方法,render 方法内部是使用的 vdom.createElement 方法,生成 vnode 对象
  5. vm._update:调用 vdom.patch 来渲染 vnode 为真实 dom
  6. instance: 生成 vm 实例,实现了 data、injections、provide 数据管理、自定义事件管理、lifecycle 渲染 dom、render 生成 vnode 模块; 其中 lifecycle 定义了 vm._update 渲染 dom,render 定义了 vm._render 生成 vnode 对象,这2个核心衔接模块,由 watcher 统一调度,包括初次渲染和后续更新

总结

  1. 整体来看,instance 作为核心对象,内部调用 watcher 来管理自身关键方法调用,自身关键方法调用了 vdom 的生成 vnode 和渲染真实 dom 的接口
  2. 一个组件实例,包含了自身一系列属性和方法,包括 vnode、$el 对象

参考资料

vue 函数式组件