HuazhuLi / Blog

3 stars 2 forks source link

Vue.js源码阅读 #16

Open HuazhuLi opened 7 years ago

HuazhuLi commented 7 years ago

在大部分时间里,我们使用vue.js,几乎必然都是在使用vue的模版功能; 在初学vue的时候,每当编译报错时,笔者第一反应就是:没按照官方文档写(这是DSL (Domain-Specific Language, 领域特定语言)带来的一些问题);

vue 的模版简单易用,但是由于有些开发者不能完全知晓内部的处理逻辑,会显得有点“黑魔法”, 所以笔者特意去阅读了vue的源码,来了解vue到底为我们开发者提供了何种便利,能让我们舒服自然地进行声明式编程;

在开始阅读compile这部分的源码之前,首先我们想知道的是:vue的compile部分,在vue实例化过程中,起着怎样的作用? 其实,在vue里面写render function,或者说使用jsx写法的时候,是不需要经过compile这部分的。 compile代码,只做了一件事:把template转成render function。

在vue实例创建完成以后(created后),首先会检测有没有el选项,有的话就把对应的outerHTML取出来, 并且当作template来处理;如果没有,就检测有没有template选项; 完成以上操作以后,开始了compile这一阶段。 (使用render function 和 template的区别,也是使用独立构建和运行时构建的区别。这在后面会提及) 放一张Vue的生命周期图:

在上图,我们可以清晰地看到:compile发生在created和mounted的时间点之前,那问题来了,在这个阶段,一个Vue实例到底经历了什么? 或者说,template要转成render function,需要哪些步骤?

(首先,我们必须先确定正在讨论的问题,因为vue2以及vue1的compile阶段,差别较大;所以在本文,我们只讨论vue2的compile阶段)

compile部分文件结构以及对应作用如下:

compile |——parser (template -> ast) |——index.js(入口文件) |——text-parser.js(对ast里面的ASTTEXT & ASTEXPRESSION进行判断以及处理) |——html-parser.js (对template里面的dom树进行遍历操作) |——filter-parser.js (对template的filter进行解析) |——entity-decoder.js |——codegen (optimized ast -> render function) |——events.js |——index.js |——directives (对特殊指令的一些处理) |——bind.js |——index.js |——model.js |——on.js |——creatCompiler.js(创造compile实例) |——error-detector.js |——helpers.js(公用函数库) |——optimizer.js(ast -> optimized ast) |——index.js(入口文件) |——to-function.js

在compile这个过程里,框架主要做了三件事: 1.parse(解释template,使其转成ast) 2.optimize(优化ast) 3.code generate(把ast转成render function)

结束这三个过程以后,我们得到了render function(渲染函数);render function会渲染出VNODE(也就是virtual dom的节点) (至于template为什么要被编译成render function,以及vue为何支持jsx,我们在稍后会提及相应的内容,我们先把精力放在compile这部分的源码上)

附上一张compile/index.js里的compile函数调用图:

(在vue.js 2.4里,作者在部分函数里使用typescript,以上的typescript其实很好懂:typescript需要对函数的参数,以及函数的返回值,定义类型; 在以上函数,函数的参数 template/options 分别是string类型和compileroptions类型; 返回值类型,是compiledresult。)

简而言之,creatCompilerCreator函数接受baseCompile函数为参数,而后者又调用了parse/optimize函数/generate函数; 最后把值返回给creatCompiler。

这一流程简单的图示如下:

解说版: v-text指令冒险记

v-text指令经过parse里面的html parser,htmlparser读到了

&

,于是调用了parser里的start和end方法,这两个方法,生成了 一个astelement,这个astelement里面有directive这个属性,里面有v-text这个指令作为属性名以及对应的值;

然后在经过optimize这一阶段,这个astelement被标记成static(静态的)

最后经过codegen,这个astelement被输出成staticrenderfn(静态渲染函数)

然后这个static render function会渲染出一个虚拟dom节点,也就是VNODE这种数据结构,最后virtual dom tree会和之前的virtual dom tree进行diff,把diff的结果patch到实际的dom节点上,完成了下图过程。

HuazhuLi commented 7 years ago

parser,也叫解释器,在vue.js里,parser所做的事情很简单,就是把模版里层层嵌套的dom结构,转换成一个parent属性为空,而有children属性的的ASTElement,这个ASTElement,vue里边把它命名为根(root)。

(ast,也就是我们常说的,抽象语法树,后文提到的ASTElement,ASTExpression,ASTText,都是抽象语法树上面的节点,上文提到的root,就是一个ASTElement。)

由于parser在最后,只会返回一个ASTElement,如上文提到的那样,这个ASTElement叫做root,它的parent属性为空,只有child属性。 (所以在这里,我们知道了,为什么vue.js需要我们在template模版最外层,必须安排一个根节点;假设你不这么干,你在template里面写了div1 div2 div3节点,然后vue会怎么做呢?他会遍历div1的dom结构,并且返回div1这个标签对应的ast作为root,最后直接跳过div2 div3;) todo:这段有待商榷

过程简介

就如我们第一章讲的那样: parse这个过程,就是把template转成ast的过程,这个过程并非一蹴而就,而是分为好几个子过程:

为了说明这个过程,笔者在此以下面的模版作为例子:

对于这个模版,parser做了以下的几步:

第一步:parser调用html parser(一个开源的html解释器,作者是John Resig),并且给html parser传入四个函数,分别start() end() chars() comment(); 这个解释器原理比较简单,解析的主要过程是:

这个parser在于遇到开始标签的时候,举个例子:

,他就会调用一个对应的函数start(),这个函数(vue作者定制)会生成一个ASTElement,记录节点 tagName, attributes 等信息; 这个parser在遇到结束标签的时候,举个例子

,他就会调用对应的函数方法end(); 这个parser在遇到开始标签和结束标签之间的内容时候,就会调用一个方法char(); 在遇到注释时,会调用对应的comment()函数;

了解这四个函数,是理解其生成的三种ast节点的关键;

ASTNode

上文提及到的四个函数,直接生成了三种ast节点,这三种ast类型如下:

-ASTElement -ASTText -ASTExpression

对应地看一下 Vue 2.0 源码中 AST 数据结构 的定义: img

注意:为了避免文章过长,我在以上的代码中注释了 ASTElement 中的许多属性,点击上方 AST 数据结构的链接可查看完整代码。

img

ASTExpression & ASTText

function name & function involved

ASTElement & VUE指令编译优先级

只要在调用char()方法的时候,vuejs才会生成astelement以及astexpression,否则都是生成astelement (图示)

2.convert node to ast 对as

理解ast的三种类型 以及对应的html代码

编译的优先级 每一个指令对应的parse过程

为什么会出现编译优先级的问题

区分更改代码/重新销毁/在使用中我们只更改数据

说说function调用判断逻辑