1️⃣ render 函数的基本使用

image.png

1️⃣ 节点、树、以及虚拟 DOM

在深入渲染函数之前,先来了解一些浏览器的工作原理。例如,下面这段 HTML:

  1. <div>
  2. <h1>My title</h1>
  3. Some text content
  4. <!-- TODO: Add tagline -->
  5. </div>

当浏览器读到这些代码时,它会建立一个DOM 节点树 来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。
以上 HTML 对应的 DOM 节点树如下图所示:
image.png
每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点。
高效地更新所有这些节点是比较困难的,不过幸运的是,我们不需要手动完成这个工作。只需要告诉 Vue 希望页面上的 HTML 是什么,例如在模板中:

  1. <h1>{{ blogTitle }}</h1>

或者是在一个渲染函数中:

  1. render: function (createElement) {
  2. return createElement('h1', this.blogTitle)
  3. }

在这两种情况下,Vue 都会自动保持页面的更新,即便 blogTitle 发生了改变。

2️⃣ 虚拟 DOM

Vue 通过建立一个虚拟 DOM来追踪自己要如何改变真实 DOM。例如:

  1. return createElement("h1", this.blogTitle);

createElement 会返回什么呢?

  1. 1. 它不会返回一个实际的 DOM 元素。更准确的名字可能是:createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括其子节点的描述信息。我们把这样的节点描述为“虚拟节点(virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

1️⃣ createElement 参数

2️⃣ createElement 接收的参数

  1. createElement(标签名(必需), 与模板中属性对应的数据对象(可选), 子级虚拟节点(可选));
  1. createElement(
  2. // {String | Object | Function}
  3. // 一个 HTML 标签名或者组件选项对象
  4. // resolve 了上述任何一种的一个 async 函数, 必填.
  5. 'div',
  6. // {Object}
  7. // 一个与模板中 attribute 对应的数据对象。可选。
  8. {
  9. // (详情见深入数据对象)
  10. },
  11. // {String | Array}
  12. // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  13. // 也可以使用字符串来生成“文本虚拟节点”。可选。
  14. [
  15. '文本虚拟节点',
  16. createElement('h1', '生成一条 h1 标签包含的文本'),
  17. createElement(MyComponent, {
  18. props: {
  19. someProp: 'foobar'
  20. }
  21. })
  22. ]
  23. )

2️⃣ 深入数据对象

有一点要注意:正如 v-bind:class 和 v-bind:style 在模板语法中会被特别对待一样,它们在 VNode 数据对象中也有对应的顶层字段。该对象也允许你绑定普通的 HTML attribute,也允许绑定如 innerHTML 这样的 DOM property (这会覆盖 v-html 指令)。

  1. {
  2. // 与 `v-bind:class` 的 API 相同,接受一个字符串、对象或字符串和对象组成的数组
  3. class: {
  4. foo: true,
  5. bar: false
  6. },
  7. // 与 `v-bind:style` 的 API 相同,接受一个字符串、对象,或对象组成的数组
  8. style: {
  9. color: 'red',
  10. fontSize: '14px',
  11. },
  12. // 普通的 HTML attribute
  13. attrs: {
  14. id: 'foo',
  15. },
  16. // 组件 prop
  17. props: {
  18. myProp: 'bar',
  19. },
  20. // DOM属性
  21. domProps: {
  22. innerHTML: 'baz',
  23. },
  24. // 事件监听器,不支持如“v-on:keyup.enter”这样的修饰器
  25. on: {
  26. click: this.onClick
  27. },
  28. // 仅用于组件,用于监听原生事件,而不是组件内部使用 vm.$emit 触发的事件。
  29. nativeOn: {
  30. click: this.nativeClickHandler
  31. },
  32. // 自定义指令。注意,无法对 `binding` 中的 `oldValue`赋值,因为 Vue 已经自动为你进行了同步。
  33. directives: [
  34. {
  35. name: 'my-custom-directive',
  36. value: '2',
  37. expression: '1 + 1',
  38. arg: 'foo',
  39. modifiers: {
  40. bar: true
  41. }
  42. }
  43. ],
  44. // 其它特殊顶层属性
  45. key: 'myKey',
  46. ref: 'myRef',
  47. // 如果在渲染函数中给多个元素都应用了相同的 ref 名,那么 `$refs.myRef` 会变成一个数组。
  48. refInFor: true
  49. // 作用域插槽,格式为:{ name: props => VNode | Array<VNode> }
  50. // 如果组件是其它组件的子组件,需为插槽指定名称
  51. slot: 'name-of-slot',
  52. scopedSlots: {
  53. default: props => createElement('span', props.text)
  54. },
  55. }

1️⃣ 使用 JavaScript 代替模板功能

2️⃣ v-if 和 v-for

只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的渲染函数就不会提供专有的替代方法。比如,在模板中使用的 v-if 和 v-for:
image.png
这些都可以在渲染函数中用 JavaScript 的 if/else 和 map 来重写:
image.png

2️⃣ v-model

渲染函数中没有与 v-model 的直接对应——你必须自己实现相应的逻辑:

  1. props: ['value'],
  2. render: function (createElement) {
  3. var self = this
  4. return createElement('input', {
  5. domProps: {
  6. value: self.value
  7. },
  8. on: {
  9. input: function (event) {
  10. self.$emit('input', event.target.value)
  11. }
  12. }
  13. })
  14. }

这就是深入底层的代价,但与 v-model 相比,这可以让你更好地控制交互细节。

2️⃣ 事件 & 按键修饰符

对于 .passive、.capture 和 .once 这些事件修饰符,Vue 提供了相应的前缀可以用于 on:

修饰符 前缀
.passive &
.capture !
.once ~
.capture.once

.once.capture
~!

例如:

  1. render(createElement) {
  2. return createElement(
  3. "p",
  4. {
  5. on: {
  6. "!click": function () {},
  7. "~keyup": function () {},
  8. "~!mouseover": function () {},
  9. },
  10. },
  11. "点击"
  12. );
  13. },

对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:

修饰符 处理函数中的等价操作
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
按键:
.enter
, .13
if (event.keyCode !== 13) return
(对于别的按键修饰符来说,可将 13
改为另一个按键码
)
修饰键:
.ctrl
, .alt
, .shift
, .meta
if (!event.ctrlKey) return
(将 ctrlKey
分别修改为 altKey
shiftKey
或者 metaKey
)

这里是一个使用所有修饰符的例子:

  1. on: {
  2. keyup: function (event) {
  3. // 如果触发事件的元素不是事件绑定的元素
  4. // 则返回
  5. if (event.target !== event.currentTarget) return
  6. // 如果按下去的不是 enter 键或者
  7. // 没有同时按下 shift 键
  8. // 则返回
  9. if (!event.shiftKey || event.keyCode !== 13) return
  10. // 阻止 事件冒泡
  11. event.stopPropagation()
  12. // 阻止该元素默认的 keyup 事件
  13. event.preventDefault()
  14. // ...
  15. }
  16. }

1️⃣ render 使用插槽

2️⃣ 默认插槽

你可以通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组:
image.png

2️⃣ 具名插槽

image.png

2️⃣ 作用域插槽

也可以通过 this.$scopedSlots 访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数:
image.png

如果要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据对象中的 scopedSlots 字段:

  1. render: function (createElement) {
  2. // `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
  3. return createElement('div', [
  4. createElement('child', {
  5. // 在数据对象中传递 `scopedSlots`
  6. // 格式为 { name: props => VNode | Array<VNode> }
  7. scopedSlots: {
  8. default: function (props) {
  9. return createElement('span', props.text)
  10. }
  11. }
  12. })
  13. ])
  14. }