前言
vue原理学习系列,本期为主要学习下vue的
渲染函数render function运作原理,和Virtual Dom是什么,欢迎大家参考和提出意见。
vue原理学习系列往期文章:
⚡「内容速览」⚡
- render function介绍
- 响应性和渲染函数的结合
- virtual dom
- 动态渲染标签
- 动态渲染组件
- 高阶组件
下面的内容是基于vue2来进行展开的
render function介绍
render function(渲染函数)实际上是一个返回虚拟节点 (virtual node)的函数,整个vnode树称为Virtual Dom,Vue基于虚拟dom生成真实的dom。
vue生成虚拟dom的过程本质上是调用渲染函数,在vue实例中渲染函数和data属性具有依赖关系,同时由于这些data属性是具有响应性的,因此这些data属性会帮助组件的渲染函数收集依赖,当这些依赖关系发生变化时,就会再次调用渲染函数,返回一个新的虚拟dom。
旧的虚拟dom会和新的虚拟dom进行比较和区分,也就是常说的diff算法。
render函数需要一个参数createElement,返回createElement创建virtual node
render: function(createElement) {
return createElement()
}
此外createElement还有一个别名h,这是vue中通用的写法。
render: function(h) {
return h()
}
当我们申明组件为函数式组件时(函数式组件没有this),render会提供context作为第二个参数来获取上下文。
render: function(h, context) { //当组件申明 functional: true
return h()
}
响应性和渲染函数的结合
上面的图片很直观说明了响应性和渲染函数的运行流程,每一个组件都有自己的一个渲染函数,而这个渲染函数实际上是包装在之前(响应性)中实现的autorun函数,当组件渲染的时候,vue会通过调用data属性的getter来收集它们的依赖项,当调用属性setter方法会触发通知的执行。
这里我们可以看到还有一个额外的概念watcher(观察者),每个组件都有一个负责监视的观察者,它们主要作用保存收集的依赖项,通知所有内容,来触发重新渲染。
渲染函数则返回虚拟dom树。
整个流程是循环的,因为渲染函数是放在autorun中,只要我们依赖的渲染属性发生变化,渲染就会被反复调用。
每个组件都有自己的自动循环渲染,组件树由许多这些组件构成,每个组件都只负责自己的依赖。因此当组件树较大的时候,这实际上是一个优势,我们可以在任意地方对数据进行修改,因为在组件树中每个组件只负责自己那部分依赖,我们可以确切知道那些组件受到哪些数据影响,通过精确的依赖跟踪系统,不会造成过多的组件重新渲染。
virtual dom
- actual dom(真实dom):
例如调用document.createElement('div')创建一个真实div节点并插入到文档流中,这种原生DOM API实际上是通过c++编写的浏览器引擎实现的。一个真实DOM节点会包含很多属性,它的底层实现很复杂,但我们可以通过原生代码的javascript接口来进行很多dom操作。
- virtual dom(虚拟dom):
所谓虚拟dom,实际上是一个用于表示真实dom结构和属性的javascript对象。
在vue中,Virtual dom就是由 VNode节点构建的树,VNode可以理解为vue框架的虚拟dom的基类,每个DOM元素或组件都对应一个VNode对象,它包含的信息告诉 Vue 页面上需要渲染什么样的节点,节点属性,和其子节点的描述信息。
// 一个div标签
// Actual DOM
"[object HTMLDivElement]"
// Virtual DOM
{ tag: 'div', data: { attrs: {}, ... }, children: [] }
那么virtual dom的特点和相较和actual dom优势在哪?
- ① 资源消耗: 虚拟DOM比真实DOM更节省资源。假设有1000个元素的列表,创建1000个javascript对象是相较于创建1000个div节点是非常节省,也是更快的。
虚拟DOM本质上是轻量javacript数据,用来表示真实DOM在特定时间的外观。vue能够在每次更新时生成虚拟DOM的副本正是因为虚拟DOM比真实DOM节省。
- ② 性能和效率: 假设我们通过innerHtml去更新应用,此时我们需要丢弃之前的DOM节点,再从新生成所有的DOM节点,如果只有某一行数据改变了,重置整个innerHTML的代价无疑是巨大的。而虚拟DOM我们只需要修改对象的属性,并计算它们差异,再将这些更改应用到DOM上,性能上是具备优势的:
virtual dom使用能够减少页面操作,对于页面的操作,比较VNode的区别,在最后一步才对真实dom进行更新,减少dom频繁操作,提高性能。
尤大对InnerHTML和Virtual DOM 的重绘性能消耗比较:
-
innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size) -
Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change) -
③ 函数式的UI编程: virtual DOM为函数式的 UI 编程方式打开了大门。
-
④ 虚拟DOM的架构优势: virtual DOM让我们可以渲染到 DOM 以外的 backend。virtual DOM是抽象的javascript节点,我们可以创建相同应用程序虚拟运行在任何支持javascript的环境中,可以是原生渲染引擎,例如(Ios,Andriod),让虚拟DOM进行原生渲染,使得React Native、Native script成为可能。
动态渲染标签
编写一个组件,根据组件的tags属性,利用渲染函数渲染出相应的HTML标签:
// 实现目标
Implement the "example" component which given the following usage:
<example :tags="['h1', 'h2', 'h3']"></example>
which renders the expected output:
<div>
<h1>0</h1>
<h2>1</h2>
<h3>2</h3>
</div>
<div id="app">
<example :tags="['h1', 'h2', 'h3']"></example>
</div>
<script>
Vue.component('example', {
functional: true,
// 验证props
props: {
tags: {
type: Array,
validator(arr) { return !!arr.length; }
}
},
render: (h, context) => {
const tags = context.props.tags;
// tags.map 返回动态渲染的children
return h('div', context.data, tags.map((tag, index) => h(tag, index)));
}
})
</script>
动态渲染组件
渲染函数除了渲染html标签以外,还可以渲染组件,根据ok的值渲染Foo和Bar:
<div id="app">
<example :ok="ok"></example>
<button @click="ok = !ok"></button>
</div>
<script>
const Foo = {
functional: true,
render: h => h('div', 'foo')
}
const Bar = {
functional: true,
render: h => h('div', 'bar')
}
Vue.component('example', {
functional: true,
props: {
ok: Boolean
},
// ok为true渲染Foo,为false渲染Bar
render: (h, context) => h(context.props.ok ? Foo: Bar)
})
// ok值随button点击改变
new Vue({
el: '#app',
data: {
ok: true
}
})
</script>
高阶组件
- 高阶函数:
指对其他函数进行操作的函数,这里的操作可以是将函数作为参数,或将函数作为返回值。简单来说,就是一个函数的参数或返回值为函数称为高阶函数。
- 高阶组件
高阶组件同样是一个函数(不是组件),它接受一个组件作为参数并返回一个新组件,利用高阶组件我们将组件中某些相同逻辑抽离出来,通过props来给需要进行包装的组件传递数据。利用高阶组件的灵活性,我们能够更好复用组件。
- 高阶组件实现 实现目标:avatar是一个接受src属性来展示头像的组件,现在希望实现SmartAvatar组件,组件接受用户名字,返回对于用户头像URL。
<div id="app">
<smart-avatar username="vuejs"></smart-avatar>
</div>
<script>
function fetchURL (username, cb) {
setTimeout(() => {
cb('https://avatars3.githubusercontent.com/u/6128107?v=4&s=200')
}, 500)
}
const Avatar = {
props: ['src'],
template: `<img :src="src">`
}
// 高阶组件
function withAvatarURL (InnerComponent) {
return {
props: {
username: String
},
data() {
return {
url: 'http://via.placeholder.com/200x200'
}
},
created() {
fetchURL(this.username, (url) => { this.url = url })
},
render(h) {
return h(InnerComponent, {
props: {
src: this.url
}
})
}
}
}
const SmartAvatar = withAvatarURL(Avatar);
new Vue({
el: '#app',
components: {
SmartAvatar
}
})
</script>
- 高阶组件和mixin的区别和选择:
实际刚刚上面的高阶组件所实现的功能通过mixin也能够实现,那么高阶组件和mixin有什么区别呢?
-
高阶组件更具有重用性,特别是在我们想把组件应用在很多地方,高阶组件实际上只是调用了原始组件,并没有修改原始组件的内容。
-
高阶组件更易于测试,高阶组件没有逻辑上耦合,对于高阶组件和原始组件可以分别测试,可以分为渲染和数据获取逻辑这两个部分。
-
实际上高阶组件和mixin应看场景来使用,过多使用高阶组件,会使得组件层级变得复杂,难以弄清层叠关系,多层嵌套时数据可能需要经过多层传递,此外嵌套多层也会导致一些性能的开销。
自己前端博客:前端学习笔记
参考链接: