vue 快速入门 系列 —— 模板
其他章节请看:
模板
前面提到 vue 中的虚拟 dom 主要做两件事:
- 提供与真实节点对应的 vNode
- 新旧 vNode 对比,寻找差异,然后更新视图
①、vNode 从何而来?
前面也说了声明式框架只需要我们描述状态
与 dom
之间的映射关系。状态
到视图
的转换,框架会给我们做。
②、用什么描述状态与 dom 之间的映射关系?
Tip:jQuery 是命令式的框架,现代的 vue、react属于声明式框架。
简介
首先公布问题 ② 的答案:用模板描述状态与 dom 之间的映射关系。
于是我们知道这三者之间的关系:
状态 --> 模板 --> dom
模板编译器
请先看一个模板的示例:
<span>Message: {{ msg }}</span>
<h1 v-if="awesome">Vue is awesome!</h1>
<ul id="example-1">
<li v-for="item in items" :key="item.message">
{{ item.message }}
</li>
</ul>
v-if
、v-for
、{{}}
是什么?html 中根本不存在这些东西。
我们知道 javascript 代码只有 javascript 引擎认识,同理,模板也只有类似模板引擎的东西认识它。
在 vue 中,类似模板引擎的叫做模板编译器。通过模板编译器将模板编译成渲染函数,而执行渲染函数就会使用当前最新的状态生成一份 vnode。
模板 -- 编译 --> 渲染函数 -- 执行 --> vNode
至此,问题 ① 的答案显而易见了,vNode 由渲染函数生成。
模板和虚拟 dom 所处位置
我们根据上文,能轻易的知道模板
所处位置:
状态 --> 模板
subgraph a[模板]
模板 -- 编译 --> 渲染函数 -- 执行 --> vNode
end
vNode --> 视图
在 虚拟 dom 的作用 中,我们知道虚拟 dom
所处位置:
状态 --> a
subgraph a[虚拟 dom]
vNode
patch
end
a --> 视图
最后,我们将这两个图合并成一个即可:
状态 --> 模板
subgraph a[模板]
模板 -- 编译 --> 渲染函数
end
渲染函数 -- 执行 --> b
subgraph b[虚拟 dom]
vNode
patch
end
b --> 视图
Tip: 将渲染函数指向虚拟 dom
,是因为 vue 官网有这么一句话:“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼
模板是如何编译成渲染函数,以及为什么执行渲染函数就可以生成 vNode?请继续看下文。
渲染函数
将模板编译成渲染函数,只需要 3 步:
- 解析器:将HTML字符串转换为
AST
AST
就是一个普通的 javascript 对象,描述了该节点的信息以及子节点的信息,类似 vNode
- 优化器:遍历 AST,标记静态节点,用于提高性能
<p>hello</p>
是静态节点,渲染之后不会再改变<p>{{hello}}</p>
不是静态节点,因为状态会影响它
- 生成器:使用
AST
生成渲染函数
- 执行渲染函数就会根据现在的状态生成一份虚拟 dom(
vNode
)
- 执行渲染函数就会根据现在的状态生成一份虚拟 dom(
为什么是这 3 步?不重要,这只是一种算法而已。
Tip:倘若我们能理解这 3 步确实能将模板编译成渲染函数,而渲染函数执行后能生成 vNode。那么 vue 中模板这一部分,也算是入门了。
分析
我们采用最直接的方法,即运行一段代码,看看 AST
是什么?优化器
做了什么?渲染函数
是什么?渲染函数又是如何生成 vNode
的?
代码很简单,一个 html 页面,里面引入 vue.js
,然后在 vue.js
中打上一个断点(输入 debugger),最后运行 test.html
:
// test.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src='vue.js'></script>
</head>
<body>
<!-- 模板 -->
<div id='app'>
<p title='testTitle' @click='say'>number: {{num}}</p>
</div>
<!-- /模板 -->
<script>
const app = new Vue({
el: '#app',
data: {
num: 0
},
methods: {
say(){
this.num += 1;
}
}
})
</script>
</body>
</html>
// vue.js
// 打上断点(行{1})
var createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
debugger // {1}
// 解析器
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
// 优化器
optimize(ast, options);
}
// 生成器
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});
AST
执行完 var ast = parse(template.trim(), options);
,ast
为:
// ast:
{
"type":1,
"tag":"div",
"attrsList":[
{
"name":"id",
"value":"app"
}
],
"attrsMap":{
"id":"app"
},
"children":[
{
"type":1,
"tag":"p",
"attrsList":[
{
"name":"title",
"value":"testTitle"
},
{
"name":"@click",
"value":"say"
}
],
"attrsMap":{
"title":"testTitle",
"@click":"say"
},
"children":[
{
"type":2,
"expression":"'number: '+_s(num)",
"tokens":[
"number: ",
{
"@binding":"num"
}
],
"text":"number: {{num}}"
}
],
"plain":false,
"attrs":[
{
"name":"title",
"value":"testTitle"
}
],
"hasBindings":true,
"events":{
"click":{
"value":"say"
}
}
}
],
"plain":false,
"attrs":[
{
"name":"id",
"value":"app"
}
]
}
于是我们知道,AST
就是一个普通的 javascript 对象,类似虚拟节点或 dom Node,里面有节点的类型、属性、子节点等等。
优化器的作用
将 ast 交给优化器处理后(optimize(ast, options);
),ast
为:
// 优化器:(在上一步的基础上增加 static 和 staticRoot 两个属性)
{
"type":1,
"tag":"div",
"attrsList":[
{
"name":"id",
"value":"app"
}
],
"attrsMap":{
"id":"app"
},
"children":[
{
"type":1,
"tag":"p",
"attrsList":[
{
"name":"title",
"value":"testTitle"
},
{
"name":"@click",
"value":"say"
}
],
"attrsMap":{
"title":"testTitle",
"@click":"say"
},
"children":[
{
"type":2,
"expression":"'number: '+_s(num)",
"tokens":[
"number: ",
{
"@binding":"num"
}
],
"text":"number: {{num}}",
"static":false
}
],
"plain":false,
"attrs":[
{
"name":"title",
"value":"testTitle"
}
],
"hasBindings":true,
"events":{
"click":{
"value":"say"
}
},
"static":false,
"staticRoot":false
}
],
"plain":false,
"attrs":[
{
"name":"id",
"value":"app"
}
],
"static":false,
"staticRoot":false
}
优化器给 ast
增加 static
和 staticRoot
两个属性,用于标记静态节点。
生成器
接着将 ast
交给生成器处理(var code = generate(ast, options);
),code
为:
// code
{"render":"with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_c('p',{attrs:{\"title\":\"testTitle\"},on:{\"click\":say}},[_v(\"number: \"+_s(num))])])}","staticRenderFns":[]}
将 code.render
字符串格式化:
// code.render
with(this) {
return _c(
'div',
{
attrs: {
"id": "app"
}
},
[
_c(
'p',
{
attrs: {
"title": "testTitle"
},
on: {
"click": say
}
},
[
_v("number: " + _s(num))
]
)
]
)
}
code.render
这个字符串导出到外界,会放到一个函数中,这个函数就是渲染函数。
不理解?没关系,我们先看另一个示例:
new Function ([arg1[, arg2[, ...argN]],] functionBody)
const obj = {name: 'ph'}
const code = `with(this){console.log('hello: ' + name)}`
const renderFunction = new Function(code)
renderFunction.call(obj)
// 等同于
const obj = {name: 'ph'}
function renderFunction(){
with(this){console.log('hello: ' + name)}
}
renderFunction.call(obj) // hello: ph
这下理解了吧。我们将 code.render
指向的字符串导出到外界,外界利用 new Function()
创建渲染函数。
前面提到执行渲染函数会生成 vNode
。看看 code.render
就能知晓,里面出现的 _v
和 _c
,分别用于生成元素类型的 vNode 和文本类型的 vNode。请看相关源码:
// 创建文本类型的 vNode
target._v = createTextVNode;
function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(val))
}
// 创建元素类型的 vNode
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
function createElement (
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
...
return _createElement(context, tag, data, children, normalizationType)
}
Tip: 关于 vue 中解析器、优化器和生成器里面具体是如何实现的,本系列就不展开了。
其他章节请看:
vue 快速入门 系列 —— 模板的更多相关文章
- vue 快速入门 系列 —— 初步认识 vue
其他章节请看: vue 快速入门 系列 初步认识 vue vue 是什么 Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架. 所谓渐进式,就是你可以一步一步.有阶段 ...
- vue 快速入门 系列 —— 侦测数据的变化 - [基本实现]
其他章节请看: vue 快速入门 系列 侦测数据的变化 - [基本实现] 在 初步认识 vue 这篇文章的 hello-world 示例中,我们通过修改数据(app.seen = false),页面中 ...
- vue 快速入门 系列 —— vue 的基础应用(上)
其他章节请看: vue 快速入门 系列 vue 的基础应用(上) Tip: vue 的基础应用分上下两篇,上篇是基础,下篇是应用. 在初步认识 vue一文中,我们已经写了一个 vue 的 hello- ...
- vue 快速入门 系列 —— vue 的基础应用(下)
其他章节请看: vue 快速入门 系列 vue 的基础应用(下) 上篇聚焦于基础知识的介绍:本篇聚焦于基础知识的应用. 递归组件 组件是可以在它们自己的模板中调用自身的.不过它们只能通过 name 选 ...
- vue 快速入门 系列 —— vue loader 上
其他章节请看: vue 快速入门 系列 vue loader 上 通过前面"webpack 系列"的学习,我们知道如何用 webpack 实现一个不成熟的脚手架,比如提供开发环境和 ...
- vue 快速入门 系列 —— vue loader 下
其他章节请看: vue 快速入门 系列 vue loader 下 CSS Modules CSS Modules 是一个流行的,用于模块化和组合 CSS 的系统.vue-loader 提供了与 CSS ...
- vue 快速入门 系列 —— vue-cli 下
其他章节请看: vue 快速入门 系列 Vue CLI 4.x 下 在 vue loader 一文中我们已经学会从零搭建一个简单的,用于单文件组件开发的脚手架:本篇,我们将全面学习 vue-cli 这 ...
- vue 快速入门 系列 —— 使用 vue-cli 3 搭建一个项目(上)
其他章节请看: vue 快速入门 系列 使用 vue-cli 3 搭建一个项目(上) 前面我们已经学习了一个成熟的脚手架(vue-cli),笔者希望通过这个脚手架快速搭建系统(或项目).而展开搭建最好 ...
- vue 快速入门 系列 —— 使用 vue-cli 3 搭建一个项目(下)
其他章节请看: vue 快速入门 系列 使用 vue-cli 3 搭建一个项目(下) 上篇 我们已经成功引入 element-ui.axios.mock.iconfont.nprogress,本篇继续 ...
随机推荐
- Sysenter/Kifastcallentry hook 检测与恢复
关于Sysenter.Kifastcallentry.中断之类的内核入口hook技术早就烂大街了,可是对hook的检测与恢复代码却是寥寥无几,一切抛开代码将原理的行为都是耍流氓. 下面以Sysente ...
- 联盛德 HLK-W806 (九): 软件SPI和硬件SPI驱动ST7789V液晶LCD
目录 联盛德 HLK-W806 (一): Ubuntu20.04下的开发环境配置, 编译和烧录说明 联盛德 HLK-W806 (二): Win10下的开发环境配置, 编译和烧录说明 联盛德 HLK-W ...
- 显卡不是你学习 Deep Learning 的借口
显卡不是你学习 Deep Learning 的借口 很多人在学习深度学习的时候会以自己没有 RTX N 卡的理由不动手实操,只满足于看看"娱乐"视频,听几节基础知识.当然,如果只是 ...
- 显示大纲数字(Project)
<Project2016 企业项目管理实践>张会斌 董方好 编著 话说摘要任务,给人的感觉,就像Word里的大纲级别,可我也知道,好多同学不习惯用大纲级别,而是偏爱用编号级别,最常见的也就 ...
- 如何把maven项目转为eclipse项目
如何把maven项目转为eclipse项目,按照如下操作便可. 在cmd窗口, 载cmd窗口进入到maven项目所在目录下,输入如下命令: mvn eclipse:eclipse 这样便可.
- [源码解析] PyTorch 分布式之弹性训练(3)---代理
[源码解析] PyTorch 分布式之弹性训练(3)---代理 目录 [源码解析] PyTorch 分布式之弹性训练(3)---代理 0x00 摘要 0x01 总体背景 1.1 功能分离 1.2 Re ...
- Windows c(++)获取磁盘剩余容量
头文件 #include <windows.h> #include <wtypes.h> 函数 GetDiskFreeSpaceExA 获取剩余可用空间 /// 得到盘符, 例 ...
- qt5之设置无边窗口移动
Note qt version: 5.12 qt creator: 4.13 本文将介绍 设置无边窗口和设置窗口的移动 你要知道: QDialog 和 QMainWindow都是 QWidget的派生 ...
- 【linux项目】lichee nano linux烧写
目录 前言 参考: 安装交叉编译链 搭建 SPI FLASH 烧录环境 让芯片进入烧写模式 sunxi 烧写命令 u-boot 裁剪 拉取 u-boot 源码 配置 u-boot 检查 flash 驱 ...
- 【LeetCode】491. Increasing Subsequences 解题报告(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...