其他章节请看:

vue 快速入门 系列

模板

前面提到 vue 中的虚拟 dom 主要做两件事:

  1. 提供与真实节点对应的 vNode
  2. 新旧 vNode 对比,寻找差异,然后更新视图

①、vNode 从何而来?

前面也说了声明式框架只需要我们描述状态dom 之间的映射关系。状态视图的转换,框架会给我们做。

②、用什么描述状态与 dom 之间的映射关系?

Tip:jQuery 是命令式的框架,现代的 vue、react属于声明式框架。

简介

首先公布问题 ② 的答案:用模板描述状态与 dom 之间的映射关系。

于是我们知道这三者之间的关系:

graph LR
状态 --> 模板 --> dom

模板编译器

请先看一个模板的示例:

  1. <span>Message: {{ msg }}</span>
  2. <h1 v-if="awesome">Vue is awesome!</h1>
  3. <ul id="example-1">
  4. <li v-for="item in items" :key="item.message">
  5. {{ item.message }}
  6. </li>
  7. </ul>

v-ifv-for{{}} 是什么?html 中根本不存在这些东西。

我们知道 javascript 代码只有 javascript 引擎认识,同理,模板也只有类似模板引擎的东西认识它。

在 vue 中,类似模板引擎的叫做模板编译器。通过模板编译器将模板编译成渲染函数,而执行渲染函数就会使用当前最新的状态生成一份 vnode

graph LR
模板 -- 编译 --> 渲染函数 -- 执行 --> vNode

至此,问题 ① 的答案显而易见了,vNode 由渲染函数生成

模板和虚拟 dom 所处位置

我们根据上文,能轻易的知道模板所处位置:

flowchart LR
状态 --> 模板
subgraph a[模板]
模板 -- 编译 --> 渲染函数 -- 执行 --> vNode
end
vNode --> 视图

虚拟 dom 的作用 中,我们知道虚拟 dom 所处位置:

flowchart LR
状态 --> a
subgraph a[虚拟 dom]
vNode
patch
end
a --> 视图

最后,我们将这两个图合并成一个即可:

flowchart LR
状态 --> 模板
subgraph a[模板]
模板 -- 编译 --> 渲染函数
end
渲染函数 -- 执行 --> b
subgraph b[虚拟 dom]
vNode
patch
end
b --> 视图

Tip: 将渲染函数指向虚拟 dom,是因为 vue 官网有这么一句话:“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼

模板是如何编译成渲染函数,以及为什么执行渲染函数就可以生成 vNode?请继续看下文。

渲染函数

将模板编译成渲染函数,只需要 3 步:

  1. 解析器:将HTML字符串转换为 AST

    • AST 就是一个普通的 javascript 对象,描述了该节点的信息以及子节点的信息,类似 vNode
  2. 优化器:遍历 AST,标记静态节点,用于提高性能
    • <p>hello</p> 是静态节点,渲染之后不会再改变
    • <p>{{hello}}</p> 不是静态节点,因为状态会影响它
  3. 生成器:使用 AST 生成渲染函数
    • 执行渲染函数就会根据现在的状态生成一份虚拟 dom(vNode

为什么是这 3 步?不重要,这只是一种算法而已。

Tip:倘若我们能理解这 3 步确实能将模板编译成渲染函数,而渲染函数执行后能生成 vNode。那么 vue 中模板这一部分,也算是入门了。

分析

我们采用最直接的方法,即运行一段代码,看看 AST 是什么?优化器做了什么?渲染函数是什么?渲染函数又是如何生成 vNode 的?

代码很简单,一个 html 页面,里面引入 vue.js,然后在 vue.js 中打上一个断点(输入 debugger),最后运行 test.html

  1. // test.html
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Document</title>
  8. <script src='vue.js'></script>
  9. </head>
  10. <body>
  11. <!-- 模板 -->
  12. <div id='app'>
  13. <p title='testTitle' @click='say'>number: {{num}}</p>
  14. </div>
  15. <!-- /模板 -->
  16. <script>
  17. const app = new Vue({
  18. el: '#app',
  19. data: {
  20. num: 0
  21. },
  22. methods: {
  23. say(){
  24. this.num += 1;
  25. }
  26. }
  27. })
  28. </script>
  29. </body>
  30. </html>
  1. // vue.js
  2. // 打上断点(行{1})
  3. var createCompiler = createCompilerCreator(function baseCompile (
  4. template,
  5. options
  6. ) {
  7. debugger // {1}
  8. // 解析器
  9. var ast = parse(template.trim(), options);
  10. if (options.optimize !== false) {
  11. // 优化器
  12. optimize(ast, options);
  13. }
  14. // 生成器
  15. var code = generate(ast, options);
  16. return {
  17. ast: ast,
  18. render: code.render,
  19. staticRenderFns: code.staticRenderFns
  20. }
  21. });

AST

执行完 var ast = parse(template.trim(), options);ast 为:

  1. // ast:
  2. {
  3. "type":1,
  4. "tag":"div",
  5. "attrsList":[
  6. {
  7. "name":"id",
  8. "value":"app"
  9. }
  10. ],
  11. "attrsMap":{
  12. "id":"app"
  13. },
  14. "children":[
  15. {
  16. "type":1,
  17. "tag":"p",
  18. "attrsList":[
  19. {
  20. "name":"title",
  21. "value":"testTitle"
  22. },
  23. {
  24. "name":"@click",
  25. "value":"say"
  26. }
  27. ],
  28. "attrsMap":{
  29. "title":"testTitle",
  30. "@click":"say"
  31. },
  32. "children":[
  33. {
  34. "type":2,
  35. "expression":"'number: '+_s(num)",
  36. "tokens":[
  37. "number: ",
  38. {
  39. "@binding":"num"
  40. }
  41. ],
  42. "text":"number: {{num}}"
  43. }
  44. ],
  45. "plain":false,
  46. "attrs":[
  47. {
  48. "name":"title",
  49. "value":"testTitle"
  50. }
  51. ],
  52. "hasBindings":true,
  53. "events":{
  54. "click":{
  55. "value":"say"
  56. }
  57. }
  58. }
  59. ],
  60. "plain":false,
  61. "attrs":[
  62. {
  63. "name":"id",
  64. "value":"app"
  65. }
  66. ]
  67. }

于是我们知道,AST 就是一个普通的 javascript 对象,类似虚拟节点或 dom Node,里面有节点的类型、属性、子节点等等。

优化器的作用

将 ast 交给优化器处理后(optimize(ast, options);),ast 为:

  1. // 优化器:(在上一步的基础上增加 static 和 staticRoot 两个属性)
  2. {
  3. "type":1,
  4. "tag":"div",
  5. "attrsList":[
  6. {
  7. "name":"id",
  8. "value":"app"
  9. }
  10. ],
  11. "attrsMap":{
  12. "id":"app"
  13. },
  14. "children":[
  15. {
  16. "type":1,
  17. "tag":"p",
  18. "attrsList":[
  19. {
  20. "name":"title",
  21. "value":"testTitle"
  22. },
  23. {
  24. "name":"@click",
  25. "value":"say"
  26. }
  27. ],
  28. "attrsMap":{
  29. "title":"testTitle",
  30. "@click":"say"
  31. },
  32. "children":[
  33. {
  34. "type":2,
  35. "expression":"'number: '+_s(num)",
  36. "tokens":[
  37. "number: ",
  38. {
  39. "@binding":"num"
  40. }
  41. ],
  42. "text":"number: {{num}}",
  43. "static":false
  44. }
  45. ],
  46. "plain":false,
  47. "attrs":[
  48. {
  49. "name":"title",
  50. "value":"testTitle"
  51. }
  52. ],
  53. "hasBindings":true,
  54. "events":{
  55. "click":{
  56. "value":"say"
  57. }
  58. },
  59. "static":false,
  60. "staticRoot":false
  61. }
  62. ],
  63. "plain":false,
  64. "attrs":[
  65. {
  66. "name":"id",
  67. "value":"app"
  68. }
  69. ],
  70. "static":false,
  71. "staticRoot":false
  72. }

优化器给 ast 增加 staticstaticRoot 两个属性,用于标记静态节点。

生成器

接着将 ast 交给生成器处理(var code = generate(ast, options);),code 为:

  1. // code
  2. {"render":"with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_c('p',{attrs:{\"title\":\"testTitle\"},on:{\"click\":say}},[_v(\"number: \"+_s(num))])])}","staticRenderFns":[]}

code.render 字符串格式化:

  1. // code.render
  2. with(this) {
  3. return _c(
  4. 'div',
  5. {
  6. attrs: {
  7. "id": "app"
  8. }
  9. },
  10. [
  11. _c(
  12. 'p',
  13. {
  14. attrs: {
  15. "title": "testTitle"
  16. },
  17. on: {
  18. "click": say
  19. }
  20. },
  21. [
  22. _v("number: " + _s(num))
  23. ]
  24. )
  25. ]
  26. )
  27. }

code.render 这个字符串导出到外界,会放到一个函数中,这个函数就是渲染函数

不理解?没关系,我们先看另一个示例:

new Function ([arg1[, arg2[, ...argN]],] functionBody)

  1. const obj = {name: 'ph'}
  2. const code = `with(this){console.log('hello: ' + name)}`
  3. const renderFunction = new Function(code)
  4. renderFunction.call(obj)
  5. // 等同于
  6. const obj = {name: 'ph'}
  7. function renderFunction(){
  8. with(this){console.log('hello: ' + name)}
  9. }
  10. renderFunction.call(obj) // hello: ph

这下理解了吧。我们将 code.render 指向的字符串导出到外界,外界利用 new Function() 创建渲染函数。

前面提到执行渲染函数会生成 vNode。看看 code.render 就能知晓,里面出现的 _v_c,分别用于生成元素类型的 vNode 和文本类型的 vNode。请看相关源码:

  1. // 创建文本类型的 vNode
  2. target._v = createTextVNode;
  3. function createTextVNode (val) {
  4. return new VNode(undefined, undefined, undefined, String(val))
  5. }
  6. // 创建元素类型的 vNode
  7. vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
  8. function createElement (
  9. context,
  10. tag,
  11. data,
  12. children,
  13. normalizationType,
  14. alwaysNormalize
  15. ) {
  16. ...
  17. return _createElement(context, tag, data, children, normalizationType)
  18. }

Tip: 关于 vue 中解析器、优化器和生成器里面具体是如何实现的,本系列就不展开了。

其他章节请看:

vue 快速入门 系列

vue 快速入门 系列 —— 模板的更多相关文章

  1. vue 快速入门 系列 —— 初步认识 vue

    其他章节请看: vue 快速入门 系列 初步认识 vue vue 是什么 Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架. 所谓渐进式,就是你可以一步一步.有阶段 ...

  2. vue 快速入门 系列 —— 侦测数据的变化 - [基本实现]

    其他章节请看: vue 快速入门 系列 侦测数据的变化 - [基本实现] 在 初步认识 vue 这篇文章的 hello-world 示例中,我们通过修改数据(app.seen = false),页面中 ...

  3. vue 快速入门 系列 —— vue 的基础应用(上)

    其他章节请看: vue 快速入门 系列 vue 的基础应用(上) Tip: vue 的基础应用分上下两篇,上篇是基础,下篇是应用. 在初步认识 vue一文中,我们已经写了一个 vue 的 hello- ...

  4. vue 快速入门 系列 —— vue 的基础应用(下)

    其他章节请看: vue 快速入门 系列 vue 的基础应用(下) 上篇聚焦于基础知识的介绍:本篇聚焦于基础知识的应用. 递归组件 组件是可以在它们自己的模板中调用自身的.不过它们只能通过 name 选 ...

  5. vue 快速入门 系列 —— vue loader 上

    其他章节请看: vue 快速入门 系列 vue loader 上 通过前面"webpack 系列"的学习,我们知道如何用 webpack 实现一个不成熟的脚手架,比如提供开发环境和 ...

  6. vue 快速入门 系列 —— vue loader 下

    其他章节请看: vue 快速入门 系列 vue loader 下 CSS Modules CSS Modules 是一个流行的,用于模块化和组合 CSS 的系统.vue-loader 提供了与 CSS ...

  7. vue 快速入门 系列 —— vue-cli 下

    其他章节请看: vue 快速入门 系列 Vue CLI 4.x 下 在 vue loader 一文中我们已经学会从零搭建一个简单的,用于单文件组件开发的脚手架:本篇,我们将全面学习 vue-cli 这 ...

  8. vue 快速入门 系列 —— 使用 vue-cli 3 搭建一个项目(上)

    其他章节请看: vue 快速入门 系列 使用 vue-cli 3 搭建一个项目(上) 前面我们已经学习了一个成熟的脚手架(vue-cli),笔者希望通过这个脚手架快速搭建系统(或项目).而展开搭建最好 ...

  9. vue 快速入门 系列 —— 使用 vue-cli 3 搭建一个项目(下)

    其他章节请看: vue 快速入门 系列 使用 vue-cli 3 搭建一个项目(下) 上篇 我们已经成功引入 element-ui.axios.mock.iconfont.nprogress,本篇继续 ...

随机推荐

  1. malloc实现

    任何一个用过或学过C的人对malloc都不会陌生.大家都知道malloc可以分配一段连续的内存空间,并且在不再使用时可以通过free释放 掉.但是,许多程序员对malloc背后的事情并不熟悉,许多人甚 ...

  2. Explain的详细使用

    官方文档 https://dev.mysql.com/doc/refman/5.7/en/explain-output.html explain俩种类型 explain extended 会在 exp ...

  3. 🏆【CI/CD技术专题】「Docker实战系列」(1)本地进行生成镜像以及标签Tag推送到DockerHub

    背景介绍 Docker镜像构建成功后,只要有docker环境就可以使用,但必须将镜像推送到Docker Hub上去.创建的镜像最好要符合Docker Hub的tag要求,因为在Docker Hub注册 ...

  4. 12 - Vue3 UI Framework - 打包发布

    基础组件库先做到这个阶段,后面我会继续新增.完善 接下来,我们对之前做的组件进行打包发布到 npm 返回阅读列表点击 这里 组件库优化 通用层叠样式表 我想大家都注意到了,前面我们在写组件的时候,sc ...

  5. CF1065A Vasya and Chocolate 题解

    Content 小 V 有 \(s\) 块钱,商店里有巧克力卖,每块巧克力 \(c\) 块钱,现在商店给出优惠:购买 \(a\) 块巧克力可以免费获得 \(b\) 块巧克力,求小 V 最多能够买到的巧 ...

  6. 基于DNN的残余回声抑制

    摘要 由于功率放大器或扬声器的限制,即使在回声路径完全线性的情况下,麦克风捕获的回声信号与远端信号也不是线性关系.线性回声消除器无法成功地消除回声的非线性分量.RES是在AES后对剩余回声进行抑制的一 ...

  7. 【LeetCode】107. Binary Tree Level Order Traversal II 解题报告 (Python&C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 方法一:DFS 方法二:迭代 日期 [LeetCode ...

  8. 【LeetCode】723. Candy Crush 解题报告 (C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 暴力 日期 题目地址:https://leetcode ...

  9. 【LeetCode】361. Bomb Enemy 解题报告(C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 暴力搜索 日期 题目地址:https://leetco ...

  10. 【LeetCode】665. 非递减数列 Non-decreasing Array(Python & C++)

    作者: 负雪明烛 id: fuxuemingzhu 公众号:每日算法题 本文关键词:数组,array,非递减,遍历,python,C++ 目录 题目描述 题目大意 解题方法 一.错误代码 二.举例分析 ...