Vue 渲染机制-Rendering Mechanism 和 渲染函数

渲染机制

拥有虚拟 DOM 层有一些好处,最重要的是它让组件的渲染逻辑完全从真实 DOM 中解耦,并让它更直接地在其他环境中重用框架的 runtime。
例如,Vue 允许第三方开发人员创建自定义渲染解决方案目标,不仅仅是浏览器,也包括 IOS 和 Android 等原生环境,也可以使用 API 创建自定义渲染器直接渲染到 WebGL 而不是 DOM节点。
在 Vue 3 中,我们让自定义渲染器 API 成为一等公民。因此开发人员可以直接拉取 Vue runtime 核心作为依赖项,然后利用自定义渲染器 API 构建自己的自定义渲染器。事实上,我们已经有了早期用户报告他们已经成功地构建了一个使用 Vue 3 API关于虚拟 DOM 的 WebGL 渲染器。

虚拟 DOM 的另一个重要的地方,是它提供了一种能力,能在实际返回渲染引擎之前,以编程方式构造、检查、克隆以及操作所需的 DOM 结构。你可以完全利用 JavaScript 的能力做到这些。这个能力很重要,因为总会有某些情况在 UI 编程中使用模板语法会有一些限制,你只需要一种有充分灵活性的合适的编程语言来表达潜在的逻辑。
这种情况更常见于当你在创作一个库、或 UI 组件套件,这些打算上传供第三方开发者使用的代码的时候。实际在日常 UI 开发中是相当罕见的。

让我们想象一下一个像复杂类型的顶部框或者一个与一堆文本相关联的输入框,这些类型的组件通常包含很少的标签,但它们将包含很多交互逻辑。在这些情况下,模板语法有时候会限制你更容易地表达潜在的逻辑,或者有时你在模板中加入了很多逻辑,同时还是很多逻辑在 JavaScript 中,而 render 函数允许你把这些逻辑组合在一个地方。你通常不需要想太多关于这些情况下的标签。

所以我的理解是模板会完成你要做的事,99%的情况下你只需要写出 HTML 就好了,但偶尔可能想做些更可控的事情,这时候你就需要编写一个渲染函数了。

渲染函数

Vue 2 中的渲染函数如下所示:

render(h) {
  return h(
    'div', {
    attrs: { id: foo },
    on: { click: this.onClick },
    'hello'
  })
}

render是选项 API 中的一个选项,相对于提供一个template option,你可以为组件提供一个渲染函数,函数内能得到h参数,直接用它来创造我们的虚拟 DOM 节点,简称vnode

vnode 接受三个参数:

  • tag:(必须) 第一个参数是节点的类型,在这里我们创建一个 div。
    {String | Object | Function}
    一个 HTML 标签名、一个组件、一个异步组件、或一个函数式组件。
  • props:(可选) 第二个参数是一个对象,包含vnode上的所有数据或属性
    {Object}
    与 attribute、prop 和事件 相对应的对象。
    我们会在模板中使用。
    从某种意义上说,你必须指明传递给节点的绑定类型。例如,如果要绑定属性你必须把它嵌套在attrs对象下;如果要绑定事件侦听器必须把它列在on下。
  • children:(可选) 第三个参数是这个vnode子节点
    {String | Array | Object}
    使用 h() 构建的子 VNodes 数组,或使用字符串获取 "文本 Vnode" 或者 有插槽的对象。
    直接传递一个字符串表明此节点只包含文本子节点,它也可以是包含更多子节点的数组。所以你可以在这里有一个数组里面并且嵌套更多的嵌套 h 调用。
    如果没有传prop,那么通常可以将children作为第二个参数传入。如果会产生歧义,可以将null作为第二个参数传入,

在 Vue 3 中我们改变了API,目标是简化它。

import { h } from 'vue'

render () {
  return h(
    'div', 
    {
      id: 'foo',
      onClick: this.onClick
    },
    'hello'
  })
}

第一个显著的变化是我们现在有了一个扁平的props结构。当你调用h时,第二个参数现在总是一个扁平的对象。你可以直接给它传递一个属性,这里我们给了它一个 ID。按惯例监听器以on开头,所以任何带on的都会自动绑定为一个监听器。所以你不必考虑太多嵌套的问题。

在大多数情况下,你也不需要思考是应将其作为attribute绑定还是 DOM 属性绑定,因为 Vue 将智能地找出为你做这件事的最好方法。我们检查这个key是否作为属性存在在原生 DOM 中。如果存在,我们会将其设置为property;如果它不存在,我们将它设置为一个attribute

render API 的另一项改动是h helper 现在是直接从 Vue 本身全局导入的。一些用户在 Vue 2 中很困扰因为h只在这里面传递,而h绑定到当前组件实例。当你想拆分一个大的渲染函数时,你必须把这个h函数一路传递给这些分割函数。当有了全局引入的h你导入一次就可以分割你的渲染函数,在同一个文件里分割多少个都行。

渲染函数不再有h参数了,在内部它确实接收参数,但这只是编译器使用的,用来生成代码。当用户直接使用时,他们不需要这个参数。所以,如果你用 TypeScript 使用定义的组件 API 你也会得到 this 的完整类型推断。

Q&A

1.我知道原始的那种虚拟 DOM 的实现得到了启发来自其他项目对吗?

是的有一个库叫snabbdom,Vue 2 基本上就是从这个库中分离出来的。

2.好的然后是 Vue 3,你在这里的编码方式只是改进了 Vue 2 的模式吗?

好吧,Vue 3 是一个彻底的重写,几乎从头开始一切都是定制的。显然,有些现有的算法看起来像没有变化,因为这些是我们看到社区在做广泛研究的领域所以这是建立在所有这些以前的实现的基础上的,但代码本身现在是从头开始。

3.都是用TypeScript写的,对吧?

是的,都是 TypeScript 写的。

何时/如何使用 render 函数

在 Vue 2 中,一个传统的 Vue 组件,有一个template选项。但为了重用渲染函数我们可以用一个render函数来代替它,我们会通过参数得到h(hyperscript)。但这里只示范一下我们如何在 Vue 3 中使用它。

import { h } from 'vue'

const App = {
  render () {
    return h('div') // 等效模板中的普通 div
  }
}
  1. 所以它返回 div 的 JavaScript 对象表示?

完全正确。

  1. 那么,你的虚拟 DOM 就像…编译器?是编译器接收它吗?

是渲染器,渲染器接收它。

  1. 然后它实际上进行 DOM 调用将其带入浏览器?

完全正确。

所以我们可以给这个虚拟节点一些props

import { h } from 'vue'
const App = {
  render () {
    return h(
      'div',
      {  id: 'hello' },
      [ h('span', 'world') ]
    ) 
  }
}

// <div id="hello"><span>world</span></div>

现在,我们知道了如何生成静态结构。但是当人们第一次使用 render 函数会问 “我该怎么写比如v-if或者v-for”?我们没有像v-if或者类似的东西。相反,你可以直接使用 JavaScript。

import { h } from 'vue'

const App = {
  render () {
    return this.ok
      ? h('div', { id: 'hello' }, [h('span', 'world')]
      : h('p', 'other branch')
    ) 
  }
}

如果 ok 的值为true,它将呈现div;反之,它将呈现p。同样,如果你想做v-else-if你需要嵌套这个三元表达式:

import { h } from 'vue'

const App = {
  render () {
    return this.ok
      ? h('div',{ id: 'hello' }, [h('span', 'world')]
      : this.otherCondition
        ? h('p', 'other branch')
        : h('span')
    ) 
  }
}

我想你可能会喜欢创建一个变量,将不同的节点添加到该变量。所以当你不得不将这些整个嵌套在一个表达式中,这会很有用。

import { h } from 'vue'

let nodeToReturn
if(this.ok) {
  nodeToReturn = ...
} else if (xxx) {
}

const App = {
  render () {
    return nodeToReturn
  }
}

这就是 JavaScript 灵活的地方,这看起来更像普通的 JavaScript。当你的代码变得更加复杂时您可以使用普通的 JavaScript 重构技巧使它们更容易理解。

我们讨论了v-if, 接下来看v-for。 你也可以给它们加上key,这是渲染函数中的渲染列表。

import { h } from 'vue'
const App = {
  render () {
    return this.list.map(item => {
      return h('div', {key: item.id}, item.text)
    })) 
  }
}

在渲染函数中,你很可能总是要处理插槽。
当你写一个重标记组件(markup heavy component),或者我更喜欢称之为特性组件(feature component),它与你的应用程序的外观有关,布局结构、实际的显示给用户的 HTML 。对于这些类型的组件,我更喜欢始终使用模板。
只有在必须使用渲染函数的时候,比如写一些功能型的组件,有时会期望获取一些插槽内容,将其打包或者以某种方式操纵他们。在 Vue 3 里默认插槽将暴露在this.$slot.default。如果对组件什么都没有提供,这将是undefined,所以你得先检查一下它是否存在。如果它存在,它将永远是一个数组。有了作用域槽,我们可以将props传递给作用域槽,所以把数据传递到作用域槽只是通过传递一个参数到这个函数调用中。因为这是一个数组,你可以将它直接放在children位置。

import { h } from 'vue'

const App = {
  render () {
    const slot = this.$slot.default ? this.$slot.default() : []
    return h('div', slot)
  }
}

你可以在render函数中用插槽做一件很强大的事,比如以某种方式操纵插槽,因为它是一个 JavaScript 对象数组,你可以用filter过滤,也可以用map遍历它。

import { h } from 'vue'

const App = {
  render () {
    const slot = this.$slot.default ? this.$slot.default() : []
    slot.map(vnode => {
      return h('div', [vnode])
    })
  }
}

这里有一个拦截并更改插槽数据的例子。
假设我们有一个堆栈组件(tack component),在一些用户界面库(UI libraries)中很常见。你可以传递很多属性给它,得到嵌套的堆栈渲染结果,有点像 HTML 中ulol的默认样式。

<Stack size="4">
  <div>hello</div>
  <Stack size="4">
    <div>hello</div>
    <div>hello</div>
  </Stack>
</Stack>

渲染结果会是这样:

<div class="stack">
  <div class="mt-4">
    <div>hello</div>
  </div>
  <div class="mt-4">
    <div class="stack">
      <div class="mt-4">
        <div>hello</div>
      </div>
    </div>
  </div>
</div>

它只是创造了一堆包裹元素,唯一的目的就是给其中一些元素加上额外的marginpadding用于布局。

普通的基于模板的语法,在同一个插槽内它们都是默认插槽,你能做的只有渲染这个部分,因此这在模板很难实现。但是用渲染函数则能够程序化地遍历插槽内的每个项目,然后把它们变成别的东西。

import { h } from 'vue'
const Stack = {
  render () {
    const slot = this.$slots.default ? this.$slots.default() : []
    return h(
      'div',
      {class: 'stack'},
      slot.map(child => {
        return h('div',  {class: `mt-${this.$props.size}`}, [child])
      })
    )
  }
}

我们用slot.map生成新的 vnode 列表,原来的子插槽被包装在里面。我们把它放到一个stack.html文件里 :

// stack.html
<script src="https://unpkg.com/vue@next"></script>
<style>
  .mt-4 {
    margin: 10px
  }
</style>

<div id="app"></div>
<script>
  const { h, createApp } = Vue
  const Stack = {
    render() {
      const slot = this.$slots.default ? this.$slots.default() : []
      return h(
        'div',
        { class: 'stack' },
        slot.map(child => {
          return h('div', { class: `mt-${this.$attrs.size}` }, [child]) // this.$props.size ?
        })
      )
    },
  }
  const App = {
    components: {
      Stack
    },
    template: `
      <Stack size="4">
        <div>hello</div>
        <Stack size="4">
          <div>hello</div>
          <div>hello</div>
        </Stack>
      </Stack>
    `
  }
  createApp(App).mount('#app')
</script>

当创作类似这些底层的公用设施组件,有时真的会遇到麻烦,这时渲染函数会更有效。但话说回来,也需要了解每种方法的利弊,这些是为了让你更好地理解在什么情况下应该使用模板或使用渲染函数。
基本上是当你用一个模板时遇到限制时,比如就像我们刚才看到的那样,可能改为使用渲染函数会更有效。当你意识到想表达的逻辑用 JavaScript 更容易而不是使用模板语法时就使用它。
从我的经验来看,这种情况在您创作可重用的功能组件,要跨多个应用程序共享或者在组织内部共享时更常见。在日常开发中你主要是在编写特性组件,模板通常是有效的方式,模板的好处是更简单,当你有很多标记的时候会通过编译器优化,它的另一个好处是它更容易让设计师接管组件并用 CSS 设计样式。因此,Vue 提供了这两个选项,当情况出现的时候以便您可以选择合适的方式。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容