Vue中“h函数”和“render函数”的关系和用法

1,821 阅读7分钟

我们有时候在工作和学习中,或多或少都了解掌握了不少关于Vue原理方面的一些知识。但真正遇到一些具体的方法或函数时,还是会有些一知半解。因为有些内容,牵扯比较多,需要连贯在一起才能理解其真正含义。

现在举个例子,有个需要直接在JS代码里引用组件并使其运行的功能(比如,一个自定义的弹窗,类似于 antDSmessage组件)。我们可以想到一个Vue3提供的方法,render函数,那么我们该如何使用,这个函数又是如何要运作的,这就是我们今天要探讨的问题。

前置讨论:

进入正题前,我觉得可以先简单理解下Vue的三大模块:

Compiler模块:编译模板系统;

Runtime模块:也可以称之为Renderer模块,真正渲染的模块;

Reactivity模块:响应式系统;

image.png

先放一张网上的图,将各个环节,简单地分为了上诉三类。

第一个:

template模板通过编译系统,经过转化为语法抽象树 AST(反应标签属性的树形结构)并加以优化处理,最后生成一个渲染函数,它接受数据作为参数,并返回一个 VNode(虚拟节点)树。

需要注意的是,这个渲染函数很重要,Vue会在后面对新旧VNode进行比较时,需要使用它。

在Vue3里,这一段代码就是作为生成VNode

 function createBaseVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, shapeFlag = type === Fragment ? 0 : 1, isBlockNode = false, needFullChildrenNormalization = false) {
    const vnode = {
      __v_isVNode: true,
      __v_skip: true,
      type,
      props,
      key: props && normalizeKey(props),
      ref: props && normalizeRef(props),
      scopeId: currentScopeId,
      slotScopeIds: null,
      children,
      component: null,
      suspense: null,
      ssContent: null,
      ssFallback: null,
      dirs: null,
      transition: null,
      el: null,
      anchor: null,
      target: null,
      targetAnchor: null,
      staticCount: 0,
      shapeFlag,
      patchFlag,
      dynamicProps,
      dynamicChildren: null,
      appContext: null,
      ctx: currentRenderingInstance
    };
    if (needFullChildrenNormalization) {
      normalizeChildren(vnode, children);
      if (shapeFlag & 128) {
        type.normalize(vnode);
      }
    } else if (children) {
      vnode.shapeFlag |= isString(children) ? 8 : 16;
    }
    if (vnode.key !== vnode.key) {
      warn$1(`VNode created with invalid key (NaN). VNode type:`, vnode.type);
    }
    if (isBlockTreeEnabled > 0 && // avoid a block node from tracking itself
    !isBlockNode && // has current parent block
    currentBlock && // presence of a patch flag indicates this node needs patching on updates.
    // component nodes also should always be patched, because even if the
    // component doesn't need to update, it needs to persist the instance on to
    // the next vnode so that it can be properly unmounted later.
    (vnode.patchFlag > 0 || shapeFlag & 6) && // the EVENTS flag is only for hydration and if it is the only flag, the
    // vnode should not be considered dynamic due to handler caching.
    vnode.patchFlag !== 32) {
      currentBlock.push(vnode);
    }
    return vnode;
  }


此处定义了VNode的一些属性。

第二步:

渲染模块,简单来说,就是将我们得到的VNode,经过一系列的操作,转换成被浏览器识别的真实DOM,并且这期间,第三步提到的响应式功能也在此过程中被运用上去。此处涉及到源码方法是 baseCreateRenderer,因为里面内容比较多,包括各种标签属性的生成,patch方法等,这里就不详解了。后面的render函数会讲到部分内容。

所以,与其说是三个模块,不如说是一个整体,区分开只是为了便于理解。

现在我们重回正题,首先,我们理解一下h函数。

h函数

官方解释为:创建虚拟 DOM 节点 (vnode)。那就很好理解了,对应到我们上面第一步的编译部分。


function h(

  type: string | Component,

  props?: object | null,

  children?: Children | Slot | Slots

): VNode

第一个参数既可以是一个字符串 (用于原生元素) 也可以是一个 Vue 组件定义。第二个参数是要传递的 prop,第三个参数是子节点。

当创建一个组件的 vnode 时,子节点必须以插槽函数进行传递。如果组件只有默认槽,可以使用单个插槽函数进行传递。否则,必须以插槽函数的对象形式来传递。

用法1-可以创建原生元素(就贴几个官网的例子了)
// attribute 和 property 都可以用于 prop

// Vue 会自动选择正确的方式来分配它

h('div', { class: 'bar', innerHTML: 'hello' })

// children 可以是一个字符串

h('div', { id: 'foo' }, ‘hello')

// children 可以是一个字符串

h('div', { id: 'foo' }, ‘hello')
用法2-创建组件

import Foo from './Foo.vue' 

// 传递 prop

h(Foo, {

  // 等价于 some-prop="hello"

  someProp: 'hello',

  // 等价于 @update="() => {}"

  onUpdate: () => {}

})
使用h函数

现在我们定义好了 h 函数,那我们就得去使用。但是,Vue2和Vue3的使用方法略有不同。我这里就简单做个区分。

Vue2:
import Vue from 'vue';

export default {

  render(h) {

   *//* 使用 *h* 函数

  }

}
Vue3:
*// vue3*

import { h } from 'vue';

export default {

  setup() {

  return () => h('div', 'Hello World');

  }

}
// two

import { defineComponent, h } from "vue";

export default defineComponent({

  render() {

    const props = { style: { color: "red" } };

    return h("h2", props, "123456789");

  },

可以看到,Vue3相较于Vue2最大区别是函数的引入方式。一个是通常在 render 函数中通过参数传递,另一个可以通过vue模块引入。并且,vue3的setup语法,支持不用render方法,直接return即可。

render函数

上述已经使用到了render函数,这里的render函数很好理解,一种代替template去创建html的另一种方式。这是render函数的一种用法,多用于一个组件文件。

除此之外,还有另一种使用方式,直接从vue模块里引入,直接在JS方法里使用。

这里举一个例子:

export const useRender = function () {
  //获取调用者的上下文
  const { appContext } = getCurrentInstance() || {};
  return function (component, props, slots = {}) {
    return new Promise((resolve) => {
      // 此处的container指的是挂载元素,此处是额外定义一个DOM,用于类似弹窗的功能
      //DocumentFragments 是 DOM 节点。它们不是主 DOM 树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到 DOM 树。在 DOM 树中,文档片段被其所有的子元素所代替。
      //因为文档片段存在于内存中,并不在 DOM 树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。

      const container = document.createDocumentFragment();
      let vm = h(
        component,
        {
          ...props,
          onClose: function (data) {
            resolve(data);
            //取消渲染
            setTimeout(() => {
              render(null, container);
              vm = null;
            }, 300);
          },
        },
        { ...slots },
      );
      // 将生成的虚拟DOM的上下文对应到调用法那里
      if (appContext) {
        vm.appContext = appContext;
      }
      render(vm, container);
    });
  };
};

通过上面的内容,我们可以总结出,使用render函数渲染没有编译过程,相当于直接将Vnode给出去。render的性能较高,template性能较低。同时,在方法一中,Render 函数的优先级要比template的级别要高,并且如果一个组件中同时存在 render 和 template,则 render 将具有更高的优先级。

再拿出官方的解释:render 是字符串模板的一种替代,可以使你利用 JavaScript 的丰富表达力来完全编程式地声明组件最终的渲染输出。

我们可以看一下render函数源码

const render = (vnode, container, namespace) => {
      if (vnode == null) {
        if (container._vnode) {
          unmount(container._vnode, null, null, true);
        }
      } else {
        patch(
          container._vnode || null,
          vnode,
          container,
          null,
          null,
          null,
          namespace
        );
      }
      if (!isFlushing) {

    。。。

      }
      container._vnode = vnode;
    };

省略一些不相关的代码,render函数接受三个参数,vnode(创建的虚拟DOM),container(挂载 的节点),namespace(命名空间)。方法内首先判断是否是空vnode,是的话,则通过存在container内的就vnode进行卸载。否则通过patch方法,去更新vnode的同时,对真实DOM进行修观。此处就涉及到diff算法了,不详细解读。最后,将当前的vnode缓存在container中备用,就是我们的第一步。

结论

至此,关于h函数和render函数的内容,我大概做了一个简单分析。先从Vue渲染本质出发,一步步找到他们的用武之地,并大致理解一下内部原理。最后我们发现,这两者配合使用,可以组成一个区别于template写法的组件建立思路。

其实,Vue 里的各个方法都是环环相扣的,你会发现,一个方法里往往调用了多个其他方法,只有仔细的去将其理解清楚,才能窥得其中真义。这对我来说是还是任重而道远的,以上只是一些个人见解,还望大佬发现问题能及时提出,虚心接受,感谢!