深入Vue2.0底层思想–模板渲染

初衷

在使用vue2.0的过程,有时看API很难理解vue作者的思想,这促使我想要去深入了解vue底层的思想,了解完底层的一些思想,才能更好的用活框架,虽然网上已经有很多源码解析的文档,但我觉得只有自己动手了,才能更加深印象。

vue2.0和1.0模板渲染的区别

Vue 2.0 中模板渲染与 Vue 1.0 完全不同,1.0 中采用的 DocumentFragment (想了解可以观看这篇文章),而
2.0 中借鉴 React 的 Virtual DOM。基于 Virtual DOM,2.0 还可以支持服务端渲染(SSR),也支持 JSX
语法。

知识普及

在开始阅读源码之前,先了解一些相关的知识:AST 数据结构,VNode 数据结构,createElement 的问题,render函数。

AST 数据结构

AST 的全称是 Abstract Syntax Tree(抽象语法树),是源代码的抽象语法结构的树状表现形式,计算机学科中编译原理的概念。而vue就是将模板代码映射为AST数据结构,进行语法解析。

我们看一下 Vue 2.0 源码中 AST 数据结构 的定义:


  1. declare type ASTNode = ASTElement | ASTText | ASTExpression 
  2.  
  3. declare type ASTElement = { // 有关元素的一些定义 
  4.  
  5.   type: 1; 
  6.  
  7.   tag: string; 
  8.  
  9.   attrsList: Array{ name: string; value: string }>; 
  10.  
  11.   attrsMap: { [key: string]: string | null }; 
  12.  
  13.   parent: ASTElement | void; 
  14.  
  15.   children: ArrayASTNode>; 
  16.  
  17.   //...... 
  18.  
  19.  
  20. declare type ASTExpression = { 
  21.  
  22.   type: 2; 
  23.  
  24.   expression: string; 
  25.  
  26.   text: string; 
  27.  
  28.   static?: boolean; 
  29.  
  30.  
  31. declare type ASTText = { 
  32.  
  33.   type: 3; 
  34.  
  35.   text: string; 
  36.  
  37.   static?: boolean; 
  38.  
  39. }  

我们看到 ASTNode 有三种形式:ASTElement,ASTText,ASTExpression。用属性 type 区分。

VNode数据结构

下面是 Vue 2.0 源码中 VNode 数据结构 的定义 (带注释的跟下面介绍的内容有关):


  1. constructor { 
  2.  
  3.   this.tag = tag   //元素标签 
  4.  
  5.   this.data = data  //属性 
  6.  
  7.   this.children = children  //子元素列表 
  8.  
  9.   this.text = text 
  10.  
  11.   this.elm = elm  //对应的真实 DOM 元素 
  12.  
  13.   this.ns = undefined 
  14.  
  15.   this.context = context 
  16.  
  17.   this.functionalContext = undefined 
  18.  
  19.   this.key = data && data.key 
  20.  
  21.   this.componentOptions = componentOptions 
  22.  
  23.   this.componentInstance = undefined 
  24.  
  25.   this.parent = undefined 
  26.  
  27.   this.raw = false 
  28.  
  29.   this.isStatic = false //是否被标记为静态节点 
  30.  
  31.   this.isRootInsert = true 
  32.  
  33.   this.isComment = false 
  34.  
  35.   this.isCloned = false 
  36.  
  37.   this.isOnce = false 
  38.  
  39. }  

真实DOM存在什么问题,为什么要用虚拟DOM

我们为什么不直接使用原生 DOM 元素,而是使用真实 DOM 元素的简化版 VNode,最大的原因就是
document.createElement 这个方法创建的真实 DOM 元素会带来性能上的损失。我们来看一个
document.createElement 方法的例子


  1. let div = document.createElement('div'); 
  2.  
  3. for(let k in div) { 
  4.  
  5.   console.log(k); 
  6.  
  7. }  

打开 console 运行一下上面的代码,会发现打印出来的属性多达 228 个,而这些属性有 90% 多对我们来说都是无用的。VNode
就是简化版的真实 DOM 元素,关联着真实的dom,比如属性elm,只包括我们需要的属性,并新增了一些在 diff 过程中需要使用的属性,例如
isStatic。

render函数

这个函数是通过编译模板文件得到的,其运行结果是 VNode。render 函数 与 JSX 类似,Vue 2.0 中除了 Template 也支持 JSX 的写法。大家可以使用 Vue.compile(template)方法编译下面这段模板。


  1. div id="app"> 
  2.  
  3.   header> 
  4.  
  5.     h1>I am a template!/h1> 
  6.  
  7.   /header> 
  8.  
  9.   p v-if="message"> 
  10.  
  11.     {{ message }} 
  12.  
  13.   /p> 
  14.  
  15.   p v-else> 
  16.  
  17.     No message. 
  18.  
  19.   /p> 
  20.  
  21. /div>  

方法会返回一个对象,对象中有 render 和 staticRenderFns 两个值。看一下生成的 render函数


  1. (function() { 
  2.  
  3.   with(this){ 
  4.  
  5.     return _c('div',{   //创建一个 div 元素 
  6.  
  7.       attrs:{"id":"app"}  //div 添加属性 id 
  8.  
  9.       },[ 
  10.  
  11.         _m(0),  //静态节点 header,此处对应 staticRenderFns 数组索引为 0 的 render 函数 
  12.  
  13.         _v(" "), //空的文本节点 
  14.  
  15.         (message) //三元表达式,判断 message 是否存在 
  16.  
  17.          //如果存在,创建 p 元素,元素里面有文本,值为 toString(message) 
  18.  
  19.         ?_c('p',[_v("\n    "+_s(message)+"\n  ")]) 
  20.  
  21.         //如果不存在,创建 p 元素,元素里面有文本,值为 No message. 
  22.  
  23.         :_c('p',[_v("\n    No message.\n  ")]) 
  24.  
  25.       ] 
  26.  
  27.     ) 
  28.  
  29.   } 
  30.  
  31. })  

要看懂上面的 render函数,只需要了解 _c,_m,_v,_s 这几个函数的定义,其中 _c 是
createElement(创建元素),_m 是 renderStatic(渲染静态节点),_v 是
createTextVNode(创建文本dom),_s 是 toString (转换为字符串)

除了 render 函数,还有一个 staticRenderFns 数组,这个数组中的函数与 VDOM 中的 diff
算法优化相关,我们会在编译阶段给后面不会发生变化的 VNode 节点打上 static 为 true 的标签,那些被标记为静态节点的 VNode
就会单独生成 staticRenderFns 函数


  1. (function() { //上面 render 函数 中的 _m(0) 会调用这个方法 
  2.  
  3.   with(this){ 
  4.  
  5.     return _c('header',[_c('h1',[_v("I'm a template!")])]) 
  6.  
  7.   } 
  8.  
  9. })  

模板渲染过程(重要的函数介绍)

了解完一些基础知识后,接下来我们讲解下模板的渲染过程

$mount 函数,主要是获取 template,然后进入 compileToFunctions 函数。

compileToFunctions 函数,主要将 template 编译成 render 函数。首先读缓存,没有缓存就调用 compile 方法拿到 render 函数 的字符串形式,再通过 new Function 的方式生成 render 函数。


  1. // 有缓存的话就直接在缓存里面拿 
  2.  
  3. const key = options && options.delimiters 
  4.  
  5.             ? String(options.delimiters) + template 
  6.  
  7.             : template 
  8.  
  9. if (cache[key]) { 
  10.  
  11.     return cache[key] 
  12.  
  13.  
  14. const res = {} 
  15.  
  16. const compiled = compile(template, options) // compile 后面会详细讲 
  17.  
  18. res.render = makeFunction(compiled.render) //通过 new Function 的方式生成 render 函数并缓存 
  19.  
  20. const l = compiled.staticRenderFns.length 
  21.  
  22. res.staticRenderFns = new Array(l) 
  23.  
  24. for (let i = 0; i  l; i++) { 
  25.  
  26.     res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i]) 
  27.  
  28.  
  29. ...... 
  30.  
  31.  
  32. return (cache[key] = res) // 记录至缓存中  

compile 函数就是将 template 编译成 render 函数的字符串形式,后面一小节我们会详细讲到。

完成render方法的生成后,会进入 _mount 中进行DOM更新。该方法的核心逻辑如下:


  1. // 触发 beforeMount 生命周期钩子 
  2.  
  3. callHook(vm, 'beforeMount') 
  4.  
  5. // 重点:新建一个 Watcher 并赋值给 vm._watcher 
  6.  
  7. vm._watcher = new Watcher(vm, function updateComponent () { 
  8.  
  9.   vm._update(vm._render(), hydrating) 
  10.  
  11. }, noop) 
  12.  
  13. hydrating = false 
  14.  
  15. // manually mounted instance, call mounted on self 
  16.  
  17. // mounted is called for render-created child components in its inserted hook 
  18.  
  19. if (vm.$vnode == null) { 
  20.  
  21.   vm._isMounted = true 
  22.  
  23.   callHook(vm, 'mounted') 
  24.  
  25.  
  26. return vm  

首先会new一个watcher对象(主要是将模板与数据建立联系),在watcher对象创建后,会运行传入的方法
vm._update(vm._render(), hydrating)
。其中的vm._render()主要作用就是运行前面compiler生成的render方法,并返回一个vNode对象。vm.update()
则会对比新的 vdom 和当前 vdom,并把差异的部分渲染到真正的 DOM 树上。

推荐个图,响应式工程流程

(想深入了解watcher的背后实现原理的,可以观看这篇文章 Vue2.0 源码阅读:响应式原理)

compile

上文中提到 compile 函数就是将 template 编译成 render 函数 的字符串形式。


  1. export function compile ( 
  2.  
  3.   template: string, 
  4.  
  5.   options: CompilerOptions 
  6.  
  7. ): CompiledResult { 
  8.  
  9.   const AST = parse(template.trim(), options) //1. parse 
  10.  
  11.   optimize(AST, options)  //2.optimize 
  12.  
  13.   const code = generate(AST, options) //3.generate 
  14.  
  15.   return { 
  16.  
  17.     AST, 
  18.  
  19.     render: code.render, 
  20.  
  21.     staticRenderFns: code.staticRenderFns 
  22.  
  23.   } 
  24.  
  25. }  

这个函数主要有三个步骤组成:parse,optimize 和 generate,分别输出一个包含 AST,staticRenderFns 的对象和 render函数 的字符串。

parse 函数,主要功能是将 template字符串解析成 AST。前面定义了ASTElement的数据结构,parse 函数就是将template里的结构(指令,属性,标签等)转换为AST形式存进ASTElement中,最后解析生成AST。

optimize 函数(src/compiler/optimizer.js)主要功能就是标记静态节点,为后面 patch 过程中对比新旧 VNode 树形结构做优化。被标记为 static 的节点在后面的 diff 算法中会被直接忽略,不做详细的比较。

generate 函数(src/compiler/codegen/index.js)主要功能就是根据 AST 结构拼接生成 render 函数的字符串。


  1. const code = AST ? genElement(AST) : '_c("div")' 
  2.  
  3. staticRenderFns = prevStaticRenderFns 
  4.  
  5. onceCount = prevOnceCount 
  6.  
  7. return { 
  8.  
  9.     render: `with(this){return ${code}}`, //最外层包一个 with(this) 之后返回 
  10.  
  11.     staticRenderFns: currentStaticRenderFns 
  12.  
  13. }  

其中 genElement 函数(src/compiler/codegen/index.js)是会根据 AST 的属性调用不同的方法生成字符串返回。


  1. function genElement (el: ASTElement): string { 
  2.  
  3.   if (el.staticRoot && !el.staticProcessed) { 
  4.  
  5.     return genStatic(el) 
  6.  
  7.   } else if (el.once && !el.onceProcessed) { 
  8.  
  9.     return genOnce(el) 
  10.  
  11.   } else if (el.for && !el.forProcessed) { 
  12.  
  13.     return genFor(el) 
  14.  
  15.   } else if (el.if && !el.ifProcessed) { 
  16.  
  17.     return genIf(el) 
  18.  
  19.   } else if (el.tag === 'template' && !el.slotTarget) { 
  20.  
  21.     return genChildren(el) || 'void 0' 
  22.  
  23.   } else if (el.tag === 'slot') { 
  24.  
  25.   } 
  26.  
  27.     return code 
  28.  
  29.   } 
  30.  
  31. }  

以上就是 compile 函数中三个核心步骤的介绍,compile 之后我们得到了 render 函数 的字符串形式,后面通过 new
Function 得到真正的渲染函数。数据发现变化后,会执行 Watcher 中的 _update
函数(src/core/instance/lifecycle.js),_update 函数会执行这个渲染函数,输出一个新的 VNode
树形结构的数据。然后在调用 patch 函数,拿这个新的 VNode 与旧的 VNode 进行对比,只有发生了变化的节点才会被更新到真实 DOM
树上。

patch

patch.js 就是新旧 VNode 对比的 diff 函数,主要是为了优化dom,通过算法使操作dom的行为降到最低,diff
算法来源于 snabbdom,是 VDOM 思想的核心。snabbdom 的算法为了 DOM
操作跨层级增删节点较少的这一目标进行优化,它只会在同层级进行, 不会跨层级比较。

想更加深入VNode diff算法原理的,可以观看(解析vue2.0的diff算法)

总结

  • compile 函数主要是将 template 转换为 AST,优化 AST,再将 AST 转换为 render函数;
  • render函数 与数据通过 Watcher 产生关联;
  • 在数据发生变化时调用 patch 函数,执行此 render 函数,生成新 VNode,与旧 VNode 进行 diff,最终更新 DOM 树。 

作者:佚名

来源:51CTO

时间: 2024-10-01 14:58:33

深入Vue2.0底层思想–模板渲染的相关文章

Vuex2.0+Vue2.0构建备忘录应用实践_javascript技巧

一.介绍Vuex Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式.它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化,适合于构建中大型单页应用. 1.什么是状态管理模式?看个简单的例子: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content=&qu

vue2.0源码分析之理解响应式架构

分享前啰嗦 我之前介绍过vue1.0如何实现observer和watcher.本想继续写下去,可是vue2.0横空出世..所以 直接看vue2.0吧.这篇文章在公司分享过,终于写出来了.我们采用用最精简的代码,还原vue2.0响应式架构实现 以前写的那篇 vue 源码分析之如何实现 observer 和 watcher可以作为本次分享的参考. 不过不看也没关系,但是最好了解下Object.defineProperty 本文分享什么 理解vue2.0的响应式架构,就是下面这张图 顺带介绍他比rea

vue2.0开发实践总结之入门篇_javascript技巧

vue2.0 据说也出了很久了,博主终于操了一次实刀. 整体项目采用  vue +  vue-router +  vuex (传说中的vue 全家桶),构建工具使用尤大大推出的vue-cli 项目是图片分享社交平台.   项目预览:   1 .vue-cli构建工具必知 我选用的vue-cli 是基于webpack的版本 ,不了解webpack的可以先粗略看下下面的基本知识 webpack 基本知识点: entry:入口点,webpack会从入口点设置的js文件开始对项目进行构建,过程中,所有入

vue2.0开发实践总结之疑难篇_javascript技巧

续上一篇文章:vue2.0 开发实践总结之入门篇 ,如果没有看过的可以移步看一下.  本篇文章目录如下: 1.  vue 组件的说明和使用 2.  vuex在实际开发中的使用 3.  开发实践总结  1.  vue 组件的说明和使用 一个组件实质上是一个拥有预定义选项的一个 Vue 实例 在header组件内部允许外部使用,需要导出属性,有2种导出方法 1.  默认导出(不用命名) export default { data () { return { msg: 'header' } } } 以

自定义vue2.0全局组件(上篇)

随着vue.js的发展,一些基于vue.js的框架如雨后春笋般出现在开发者面前(例如:Element-ui.Mint-ui).但是,无论哪一种框架都不可能完全满足项目需求,有时需要开发者自己编写自定义组件.那怎样编写自定义组件呢?今天,老K为大家分享一下自己常用的方法. 按钮是经常使用的组件之一.Element-ui中的按钮组件说明,如下图: 今天,我们就拿这个按钮组件为例为大家编写一个自己的按钮组件. 前期准备:node.js开发环境,npm包管理器或者cnpm包管理器(推荐cnpm,速度快)

vue2.0开发实践总结之入门篇

vue2.0 据说也出了很久了,博主终于操了一次实刀. 整体项目采用  vue +  vue-router +  vuex (传说中的vue 全家桶),构建工具使用尤大大推出的vue-cli 项目是图片分享社交平台.   项目预览:  开发实践总结之入门篇-vuex2.0例子实践"> 1 .vue-cli构建工具必知 我选用的vue-cli 是基于webpack的版本 ,不了解webpack的可以先粗略看下下面的基本知识 webpack 基本知识点: entry:入口点,webpack会从

Vue2.0中v-for迭代语法变化(key、index)

今天,在写关于Vue2.0的代码中发现 $key这个值并不能渲染成功,问题如下: 结果这个对象的key值并不能够显示: 后来查阅了文档才知道,这是因为在Vue2.0中,v-for迭代语法已经发生了变化: 丢弃了: 新数组语法 value in arr (value, index) in arr 新对象语法 value in obj (value, key) in obj (value, key, index) in obj 解决后:

Android 3.0设计思想规范和UI设计规范

文章描述:Android 3.0(蜂巢)交互&UI设计规范. Android OS自上市以来,由于缺乏统一规划,使得不同设备在 1.5.1.6.2.0.2.1.2.2.2.3几大版本徘徊,本人用的HTC Hero(俗称G3)也是从1.5~2.3一个个版本,10多个rom手动刷机试过来的,过程及其纠结 ~.多系统版本带来的问题就是缺乏交互.UI的一致性,外加硬件厂商HTC.摩托罗拉.三星.夏普(创新工场点心OS).小米(MIUI)等公司热衷于UI的个性化发挥,以及民间高手的DIY rom 等因素,

ASP.NET 2.0中实现模板中的数据绑定

asp.net|模板|数据 模板化的数据绑定控件为我们在页面上显示数据提供了根本的灵活性.你可能还记得ASP.NET v1.x中的几个模板化控件(例如DataList和Repeater控件).ASP.NET 2.0仍然支持这些控件,但在模板中绑定数据的语法已经被简化和改善了.本文将讨论在数据绑定控件模板中绑定数据的多种方法. 数据绑定表达式 ASP.NET 2.0改善了模板中的数据绑定操作,把v1.x中的数据绑定语法DataBinder.Eval(Container.DataItem, fiel