exp-parser | 每天读一点Vue源码

1,278 阅读4分钟

今天是2020年4月4日清明节,向英雄致敬,向逝者致哀...

前言

面试的时候经常被问一些Vue源码相关的问题,通常情况下, 我会在面试前恶补掘金上的面筋来对付面试,什么双向绑定的原理呀,什么虚拟dom树呀,实际上我压根儿就没仔细研究过,其一是自己真的比较菜,其二工作上也用不上,别自己给自己添堵。但后面想一下,很多事情,为之则易,不为则难,给自己设立困难(负重)才能进步,决定每天多一点Vue的源码,在Vue的源码选择上,我选择了最老的版本(0.1)😬(真的怕自己看起来吃力), 阅读的模式为通读,从易到难一个文件一个文件的看,看完一个文件后再看它的单元测试,等完全吃透后复制粘贴代码到本地运行测试用例为代码块写一些中文注释,打上tag推到自己的仓库,开始梳理写文章总结(之前有犹豫过是否应该在掘金上写文章,因为这类Vue源码解析的文章已经很多了,而且还写的很好,我再写一遍是否还存在意义,后面想还是写吧,流水总结也不错💧)。

正文

这是我发的第五篇关于Vue源码的文章, 本文介绍exp-parser(表达式解析), exp-parser.js的功能可以概括为:把像a + b这样的表达式转换为像() => {return this.a + this.b;}这样可以执行的匿名函数。

下面是更详细的例子:

<span>{{a - b * 2 + 45}}</span> 
<span>{{(a && b) ? c : d || e}}</span>
<span>{{todo.title + ' : ' + (todo.done ? 'yep' : 'nope')}}</span>
<span>{{sortRows({ column: 'name', test: 'haha', durrr: 123 })}}</span>
// 上面有4个表达式,
// 4个表达式经过exp-parser将转换成大致这样:
// a - b * 2 + 45
() => {
    return this.a - this.b * 2 + 45
}
// (a && b) ? c : d || e
() => {
    return (this.a && this.b) ? this.c : this.d || this.e
}
// todo.title + ' : ' + (todo.done ? 'yep' : 'nope')
() => {
    return this.todo.title + ' : ' + (this.todo.done ? 'yep' : 'nope')
}
// sortRows({ column: 'name', test: 'haha', durrr: 123 })
() => {
    return this.sortRows({column: 'name', test: 'haha', durre: 123})
}

从上面的例子中可以大致得知exp-parser的实现分成三步, 第一步获得表达式里面的变量名 比如上面的a, b, c, todo, sortRows

第二步根据变量名找到其所在的作用域this.a 或者 this.$parent.a

第三步替换上述的变量,返回匿名函数() => {return this.a}

// 下面是伪代码
exports.parse = function (exp, compiler, data) {
    // 获得变量名
    var vars = getVariables(exp)
    // 获得变量的作用域
    vars.forEach((path, i) => {
      vars[i] = traceScope(path, compiler, data)  
    })
    // 根据exp,和vars生成匿名函数
    return makeGetter(vars, exp)
}

上面是exp-parser源码的大致雏型,exp-parser大致160行左右,整体阅读不难,而我在阅读源码的时候,遇到的最大问题是exp-parser的正则表达式是真的多,看到脑壳痛,时间也大部分花在阅读正则表达式上面去了,下面通过讲解getVariables函数里的正则表达式,来帮助你节省阅读exp-parser源码时间。

/**
 *  Strip top level variable names from a snippet of JS expression
 */
// 提取表达式中的变量
// getVariables("(a && b) ? c : d || e") => [a, b, c, d, e]
// REMOVE_RE 匹配删除的内容, 把需要删除的内容清空
// SPLIT_RE: /[^\w$]+/g 匹配非字符内容,把他们替换成 ','
// KEYWORDS_RE: js里的关键字,比如var,let这些,把他们也清空.
// NUMBER_RE:/\b\d[^,]*/g, 匹配以数字开头的内容,数字开头的肯定不是变量,把这些也清空。
// BOUNDARY_RE:/^,+|,+$/g,把表达式开头的,结尾的,清空。
function getVariables(code) {
    code = code.replace(REMOVE_RE, '')
        .replace(SPLIT_RE, ',')
        .replace(KEYWORDS_RE, '')
        .replace(NUMBER_RE, '')
        .replace(BOUNDARY_RE, '')
    return code ? code.split(/,+/) : []
}

getVariables函数用了5个正则表达式才把表达式中的变量提取出来,下面讲解下REMOVE_RE, KEYWORDS_RE这两个正则表达式,还别说有点小复杂,完全吃透也能学到一些东西。

REMOVE_RE

用于删除内容的正则

var REMOVE_RE = /\/\*(?:.|\n)*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|'[^']*'|"[^"]*"|[\s\t\n]*\.[\s\t\n]*[$\w\.]+|[\{,]\s*[\w\$_]+\s*:/g

这段正则巨长,其实可以分割成一下几段正则:

var REMOVE_RE1 =  /\/\*(?:.|\n)*?\*\//
REMOVE_RE1.test("/*xxxxxxx*/") // true
var REMOVE_RE2 = /\/\/[^\n]*\n/
REMOVE_RE2.test("//xxxxxx\n") // true
var REMOVE_RE3 = /\/\/[^\n]*$/
REMOVE_RE3.test("//xxxxxx") // true
var REMOVE_RE4 = /'[^']*'/
REMOVE_RE4.test("'xxxxxx'") // true
var REMOVE_RE5 = /"[^"]*"/
REMOVE_RE5.test('"xxxxxxx"') // true
var REMOVE_RE6 = /[\s\t\n]*\.[\s\t\n]*[$\w\.]+/
REMOVE_RE6.test(".xxxx") // true
var REMOVE_RE7 = /[\{,]\s*[\w\$_]+\s*:/
REMOVE_RE7.test("{ a: 1, b: ")

上述正则能匹配的内容为test函数里面的字符串,我顺便解释下这些正则里面的元字符: (?:)非捕获组, \n匹配换行, \s匹配空格, \t匹配制表符即键盘上的tab键, \w匹配字符或数字。

KEYWORDS_RE

匹配js里面的关键字

// 看代码的那天正好得知司徒正美大佬去了只有二次元的世界 R.I.P。
// Variable extraction scooped from https://github.com/RubyLouvre/avalon

// javascript中的关键字
var KEYWORDS =
    // keywords
    'break, case, catch,continue,debugger,default,delete,do,else,false' +
    ',finally,for,function,if,in,instanceof,new,null,return,switch,this' +
    ',throw,true,try,typeof,var,void,while,with,undefined' +
    // reserved
    ',abstract,boolean,byte,char,class,const,double,enum,export,extends' +
    ',package,private,protected,public,short,static,super,synchronized' +
    'throws,transient,volatile' +
    // ECMA 5 - use strict
    ',arguments,let,yield' +
    //allow using Math in expressions
    ',Math'
    
    // 匹配关键字
var KEYWORDS_RE = new RegExp(["\\b" + KEYWORDS.replace(/,/g, '\\b|\\b')].join('|')),

Vue-0.1中keywords来源于avalon,而我看代码的那天顺便在刷掘金沸点,刷到了avalon的作者去世了,这也是缘分呀,愿大佬R.I.P。

这段正则表达式不难,我当时看的时候,唯一疑惑的是\b, 因为我分不清\b\B的区别, 我相信应该有很多人像我一样分不清,我查资料获得\b非字母和数字字母和数字的分隔符, \B字符的分隔符,是不是听起来绕,看下面一段代码: 我们用 |代表\b, 用-代表\B

var tt = "abc"
tt.replace(/\b/g, '|').replace(/\B/g, '-')
// 结果: tt: "-|a-b-c|-"

最后

如果实在是被exp-parser.js里面的正则搞晕了,建议看下这篇文章,帮你复习一下正则表达式javascript正则表达式 | 知识梳理💪。

exp-parser.js

exp-parser.test.js

持续更新...❤️