Vue系列:vue模板的编译与vue组件的渲染、更新过程

2,573 阅读4分钟

模板编译的总体过程:使用 vue template complier 将模板编译为 render 函数,然后执行 render 函数生成vnode

前置知识:JS的 with 语法

image。png

  • with 语法改变 {} 块内自由变量的查找规则,当做 obj 属性来查找
  • 如果找不到匹配的 obj 属性,就会报错
  • with 要慎用,它打破了作用域规则,易读性变差

vue template complier 将模板编译为 render 函数

vue模板被编译成什么?

  1. 模板不是html , 有指令、插值、JS 表达式,能实现判断、循环
  2. html是标签语言,只有JS才能实现判断、循环(图灵完备的)
  3. 因此,模板一定是转换为某种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")])])}

循环

通过 _lrenderList )函数,传入数组或者对象,即可返回列表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的原理就是 valueattrinput 事件监听的语法糖 最后执行 render 函数,生成vnode

总结

模板编译的过程:模板编译为 render 函数,执行 render 函数返回vnode

后面会基于vnode再执行 patchdiff (详细看我的这篇文章Vue系列:虚拟DOM和diff算法

注意:使用webpack vue-loader ,会在开发环境下编译模板(重要)。所以最后打包出来产生的代码就没有模板代码了,全部变为render函数的形式。但是我们直接使用<script>引用的vue.js文件,就是运行时编译的,会比webpack打包慢一点,因为首先要编译成render函数。做项目的时候一定要集成webpack环境

render 函数

vue组件中可以使用 render 代替template

image。png

在有些复杂情况中,不能用template , 可以考虑用 render

总结一下组件是如何进行渲染和更新的

组件渲染/更新过程包裹三点:

  • 初次渲染过程
  • 更新过程
  • 异步渲染

初次渲染的过程

  1. 解析模板为render函数(webpack使用 vue-loader 在开发环境已完成)(这个过程就是上面的模板编译的原理)

  2. 触发响应式,监听data属性,设置 getter setter (详细看我的这篇文章Vue系列:Vue2 响应式原理

    image。png

  3. 执行 render 函数,生成vnode,然后执行 patch(elem, vnode) 将vnode渲染到DOM上。详细看我的这篇文章Vue系列:虚拟DOM和diff算法

更新过程

  1. 修改 data ,触发 setter (此前在 getter 中已被监听)
  2. 重新执行 render 函数,生成 newVnode
  3. patch(vnode, newVnode) (diff算法)

总结渲染更新的流程图

image。png

模板编译,然后执行 render 函数, render 函数会触发响应式的 getter ,进行依赖收集(在模板里触发了哪个变量的getter就对其进行watcher)。

我们在修改data的时候,触发 setter ,通知(notify)watcher去 重新触发re-reder进行重新渲染

vue组件是异步渲染的

回顾一下 this.$nextTickimage。png

  1. vue组件时异步渲染的。代码没执行完,DOM不会立即渲染。 this.$nextTick 会在DOM渲染完成时回调
  2. 页面渲染时会将data的修改做一个整合,多次data的修改最后只会渲染一个最终值

这样可以减少DOM操作次数,提升性能