乐趣区

关于vue.js:vue-render-函数使用

render 函数

在 vue 的我的项目入口文件中,上面的代码新建一个 vue 的根组件,并默认命名为 Root,并将其挂载在 HTML 模板 #app div 上,它的模板在哪?

new Vue({render: h => h(App),
}).$mount('#app')

这是一个没有模板的组件。

$mount(‘#app’),选择器对应 dom 会被渲染后果替换,然而会智能地把 dom 上地属性增加到依据组件的根元素上。
手动通过 $mount(elector) 挂载元素,替换 selector 后,dom 的属性失落。两者体现不同,有点奇怪,然而不须要太关注这个区别。

明天再来温习 render 函数,重点关注这些容易踩坑的中央:

  1. 学习 render 函数的应用,重点:款式、事件、插槽、指令、props、v-model、函数组件的解决。
  2. 学习应用 jsx 封装组件。
  3. 在 render 函数中应用表单组件,因为表单组件波及到 v-model 容易踩坑。

为何要理解 render ?

在 render 函数中,可用 jsx 代替模板语法,可充分发挥 js 的能力,使得组件扩展性更好、封装更加优雅。

官网说得比拟明确了,然而例子过于简略,只能体现优雅,没有体现扩展性,稍后封装一个能体现 扩展性 的组件。

render 根底语法

render 函数签名:

render(createElement: CreateElement, hack: RenderContext<Props>): VNode;
  1. 返回值 — VNode (虚构节点) — 一个用于形容 vue 组件构造的 JS 对象

  1. 返回值往往是一个单节点,划线的 context 是 vue 全局对象,有 $children 属性,是一个 VNode 数组,元素是一个 VNode,这样的层层嵌套,正好对应的组件的嵌套。$el 是组件挂载点,\_uid 是组件 id。调试时 可能会用到。
  2. 也能够返回 VNode 数组。比方返回 this.$scopedSlots.default() 这是一个作用域插槽,可能是数组。
  1. 参数

第一个参数 createElement,是一个函数,DOM 中有 createElement 用于创立 DOM 节点。vue 中的 createElement 是用于创立 VNode 的。

h 是 createElement 的别名,代表 Hyperscript (生成 HTML 的脚本),Hyperscript itself stands for “script that generates HTML structures”。

写成h,更加不便输出,更加语义化。

第二个参数 hack 是渲染上下文,组件的propslistenersslots 都在这个参数里。

第二个参数在函数组件中才有,非函数组件为 undefined。因为函数组件中不存在组件实例 this,要提供这个参数获取 props 等内容。

第二个参数通常写成context,更加语义化。

render 写成这样:

render(h,context){return <span>render 函数 </span>}

render 应用 es6 申明办法的形式且返回 jsx,vue 会主动注入 const h = this.$createElement,仅限于 render 函数,其余函数要应用,就要手动通过参数传入。

render(){return <span>render 函数 </span>}

这是 es5 的形式:

render:function(){ // 显示传递 h:  function(h) 才行
  return <span>render 函数 </span>
}

render 不能返回文本,想要显示文本内容,必须套一个标签,react 的 render 能够返回文本。

返回 jsx,vue 外部会调用 createElement编译成 VNode。

如何返回纯文本?

return this._v('someText'),不举荐这么写,会导致别人难以了解。

createElement

返回值:VNode

VNode 是一个形容组件的一般 js 对象。

参数

createElement(
  // html 标签、自定义标签,比方 el-input
  // template // NOTE 用于传递插槽
  // 一个组件选项对象
  // resolve 了上述任何一种的一个 async 函数 // TODO 如何应用
  'div', // NOTE 必须的
  // 模板应用到的数据对象
  {},
  // string 或者 子 VNode
  [])

第一个参数不能省略,所以不能返回纯文本。和 react 的 render 不同。

第一个参数能够是 template, 往往和第二个参数的 slot 属性一起应用,指定插槽名称,传递插槽时能够用到。

resolve 的用法我没有搜寻到例子,欢送大佬通知我。

重点关注第二个参数

解决款式和类

{// :class = "{foo:true,bar:false}"
  class:{
    foo: true,
    bar: false
  },
  // :style="{color:'red','font-size':'14px'}"
  style:{
    color:'red',
    fontSize:'14px'
  }
}

组件 props

{
  props: {myCustomProp: '组件属性值'}
}

HTML 个性和 DOM 属性

{
  // HTML 个性
  // NOTE 在组件外部应用 $attrs 获取 attrs
  // NOTE 会和 class 属性合并吗?// NOTE 和 class 属性的优先级,谁高?// 这里的 class 不会增加到 标签上
  attrs: {
    id: 'divId',
    class: 'className'
  },
  // DOM 属性
  domProps:{
    textContent: 'div 文本',// 优先级高于 v-text
    innerHTML: 'BAR'  // 优先级高于 v-html
  }
}

①. attrs 个性中的 class 不会被增加到标签上。

②. 留神辨别 HTML 个性和 DOM 属性的区别。

处理事件

{
  // v-bind:event
  on: {
    customEventName: value => {// 监听组件的自定义事件 即 emit 触发事件}
  },
  // 监听组件上的原生事件,只能用在组件上
  nativeOn: {
    click: target => {// 监听原生事件 即非 emit 触发的事件}
  }
}

nativeOn 只能用于自定义组件。

插槽

{
  scopedSlots: {
    // 默认插槽
    default: props => h('span',props.text),
    otherSlot: props => h('div',props.customProp)
  },
  slot: 'slotName'// 个别和第一个参数 template 一起应用
}

应用模板定义一个按钮:

<template>
  <div>
    <slot name="left"></slot>
    <button>
      <slot v-bind:person="person">
        <span> 按钮 </span>
      </slot>
    </button>
    <slot name="right" v-bind:age="person.age"></slot>
  </div>
</template>

<script>
  export default {
    name: 'MyButton',
    data() {
      return {
        person: {
          name: 'jack',
          age: 23,
        },
      }
    },
  }
</script>

在模板种应用该组件:

<MyButton>
  <template #right="{age}">
    <span> 按钮左边 {{age}} 岁 </span>
  </template>
  <template v-slot="{person}"> 这是按钮,{{person}}</template>
  <template #left>
    <span> 按钮右边 </span>
  </template>
</MyButton>

在 render 中应用该组件

import MyButton from './MyButton.vue'
export default {
  name: 'UseButton',
  render(h) {
    //NOTE h 第一个参数为 template 第二个参数里的 slot 属性指定插槽名称
    const slotLeft = h('template', { slot: 'left'}, '按钮右边')
    const slotRight = h('template', { slot: 'right'}, '按钮左边')
    const slotDefault = h('template', { slot: 'default'}, '默认插槽')
    const children = [slotLeft, slotDefault, slotRight]
    return h(MyButton, {}, children)
  },
}

在 render 中获取作用域插槽抛出的数据

import MyButton from './MyButton.vue'
export default {
  name: 'UseButton',
  render(h) {const slotLeft = h('template', { slot: 'left'}, '按钮右边')
    const children = [slotLeft]
    return h(
      MyButton,
      {
        scopedSlots: {
          default: props => {console.log(props)
            const {person} = props
            const text = ` 作用域插槽,${JSON.stringify(person)}`
            // 返回 h 创立的 VNode
            return h('span', {}, text)
          },
          right: props => {console.log(props)
            const {age} = props
            // 返回 jsx
            return <span> 按钮左边 {age} 岁 </span>
          },
        },
      },
      children
    )
  },
}

总结

①. 一般命名插槽,应用 h('template',{slot:'slotName'},children) 编写,而后放渲染组件的 第三个参数 里。

②. 作用域插槽在第二个参数的 scopedSlots 对象里,该对象的每个属性名是组件的 插槽名,值是一个函数,参数为插槽绑定的数据。

应用 render 函数重写编写 MyButton

export default {
  name: 'MyButton',
  data() {
    return {
      person: {
        name: 'jack',
        age: 23,
      },
    }
  },
  render(h) {
    // NOTE default 关键字 不重命名 无奈解构
    const {left, right, default: _defaultSlot} = this.$scopedSlots

    // NOTE 传递一个对象,在模板中应用解构取出属性
    const defaultSlot = _defaultSlot({person: this.person})
    const leftSlot = left()
    const rightSlot = right(this.person)
    const button = h('button', {}, [defaultSlot])
    return h('div', {}, [leftSlot, button, rightSlot])
  },
}

返回 jsx

export default {
  name: 'MyButton',
  data() {
    return {
      person: {
        name: 'jack',
        age: 23,
      },
    }
  },
  render(h) {const { left, right, default: _defaultSlot} = this.$scopedSlots
    // NOTE 查看插槽是否存在
    const defaultSlot = (_defaultSlot && _defaultSlot({ person: this.person})) || <span> 按钮 </span>
    const leftSlot = (left && left()) || ''
    const rightSlot = right(this.person)
    const button = h('button', {}, [defaultSlot])
    //  返回 jsx 使得 dom 构造更加清晰
    return (
      <div>
        {leftSlot}
        {defaultSlot}
        {rightSlot}
      </div>
    )
  },
}

函数式组件:

export default {
  name: 'MyButton',
  functional: true,
  props: {
    person: {
      type: Object,
      default: () => ({ name: 'jack', age: 23}),
    },
  },
  // NO DATA in functional component
  // data() {
  //   return {
  //     person: {
  //       name: 'jack',
  //       age: 23,
  //     },
  //   }
  // },
  render(h, { props, scopedSlots}) {const { left, right, default: _defaultSlot} = scopedSlots
    const defaultSlot = (_defaultSlot && _defaultSlot({ person: props.person})) || <span> 按钮 </span>
    const leftSlot = (left && left()) || ''
    const rightSlot = right(props.person)
    const button = h('button', {}, [defaultSlot])
    return (
      <div>
        {leftSlot}
        {button}
        {rightSlot}
      </div>
    )
  },
}

①. 一般插槽、命名插槽、作用域插槽都通过 this.$scopedSlots 获取,它们都是返回 VNode 的函数。

②. 插槽绑定的数据通过插槽函数传递,根本数据应用 {} 包裹,不便在模板中解构。

③. 返回 jsx 能让 div 构造更加清晰。

④. 留神查看是否存在插槽,以启用后备内容。

指令

{directives: [{ name: 'directive-name', value: '2', expression: '1+1', arg: 'foo', modifiers: { foo: true} }]
}

在模板中定义指令

<template>
  <!-- title 是名字,指令的 value 由表达式计算出来 -->
  <!-- v-title:argument.modifier1.modifier2="expression" -->
  <div>
    在模板中编写指令
    <p v-title> 这是简略指令 </p>
    <!-- 只能带一个参数 -->
    <p v-title:argu> 这是带参数的指令 </p>
    <!-- 动静参数 -->
    <p v-title:[dynamicArgu()]> 这是带动静参数的指令 </p>
    <p v-title:argu.foo.bar> 这是带参数和修饰符的指令 </p>
    <p v-title:job.foo="data"> 这是带参数、修饰符和一般表达式的指令 </p>
    <p v-title:job.foo="expresFun"> 这是带参数、修饰符和函数表达式的指令 </p>
  </div>
</template>

<script>
  export default {
    name: 'Title',
    directives: {
      title: {inserted(el, bindings, vnode) {const { context: that} = vnode
          const {value = false} = bindings
          if (typeof value === 'function') {that.setTile(el, value(that.data))
          } else {that.setTile(el, value)
          }
        },
        componentUpdated(el, bindings, vnode) {const { context: that} = vnode
          const {value = false} = bindings
          if (typeof value === 'function') {that.setTile(el, value(that.data))
          } else {that.setTile(el, value)
          }
        },
      },
    },
    data() {
      return {data: { age: 23, job: 'web dev'},
      }
    },
    methods: {setTile(el, titleValue) {
        const textContent = el.textContent
        const title = textContent.trim() || '暂无数据'
        el.title = typeof titleValue === 'string' ? titleValue : title
      },
      dynamicArgu() {return Math.random() > 0.5 ? 'argu1' : 'argu0'
      },
      expresFun(data) {return data.age + '岁'},
    },
  }
</script>

指令对象 bindings

不倡议在 render 函数中编写指令,难以了解,指令须要在模板应用能力施展其设计的目标。render 中可间接管制 DOM。

v-model 指令

应用 render 定义组件,如何提供 v-model

prop:–value + 应用 on 监听组件的事件,在处理函数中触发 input 自定义事件。

在 render 函数中应用 v-model 指令的解决有三种计划:

① . 在数据对象中应用 model 属性:

{
  model: {
    value: this.value,// value 是 data 里的属性
    callback: value => {
      // 能够再赋值之前做其余逻辑
      // 验证数据
      // 触发事件
      this.value = value
    }
  }
}

②. 传递 value + 监听 input 事件

{
  props: {
    // value 是 data 中的属性
    value: this.value
  },
  on: {
    input: value => {
      // 可做其余事件
      // 触发事件
      this.value = value
    }
  }
}

③. 在 jsx 中应用 vModel 属性

// input 是 data 中的属性
<MyInput vModel={this.input} />

三种计划的优缺点:

model 属性更加好,当表单项还有其余事件时,还能够在 on 中监听它们,比方 element 的下拉,有changeclear 等事件。

props value + input, 很合乎 v-model 的语法糖。

jsx+ vModel 属性,简洁,罕用。

其余属性

{
  key: 'v for 中的 key',
  ref:'模板变量',
  refInFor: true, // 循环中的 ref 是一个数组
}

应用 render 封装一个输入框

MyInput.jsx

import './my-input.css'
export default {
  name: 'MyInput',
  props: {
    // 须要实现 v-model 指令
    value: {type: [String, Number],
      default: '',
    },
  },
  render(h) {
    return h('input', {
      class: {'my-input': true,},
      style: {backgroundColor: '#ccc',},
      attrs: {
        id: 'my-input',
        class: 'a-my-input',
        'data-key': 'key',
      },
      domProps: {value: this.value,},
      // 监听 input 的 input 事件
      on: {input: ({ target}) => {this.$emit('input', target.value)
        },
      },
    })
  },
}

还能够应用 computed:domProp 的 value 接管一个计算属性,为该计算属性提供 setter 和 getter,在 input 事件处理函数中设置计算属性的值,在 setter 中触发 自定义的 input 事件。

这种办法不如下面的明确,代码量也多了。

在模板中应用该组件

<MyInput v-model="myInput" />

在 render 函数中应用

export default {
  name: 'UseInput',
  data() {
    return {input: '',}
  },
  render(h) {return h('div', {}, [
      h(MyInput, {
        model: {
          value: this.input,
          callback: value => {
            // 可在此做其余事件
            this.input = value
          },
        },
      }),
      h('h3', {}, this.input),
    ])
  },
}

心愿 UseInput,反对 v-model,即在二次封装 MyInput。

计划 1:增加 value props 在 model 中触发 input,删除 data 中的 input。

import MyInput from './my-input.jsx'
export default {
  name: 'UseInput',
  props: {value: { type: [String, Number], default: '' },
  },
  render(h) {return h('div', {}, [
      h(MyInput, {
        model: {
          value: this.value,
          callback: value => {
            // 可在此做其余事件
            this.$emit('input', value)
          },
        },
      }),
      h('h3', {}, this.value),
    ])
  },
}

计划 2: 增加 value props,将其通过 props 传入 UseInput,监听 UseInput 的 input 事件,在此触发 input 事件。

import MyInput from './my-input.jsx'
export default {
  name: 'UseInput',
  props: {value: { type: [String, Number], default: '' },
  },
  render(h) {return h('div', {}, [
      h(MyInput, {
        props: {value: this.value,},
        on: {
          input: value => {this.$emit('input', value)
          },
        },
      }),
      h('h3', {}, this.value),
    ])
  },
}

对于具备多种事件的表单项,比方 element 的下拉框,第一种计划更加好,on 属性留地位给从外传入的处理函数。

计划 3:jsx + vModel + prop value

import MyInput from './my-input.jsx'
export default {
  name: 'UseInput',
  props: {value: { type: [String, Number], default: '' },
  },
  data() {
    return {input: this.value,}
  },
  render(h) {
    return (
      <div>
        <MyInput vModel={this.input} />
        {/* <h2>{this.input}</h2> */}
      </div>
    )
  },
}

这种计划不能实现双向绑定

其余问题

  1. 如何限度继承的属性,inheritAttrs 设置为 false,无奈显示。

在模板定义的组件中,inheritAttrs 属性设置为 false, 除styleclass 认为的属性不会增加到根组件,实现手动管制。

render 定义的组件中,也是一样的。

参考

What does the‘h’stand for in Vue’s render method?

A Practical Use Case for Vue Render Functions: Building a Design System Typography Grid

How to use v-model (for custom input component) in render function?

退出移动版