// 2.2 process
从上图中可以看到body
属性是一个数组,分别对应的是源代码中的两行代码。
数组的第一项对应的Node节点类型是VariableDeclaration
,他是一个变量声明类型的节点。对应的就是源代码中的第一行:const { name: localName } = defineProps(["name"])
数组中的第二项对应的Node节点类型是ExpressionStatement
,他是一个表达式类型的节点。对应的就是源代码中的第二行:console.log(localName)
我们接着来看compileScript
函数中的外层for循环,也就是遍历前面讲的body数组,代码如下:
function compileScript(sfc, options) {
// ...省略
// 2.2 process
function processDefineProps(ctx, node, declId) {
if (!isCallOf(node, DEFINE_PROPS)) {
return processWithDefaults(ctx, node, declId);
}
// handle props destructure
if (declId && declId.type === "ObjectPattern") {
processPropsDestructure(ctx, declId);
}
return true;
}
processDefineProps
函数接收3个参数。
第二个参数node
,这个节点对应的是变量声明语句中的初始化值的部分。也就是源代码中的defineProps(["name"])
。
第三个参数declId
,这个对应的是变量声明语句中的变量名称。也就是源代码中的{ name: localName }
。
在 为什么defineProps宏函数不需要从vue中import导入?文章中我们已经讲过了这里的第一个if语句就是用于判断当前是否在执行defineProps
函数,如果不是那么就直接return false
我们接着来看第二个if语句,这个if语句就是判断当前变量声明是不是“对象解构赋值”。很明显我们这里就是解构出的localName
变量,所以代码将会走到processPropsDestructure
函数中。
processPropsDestructure
函数
接着将断点走进processPropsDestructure
函数,在我们这个场景中简化后的代码如下:
function processPropsDestructure(ctx, declId) {
const registerBinding = (
key: string,
local: string,
defaultValue?: Expression
) => {
ctx.propsDestructuredBindings[key] = { local, default: defaultValue };
};
for (const prop of declId.properties) {
const propKey = resolveObjectKey(prop.key);
registerBinding(propKey, prop.value.name);
}
}
前面讲过了这里的两个入参,ctx
表示当前上下文对象。declId
表示变量声明语句中的变量名称。
首先定义了一个名为registerBinding
的箭头函数。
接着就是使用for循环遍历declId.properties
变量名称,为什么会有多个变量名称呢?
答案是解构的时候我们可以解构一个对象的多个属性,用于定义多个变量。
prop
属性如下图:![](http://mmbiz.qpic.cn/mmbiz_png/8hhrUONQpFuTA5vnKPAnbAesdfNopYsVae2N8ic4JIIDicwiaLutXHyX2GxCjdVZmQp4oAMmic3hONZxJupCmlm4zw/640?wx_fmt=png&from=appmsg)
从上图可以看到prop
中有两个属性很显眼,分别是key
和value
。
其中key
属性对应的是解构对象时从对象中要提取出的属性名,因为我们这里是解构的name
属性,所以上面的值是name
。
其中value
属性对应的是解构对象时要赋给的目标变量名称。我们这里是赋值给变量localName
,所以上面他的值是localName
。
接着来看for循环中的代码。
执行const propKey = resolveObjectKey(prop.key)
拿到要从props
对象中解构出的属性名称。
将断点走进resolveObjectKey
函数,代码如下:
function resolveObjectKey(node: Node) {
switch (node.type) {
case "Identifier":
return node.name;
}
return undefined;
}
如果当前是标识符节点,也就是有name属性。那么就返回name属性。
最后就是执行registerBinding
函数。
registerBinding(propKey, prop.value.name)
第一个参数为传入解构对象时要提取出的属性名称,也就是name
。第二个参数为解构对象时要赋给的目标变量名称,也就是localName
。
接着将断点走进registerBinding
函数,他就在processPropsDestructure
函数里面。
function processPropsDestructure(ctx, declId) {
const registerBinding = (
key: string,
local: string,
defaultValue?: Expression
) => {
ctx.propsDestructuredBindings[key] = { local, default: defaultValue };
};
// ...省略
}
ctx.propsDestructuredBindings
是存在ctx上下文中的一个属性对象,这个对象里面存的是需要解构的多个props。
对象的key就是需要解构的props。
key对应的value也是一个对象,这个对象中有两个字段。其中的local
属性是解构props后要赋给的变量名称。default
属性是props的默认值。
在debug终端来看看此时的ctx.propsDestructuredBindings
对象是什么样的,如下图:![](http://mmbiz.qpic.cn/mmbiz_png/8hhrUONQpFuTA5vnKPAnbAesdfNopYsVibq3rS45PibS6K9cR0bZtTBpkGVZj4Fz0x2L2e1cx1ETzs8jcJKQYyiaQ/640?wx_fmt=png&from=appmsg)
从上图中就有看到此时里面已经存了一个name
属性,表示props
中的name
需要解构,解构出来的变量名为localName
,并且默认值为undefined
。
经过这里的处理后在ctx上下文对象中的ctx.propsDestructuredBindings
中就已经存了有哪些props需要解构,以及解构后要赋值给哪个变量。
有了这个后,后续只需要将script模块中的所有代码遍历一次,然后找出哪些在使用的变量是props解构的变量,比如这里的localName
变量将其替换成__props.name
即可。
transformDestructuredProps函数
接着将断点层层返回,走到最外面的compileScript
函数中。再来回忆一下compileScript
函数的代码,如下:
function
compileScript(sfc, options) {
const ctx = new ScriptCompileContext(sfc, options);
const scriptSetupAst = ctx.scriptSetupAst;
// 2.2 process
propsLocalToPublicMap
对象如下图:![](http://mmbiz.qpic.cn/mmbiz_png/8hhrUONQpFuTA5vnKPAnbAesdfNopYsVn2HWhjIRYlJKD2nzcB44aI6y8POEOia9lIZedqT5MndDZnDeVVDF43Q/640?wx_fmt=png&from=appmsg)
经过这个for循环的处理后,我们已经知道了有哪些变量其实是经过props解构来的,以及这些解构得到的变量和props的映射关系。
接下来就是使用walk
函数去递归遍历script模块中的所有代码,这个递归遍历就是遍历script模块对应的AST抽象语法树。
在这里是使用的walk
函数来自于第三方库estree-walker
。
在遍历语法树中的某个节点时,进入的时候会触发一次enter
回调,出去的时候会触发一次leave
回调。
walk
函数的执行代码如下:
walk(ast, {
enter(node: Node) {
if (node.type === "Identifier") {
if (currentScope[node.name]) {
rewriteId(node);
}
}
},
});
我们这个场景中只需要enter
进入的回调就行了。
在enter
回调中使用外层if判断当前节点的类型是不是Identifier
,Identifier
类型可能是变量名、函数名等。
我们源代码中的console.log(localName)
中的localName
就是一个变量名,当递归遍历AST抽象语法树遍历到这里的localName
对应的节点时就会满足外层的if条件。
在debug终端来看看此时满足外层if条件的node节点,如下图:![](http://mmbiz.qpic.cn/mmbiz_png/8hhrUONQpFuTA5vnKPAnbAesdfNopYsVggUVgiadCSWiaOfKDZILgl5gSEQdTHiblp5KrcXqexo9Gvh1ctTrIiaaKg/640?wx_fmt=png&from=appmsg)
从上面的代码可以看到此时的node节点中对应的变量名为localName
。其中start
和end
分别表示localName
变量的开始位置和结束位置。
我们回忆一下前面讲过了currentScope
对象中就是存的是有哪些本地的变量是通过props解构得到的,这里的localName
变量当然是通过props解构得到的,满足里层的if条件判断。
最后代码会走进rewriteId
函数中,将断点走进rewriteId
函数中,简化后的代码如下:
function rewriteId(id: Identifier) {
// x --> __props.x
ctx.s.overwrite(
id.start + ctx.startOffset,
id.end + ctx.startOffset,
genPropsAccessExp(propsLocalToPublicMap[id.name])
);
}
这里使用了ctx.s.overwrite
方法,这个方法接收三个参数。
第一个参数是:开始位置,对应的是变量localName
在源码中的开始位置。
第二个参数是:结束位置,对应的是变量localName
在源码中的结束位置。
第三个参数是想要替换成的新内容。