【vue源码学习】——模板编译

11 篇文章 0 订阅

Vue源码学习



前言

  Vue.js提供了模板语法,允许我们声明式地描述状态和DOM之间的绑定关系,通过模板来生成真实DOM并将其呈现在用户界面上。
  在底层实现上,Vue.js会将模板编译成虚拟DOM渲染函数。当应用内部的状态发生变化时,Vue.js结合响应式系统,聪明地找出最小数量的组件进行重新渲染以及最少量地进行DOM操作。
  也就是说,平时写的vue模板经过模板编译后,生成render函数,初次渲染或数据发生变化时,都会执行render函数,生成虚拟DOM(数据变化时会再比对新老DOM节点),最后生成真实DOM,渲染在页面上。

一、什么是模板编译?

平时开发写的<template></template>以及里面的变量、表达式、指令等,不是html语法,是浏览器识别不出来的。模板编译的主要目标是生成渲染函数,渲染函数会将当前的状态生成一份vnode, 再用vnode转成真实的dom进行渲染。
在这里插入图片描述

二、模板编译成渲染函数的流程

可分为三部分内容:
1、将模板解析成AST(Abstract Syntax Tree,抽象语法树);
2、遍历AST标记为静态节点;(静态节点不需要总是重新渲染,标记后,更新节点时,有这个标记就不会重新渲染)
3、将AST生成渲染函数

可以抽象成三个模块实现各自的功能:
1、解析器
2、优化器
3、代码生成器

在这里插入图片描述

// 源码位置 src/compiler/parser/index.js
import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
//1、 模板解析阶段:用正则等方式解析 template 模板中的指令、class、style等数据,形成AST
  const ast = parse(template.trim(), options) 
  if (options.optimize !== false) {
  // 2、优化阶段:遍历AST,找出其中的静态节点,并打上标记;
    optimize(ast, options)
  }
  // 3、代码生成阶段:将AST转换成渲染函数;
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

(1)解析器

把用户在<template></template>标签内写的模板使用正则等方式解析成抽象语法树(AST)。

// 代码位置:/src/complier/parser/index.js
/**
 * Convert HTML string to AST.
 */
export function parse(template, options) {
   // ...
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    start (tag, attrs, unary) {
    },
    end () {
    },
    chars (text: string) {
    },
    comment (text: string) {
    }
  })
  return root
}

parse函数是解析器的主函数,函数内调用了parseHTML函数对模板进行解析。具体怎么解析的可以进行代码调试或者看《深入浅出vue.js》。
最终要解析成AST语法树,例如解析:

<div>
	<p>{{name}}</p>
</div>

解析后长这样:

{
	tag:"div",
	type:1,
	staticRoot:false,
	static:false,
	plain:true,
	parent:undefined,
	attrsList:[],
	attrsMap:{},
	children:[
		{
			tag:"p",
			type:1,
			staticRoot:false,
			static:false,
			plain:true,
			parent:{tag:"div",...},
			attrsList:[],
			attrsMap:{},
			children:[{
				type:2,
				text:"{{name}}",
				static:false,
				expression:"_s(name)"
			}]
		}
	]
}

其实AST并不是个神奇的东西。它只是用JS中的对下来描述一个节点,一个对象表示一个节点,对象中的属性用来保存节点所需的各种数据。比如,parent属性保存了父节点的描述对象,children属性是一个数组,里面保存了一些子节点的描述对象。再比如,type属性表示一个节点的类型等。当很多个独立的节点通过parent属性和children属性连在一起时,就变成了一棵树,而这样用对象描述的节点数就是AST。

(2)优化器

优化器的目标是遍历AST,检测出所有静态子树(永远都不会发生变化的DOM节点)并给其打标记。

<ul>
    <li>我是静态节点,我不要发生变化</li>
    <li>我是静态节点,2、我不要发生变化</li>
</ul>

ul下的li标签里的内容都是不含任何变量的纯文本,也就是说这种标签一旦第一次被渲染成DOM节点以后,之后不管状态怎么变化它都不会变了,像li标签这种节点称为静态节点,其父节点ul节点称为静态根节点

有了这两个概念之后,我们再仔细思考,模板编译最终目的是生成render函数,render函数生成与模板对应的VNode,之后再进行patch算法,最后渲染视图。而中间的patch算法是用来比对新旧VNode差异的。静态节点是不会变的,所以无需比对静态节点。这样就可以提高一些性能。

故,优化阶段干了两件事:
1、标记静态节点
2、标记静态根节点

对应源码位置:

// src/compiler/optimizer.js
export function optimize (root) {
  if (!root) return
  markStatic(root) // 标记静态节点
  markStaticRoots(root)   // 标记静态根节点
}

标记后的AST树:

{
	tag:"ul",
	type:1,
	staticRoot:true,
	static:true,
	...
}

(3)代码生成器

将AST转换成渲染函数中的内容,这个内容可以称为代码字符串。
代码字符串可以被包装在函数中执行,这个函数就是我们常说的渲染函数(update)。
比如,有一份简单的模板:

<div id="el">Hello {{name}}</div>

AST优化后,长这样:

{
	tag:"div",
	type:1,
	plain:false,
	attrsList:[{
		"name":"id",
		"value":'el'
	}],
	attrsMap:{
		"id":"el"
	},
	children:[
		{
			type:2,
			"expression":"Hello"+_s(name),
			"text":"Hello {{name}}",
			"static":false
		}
	],
	staticRoot:false,
	static:false,
}

代码生成器可以通过上面这个AST来生成代码字符串,生成后的代码字符串是这样的:

'with(this){return _c("div",{attrs:{"id":"el"}},[_v("Hello"+_s(name)])}'

在这里插入图片描述
_c()是元素节点的函数调用字符串,_v是文本节点的函数调用字符串,_e是生成注释节点的函数调用字符串。

源码位置src/compiler/codegen/index.js :

export function generate (ast,option) {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}
const code = generate(ast, options)
   Vue.prototype._c = function() {
       return createElementVNode(this, ...arguments) // 生成元素节点
    }
    Vue.prototype._v = function() {
        return createTextVNode(this, ...arguments) // 生成文本节点
    }
    Vue.prototype._e = function(value) {
        if (typeof value != 'object') return value;
        return JSON.stringify(value); // 注释节点
    }

三、v-if、v-for的优先级

v-for的优先级高于v-if

<div id="app">
  <span v-if="flag" v-for="i in 3"></span>
</div>

vue2模板编译生成render函数网站,用这个网站可以生成render函数:

function render() {
  with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, _l((3), function (i) {
      return (flag) ? _c('span') : _e()
    }), 0)
  }
}

对应源码位置:
在这里插入图片描述
v-for会生成_l()函数,_l()函数里面再进行v-if的判断,所以v-for的优先级高于v-if。

vue官网写,永远不要把v-for和v-if同时写在同一个元素上。因为,渲染后v-if在v-for的循环里,这让本不应该渲染的元素渲染了。最好在渲染前把v-for里的数组先筛选下,再进行渲染。或者把v-if放在v-for的父元素上。

//  v-if 移动至容器元素上 (比如 ul、ol)
<ul v-if="shouldShowUsers">
  <li v-for="user in users" :key="user.id" >
    {{ user.name }}
  </li>
</ul>

总结

模板编译,可以理解为,给它输入模板字符串,会输出render函数。

render函数是在挂载时调用的。也就是Vue原型上的$mount方法。render函数将AST转化为虚拟DOM,再转化为真实DOM,最后进行渲染。

模板编译可分为三个流程:模板解析成AST语法树、AST标记静态节点、将AST生成渲染函数。

v-for的优先级比v-if高,永远不要把v-for和v-if同时写在同一个元素上。

参考:《深入浅出Vue.js》、模板编译总结

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值