solidjs编译技术源码解析

118 阅读3分钟

babel-plugin-jsx-dom-expression 0.5.0解析

  1. 项目结构

使用rollup打包,入口文件src/index.js,输出到lib/index.js; 使用babel调用产物(babel插件)转换jsx。jsx测试文件放在jsx目录下。

  1. babel插件基本原理:

babel生成AST后,遍历AST过程中会调用插件visitor的对应方法,可以更改AST结构,修改后的AST返回给babel用来代码生成,并且可以影响babel遍历的路径。

  1. 基本思路:目标是将jsx转换成js代码,jsx只有一个根元素,有两种case:jsxelement和fragment,所以实现的visitor里有两个入口。当babel调用插件时,从入口获取到AST的根元素,然后插件内部递归遍历AST,收集信息改写AST,最终babel用改写后的AST生成js代码。
  2. 递归遍历AST 实现方法generateHTMLNode,遍历ast对node操作的目的是将静态部分和动态部分分离开,静态部分就拼模板,动态部分通过解析attribute获取。
const HelloMessage = ({name}) => (
  <div class={(name=='t'? 't' : '')} onClick={e => console.log(name)}>
    Hello {name}
  </div>
);
// 转换成
const _tmpl$ = document.createElement("template");
_tmpl$.innerHTML = "<div>\n    Hello </div>";
const HelloMessage = ({
  name
}) => function () {
  const _el$ = _tmpl$.content.firstChild.cloneNode(true),
    _el$2 = _el$.firstChild,
    _el$3 = _el$.insertBefore(document.createTextNode(""), _el$2.nextSibling);
  r.addEventListener(_el$, "click", e => console.log(name));
  r.wrap(() => _el$.className = name == 't' ? 't' : '');
  r.insertM(_el$, name, null, _el$3);
  return _el$;
}();

示例的jsx根元素是div,有两个动态属性,children有静态文本和插值。转换的结果是模板静态部分抽离出来,提到上层创建dom,div有动态属性,每个动态绑定对应会生成一条js语句绑定属性。根元素赋值语句,动态子元素之前的所有子元素赋值语句,插值语句生成一条js语句。

访问node的返回结果是一个对象,包含静态模板,动态表达式,以及变量声明,变量声明主要是父子元素绑定,返回结果id。id主要是动态绑定时需要引用元素,所以需要id作为标识符。

元素id

顶层jsxelement是有id的; 有id必须父元素有id并且该元素往后存在动态绑定,需要此元素有id,以便索引。 因为根jsxelement是有id的,那么第一没有id的子元素必然是由于往后都是静态内容,不需要绑定所以是没有id的;

属性

遍历元素的属性,

  • 如果是静态的,将属性的键值拼到模板上;
  • 如果是表达式,封装成表达式语句放入表达式中
class={name=='t'? 't' : ''}
比如是表达式,需要转换成
elem.class = name=='t'? 't' : ''
用AST表示就是
t.assignmentExpression('=', t.memberExpression(elem, t.identifier(name)), value)

整体是一个赋值表达式,左值是成员表达式,成员表达式左值是元素标识,右值是属性键标识符
, 赋值表达式右值是属性值
class={(name=='t'? 't' : '')}
特殊逻辑:如果动态属性表达式用小括号包裹,希望生成如下

r.wrap(() => elem.class = name=='t'? 't' : '')
对应的AST是
t.expressionStatement(  // 整体是一个表达式语句
  t.callExpression( // 函数调用
    t.identifier(`${moduleName}.wrap`), //调用左值
    [   // 参数数组
      t.arrowFunctionExpression(  //第一个参数是箭头函数
        [], // 箭头函数参数

        t.assignmentExpression... // 同上赋值语句
      )
    ]

  )

)

子元素

当元素的模板和属性动态表达式收集完成后,就是递归遍历子元素。 调用子元素返回结果中分为两种情况:

  1. 插值表达式没有id,细分为两种case 插值表达式是唯一children:
r.insert(ele, expr)
对应
t.expressionStatement(t.callExpression(t.identifier(`r.insert`), [ele, expr]))

非唯一:需要先向父元素的前兄弟节点的后面插入一个占位元素,然后向占位元素赋值内容,所以额外需要生成一条变量声明

  1. 有id的,包含子元素的静态模板和动态表达式,静态模板拼到父元素模板中,动态表达式放入父元素表达式数组中。向父元素声明数组中加一条变量声明
ele1 = ele.firstChild // 这种变量声明语句
ast表达形式
t.variableDeclarator(child.id, t.memberExpression(ele, t.identifier('xxx')))

有id的case很重要的一点是需要额外生成变量声明,后续依赖此变量绑定动态内容,因为只有根元素对应的变量会赋予dom,其他节点的变量标识符需要借助于父子或兄弟节点关系赋值,才会有定义。这样标识符的动态绑定表达式才是对真实dom的操作

普通文本

普通文本生成结果也是一个对象,id是否生成看父元素是否有id,以及后续有没有动态内容。 只有template,表达式和声明为空。

插值

返回的结果包含表达式,id为空。这里需要特殊处理,以便生成insert语句

组件jsx调用

当tagname开头大写,判断为组件,需要转换成函数调用。 最终返回结果也是一个对象,表达式部分是一个函数调用


const HelloMessage2 = ({name}) => (
    <Hello>
      Hello {name}
    </Hello>
);

// 转换成
const HelloMessage2 = ({
  name
}) => Hello({
  children: [(() => {
    const _el$ = _tmpl$.content.firstChild.cloneNode(true);
    return _el$;
  })(), name]
});

ast形式
t.callExpression(t.identifier(tagname, props))

props是对象

props的生成:

遍历所有属性,生成t.objectProperty(t.identifier(attrname, value)),放到runningObject数组中,这代表一个键值对 最终通过t.objectExpression(runningObject),生成对象

children props:

生成的数组,如果是插值,直接返回变量;非插值需要正常返回dom

t.callExpression(t.arrowFunctionExpression([], t.blockStatement([child.decl, ...child.exprs, t.returnStatement(child.id)])), [])

正常逻辑: 拷贝模板,赋值,绑定,返回dom

dom内部的组件调用类似插值,调用insert方法将组件调用表达式的结果插入