Vue渲染函数该如何使用?有哪些需要注意的地方?

场景分析

Vue的模板语法适用于绝大部分的需求场景(模板最终会被编译为渲染函数),在绝大多数情况下,Vue 推荐使用模板语法来创建应用。然而在某些使用场景下,我们真的需要用到 JavaScript 完全的编程能力,举例如下:

1.不确定层级的菜单

假设设计一个开源的后台管理系统,侧边栏菜单需要根据路由自动生成菜单,由于系统可能会被用于不同的功能需求。所以路由的层级、数量都是不确定的。

如果通过模板语法来写,假设路由最多只有三层,我们当然可以在模板内通过if加循环来适配所有需求场景,但是实际场景并非如此。

2.组织架构

组织架构的常见实现就是Tree组件,Tree组件的特点之一就是没有确定数量的数据、没有确定数量的层级。此处可以思考一下,如果使用模板语法该如何去实现这样的一个功能组件?

3.总结分析

通过渲染函数,对于以上的例子我们完全可以通过递归满足生成任意层级、数量的菜单栏、Tree分支。(此处不作具体展开)。

我们可以先推出结论:模板适用于“组件结构是确定的” 这种需求场景,此处的确定可以简单理解为:“嵌套的层级是确定的”,在这种情况下模板语法比渲染函数更加简单易用。但是当组件结构层级不确定时,渲染函数显然更加合适。

使用渲染函数

1.选项式API

//选项式API
export default {
  props: ['message'],
  render() {
    return [
      // <div><slot /></div>
      h('div', this.$slots.default()),

      // <div><slot name="footer" :text="message" /></div>
      h(
        'div',
        this.$slots.footer({
          text: this.message
        })
      )
    ]
  }
}

2.组合式API

export default {
  props: ['message'],
  setup(props, { slots }) {
    return () => [
      // 默认插槽:
      // <div><slot /></div>
      h('div', slots.default()),

      // 具名插槽:
      // <div><slot name="footer" :text="message" /></div>
      h(
        'div',
        slots.footer({
          text: props.message
        })
      )
    ]
  }
}

使用总结

1.vNode 必须唯一

同一个vNode对象,不能被多次用于渲染函数,必须保证vNode的唯一性;

2.v-model需要自己实现

v-model语法糖会被拆分为modelValue和onUpdate:modelValue事件,在渲染函数中需要我们自己实现双向绑定的逻辑处理;

3.传递插槽

// 单个默认插槽
h(MyComponent, () => 'hello')

// 具名插槽
// 注意 `null` 是必需的
// 以避免 slot 对象被当成 prop 处理
h(MyComponent, null, {
    default: () => 'default slot',
    foo: () => h('div', 'foo'),
    bar: () => [h('span', 'one'), h('span', 'two')]
})

4.渲染子元素

对于组件的子元素,每一个非纯字符串的子元素都应该通过传递一个返回Vnode的函数来指定,函数返回值可以是vNode、Vnode数组、插槽对象表示的vNode

h(FormItem,null,()=>{default:h("div")}) //对象
h(FormItem,null,()=>h("div"))  //单个VNode
h(FormItem,null,()=>[h("div")]) //数组

需要注意的是如果渲染普通的html标签时,不能返回对象格式(会导致无法渲染,并且不报错);

//这样子不会被渲染,估计是普通的html没有插槽的概念
return h("div",null,{default:()=>h(Item)}
//这样可以
return h("div",null,()=>[h(Item)])
return h("div",null,()=>h(Item))

5.渲染函数的依赖收集

假设组件某属性需要的是Array,通过Ref包装一个数组,直接把这个Ref传递给组件,组件会报错提示需要的是数组,得到的是对象,说明渲染函数中ref 对象不会转换成原数组,然后保持响应式传递给被渲染的组件。

这个过程需要我们自己完成(触发渲染函数的依赖收集机制)。测试如下:

//item是一个ref,这样会触发依赖收集保持响应式
h("input",{value:item.value});

//这样就不会
let attr={
    value:item.value
}
h("input",attr);

//这样才可以
let attr={
    value:item.value
}
h("input",Object.assign({},attr));

经过测试,在渲染函数内被调用的ref,reactive对象都会收集依赖保持响应式,在渲染函数调用前定义 let attr={ value:item.value },在这个过程没有依赖收集,value被赋值的是一个普通的值,所以不会具有响应性(直接传递ref对象,会导致类型错误)。

6.这样也会收集依赖

 () => h(
        components[item.type],
         Object.assign(
          {value: props.data[item.key] }, 
         item.attr,
         options.data.length == 0 ? {} : {options: options.data}))
 )));

7.依赖收集

/* 这样会收集,options改变会进行响应 */
Object.assign(
        {
           value: props.data[item.key]
        }, item.attr,
        isRef(item.attr.options) ? {options: item.attr.options.value} : {},
    options.data.length == 0 ? {} : {options: options.data}))

/* 这样options改变不会进行响应 */
Object.assign(
       {
         value: props.data[item.key]
       },
        Object.assign(item.attr, isRef(item.attr.options) ? {options: item.attr.options.value} : {}),
        options.data.length == 0 ? {} : {options: options.data}))

其它的知识

1.reactive

reactive() API 有两条限制:仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效。

Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失:

let a=reactice({
   b:{c:1}
})

a.b.c++; //响应性保持

let c=a.b.c;
c++; //c已经独立了,没有响应性

let c=a.b;
c.c++; //还保持着引用,响应性存在

let d=a.b;
d={c:1};
d.c++; //这就没了,因为d整个Proxy对象被替换了,变成没有代理的对象了。

2.绑定事件

事件绑定和属性是一样的,只不过事件属性需要以on开始,例如onUpdate:value,监听的就是update:value事件。

3.渲染的时机

每次依赖更新的时候,都会重新调用渲染函数然后刷新DOM,简单说就是setup只会运行一次,渲染函数每次刷新的时候都会调用。