若是咱们把这个 HTML 加载到浏览器中,浏览器建立这些节点,用来显示网页。因此这个HTML映射到一系列DOM节点,而后咱们可使用JavaScript进行操做。例如:javascript
let item = document.getElementByTagName('h1')[0] item.textContent = "New Heading"
网页能够有不少DOM节点,这意味着DOM树能够有数千个节点。这就是为何咱们有像Vue这样的框架,帮咱们干这些重活儿,并进行大量的JavaScript调用。html
然而,搜索和更新数千个DOM节点很明显会变慢。这就是Vue和其余相似框架有一种叫作虚拟DOM的东西。虚拟DOM是表示DOM的一种方式。例如,这个HTML也能够经过一个虚拟节点来表示,看起来像这样。如您所见,它只是一个JavaScript对象。vue
<div>Hello</div>
{ tag: 'div', children: [ { text: 'Hello' } ] }
Vue知道如何使用此虚拟节点并挂载到DOM上,它会更新咱们在浏览器中看到的内容。实际上还有一个步骤其中,Vue基于咱们的模板建立一个渲染函数,返回一个虚拟DOM节点。java
渲染函数能够是这样的:node
render(h) { return h('div', 'hello') }
当组件更改时,Render函数将从新运行,它将建立另外一个虚拟节点。而后发送旧的 VNode 和新的 VNode 到Vue中进行比较并以最高效的方式在咱们的网页上更新。web
咱们能够将虚拟DOM和实际DOM的关系类比为蓝图和实际建筑的关系。假设我更改了29楼的一些数据。我改变了家具的布局还加了一些橱柜。我有两种方法能够改变。首先,我能够拆除29楼的一切从头开始重建。或者我能够创造新的蓝图,比较新旧蓝图并进行更新以尽量减小工做量。这就是虚拟DOM的工做原理。Vue 3让这些更新更快而且更高效。算法
Vue 的三个核心模块:编程
响应式模块容许咱们建立 JavaScript 响应对象并能够观察其变化。当使用这些对象的代码运行时,它们会被跟踪,所以,它们能够在响应对象发生变化后运行。数组
编译器模块获取 HTML 模板并将它们编译成渲染函数。这可能在运行时在浏览器中发生,但在构建 Vue 项目时更常见。这样浏览器就能够只接收渲染函数。浏览器
渲染模块的代码包含在网页上渲染组件的三个不一样阶段:
在渲染阶段,将调用 render 函数,它返回一个虚拟 DOM 节点。
在挂载阶段,使用虚拟DOM节点并调用 DOM API 来建立网页。
在补丁阶段,渲染器将旧的虚拟节点和新的虚拟节点进行比较并只更新网页变化的部分。
如今让咱们来看一个例子,一个简单组件的执行。它有一个模板,以及在模板内部使用的响应对象。首先,模板编译器将 HTML 转换为一个渲染函数。而后初始化响应对象,使用响应式模块。接下来,在渲染模块中,咱们进入渲染阶段。这将调用 render 函数,它引用了响应对象。咱们如今监听这个响应对象的变化,render 函数返回一个虚拟 DOM 节点。接下来,在挂载阶段,调用 mount 函数使用虚拟 DOM 节点建立 web 页面。最后,若是咱们的响应对象发生任何变化,正在被监视,渲染器再次调用render函数,建立一个新的虚拟DOM节点。新的和旧的虚拟DOM节点,发送到补丁函数中,而后根据须要更新咱们的网页。
拥有虚拟DOM层有一些好处,最重要的是它让组件的渲染逻辑彻底从真实DOM中解耦,并让它更直接地重用框架的运行时在其余环境中。例如,Vue容许第三方开发人员建立自定义渲染解决方案目标,不只仅是浏览器也包括IOS和Android等原生环境,也可使用API建立自定义渲染器直接渲染到WebGL而不是DOM节点。在Vue 2中咱们实际上已经有了这种能力可是,咱们在Vue 2中提供的API没有正式记录而且须要分叉源代码。因此这给维护带来了很大的负担,对开发这些定制解决方案的开发人员在Vue 3中,咱们让自定义渲染器API成为一等公民。所以开发人员能够直接拉取Vue运行时核心做为依赖项,而后利用自定义渲染器API构建本身的自定义渲染器。事实上,咱们已经有了早期用户报告他们已经成功地构建了一个使用Vue 3 API关于虚拟DOM的WebGL渲染器。
另外一个重要方面,它提供了能力以编程方式构造、检查、克隆以及操做所需的DOM结构,在实际返回渲染引擎以前你能够利用JavaScript的所有能力作到这些。这个能力很重要,由于总会有某些状况在UI编程中使用模板语法会有一些限制,你只须要一种有充分灵活性的合适的编程语言来表达潜在的逻辑。如今,这种状况其实是至关罕见的在平常UI开发中。但当你在创做一个库的时候,这种状况更常见或编写UI组件套件,你打算上传供第三方开发者使用。让咱们想象一下一个,像复杂类型的顶部框这样的组件或者一个与一堆文本相关联的输入框,这些类型的组件一般包含不多的标记,但它们将包含不少交互逻辑在这些状况下,模板语法有时候会限制你更容易地表达潜在的逻辑,或者有时候你会发现本身在模板中加入了不少逻辑,但你仍是有不少逻辑在JavaScript 中而 render 函数容许你把这些逻辑组合在一个地方你一般不须要想太多关于这些状况下的标记。
因此我理解是模板会完成你要作的事在99%的状况下你只须要写出HTML就行了,但偶尔可能想作些更可控的事情在,你须要编写一个渲染函数。Vue 2中的渲染函数以下所示,
render(h) { return h ( 'div', { attrs: { id: foo }, on: { click: this.onClick }, 'hello' }) }
因此这是组件定义中的一个选项,相对于提供一个 template 选项,在 Vue 2 中你能够为组件提供一个渲染函数,你会获得 h 参数,直接做为渲染函数的参数。你能够用它来创造咱们称之为虚拟DOM节点,简称 vnode。
vnode 接受三个参数:
在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 的实现获得了启发来自其余项目对吗?
是的有一个库叫snabbdomVue 2基本上就是从这个库中分离出来的。
2.好的而后是Vue 3,你在这里的编码方式只是改进了Vue 2的模式吗?
好吧,Vue 3是一个完全的重写,几乎从头开始一切都是定制的显然,有现有的算法看起来像没有变化,由于这些是咱们看到社区在作普遍研究的领域因此这是创建在全部这些之前的实现的基础上的但代码自己如今是从头开始。
3.都是用TypeScript写的,对吧?
是的,都是 TypeScript 写的。
看看渲染函数在 Vue 中是什么样子。在 Vue 2 中,一个传统的 Vue 组件,有一个 template 选项,可是为了重用渲染函数咱们能够用一个名为 render
的函数来代替它,咱们会经过参数获得这个称为 h(hyperscript)。但在这里,咱们只是示范一下咱们如何在 Vue 3 中使用它。咱们会从 vue 导入 h,咱们能够用它来返回 h。
import { h } from 'vue' const App = { render () { return h('div') } } // 等效模板中的普通 div
1.因此它返回 div 的 JavaScript 对象表示?
彻底正确。
2.那么,你的虚拟dom就像…编译器?是编译器接收它吗?
是渲染器,渲染器接收它。
3.而后它实际上进行 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 () { } const App = { render () { return this.ok ? h('div',{ id: 'hello' },[h('span','world')] : this.otherCondition ? h('p', 'other branch') : h('span') ) } }
这就是 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 对象数组,你能够用 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 中 ul
和 ol
的默认样式。
<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>
这里有一个普通的基于模板的语法,在同一个插槽内它们都是默认插槽,你能作的只有渲染这个部分,在模板很难实现。可是你能够用渲染函数来实现,程序化的遍历插槽内的每一个项目而后把它们变成别的东西。
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 提供了这两个选项,当状况出现的时候以便您能够选择合适的方式。