模板编译的总体过程:使用 vue template complier 将模板编译为 render 函数,然后执行 render 函数生成vnode
前置知识:JS的 with 语法
with语法改变{}块内自由变量的查找规则,当做obj属性来查找- 如果找不到匹配的
obj属性,就会报错 with要慎用,它打破了作用域规则,易读性变差
vue template complier 将模板编译为 render 函数
vue模板被编译成什么?
- 模板不是html , 有指令、插值、JS 表达式,能实现判断、循环
- html是标签语言,只有JS才能实现判断、循环(图灵完备的)
- 因此,模板一定是转换为某种JS代码,模板怎么转成js代码的过程就是模板编译
首先我们安装 vue template complier 这个库,然后引入后写一个模板字符串,查看编译输出值
插值编译
const compiler = require('vue-template-compiler')
// 插值
// const template = ` <p>{{message}}</p> `
// 编译
const res = compiler.compile(template)
console.log(res.render)
打印结果如下
with(this){return _c('p',[_v(_s(message))])}
其中 this 在vue中就是 vm 实例,所以 _c _v _s 就是vue源码中的一些函数,所以我们去源码中搜索一下这几个函数的意义
// 从 vue 源码中找到缩写函数的含义
function installRenderHelpers (target) {
target._c = createElement//创建vnode
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;
}
所以转化后如下
with(this){return createElement('p',[createTextVNode(toString(message))])}
createElement函数的作用就是创建一个vnode
表达式编译
表达式会转变为js代码,然后把结果放到vnode里面去
const template = ` <p>{{flag ? message : 'no message found'}}</p> `
with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
动态属性
同理
//动态属性
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}})])}
条件
使用三元表达式来创建不同的vnode节点
// 条件
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")])])}
循环
通过 _l ( renderList )函数,传入数组或者对象,即可返回列表vnode
//循环
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)}
事件
on属性包含所有的事件绑定
//事件
const template = `
<button @click="clickHandler">submit</button>
`
with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}
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}}})}
- 编译完的代码里面有一个
on,监听了input事件,name=$event.target.value代表会把事件的值赋给name,这个name就是组件实例中的name,即this.name。 domProps:{"value":(name)}代表value显示的是name变量,也是this.name
也就是说v-model的原理就是 value 的 attr 加 input 事件监听的语法糖
最后执行 render 函数,生成vnode
总结
模板编译的过程:模板编译为 render 函数,执行 render 函数返回vnode
后面会基于vnode再执行 patch 和 diff (详细看我的这篇文章Vue系列:虚拟DOM和diff算法)
注意:使用webpack vue-loader ,会在开发环境下编译模板(重要)。所以最后打包出来产生的代码就没有模板代码了,全部变为render函数的形式。但是我们直接使用<script>引用的vue.js文件,就是运行时编译的,会比webpack打包慢一点,因为首先要编译成render函数。做项目的时候一定要集成webpack环境
render 函数
vue组件中可以使用 render 代替template
在有些复杂情况中,不能用template , 可以考虑用 render
总结一下组件是如何进行渲染和更新的
组件渲染/更新过程包裹三点:
- 初次渲染过程
- 更新过程
- 异步渲染
初次渲染的过程
-
解析模板为render函数(webpack使用
vue-loader在开发环境已完成)(这个过程就是上面的模板编译的原理) -
触发响应式,监听data属性,设置
gettersetter(详细看我的这篇文章Vue系列:Vue2 响应式原理) -
执行
render函数,生成vnode,然后执行patch(elem, vnode)将vnode渲染到DOM上。详细看我的这篇文章Vue系列:虚拟DOM和diff算法
更新过程
- 修改
data,触发setter(此前在getter中已被监听) - 重新执行
render函数,生成newVnode patch(vnode, newVnode)(diff算法)
总结渲染更新的流程图
模板编译,然后执行 render 函数, render 函数会触发响应式的 getter ,进行依赖收集(在模板里触发了哪个变量的getter就对其进行watcher)。
我们在修改data的时候,触发 setter ,通知(notify)watcher去 重新触发re-reder进行重新渲染
vue组件是异步渲染的
回顾一下 this.$nextTick :
- vue组件时异步渲染的。代码没执行完,DOM不会立即渲染。
this.$nextTick会在DOM渲染完成时回调 - 页面渲染时会将
data的修改做一个整合,多次data的修改最后只会渲染一个最终值
这样可以减少DOM操作次数,提升性能