react、vue组件编译区别&template解析原理

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。

通常你会在你组件的最后返回这个元素,或者把它作为另一个元素的子元素。虽然你可以读取元素的属性,但你最好把创建的元素作为黑盒,只用于渲染。

相关推荐
come112341 分钟前
Vue 响应式数据传递:ref、reactive 与 Provide/Inject 完全指南
前端·javascript·vue.js
前端风云志23 分钟前
TypeScript结构化类型初探
javascript
musk121239 分钟前
electron 打包太大 试试 tauri , tauri 安装打包demo
前端·electron·tauri
翻滚吧键盘1 小时前
js代码09
开发语言·javascript·ecmascript
万少2 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL2 小时前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl022 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang2 小时前
前端如何实现电子签名
前端·javascript·html5
海天胜景2 小时前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui
今天又在摸鱼2 小时前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js