本文产出于学习Vue源码的教程之时。
Vue是如何渲染页面的
Vue编译第一阶段:
在Vue中,渲染系统是组成响应系统的另外一半,如果使用Vue CLI构建项目,会用到webpack和vue-loader,实际上vue-loader会在构建阶段实现预编译,把模版代码编译为浏览器可直接解析的DOM代码。另外,Vue还提供了用于编译的渲染函数,它类似angular的ALT编译模式,那应用就可以运行未编译版本。
两种编译模式,一种会把编译器打包进去,一个直接把代码预先编译,包含编译器版本经过gzip压缩大概30KB,不包含编译器版本大概20KB,所以预先编译会更好。
所以Vue的templates实际上是通过渲染函数渲染出来的,参照上图的第一步骤,vue把模板编译成渲染函数。但如果你把模版直接传入Vue实例那Vue会执行完整的编译,把传入的template编译为浏览器可运行的DOM。
Vue编译第二阶段:
经过第一阶段编译为render函数后,render函数实际上是返回虚拟DOM,接着Vue基于虚拟DOM生成真实DOM。
Vue编译第三阶段:
再将生成的虚拟结点渲染/更新到真实结点(DOM)中。
虚拟DOM更新机制:
回顾之前讲的
[autorun
] https://juejin.cn/post/7107445451556651015
函数,其实我们可以把生成虚拟DOM的代码放在autorun
函数里面,因为渲染函数与所有data属性有依赖关系,当属性发生变化那就触发autorun
函数然后重新生成新的虚拟DOM,新的虚拟DOM和旧的虚拟DOM进行比较,更新差异的节点再生成真实DOM完成视图更新。
Render 函数即渲染函数,它是个函数,接受一个参数为createElement的函数,返回值为VNode(即:虚拟节点),也就是我们要渲染的节点。
createElement
createElement 是 render 函数 的参数,它本身也是个函数,并且有三个参数。
createElement 函数的返回值是 VNode(即:虚拟节点)。
createElement 函数的参数(三个)
/**
* render: 渲染函数
* 参数: createElement
* 参数类型: Function
*/
render: function (createElement) {
let _this = this['$options'].parent // 我这个是在 .vue 文件的 components 中写的,这样写才能访问this
let _header = _this.$slots.header // $slots: vue中所有分发插槽,不具名的都在default里
/**
* createElement 本身也是一个函数,它有三个参数
* 返回值: VNode,即虚拟节点
* 1. 一个 HTML 标签字符串,组件选项对象,或者解析上述任何一种的一个 async 异步函数。必需参数。{String | Object | Function} - 就是你要渲染的最外层标签
* 2. 一个包含模板相关属性的数据对象你可以在 template 中使用这些特性。可选参数。{Object} - 1中的标签的属性
* 3. 子虚拟节点 (VNodes),由 `createElement()` 构建而成,也可以使用字符串来生成“文本虚拟节点”。可选参数。{String | Array} - 1的子节点,可以用 createElement() 创建,文本节点直接写就可以
*/
return createElement(
// 1. 要渲染的标签名称:第一个参数【必需】
'div',
// 2. 1中渲染的标签的属性,详情查看文档:第二个参数【可选】
{
style: {
color: '#333',
border: '1px solid #ccc'
}
},
// 3. 1中渲染的标签的子元素数组:第三个参数【可选】
[
'text', // 文本节点直接写就可以
_this.$slots.default, // 所有不具名插槽,是个数组
createElement('div', _header) // createElement()创建的VNodes
]
)
}
虚拟DOM,顾名思义他是一个非真实DOM节点的JavaScript对象。
在Vue中的虚拟DOM会在每个实例通过this.$createElement返回一个虚拟节点,这个虚拟节点也表示一个div但他是一个纯javascript对象,他和真实DOM差异是非常大的。看到上图虚拟DOM它除了包含当前节点名字和属性,还有children表示节点的子元素,这就构成了一个虚拟DOM树。
虚拟DOM和真实的DOM的差异:
1、 资源消耗问题
使用javascript操作真实DOM是非常消耗资源的,虽然很多浏览器做了优化但是效果不大。你看到虚拟DOM是一个纯javascript对象。假设你有1000个节点,那会相应创建1000个节点,那也是非常节省资源的,但是如果创建1000个DOM节点就不同了。
2、执行效率问题
如果你要修改一个真实DOM,一般调用innerHTML
方法,那浏览器会把旧的节点移除再添加新的节点,但是在虚拟DOM中,只需要修改一个对象的属性,再把虚拟DOM渲染到真实DOM上。很多人会误解虚拟DOM比真实DOM速度快,其实虚拟DOM只是把DOM变更的逻辑提取出来,使用javascript计算差异,减少了操作真实DOM的次数,只在最后一次才操作真实DOM,所以如果你的应用有复杂的DOM变更操作,虚拟DOM会比较快。
3、虚拟DOM还有其他好处
其实虚拟DOM还可以应用在其他地方,因为他们只是抽象节点,可以把它编译成其他平台,例如android、ios。市面上利用形同架构模式的应用有React Native,Weeks,Native script,就是利用虚拟DOM的特点实现的。
俩者的本质都是用来声明DOM与状态的关系。
模版的优势:模版是一种更静态更具有约束的表现形态,它可以避免发明新语法,任何可以解析HTML的引擎都可以使用它,迁移成本更低;另外最重要的是静态模版可以在编译进行比较多的优化,而动态语言就没法实现了。
jsx的优势:更灵活,任何的js代码都可以放在jsx中执行实现你想要的效果,但是也由于他的灵活性导致在编译阶段优化比较困难,只能通过开发者自己优化。
Vue吸收了两者的优点,提供了两种渲染模式,Vue把template作为默认的编译模式,如果你需要构建更灵活的应用,完全可以使用render function实现。
export deafult {
render(h){
return h('div',{},[...])
}
}
render
函数接收一个参数h
, h
只是一种约定的简写表示超脚本(HyperScript),他没有什么特殊意义,只是就像超文本我们叫HTML一样,只是方便书写的表示形式而已。
h
函数接受三个参数,第一个是元素类型;第二是参数对象例如表示元素的attr属性,DOM属性之类的;第三个属性表示一些子节点,你可以调用h函数生成更多子节点。
h('div','only text')
h('div',{class:'foo'},'some text')
h('div',{...},[
'only text',
h('span','bar')
])
如上,h函数中的第二个参数是可以省略的,第三参数很灵活可以是数组或者单纯的文本。
例一表示创建一个只包含some text
文本的div;例二表示创建一个具有class=foo
的div;例三表示包含一个子节点span。
我需要编写一个组件,组件根据tags
属性在页面上输入相应的HTML标签,如果使用模板技术实现,会让代码变得臃肿,需要通过if
语句判断不同标签。所以这里可以利用渲染函数来实现,下面是具体实现代码。
Vue.component('example', {
props: ['tags'],
render(h) {
// 第二参数是可选参数,可接受vnodes类型的数组,数组可以是数字和字符串
return h('div', this.tags.map((tag, i) => h(tag, i)))
}
})
new Vue({ el: '#demo_4_5' })
函数组件就是不包含state和props的组件,就像它的名字一样,你可以理解为他就是一个函数,在Vue中声明一个函数组件代码如下:
const foo = {
functional: true,
render: h => h('div', 'foo')
}
函数组件特点:
render(h, context)
使用函数组件渲染标签:
Vue.component('example', {
functional: true, // 声明是函数组件
// 因为函数组件没有this,可以通过render第二参数获取相关信息
render(h, { props: { tags } }) {
// context.slots() 通过slots方法获取子节点
// context.children 获取子组件
// context.parent 父组件,因为函数组件实挂载到根节点上,也就是
// context.props 组件属性,这里得到tags属性
// return h('div', this.tags.map((tag, i) => h(tag, i)))
// 通过函数组件实现标签动态渲染
return h('div', tags.map((tag, i) => h(tag, i)))
}
})
渲染函数除了可以渲染普通标签外,还可以渲染组件,下面代码有Foo
和Bar
组件,点击toggle
按钮的时候,切换两组件的显示状态。
const Foo = {
render(h) {
return h('div', 'foo')
}
}
const Bar = {
render(h) {
return h('div', 'bar')
}
}
Vue.component('example', {
props: ['ok'],
render(h) {
return h(this.ok ? Foo : Bar)
}
})
new Vue({ el: '#demo_4_6', data: { ok: true } })
上图是Vue的响应性系统和渲染系统的运行流程,可以看到每个组件有自己的渲染函数,这个渲染函数实际上是运行在我们之前封装的autorun
函数中的,组件开始渲染时会把属性收集到依赖项中,当调用属性的setter方法,会触发watcher
执行重新渲染,因为渲染函数放在autorun
函数中,所以每当data数据发生变化,就会重新渲染。
每个组件都有自己独立的循环渲染系统,组件只负责自己的依赖项,这一特性对于你拥有大型组件树时是一个优势,你的数据可以在任何地方改变,因为系统知道数据与组件的对应关系,不会造成过度渲染问题,这一架构优势可以让我们摆脱一些优化工作。
Vue渲染页面步骤
Vue中也可以使用无状态组件,在只做单一功能的组件时可考虑使用