react、vue组件打包编译为js时的区别
1.react组件打包为js后,jsx会被编译为React.createElement.
比如:antd的button.js(函数式组件直接return jsx)
const InternalButton = (props, ref) => { //React.createElement第三个参数children一般兼容传数组和分开多个参数传递俩种形式。
const iconNode = icon && !innerLoading ? ( /*#__PURE__*/_react.default.createElement(_IconWrapper.default, { /*...省略*/ }));
const kids = children || children === 0 ? (0, _buttonHelpers.spaceChildren)(children, needInserted && autoInsertSpace) : null;
let buttonNode = /*#__PURE__*/_react.default.createElement("button", Object.assign({}, rest, {
type: htmlType, className: classes, style: fullStyle, onClick: handleClick, disabled: mergedDisabled, ref: buttonRef
}), iconNode, kids, compactItemClassnames && /*#__PURE__*/_react.default.createElement(_compactCmp.default, { key: "compact", prefixCls: prefixCls })
);
return wrapCSSVar(buttonNode);
};
const Button = /*#__PURE__*/(0, _react.forwardRef)(InternalButton);
var _default = exports.default = Button;
2.vue2组件打包为js后,template会被解析为 _c函数(即createElement)。
比如:vue2 中element-ui的button.js
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"button",
{
staticClass: "el-button",
class: [ _vm.type ? "el-button--" + _vm.type : "", _vm.buttonSize ? "el-button--" + _vm.buttonSize : "", { "is-disabled": _vm.buttonDisabled, "is-loading": _vm.loading } ],
attrs: { disabled: _vm.buttonDisabled || _vm.loading, autofocus: _vm.autofocus, type: _vm.nativeType },
on: { click: _vm.handleClick }
},
[
_vm.loading ? _c("i", { staticClass: "el-icon-loading" }) : _vm._e(),
_vm.icon && !_vm.loading ? _c("i", { class: _vm.icon }) : _vm._e(),
_vm.$slots.default ? _c("span", [_vm._t("default")], 2) : _vm._e()
]
)
}
var component = Object(componentNormalizer["a" /* default */])(
src_buttonvue_type_script_lang_js_, render, staticRenderFns, false, null, null, null
)
/* harmony default export */ var src_button = (component.exports);
src_button.install = function (Vue) { Vue.component(src_button.name, src_button); };
3.vue3组件打包为js后,template会被解析为 h函数(import {h} from vue) h函数底层调用的时createVNode方法。
比如: vue3中naive-ui 的Button.js
const Button = (0, vue_1.defineComponent)({//h函数第三个参数children一般兼容传数组和分开多个参数传递俩种形式。
render() {
const { mergedClsPrefix, tag: Component, onRender } = this;
const children = (0, _utils_1.resolveWrappedSlot)(this.$slots.default, (children) => children && ((0, vue_1.h)("span", { class: `${mergedClsPrefix}-button__content` }, children)));
return ((0, vue_1.h)(Component, { ref: "selfElRef",class: [ this.themeClass, `${mergedClsPrefix}-button` ],
this.iconPlacement === 'right' && children,
!this.text ? ((0, vue_1.h)(_internal_1.NBaseWave, { ref: "waveElRef", clsPrefix: mergedClsPrefix })) : null,
this.showBorder ? ((0, vue_1.h)("div", { "aria-hidden": true, class: `${mergedClsPrefix}-button__border`, style: this.customColorCssVars })) : null,
this.showBorder ? ((0, vue_1.h)("div", { "aria-hidden": true, class: `${mergedClsPrefix}-button__state-border`, style: this.customColorCssVars })) : null));
)
}
});
exports.default = Button;
vue提供了两类版本
1.vue.runtime.js 运行时版本, 不提供模板编译能力(代码中不能有任何的template,得用render函数);
2.vue.js 完整版本,包含了模板编译的能力(代码中可以存在template,因为有compiler可以用于编译template)
webpack中可以调整要使用哪个版本的vue
resolve: { extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
//或者 'vue/dist/vue.runtime.esm.js' 有runtime的是运行时,否则就是完整版
}
},
vue单文件组件(SFC)编译模块 @vue/sfc-compiler 与 vue-template-compiler 区别:
都是用来对.vue单文件组件(SFC)解析编译用的,@vue/compiler-sfc的定位就相当于之前vue2版本的vue-template-compiler,在Vue3项目中使用了@vue/compiler-sfc来代替vue-template-compiler,用于vue-loader插件中。
ps:关于vue-template-compiler对vue3的兼容。 vue-template-compiler在2.6.14版本之前是支持vue2.x的,2.7开始之后加上了vue3的兼容(在使用vue2项目中,vue-template-compiler插件最为适合的版本是2.6.14)。
vue2.7x中加入了针对@vue/compiler-sfc和composition-api的兼容性支持。
基于vite开发对应的打包插件
vue2.6: vite-plugin-vue2@2.6.14 + vue-template-compiler@2.6.14
vue2.7: vite-plugin-vue2@2.7.9 + vue-template-compiler@2.7.9; 或者@vitejs/plugin-vue2 + @vue/compiler-sfc
vue3: @vitejs/plugin-vue + @vue/compiler-sfc
额外:为 Vue 2 和 3 编写通用 Vue 库可以使用vue-demi。vue-demi的工作是通过postinstall和 npx vue-demi-fix指令,判断当前项目安装的vue版本,然后将对应版本的插件复制到lib的根目录,其插件的功能就是抹平vue2和vue3版本使用composition-api时的差异
Vue Demi采用了多种策略来适应不同Vue版本:
对于Vue 2.6及以下,它导出vue和@vue/composition-api。
在Vue 2.7中,由于Composition API已内建,它只导出vue。
对于Vue 3及以上版本,它同样导出vue,并提供了Vue 2的set和del API的polyfill。
此外,项目还提供了一些额外API,如isVue2和isVue3,以区分运行时环境,以及Vue2对象,用于访问Vue 2的全局API
一、template解析的几种方式:
1.vue单文件组件 (SFC) 中的template标签(vue-loade/sfc-compiler解析-运行时vue库)
1-1.需要webpack等打包工具预编译,将.vue文件编译成.js文件(import/require无法直接加载vue文件,结合打包工具才行)。
单文件组件中template标签,需要配合webpack解析,webpack可以配置vue-loader用于处理.vue的单页面组件文件,而vue-loader的核心就是Vue-template-compiler,用来把template字符串转换成render函数。
每个 .vue 文件最多包含一个 <template> 块。template内容将被提取为字符串并传递给 vue-template-compiler ,预处理为 JavaScript 渲染函数,并最终注入到从 <script> 导出的组件中.
每个 .vue 文件最多包含一个 <script> 块。这个脚本会作为一个 ES Module 来执行.它的默认导出应该是一个 Vue.js 的组件选项对象。也可以导出由 Vue.extend() 创建的扩展对象,但是普通对象是更好的选择。
ps: Vue.extend的作用是创建一个继承自Vue的子类,可接收的参数是一个包含组件选项的对象。除了它本身独有的一些属性方法,还有一些是从Vue类中继承而来,所以创建子类的过程其实就是一边给子类上添加上独有的属性,一边将父类的公共属性复制到子类上。
npm install -D vue-loader vue-template-compiler
webpack.config.js中:{ test:/\.vue$/, use:['vue-loader'] }
1-2.也可以使用@vue/sfc-compiler加载.vue文件并解析为render函数。
2.js中字符串模板写法(使用全量vue库)
export default{
data:(){ return { } },
template:"<h2 style='color:red'>haha</h2>",
}
使用template选项写法,可以不用webpack处理,但必须使用完整版的Vue,才能使用vue的compiler模块在运行时编译,解析template选项字符串为render函数。
3.render函数(运行时vue库)
一般会采用render函数+jsx的方式,简单一些(vue2中需@vue/babel-preset-jsx插件、vue3中需要@vue/babel-plugin-jsx来解析jsx语法)。
直接写render函数的话,无需vue的compiler模块,源码中会直接调用你写的render函数生成模板。 使用运行时vue即可(不提供模板编译能力)。
二、template解析原理:
1.vue-loader解析template原理:
.vue文件中<template>本身是一种字符串,需要提前使用vue-loader(vue-template-compiler)预编译解析为render函数
const compiler = require('vue-template-compiler')
const res = compiler.compile('<span class="active" :total="count">11</span>');//compiler后返回一个对象:{render, staticRenderFns, ast}
vue-template-compiler 的工作原理:
首先,它会使用 html-parser 来解析模板字符串,将其转换为一个抽象语法树 (AST)。 AST 是一种用 JavaScript 对象来表示 HTML 结构的方式,它包含了元素、属性、文本、表达式等节点,以及它们之间的关系。
然后,它会使用 optimize 来优化 AST,标记出静态节点和静态根节点,这样可以在渲染时跳过它们,提高性能。 静态节点是指不依赖于数据的节点,例如纯文本节点或固定属性的元素节点。静态根节点是指只有一个子节点,并且这个子节点是静态节点的元素节点。
接着,它会使用 codegen 来生成渲染函数的代码,包括创建元素、绑定属性、插入文本、添加事件等。 codegen 会遍历 AST,将其中的节点转换为相应的渲染函数的代码片段,然后将这些代码片段拼接成一个完整的渲染函数,并添加一些必要的辅助函数和变量。
最后,它会导出一个包含渲染函数和静态渲染函数的对象,可以被 vue-loader 或其他方式引用,从而创建一个 Vue 组件。 渲染函数是用于创建和更新动态节点的函数,静态渲染函数是用于创建和缓存静态节点的函数。
export default {
render: function () {
with (this) {
return _c( "div", { staticClass: "example" }, [_v(_s(msg))], 1 /* STATIC */ );
}
},
staticRenderFns: [],
};
2.vue2全量库编译template原理:
Vue.js 2.x 的源码托管在 src 目录,然后依据功能拆分出了 compiler(模板编译的相关代码)、core(与平台无关的通用运行时代码)、platforms(平台专有代码)、server(服务端渲染的相关代码)、sfc(.vue 单文件解析相关代码)、shared(共享工具代码)等目录。
Vue.js 3.0,整个源码是通过 monorepo 的方式维护的,根据功能将不同的模块拆分到 packages 目录下面不同的子目录中,每个 package 有各自的 API、类型定义和测试。
在大多数情况下,Vue会在编译阶段将模板(template)转换为渲染函数。
**在vue2中,通过重写Vue.prototype上的$mount方法,使得在调用原始的 $mount 函数之前,从 template 选项中获取模板字符串或 DOM 元素,并将其编译为 render 函数和 staticRenderFns 数组。**确保当前实例在调用 $mount 函数时已经具有 render 函数,从而可以正确地渲染到页面上。
编译时生成的 render 函数会被挂载到组件实例的 $options 对象上的 render 属性中。这样,在组件实例化时,Vue 就可以直接从 $options.render 中获取到预先编译好的 render 函数,而不需要每次都重新编译。
重写$mount方法(src/platforms/web/runtime-with-compiler.ts):
import { compileToFunctions } from './compiler/index'
const mount = Vue.prototype.$mount;//记录原$mount
Vue.prototype.$mount = function (el) {
const vm = this;
const options = vm.$options;
el = document.querySelector(el);
// 如果没有render方法
if (!options.render) {
let template = options.template;
// 如果没有模板但是有el
if (!template && el) {
template = el.outerHTML;
}
const render = compileToFunctions(template);
// 将render函数挂载到options上。
options.render = render;
}
mount.call(this,..)//调用原$mount方法
}
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = options;
// 初始化状态
initState(vm);
// 页面挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
}
三、Vue.comonent全局注册组件后在render中使用:
在 2.x 中,注册一个组件后,把组件名作为字符串传递给render渲染函数的第一个参数,它可以正常地工作:
Vue.component('button-counter', {
data() { return { count: 0 } },
template: ` <button @click="count++"> Clicked {{ count }} times </button> `
})
export default {
render(h) { return h('button-counter') }
}
在 3.x 中,由于 VNode 是上下文无关的,不能再用字符串 ID 隐式查找已注册组件。取而代之的是,需要使用一个导入的 resolveComponent 方法:
import { h, resolveComponent } from 'vue'
export default {
setup() {
const ButtonCounter = resolveComponent('button-counter')
return () => h(ButtonCounter)
}
}
四、vue2渲染虚拟DOM(VNode): 核心是createElement函数
vue2官方对createElement介绍:
https://v2.cn.vuejs.org/v2/guide/render-function.html#createElement-参数
在一个template模板中:<h1>{{ blogTitle }}</h1>
等价于一个渲染函数里:render: function (createElement) { return createElement('h1', this.blogTitle) }
将 h 作为 createElement 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 vue JSX 所要求的。比如:
new Vue({ router, store, render: h => h(App) }).$mount('#app');
//里render: h => h(App)其实就是render:function(createElement){return createElment(App)}
Vue.compile('<h1>{{ blogTitle }}</h1>')//将一个模板字符串编译成 render 函数。只在完整版时可用。
vue2中render函数使用案例:
export default {
name: "AnchoredHeading",
render: function (createElement, context) {
return createElement( "h" + this.level, this.$slots.default );//<h1><slot></slot></h1>
},
props: {
level: { type: Number, required: true }
}
};
-------------------------------------------------------------------------------------------
<template>
<div> <AnchoredHeading :level="1">111</AnchoredHeading> </div>
</template>
<script>
import AnchoredHeading from "./render-function-demo";
export default {
name: "RenderDemo",
components: { AnchoredHeading } // 在模板中使用, 必须先注册
};
</script>
1-1.createElement 接受的参数:
createElement(
//type: {String | Object | Function} 一个 HTML 标签名、组件选项对象,或者 resolve 了上述任何一种的一个 async 函数。必填项。
'div',
//props: {Object} 一个与模板中 attribute 对应的数据对象。可选。
{
//详细见下面
},
//children: {String | Array} 子级虚拟节点 (VNodes),由 `createElement()` 构建而成, 也可以使用字符串来生成"文本虚拟节点"。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, { props: { someProp: 'foobar' } })
]
)
1-2.createElement第二个参数详细介绍:
正如 v-bind:class 和 v-bind:style 在模板语法中会被特别对待一样,它们在 VNode 数据对象中也有对应的顶层字段。该对象也允许你绑定普通的 HTML attribute,也允许绑定如 innerHTML 这样的 DOM property (这会覆盖 v-html 指令)。
{
// 普通的 HTML attribute
attrs: {
id: 'foo'
},
// 组件 prop
props: {
myProp: 'bar'
},
// DOM property
domProps: {
innerHTML: 'baz'
},
// 与 `v-bind:class` 的 API 相同, 接受一个字符串、对象或字符串和对象组成的数组
'class': {
foo: true,
bar: false
},
// 与 `v-bind:style` 的 API 相同,接受一个字符串、对象,或对象组成的数组
style: {
color: 'red',
fontSize: '14px'
},
// 事件监听器在 `on` 内, 但不再支持如 `v-on:keyup.enter` 这样的修饰器。 需要在处理函数中手动检查 keyCode。
on: {
click: this.clickHandler
},
// 仅用于组件,用于监听原生事件,而不是组件内部使用 `vm.$emit` 触发的事件。
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令。注意,你无法对 `binding` 中的 `oldValue` 赋值,因为 Vue 已经自动为你进行了同步。
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 作用域插槽的格式为 { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其它组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
// 其它特殊顶层 property
key: 'myKey',
ref: 'myRef',
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名, 那么 `$refs.myRef` 会变成一个数组。
refInFor: true
}
1-3.返回的vnode结构中核心属性
tag,当前节点的标签名。
data,当前节点的数据对象。
children,子节点,数组,也是 VNode 类型。
context,编译作用域。
elm,当前虚拟节点对应的真实 DOM 节点。
parent,组件的占位节点。
child,当前节点对应的组件实例。
2.VNode 必须唯一: 组件树中的所有 VNode 必须是唯一的。这意味着,下面的渲染函数是不合法的:
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi');
return createElement('div', [
// 错误 - 重复的 VNode
myParagraphVNode, myParagraphVNode
])
}
如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如,下面这渲染函数用完全合法的方式渲染了 20 个相同的段落:
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
3.结合jsx使用:
Vue的jsx 转换与React的jsx 转换是不同的,不能在 Vue 应用中使用 React 的 JSX 转换
需配合babel插件解析:@vue/babel-preset-jsx来解析jsx语法为vue的createElement形式
(1).npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props
(2).babel.config.js中配置:module.exports = { presets: ['@vue/babel-preset-jsx'] }
(3).然后就可以使用jsx语法了,在 JSX 表达式中,使用大括号来嵌入动态值
将 h 作为 createElement 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的。
从 Vue 的 Babel 插件的 3.4.0 版本开始,我们会在以 ES2015 语法声明的含有 JSX 的任何方法和 getter 中 自动注入 const h = this.$createElement,这样你就可以去掉 (h) 参数了 。对于更早版本的插件,如果 h 在当前作用域中不可用,应用会抛错。
render() { //无需再加入(h)参数。 旧版本还需要:render(h){ ... }
return <input type="email" placeholder={this.placeholderText} />
}
4.函数式组件(无状态组件)
(1).如果创建的组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。一个函数式组件就像这样:
Vue.component('my-component', {
functional: true,
props: { ... },// Props 是可选的
// 为了弥补缺少的实例, 提供第二个参数作为上下文
render: function (createElement, context) {
}
})
注意:在 2.3.0 之前的版本中,如果一个函数式组件想要接收 prop,则 props 选项是必须的。在 2.3.0 或以上的版本中,你可以省略 props 选项,所有组件上的 attribute 都会被自动隐式解析为 prop。
当使用函数式组件时,该ref引用将会是 HTMLElement,因为他们是无状态的也是无实例的(html元素的ref是HTMLElement,而Vue普通组件的ref是VueComponent)。
在 2.5.0 及以上版本中,如果你使用了单文件组件,那么基于模板的函数式组件可以这样声明:<template functional> </template>
(2).组件需要的一切都是通过 context 参数传递,它是一个包括如下字段的对象:
props:提供所有 prop 的对象
children:VNode 子节点的数组
slots:一个函数,返回了包含所有插槽的对象
scopedSlots:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
parent:对父组件的引用
listeners:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
injections:(2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的 property。
(3).函数式组件的优点:
1).因为函数式组件只是函数,所以渲染开销也低很多。
2).在作为包装组件时它们也同样非常有用。比如,当你需要做这些时:
程序化地在多个组件中选择一个来代为渲染;
在将 children、props、data 传递给子组件之前操作它们。
在普通组件中,没有被定义为 prop 的 attribute 会自动添加到组件的根元素上,将已有的同名 attribute 进行替换或与其进行智能合并。 然而函数式组件要求你显式定义该行为:
//通过向 createElement 传入 context.data 作为第二个参数,我们就把 my-functional-button 上面所有的 attribute 和事件监听器都传递下去了。
Vue.component('my-functional-button', { //全局注册后,可作为<my-functional-button>标签使用
functional: true,
render: function (createElement, context) {
// 完全透传任何 attribute、事件监听器、子节点等。
return createElement('button', context.data, context.children)
}
})
**如果你使用基于模板的函数式组件,那么你还需要手动添加 attribute 和监听器。**因为我们可以访问到其独立的上下文内容,所以我们可以使用 data.attrs 传递任何 HTML attribute,也可以使用 listeners (即 data.on 的别名) 传递任何事件监听器
<template functional>
<button class="btn btn-primary" v-bind="data.attrs" v-on="listeners" > <!--普通组件v-bind="data.attrs" v-on="listeners"这一块是自动帮你做的-->
</button>
</template>
五、vue3渲染虚拟DOM(VNode): 核心是h函数(底层调用的是createVnode())
vue3官方文档对h函数介绍:
https://cn.vuejs.org/guide/extras/render-function.html
vue/runtime-core模块中h函数源码如下:
function h(type, propsOrChildren, children) {
const l = arguments.length;
if (l === 2) {
if (shared.isObject(propsOrChildren) && !shared.isArray(propsOrChildren)) {
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren]);
}
return createVNode(type, propsOrChildren);
} else {
return createVNode(type, null, propsOrChildren);
}
} else {
if (l > 3) {
children = Array.prototype.slice.call(arguments, 2);
} else if (l === 3 && isVNode(children)) {
children = [children];
}
return createVNode(type, propsOrChildren, children);
}
}
vue3中render函数使用案例:
import { ref, h } from 'vue'
export default {
name: "Counter",
props: {
/* ... */
},
setup(props) {
const count = ref(1)
// 返回一个render渲染函数
return () => h('div', props.msg + count.value)
}
}
----------------------------------------------------------------------------------------
<template>
<div> <Counter/> </div>
</template>
<script setup>
import Counter from "./counter.vue";
</script>
1-1.Vue 提供了一个 h() 函数用于创建 vnodes:
import { h } from 'vue'
const vnode = h(
'div', // type 必填
{ id: 'foo', class: 'bar' }, // props 可选
[
/* children 可选*/
]
)
1-2.h() 函数的使用方式非常的灵活:
// 除了类型必填以外,其他的参数都是可选的
h('div') 、 h('div', { id: 'foo' })
// attribute 和 property 都能在 prop 中书写, Vue 会自动将它们分配到正确的位置
h('div', { class: 'bar', innerHTML: 'hello' })
// 像 `.prop` 和 `.attr` 这样的的属性修饰符, 可以分别通过 `.` 和 `^` 前缀来添加
h('div', { '.name': 'some-name', '^width': '100' })
// 类与样式可以像在模板中一样, 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })
// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')
// 没有 props 时可以省略不写
h('div', 'hello') 、 h('div', [h('span', 'hello')])
// children 数组可以同时包含 vnodes 与字符串
h('div', ['hello', h('span', 'hello')])
1-3.h()函数返回的VNode结构:
调用h函数得到的 vnode 结构为如下形式:
const vnode = h('div', { id: 'foo' }, []);// vnode: { type:'div', props: {id: 'foo'}, children:[], key:null }
// 节点类型
type: VNodeTypes
// 节点的属性
props: (VNodeProps & ExtraProps) | null
// 便与DOM的复用,主要用在diff算法中
key: string | number | symbol | null
// 被用来给元素或子组件注册引用信息
ref: VNodeNormalizedRef | null
// 子节点
children: VNodeNormalizedChildren
// 组件实例
component: ComponentInternalInstance | null
// 指令信息
dirs: DirectiveBinding[] | null
// vnode对应的DOM
el: HostNode | null
anchor: HostNode | null // fragment anchor
// teleport需要挂载的目标DOM
target: HostElement | null
// teleport挂载所需的锚点
targetAnchor: HostNode | null
2.setup中声明渲染函数 render
当组合式 API 与模板一起使用时**,setup() 钩子的返回值是用于暴露数据给模板** 。然而当我们使用渲染函数时,可以直接把渲染函数返回:
在 setup() 内部声明的渲染函数天生能够访问在同一范围内声明的 props 和许多响应式状态。
import { ref, h } from 'vue'
export default {
props: {
/* ... */
},
setup(props) {
const count = ref(1)
// 返回一个render渲染函数
return () => h('div', props.msg + count.value)
}
}
3.Vnodes 必须唯一,和vue2一样,组件树中的 vnodes 必须是唯一的。下面是错误示范:
function render() {
const p = h('p', 'hi')
return h('div', [
// 重复的 vnodes 是无效的
p
])
}
如果你真的非常想在页面上渲染多个重复的元素或者组件,你可以使用一个工厂函数来做这件事。比如下面的这个渲染函数就可以完美渲染出 20 个相同的段落:
function render() {
return h(
'div',
Array.from({ length: 20 }).map(() => {
return h('p', 'hi')
})
)
}
4.结合jsx/tsx使用:
Vue 的 JSX 转换方式与 React 中 JSX 的转换方式不同,因此你不能在 Vue 应用中使用 React 的 JSX 转换。与 React JSX 语法的一些明显区别包括:
可以使用 HTML attributes 比如 class 和 for 作为 props - 不需要使用 className 或 htmlFor。
传递子元素给组件 (比如 slots) 的方式不同。
在 JSX 表达式中,使用大括号来嵌入动态值:const vnode = <div id={dynamicId}>hello, {userName}</div>
(1).create-vue 和 Vue CLI 都有预置的 JSX 语法支持(配合@vue/babel-plugin-jsx插件),直接使用即可。
如果你想手动配置 JSX: npm install @vue/babel-plugin-jsx -D
配置babel.config.js :{ "plugins": ["@vue/babel-plugin-jsx"] }
(2).tsx支持:
Vue 的类型定义也提供了 TSX 语法的类型推导支持。从 Vue 3.4 开始,Vue 不再隐式注册全局 JSX 命名空间。要指示 TypeScript 使用 Vue 的 JSX 类型定义,请确保在你的 tsconfig.json 中包含以下内容
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "vue"
}
}
你也可以通过在文件的顶部加入 /* @jsxImportSource vue */ 注释来选择性地开启。
如果仍有代码依赖于全局存在的 JSX 命名空间,你可以在项目中通过显式导入或引用 vue/jsx 来保留 3.4 之前的全局行为,它注册了全局 JSX 命名空间。
5.render渲染函数案例 - 传入组件
(1).在给组件创建 vnode 时,传递给 h() 函数的第一个参数应当是组件的定义。这意味着使用渲染函数时不再需要注册组件了 ------ 可以直接使用导入的组件:
ps: 通过template模板渲染需先注册组件(component:{ bar: Bar } <bar>)
import Foo from './Foo.vue'
import Bar from './Bar.jsx'
function render() {// 因为不是在模板中使用, 因此无需注册, 直接使用
return h('div', [h(Foo), h(Bar)]) //jsx语法:return ( <div> <Foo /> <Bar /> </div> )
}
(2).如果一个组件是用名字注册的,不能直接导入 (例如,由一个库Vue.component()全局注册),可以使用 resolveComponent 来解决这个问题。
为了能从正确的组件上下文进行解析,resolveComponent() 必须在setup() 或渲染函数内调用.
import { h, resolveComponent } from 'vue'
export default {
setup() {
const ButtonCounter = resolveComponent('ButtonCounter')
return () => { return h(ButtonCounter) }
}
}
(3).内置组件
诸如 <KeepAlive>、<Transition>、<TransitionGroup>、<Teleport> 和 <Suspense> 等内置组件在渲染函数中必须导入才能使用:
import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'
export default {
setup () {
return () => h(Transition, { mode: 'out-in' }, /* ... */)
}
}
六、React渲染虚拟DOM(VNode): 核心是React.createElement函数:
React官方文档中相关介绍:
https://react.docschina.org/reference/react/createElement
1.createElement 允许你创建一个 React 元素。它可以作为 JSX 的替代方案(jsx只是React.createElement的一个语法糖)。
另外要注意: 要使用hook函数的话,ReactDOM中的react和代码中react必须依赖于同一React
import { createElement } from 'react';
export function Greeting({ name }) {//React函数组件
return createElement( 'h1', { className: 'greeting' }, '你好' ); //<h1>你好</h1>
}
2.参数介绍:
createElement(type, props, ...children)
type:type 参数必须是一个有效的 React 组件类型,例如一个字符串标签名(如 'div' 或 'span'),或一个 React 组件(一个函数式组件、一个类式组件,或者是一个特殊的组件如 Fragment)。
props:props 参数必须是一个对象或 null。如果你传入 null,它会被当作一个空对象。创建的 React 元素的 props 与这个参数相同。注意,props 对象中的 ref 和 key 比较特殊,它们 不会 作为 element.props.ref 和 element.props.key 出现在创建的元素 element 上,而是作为 element.ref 和 element.key 出现。
可选 ...children:零个或多个子节点。它们可以是任何 React 节点,包括 React 元素、字符串、数字、portal、空节点(null、undefined、true 和 false),以及 React 节点数组。
3.返回值
createElement 返回一个 React 元素,它有这些属性:
type:你传入的 type。
props:你传入的 props,不包括 ref 和 key。如果 type 是一个组件,且带有过时的 type.defaultProps 属性,那么 props 中任何缺失或未定义的字段都会采用 type.defaultProps 中的值。
ref:你传入的 ref。如果缺失则为 null。
key:你传入的 key,会被强制转换为字符串。如果缺失则为 null。
通常你会在你组件的最后返回这个元素,或者把它作为另一个元素的子元素。虽然你可以读取元素的属性,但你最好把创建的元素作为黑盒,只用于渲染。