精读Vue官方文档之渲染函数 & JSX

废话少说,直接上阅读总结后的干货!!!

模板语法

模板语法的优点:

  • 上手简单方便。
  • 可对 VNode 进行优化。

因为模板内容是静态的,所以可以实现对这些静态内容进行分析,得知那些节点,属性是动态的、那些节点、属性又是静态的,有了这些信息,就可以在创建或者对比(Diff) VNode 时对其 靶向更新 —— 也就是传说中 patchFlags

当然,模板语法也存在一些局部的学习成本与心智负担,例如:

  • scopeSlots 作用域插槽传值。
  • props 、attrs、domProps 的区分。
  • 种类丰富且功能强大面向用户黑盒的指令系统。

Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数JSX,以获得更强的灵活性。

渲染函数

渲染函数指的就是optionsAPI 中提供的 render 选项,值是一个函数,接收一个 createELement 方法作为参数来创建虚拟DOM。

createElement

createElement 方法接收三个参数。

createElement(<tag | component | asyncComponent> [, VNodeAttrData, Children])
复制代码

  1. <tag | component | asyncComponent>

必传,值可以是字符串形式的HTML标签

export default {
  render(createElement) {
    return createElement("h1");
  },
};
复制代码

或者接收一个已经转换为渲染函数形式的组件对象:

import { compileToFunctions } from "vue-template-compiler";
//导入一个会被CLI转换为渲染函数的组件
import Content from "./Content.vue";
//自定义一个渲染函数形式的组件
const Badge = {
  name: "Badge",
  render(createElement) {
    return createElement("span", [
      createElement("i", { attrs: { class: "icon icon-badge" } }, this.$slots.default),
    ]);
  },
};
//使用内置的compile方法将模板字符串转换为渲染函数
const Title = compileToFunctions("<h1>Title</h1>");

export default {
  render(createElement) {
    return createElement("div", [
      createElement(Title),
      createElement(Content),
      createElement(Badge, ["open"]),
    ]);
  },
};
复制代码

对于Vue的内置组件,注意组件名小写,采用 kebab-case 命名法。

export default {
  render(createElement) {
    return createElement("transition", { props: { appear: true } }, [
      createElement("p", "this is p"),
    ]);
  },
};
复制代码

除此之外,还可以接受一个方法,用来返回一个异步组件

<script>
const asyncComponent = ()=>import('./Test.vue');
const Badge = {
    name:'Badge',
    render(createElement){
        return createElement('div', 'lazy component');
    }
};
const LazyBadge = ()=> new Promise((resolve, reject)=>{
    setTimeout(()=>{
        resolve(Badge);
    }, 1000)
})

export default {
  render(createElement) {
    return createElement('div', [asyncComponent, LazyBadge]);
  },
};
</script>
复制代码

  1. VNodeAttrData

可选参数,值是 VNodeAttributes 数据对象。

Attributes (Object) 类型 示例 说明
class Object {foo:true, bar:false} v-bind:class 的 API 相同
style Object {color:’red’, fontSize:’14px’} v-bind:style 的 API 相同
attrs Object {id:’avatar’, title:’user avatar’} 普通的 HTML attribute
props Object {level:1} 组件的 prop
domProps Object { innerHTML: “Rewrite the title” } DOM 对象的 property,例如 innerHTMLtitlelang
on Object {click: this.clickHandler} 使用vue的事件监听
nativeOn Object {click: this.nativeClickHandler} 使用原生事件监听
directives Object {name:’my-custom-directive’,value:’2′,expression:’1 + 1′,arg:’foo’,modifiers:{bar:true}} 定义使用到的自定义指令。
slot String ‘default’ 如果组件是其它组件的子组件,则为其指定插槽名称。
scopedSlots Object {default:(slotProps)=>createElement(‘span’, slotProps.message)} 定义作用域插槽生成内容的方法,这块会在父组件中定义,然后传入到子组件中,最后再由子组件进行调用并传入插槽的属性
key String Math.random().toString() 组件唯一标识的key
ref String ‘myRef’
refsInFor Boolean true 如果在渲染函数存在多个元素具有重名的ref名,则 $refs.myRef 会变成一个数组。

domProps 示例:

createElement(
  "h1",
  { domProps: { innerHTML: "Rewrite the title" } },
  "This is the first level heading"
);
复制代码

作用域插槽并传值:

先看下基于模板语法来使用作用域插槽。

<!--App.vue-->
<template>
  <world-time>
    <template v-slot:BeiJing="slotProps">
      <b>{{ slotProps.time }}</b>
    </template>
    <template v-slot:London="slotProps">
      {{ slotProps.time }}
    </template>
  </world-time>
</template>

<!--WorldTime.vue-->
<template>
  <div>
    <slot name="BeiJing" :time="new Date().toLocaleString()"></slot>
    <slot name="London" :time="new Date().toUTCString()"></slot>
  </div>
</template>
复制代码

现在将其改造成渲染函数方式,先在组件使用的地方通过 scopedSlots 选项声明作用域插槽方法,然后将其作为属性数据对象传入到定义接收插槽的组件 world-time 中。

import WorldTime from "./WorldTime.vue";
export default {
  components: { "world-time": WorldTime },
  render: function(c) {
    return c("world-time", {
      scopedSlots: {
        BeiJing: (props) => c("b", props.message),
        London: (props) => props.message,
      },
    });
  },
};
复制代码

最终由 WorldTime 组件来调用作用域插槽方法,并传入要跨作用域传递的参数。

export default {
  render(c) {
    return c("div", [
      this.$scopedSlots.BeiJing({ message: new Date().toLocaleString() }),
      this.$scopedSlots.London({ message: new Date().toUTCString() }),
    ]);
  },
};
复制代码

  1. Children

可选参数,子级虚拟节点。
值可以是文本节点:

export default {
  render(c) {
    return c("h1", { attrs: { style: "color:red" } }, "This is a title!");
  },
};
复制代码

也可以是包含多个虚拟子节点的数组。

export default {
  render(c) {
    return c("ul", [c("li", "item1"), c("li", "item2")]);
  },
};
复制代码

虚拟DOM

Vue 通过创建虚拟DOM来追踪如何改变文档中的真实DOM。

虚拟DOM 是由 虚拟节点(Virtual Node) 组成的 VNode 树。每个VNode 都描述了对应DOM节点与其子节点的相关信息。

VNode 树中的所有 VNode 都必须唯一。这一点也与真实DOM节点的规范一直,如果对文档中的某一个DOM 对象进行多次添加操作,实际上执行的是移动操作。

对于确实需要重复很多次的元素/组件,可以使用工厂函数来实现:

render: function (createElement) {
  return createElement('div',
    Array.apply(null, { length: 20 }).map(function () {
      return createElement('p', 'hi')
    })
  )
}
复制代码

JSX

JSX 是对 JavaScript 语法进行扩展的语法糖
JSX = Javascript + XML;即在 JavaScript 里面写 XML,所以它即具备了 JavaScript 的灵活性,同时又兼具 HTML 的语义化和直观性。

Vue-CLI 3+ 已经内置了对 jsx 语法的支持,如果低于此版本,则需要单独对 babel进行配置

基础示例:

<script>
export default {
  data() {
    return {
      search: "",
      list: [
        { name: "peach", key: 1 },
        { name: "grape", key: 2 },
        { name: "mango", key: 3 },
      ],
    };
  },
  computed: {
    filters() {
      return this.list.filter((item) => item.name.indexOf(this.search) !== -1);
    },
    isEmpty() {
      return !this.filters.length;
    },
  },
  render() {
    const InputProps = {
      class: { empty: this.isEmpty },
      attrs: { type: "text", placeHolder: "请输入搜索内容", ...this.$attrs },
    };

    return (
      <div>
        <input
          {...InputProps}
          onInput={(e) => {
            this.search = e.target.value;
          }}
        />
        <ul>
          {this.filters.map((item) => (
            <li key={item.key}>{item.name}</li>
          ))}
        </ul>
      </div>
    );
  },
};
</script>
复制代码

在 Vue 中使用 JSX 进行开发,还需要注意避免以下情况:

  1. 不能再 render 方法内定义名称为 h 标识符,因为该变量名已经存在,再次定义会报重命名错误。
  2. 只能在组件实例内部将标签或组件赋值给另一个标量,否则会报 createElement 方法未定义。
const h1 = <h1></h1>; // Uncaught ReferenceError: h is not defined
export default {
  data(){
    return {
        elements:[<li></li>,<li></li>] //correct!
    }
  },
  methods:{
    getImage(){  //correct!
        return <img />
    }
  },
  render() {
    return h1;
  },
};
复制代码
  1. JSX 的函数式组件可以在组件实例的内外都能定义,但是需要注意不能将其包装到对象或一个数组,否则编译程序将不能正确识别,只会返回一个渲染函数。
const H1 = () => <h1></h1>;
const H3 = [()=> <h3></h3>]; //bad
export default {
  functional: true,
  render() {
    const H2 = () => <h2></h2>;
    return (
      <div>
        <H1 />
        <H2 />
      </div>
    );
  },
};
复制代码

更多Vue 中的 JSX 实践,可以阅读 AntV 或 Vant 源码。

函数式组件

函数式组件就是无状态组件。

函数式组件本质就是一个函数,它没有生命周期、没有状态(响应式数据)、没有实例(this 上下文),只会通过 render 函数的第二个 context 参数传递 prop,因此它执行起来的渲染开销要低很多。

我们分别可以通过模板语法、渲染函数、JSX来创建不同形式的“函数式组件”。

模板语法版函数式组件

<template functional>
  <p>This is a functional component</p>
</template>
复制代码

JSX版函数式组件

const H1 = () => <h1></h1>; //子组件也是函数式
export default {
  functional: true, //声明当前组件是函数式。
  render() {
    return <H1 />;
  },
};
复制代码

渲染函数版函数式组件

export default {
  functional: true,
  render(c, context) {
    return c("p", context.data, "This is a functional component!");
  },
};
复制代码

更多关于函数式组件参数传递可以看这里>函数式组件

模板、JSX与渲染函数

Vue 创建 HTML 的方式主要有:模板、JSX、渲染函数,最终它们都会殊途同归,统一转换为“渲染函数”。

image.png

“JSX”与“渲染函数”都需要在 OptionsAPIrender 选项中使用,而模板语法则在 SFC 独立的 template 区块下使用。

生成渲染函数的方法有多种:

  • 调用 compileToFunctions 方法获得转换后的结果。
  • 通过 Vue-CLI 的辅助,导入一个模板语法的组件,获得编译后的渲染函数结果。
  • 使用 import() 异步导入Vue组件,此种方式依然是借助 Vue-CLI 的辅助。
  • 手动编写渲染函数,注意包装的结构:{render:ƒ}

最后,一个很重要的问题,我们何时该选择使用模板、JSX还是渲染函数方式?

特性 模板 JSX 渲染函数
是否需要简单直观易上手? ★★★
是否需要JS完全编程能力? ★★ ★★★
是否需要在一个模块(SFC)中定义多个组件? ★★★ ★★★
是否需要将单个组件或标签赋值给某个变量或者是定义为函数式组件,用于内容的灵活组装? ★★★ ★★★
是否需要更好的复用 propsattrsevents?例如通过 spread 或 reset 等运算符进行更好的组合、分解、绑定。 ★★★ ★★★
是否需要动态修改标签或组件名称? ★★★
是否需要访问组件实例外的变量或数据? ★★★ ★★★
组件、props、attrs、events 等是否需要更灵活的参与条件运算? ★★ ★★★ ★★★
是否需要支持 Typescript ? ★★★ ★★★
递归调用是否需要更简单直观? ★★★ ★★★
是否需要更简单易懂的方式使用 scopeSlots ? ★★ ★★★

是否需要JS完全编程能力?

由于组件模板与组件逻辑是脱离的,所以组件逻辑如果想对模板中的标签进行操作,则必须按照数据驱动的思想来实现。

export default {
  props: {
    reverse: Boolean,
  },
  render() {
    const children = [
      <li class="foo" style="color:red">
        1
      </li>,
      <li>2</li>,
      <li class="bar">3</li>,
    ];
    if (this.reverse) {
      children.reverse();
    }
    console.log(children);
    return <ul>{children.map((item) => item)}</ul>;
  },
};
复制代码

?,思考下如果采用模板的方式实现,我们则必须要基于数据驱动的思路来解决这个,要专门定义对应的数据结果,然后遍历渲染。

是否需要访问组件实例外的变量或数据?

模板的执行上下文都是在编译时绑定好的,而绑定的目标就是组件的实例 this,所以模板内要进行插值的变量都只能从组件实例已经挂载的数据中取,而不能从其它作用域中获取数据,限制了参与HTML内容生成的数据来源的灵活性。

模板编译

Vue.compile 方法可以将模板字符串转换为字符串形式的渲染函数并保留 AST 对象。

Vue.compile('<p></p>');
/*
{
    "ast": {
        "type": 1,
        "tag": "p",
        "attrsList": [],
        "attrsMap": {},
        "rawAttrsMap": {},
        "children": [],
        "plain": true,
        "static": true,
        "staticInFor": false,
        "staticRoot": false
    },
    "render": "with(this){return _c('p')}",
    "staticRenderFns": [],
    "errors": [],
    "tips": []
}
*/
复制代码

staticRenderFns 保存的是静态节点,例如使用 v-once 指令的组件或标签。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享