解析模板编译template的背后发生了什么
- 一、📑初识模板编译
- 1、vue组件中使用render代替template
- 2、模板编译总结
- 二、✏️感受模板编译的美
- 1、with语法
- (1)例子展示🌰
- (2)知识点归纳
- 三、📈编译模板
- 1、编译模板碎碎念
- 2、编译模板过程
- (1)初始化一个npm环境
- (2)安装编译器
- (3)新建新文件
- (4)了解缩写函数
- (5)编译插值
- (6)编译表达式
- (7)编译属性和动态属性
- (8)编译条件
- (9)编译循环
- (10)编译事件
- (11)编译v-model
- 3、模板编译总结
- 四、🔑组件渲染/更新过程
- 1、初识组件渲染/更新
- 2、组件渲染/更新过程
- (1)初次渲染过程
- 1)解析模板为render函数
- 2)触发响应式
- 3)执行render函数,生成vnode
- (2)更新过程
- 1)更新过程细述
- 2)完成流程图
- (3)异步渲染
- 3、小结
- 五、✔️结束语
依稀记得我们在vue时,最上方总是有一个
template
包围着。而很多时候,我们也没有很在意的去意识到
<template></template>
究竟是什么。
在今天的这篇文章中,就带大家一起来了解,模板编译 template
的背后,究竟发生了什么事情?
一起来了解模板编译的纸短情长🚋🚋🚋
一、📑初识模板编译
1、vue组件中使用render代替template
template
,即模板。模板是 vue
开发中最常用的部分,即与vue的使用关联最紧密的原理。它不是 html
,它有指令、有插值、也有 JS
表达式,那它,到底是什么呢?我们来看个例子。
在 vue
中定义一个组件,通常会使用 template
模板字符串来定义一个组件。比如:
Vue.component('heading',{template:`xxx`
})
一般情况下,模板的定义是上面这种情况。同时,在程序编译期间,模板会将 template
的这种字符串类型,编译成 render
函数。
但是呢,在有些复杂的情况下,可能就不能用 template
函数了,这个时候会考虑直接用 render
函数来定义一个组件。比如:
Vue.component('heading',{render: function(createElement){return createElement('h' + this.level, //tag props[ //childrencreateElement('a',{attrs:{name:'headerId',href:'#' + 'headerId'}},'this is a tag')])}
})
就像上面这样子,我们也可以通过使用一个 render
函数来定义一个组件。
2、模板编译总结
看完上面的例子,我们来做个小结✨
- template,即模板。这个模板会编译成
render
函数,其中render
函数用的是with
语法。 - 过程:模板→
render
函数→vnode
→组件渲染和更新过程。 vue
组件可以用render
函数代替template
。React
一直都用render
,没有模板(这里仅作知识补充,不做讲解)。
二、✏️感受模板编译的美
1、with语法
(1)例子展示🌰
先来了解模板编译中一个很重要的知识点, with
语法。下面先用一个例子来展示with语法与普通语法的不同。
不使用with语法执行程序时:
const obj = {a: 100, b: 200}console.log(obj.a)
console.log(obj.b)
console.log(obj.c) //undefined
使用with语法执行程序时:
//使用with,能改变 {} 内自由变量的查找方式
// 将 {} 内自由变量,当作 obj 的属性来查找
with(obj){console.log(a)console.log(b)console.log(c) //会报错!!!
}
(2)知识点归纳
看完上面with语法的例子,我们来对 with
语法做一个知识点归纳。
with
语法会改变{}
内自由变量的查找规则,当作obj
属性来查找;- 如果在当前
{}
内找不到匹配的obj
属性,就会报错; with
要谨慎使用,它打破了作用域规则,会让其易读性变差。
三、📈编译模板
1、编译模板碎碎念
在前面中我们讲过,模板它不是 html
,它有指令、有插值、也有JS表达式,它能实现判断、也能实现循环。
试想一下模板为什么不是 html
?
思考一下,假如你在写程序时,能用 html
写出一个判断或者循环出来吗?答案自然时不行的。
所以说, html
只是一个静态的标签语言,你写什么它就显示什么,它没有办法实现一个逻辑,或者做循环和判断。
因此,对于前端浏览器而言,只有 JS
才能实现判断和循环等各种逻辑功能。
所以,模板一定是转换为某种 JS
代码之后才进行运行的。而这个模板怎么转换成 js
代码的这个过程,就称为编译模板。
那这个模板是怎么转的呢?接下来我们来看下编译模板的过程。
2、编译模板过程
(1)初始化一个npm环境
首先先建立一个新文件,可以命名为 vue-template-complier-demo
。之后用以下命令行初始化一个npm的环境:
npm init -y
(2)安装编译器
npm
安装模板编译器。命令行如下:
npm install vue-template-compiler --save
(3)新建新文件
在根目录下初始化新建一个 index.js
文件,并引入 vue-template-compiler
。代码如下:
//引入vue-template-compiler
const compiler = require('vue-template-compiler')// 编译
const res = compiler.compile(template)
console.log(res.render)
接下来我们就来看下,模板中的插值、表达式、属性和动态属性等等类型的编译,到底是怎么样的?
(4)了解缩写函数
以下vue源码中的缩写函数先了解,将在下面的讲解中用到。
// 从 vue 源码中找到缩写函数的含义
function installRenderHelpers (target) {target._c = createElement;target._o = markOnce;target._n = toNumber;target._s = toString;target._l = renderList;target._t = renderSlot;target._q = looseEqual;target._i = looseIndexOf;target._m = renderStatic;target._f = resolveFilter;target._k = checkKeyCodes;target._b = bindObjectProps;target._v = createTextVNode;target._e = createEmptyVNode;target._u = resolveScopedSlots;target._g = bindObjectListeners;target._d = bindDynamicKeys;target._p = prependModifier;
}
(5)编译插值
//引入vue-template-compiler
const compiler = require('vue-template-compiler')// 插值
const template = `<p>{{message}}</p>`
// with(this){return createElement('p',[createTextVNode(toString(message))])}
// h -> vnode
// createElement -> vnode// 编译
const res = compiler.compile(template)
console.log(res.render)
编译以上内容,打印结果如下:
从上图中可以看到,插值类型的模板最终被编译成一个 with
语句,并且这个 with
语句的参数都指向了 this
。
同时,大家可以看到,里面有一个 _c
, _v
, _s
。那这几个元素是什么呢?
这个就是上面第四点中提到的 vue
源码中的缩写函数。 _c
对应的就是源码中的 createElement
, _v
对应的就是源码中的 createTextVNode
,_s
对应的就是源码中的 toString
。
所以,以上编译后的 with
语句 with(this){return _c('p',[_v(_s(message))])}
,事实上就是 with(this){return createElement('p',[createTextVNode(toString(message))])}
。
以上这个语句的意思为,编译创建一个 p
元素,之后呢, p
元素就没有子元素了,于是就创建它的文本节点 message
,同时 message
是字符串的形式存在,因此要进行 toString
。
额外再补充一个知识点, createElement
其实就等于我们平常所说的 h
函数,返回的是一个 虚拟DOM
节点。
以上就是一个插值模板编译的过程,下面再用几个例子让大家熟悉。
(6)编译表达式
//引入vue-template-compiler
const compiler = require('vue-template-compiler')// 表达式
const template = `<p>{{flag ? message : 'no message found'}}</p>`
// with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}// 编译
const res = compiler.compile(template)
console.log(res.render)
编译以上内容,打印结果如下:
依据上面插值的分析方法,我们来分析表达式的模板编译过程。
表达式编译后的结果返回了一个虚拟 DOM
节点,同样地,查询 vue
源码中的缩写函数我们可以发现, with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
最终的结果等于 with(this){return createElement('p',[createTextVnode(toString(flag ? message : 'no message found'))])}
。
先创建了一个 p
元素,之后 p
元素没有子元素了,于是创建文本节点,最终 toString
三目表达式里面的内容。
(7)编译属性和动态属性
//引入vue-template-compiler
const compiler = require('vue-template-compiler')// 属性和动态属性
const template = `<div id="div1" class="container"><img :src="imgUrl"/></div>
`
// with(this){return _c('div',
// {staticClass:"container",attrs:{"id":"div1"}},
// [
// _c('img',{attrs:{"src":imgUrl}})])}// 编译
const res = compiler.compile(template)
console.log(res.render)
编译以上内容,打印结果如下:
依据上面的分析方法,我们来分析属性和动态属性的模板编译过程。
属性和动态属性编译后的结果返回了一个虚拟 DOM
节点,同样地,查询 vue
源码中的缩写函数我们可以发现, with(this){return _c('div',{staticClass:"container",attrs:{"id":"div1"}},[_c('img',{attrs:{"src":imgUrl}})])}
最终的结果等于 with(this){return createElement('div',{staticClass:"container",attrs:{"id":"div1"}},[createElement('img',{attrs:{"src":imgUrl}})])}
。
此时我们可以看到,返回的 vnode
节点中,包含 class
名字, container
。此时 div
有一个 id
选择器,这个 id
选择器是该 div
的一个属性,于是就通过attrs来表示。
最外层结束后,里面还有一层, img
。 img
可以视其为跟 div
一样的标签,于是先创建 img
元素,又因为 img
绑定了一个具体的值,就像是 div
里面绑定了 id
选择器。所以在创建完 img
的值之后,继续用 attrs
来传递 img
所绑定的值。
(8)编译条件
// 条件
const template = `<div><p v-if="flag === 'a'">A</p><p v-else>B</p></div>
`
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}
编译以上内容,打印结果如下:
依据上面的分析方法,我们来分析条件的模板编译过程。
对于条件来说,首先是先创建一个 div
元素,之后呢,模板编译把 v-if
和 v-else
分割成一个三目表达式的方式来进行编译。
(9)编译循环
// 循环
const template = `<ul><li v-for="item in list" :key="item.id">{{item.title}}</li></ul>
`
// with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}
编译以上内容,打印结果如下:
依据上面的分析方法,我们来分析循环的模板编译过程。
对于以上循环来说,首先会创建一个 ul
元素,之后查询 _l
的缩写函数我们知道它是 renderlist
, 所以 list
列表会被 renderList
函数进行编译。
最后渲染后的 item
被当作函数的参数进行传递,并列返回对应 item
的 li
列表元素。
(10)编译事件
// 事件
const template = `<button @click="clickHandler">submit</button>
`
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}
编译以上内容,打印结果如下:
依据上面的分析方法,我们来分析事件的模板编译过程。
对于事件来说,首先会创建一个 button
元素,之后 @click
即 v-on:click
会被编译成 on:{"click":clickHandler}
。最后是 _v
,即 createTextVNode
。创建一个 submit
的文本节点,将 click
的内容提交上去。
(11)编译v-model
// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
// with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}
编译以上内容,打印结果如下:
依据上面的分析方法,我们来分析双向绑定v-model的模板编译过程。
对于 v-model
来说,主要看的是 input
事件。 v-model
的背后,绑定的是 name
和 value
这两个语法糖。之后通过 attrs
去创建 类型type
为 text
的属性。
最终是 input
事件, input
事件绑定 $event
,最后, name
的值就等同于 $event.target.value
,这样,数据就实现了双向绑定。
3、模板编译总结
看完上述的内容,我们来对模板编译做个小结:
(1)从render函数到vnode
模板编译后是一个 render
函数,执行 render
函数后返回一个 vnode
;
(2)vnode到patch和diff
基于 vnode
的基础上,再执行 patch
和 diff
;
(3)模板编译工具
在平常的开发中,我们可以使用 webpack
、 vue-loader
等构建工具,在开发环境下编译模板。
四、🔑组件渲染/更新过程
1、初识组件渲染/更新
讲完上完的内容,我们再来讲一个与编译模板关联性很强的知识点:组件渲染/更新过程。
一个组件,从渲染到页面上开始,再到修改 data
去触发更新(数据驱动视图),其背后的原理是什么,又需要掌握哪些要点呢?
事实上,组件在渲染之前,会先进行模板编译,模板 template
会编译成 render
函数。
之后就是数据的监听了,这就要谈到响应式数据。vue的响应式通过操作 Object.defineProperty()
,去监听 getter
和 setter
方法,来使得数据实时更新。
监听完数据之后,就是执行 render
函数,生成 vnode
。
到了 vnode
(即 vdom
)这一步之后,会进行 patch(elem,vnode)
和 patch(vnode,newVnode)
的比较。
关于响应式原理和vdom的解读,如有需要可以查看我的前两篇文章进行学习,这里不再展开细述~
2、组件渲染/更新过程
组件渲染和更新过程主要经过以下三个步骤:初次渲染过程→更新过程→异步渲染。
接下来就这三个步骤进行一一讲解。
(1)初次渲染过程
初次渲染过程,即组件第一次渲染是怎么样的,怎么把模板渲染到页面上。具体有以下三个步骤:
- 解析模板为
render
函数; - 触发响应式,监听
data
属性getter
和setter
; - 执行
render
函数,生成vnode
,进行patch(elem,vnode)
。
下面就这三个步骤来进行一一讲解。
1)解析模板为render函数
在开发环境下,解析模板为 render
函数一般是由 vue-loader
这个插件来处理的。还有一种情况就是,用户直接用 cdn
的方式引入 vuejs
的文件进行本地代码练习,这种情况下,解析模板为 render
函数就是在浏览器环境运行的。
小知识了解完,我们来看下这个步骤。
解析模板为 render
函数,即解析 template
为 render
函数,这个就是上述文章中说的编译模板。
2)触发响应式
在编译完模板之后, render
函数有了,我们来开始监听 data
属性。
监听 data
属性,这个时候我们就需要触发响应式,也就是渲染数据。
那在这个阶段怎么渲染数据呢?
这个阶段我们需要执行 render
函数, render
函数会触发 getter
方法,因为数据没有进行更新,只是进行渲染。只有在进行渲染的时候才会操作 setter
方法。
3)执行render函数,生成vnode
最后,当数据渲染完毕后,就会执行第一步生成的 render
函数,然后生成虚拟 DOM
节点 vnode
,之后进行 patch(elem,vnode)
。
(2)更新过程
1)更新过程细述
更新过程,即 data
修改之后,组件是怎么更新的。
在这个阶段呢,将会修改 data
,并且触发 setter
(注意:在此之前 data
在 getter
中已经被监听)。
触发完 setter
之后,重新执行 render
函数,并生成 newVnode
,最后进行 patch(vnode, newVnode)
的diff比较。
2)完成流程图
接下来我们用一张流程图来完整的回顾渲染和更新的过程。
(3)异步渲染
在渲染和更新结束之后,我们的程序可能还有可能会发生多个程序同时加载,这就涉及到一个异步渲染问题。
异步渲染问题,我们用 $nextTick
来作为例子讲解。
假设我们现在要实现一个功能,当我们点击按钮时,打印出列表的项数。这个时候我们大多人可能会这么操作。
<template><div id="app"><!-- ref的设置时为了方便后续可以用来:取节点的DOM元素 --><ul ref="ul1"><li v-for="(item, index) in list" :key="index">{{item}}</li></ul><button @click="addItem">添加一项</button></div>
</template><script>
export default {name: 'app',data() {return {list: ['a', 'b', 'c']}},methods: {addItem() {this.list.push(`${Date.now()}`)this.list.push(`${Date.now()}`)this.list.push(`${Date.now()}`)// 获取 DOM 元素const ulElem = this.$refs.ul1console.log( ulElem.childNodes.length )}}
}
</script>
此时浏览器的显示效果如下:
细心的小伙伴已经发现,浏览器并没有按照我们所想的打印。当页面上的列表显示 6项
内容时,此时控制台只打印 3项
;当显示 9项
时,此时控制台直接只打印 6项
。
那这究竟时为什么呢?
其实,当我们点击的那一刻, data
发生变化,但是 DOM
并不会立刻进行渲染。所以等到我们点击完成的时候,获取的元素还是原来触发的内容,而不会增添上新的内容。
那我们所期望的是,当点击之后立刻触发 DOM
渲染并拿到最新的值。这个时候就需要用到 nextTick
。具体代码如下:
<script>
export default {name: 'app',data() {return {list: ['a', 'b', 'c']}},methods: {addItem() {this.list.push(`${Date.now()}`)this.list.push(`${Date.now()}`)this.list.push(`${Date.now()}`)// 1. 异步渲染,$nextTick 待 DOM 渲染完再回调,// 即NextTick函数会在多次data修改完并且全部DOM渲染完再触发,仅在最后触发一次// 2. 页面渲染时会将 data 的修改做整合this.$nextTick(() => {// 获取 DOM 元素const ulElem = this.$refs.ul1console.log( ulElem.childNodes.length )})}}
}
</script>
我们通过给获取 DOM
元素的代码外面再嵌套一层 $nextTick
函数,来达到我们想要的效果。在此过程中,当我们点击结束后, data
的值发生变化,此时 $nextTick
会等待DOM全部渲染完成之后再进行回调。
最终浏览器的打印效果如下:
所以,也就是说, $nextTick
通过汇总 data
的修改,最后再一次性更新视图。
这样可以减少 DOM
的操作次数,大大的提高了性能。
3、小结
经过上述一系列的讲解,我们可以把内容分割成以下两个要点:
- 要理解清楚渲染和响应式、渲染和模板编译、渲染和vdom的关系。
- 要理解组件渲染/更新的过程:初次渲染过程→更新过程→异步渲染。
五、✔️结束语
从模板编译,到组件渲染更新过程,我们了解了整个 template
背后的全过程。相信通过本文的学习,大家对模板编译有了一个更深的认识。
关于模板编译的内容就讲到这里啦!如有不理解或文章有误,欢迎评论区留言或私信我交流~
- 关注公众号 星期一研究室 ,不定期分享学习干货,更多有趣的专栏待你解锁~
- 如果这篇文章对你有帮助,记得 点个赞加个关注 再走哦~
- 我们下期见!🥂🥂🥂