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。

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

相关推荐
迷雾漫步者1 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人5 小时前
前端知识补充—CSS
前端·css
GISer_Jing6 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试