vue渲染机制、渲染函数(官文简化、理解)


渲染函数

h()

h() 函数用于创建 vnodes。
使用案例

// 除了类型必填以外,其他的参数都是可选的
h('div')
h('div', { id: 'foo' })

// attribute 和 property 都能在 prop 中书写
// Vue 会自动将它们分配到正确的位置
h('div', { class: 'bar', innerHTML: 'hello' })

// props modifiers such as .prop and .attr can be added
// with '.' and `^' prefixes respectively
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')])
const vnode = h('div', { id: 'foo' }, [])
// 获取到的结果
vnode.type // 'div'
vnode.props // { id: 'foo' }
vnode.children // []
vnode.key // null

声明渲染函数

当组合式 API 与模板一起使用时,setup() 钩子的返回值是用于暴露数据给模板。然而当我们使用渲染函数时,可以直接把渲染函数返回:

import { ref, h } from 'vue'

export default {
  props: {
    /* ... */
  },
  setup(props) {
    const count = ref(1)
    // 在 setup() 内部声明的渲染函数天生能够访问在同一范围内声明的 props 和许多响应式状态。 
    // 返回渲染函数
    return () => h('div', props.msg + count.value)
  }
}

可以返回字符串

export default {
  setup() {
    return () => 'hello world!'
  }
}

可以返回多个根节点

import { h } from 'vue'

export default {
  setup() {
    // 使用数组返回多个根节点
    return () => [
      h('div'),
      h('div'),
      h('div')
    ]
  }
}

常见案例

v-if

<div>
  <div v-if="ok">yes</div>
  <span v-else>no</span>
</div>
h('div', [ok.value ? h('div', 'yes') : h('span', 'no')])

v-for

<ul>
  <li v-for="{ id, text } in items" :key="id">
    {{ text }}
  </li>
</ul>
h(
  'ul',
  // assuming `items` is a ref with array value
  items.value.map(({ id, text }) => {
    return h('li', { key: id }, text)
  })
)

v-on

h(
  'button',
  {
    onClick(event) {
      /* ... */
    }
  },
  'click me'
)

事件修饰符

h('input', {
  onClickCapture() {
    /* 捕捉模式中的监听器 */
  },
  onKeyupOnce() {
    /* 只触发一次 */
  },
  onMouseoverOnceCapture() {
    /* 单次 + 捕捉 */
  }
})

对于事件和按键修饰符,可以使用 withModifiers 函数:

import { withModifiers } from 'vue'

h('div', {
  onClick: withModifiers(() => {}, ['self'])
})

组件

import Foo from './Foo.vue'
import Bar from './Bar.jsx'

function render() {
  return h('div', [h(Foo), h(Bar)])
}

渲染插槽

在渲染函数中,插槽可以通过 setup() 的上下文来访问。每个 slots 对象中的插槽都是一个返回 vnodes 数组的函数

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
        })
      )
    ]
  }
}

传递插槽

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

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

内置组件

import { h, KeepAlive, Teleport, Transition, TransitionGroup } from 'vue'

export default {
  setup () {
    return () => h(Transition, { mode: 'out-in' }, /* ... */)
  }
}

v-model

v-model` 指令扩展为 `modelValue` 和 `onUpdate:modelValue
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    return () =>
      h(SomeComponent, {
        modelValue: props.modelValue,
        'onUpdate:modelValue': (value) => emit('update:modelValue', value)
      })
  }
}

自定义指令#

可以使用 withDirectives 将自定义指令应用于 vnode:

import { h, withDirectives } from 'vue'

// 自定义指令
const pin = {
  mounted() { /* ... */ },
  updated() { /* ... */ }
}

// <div v-pin:top.animate="200"></div>
const vnode = withDirectives(h('div'), [
  [pin, 200, 'top', { animate: true }]
])

当一个指令是以名称注册并且不能被直接导入时,可以使用 resolveDirective 函数来解决这个问题。

一堆理论输出

虚拟DOM

将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。

一个运行时渲染器将会遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树。这个过程被称为挂载 (mount)。

如果我们有两份虚拟 DOM 树,渲染器将会有比较地遍历它们,找出它们之间的区别,并应用这其中的变化到真实的 DOM 上。这个过程被称为更新 (patch),又被称为“比对”(diffing) 或“协调”(reconciliation)。

渲染管线

直接查阅官方文档:渲染机制 | Vue.js (vuejs.org)

渲染函数优势

在处理高度动态的逻辑时,渲染函数相比于模板更加灵活。

带编译时信息的虚拟 DOM

静态提升

<div>
  <div>foo</div> <!-- 静态需提升 -->
  <div>bar</div> <!-- 静态需提升 -->
  <div>{{ dynamic }}</div>
</div>

不依赖任何动态绑定DOM我们称为静态节点。编译器会自动提升该节点创建函数到这个模板渲染函数之外,渲染时跳过这部分差异对比,然后调用这个节点创建函数。
如果出现重复,将会被压缩成一个“静态vnode”,直接通过 innerHTML 来挂载。初次挂载后缓存这个DOM 节点。如果该DOM节点在其他地方重用,将使用cloneNode()克隆新的DOM节点,两个字揍是“高效”。

更新类型标记

https://cn.vuejs.org/guide/extras/rendering-mechanism.html#patch-flags

树结构打平

https://cn.vuejs.org/guide/extras/rendering-mechanism.html#tree-flattening


文章作者: 叶春锁
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 叶春锁 !