我们有时候在工作和学习中,或多或少都了解掌握了不少关于Vue原理方面的一些知识。但真正遇到一些具体的方法或函数时,还是会有些一知半解。因为有些内容,牵扯比较多,需要连贯在一起才能理解其真正含义。
现在举个例子,有个需要直接在JS代码里引用组件并使其运行的功能(比如,一个自定义的弹窗,类似于 antDS 的message组件)。我们可以想到一个Vue3提供的方法,render函数,那么我们该如何使用,这个函数又是如何要运作的,这就是我们今天要探讨的问题。
前置讨论:
进入正题前,我觉得可以先简单理解下Vue的三大模块:
Compiler模块:编译模板系统;
Runtime模块:也可以称之为Renderer模块,真正渲染的模块;
Reactivity模块:响应式系统;
先放一张网上的图,将各个环节,简单地分为了上诉三类。
第一个:
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 里的各个方法都是环环相扣的,你会发现,一个方法里往往调用了多个其他方法,只有仔细的去将其理解清楚,才能窥得其中真义。这对我来说是还是任重而道远的,以上只是一些个人见解,还望大佬发现问题能及时提出,虚心接受,感谢!