您身边的网站建设专家
成功案例

【建站服务】昌邑网站推广【昌邑办理400电话】昌邑SEO优化、昌邑微信公众号APP客户端小程序开发、昌邑网站托管、昌邑APP开发-域名申请

日期: 2022-09-20 15:40:31 浏览数:3


​

上往建站提供服务器空间服务商,百度快照排名,网站托管,百度推广运营,致力于设计外包服务与源代码定制开发,360推广,搜狗推广,增加网站的能见度及访问量提升网络营销的效果,主营:网站公司,百度推广公司电话,官网搭建服务,网站服务企业排名,服务器空间,英文域名等业务,专业团队服务,效果好。


昌邑网站推广【昌邑办理400电话】昌邑SEO优化、昌邑微信公众号APP客户端小程序开发、昌邑网站托管、昌邑APP开发

网站建设.jpg

昌邑市属于潍坊市下辖的县级市,位于山东半岛西北部,潍河下游,莱州湾畔,介于北纬36°25′-37°08′,东经119°13′-119°37′之间,东与莱州市、平度市以胶莱河为界,南与高密市、安丘市毗连,西与潍坊市坊子区、寒亭区为邻,北濒莱州湾,属市域总面积1578.7平方公里。 [1-2]  截至2016年,昌邑市辖3个街道、6个镇。

昌邑市属环渤海经济圈,为国务院确定的沿海对外开放城市之一,被誉为中国丝绸之乡、华侨之乡和中国溴·盐之乡, 先后被评为中国北方绿化苗木基地、中国纺织产业基地市、中国超纤产业基地、中国中小城市综合实力百强县市 [3]  、中国最具投资潜力中小城市百强县市 [4]  等荣誉称号。 [5]  2018年10月,昌邑市入选“综合实力百强县”、 [6]  全国投资潜力百强县市、 [7]  全国绿色发展百强县市 [8]  、全国科技创新百强县市、全国新型城镇化质量百强县市 [9]  。 [10]  2019年10月8日,被评为2019年度全国综合实力百强县市、2019年度全国绿色发展百强县市、全国科技创新百强县市 [11]  。 [12-13]  2020年山东省四星级新型智慧城市建设预试点城市, [14]  全国文明城市。 [15] 



概述

稍微了解行业现状的开发者都知道,现在前端“ES6即正义”,然而浏览器的支持还是进行时。所以我们会用一个神奇的工具将ES6都给转换成目前支持比较广泛的ES5语法。对,说的就是Babel。

本文不再介绍Babel是什么也不讲怎么用,这类文章很多,我也不觉得自己能写得更好。这篇文章的关注点是另一个方面,也是很多人会好奇的事情,Babel的工作原理是什么。

Babel工作的三个阶段

首先要说明的是,现在前端流行用的WebPack或其他同类工程化工具会将源文件组合起来,这部分并不是Babel完成的,是这些打包工具自己实现的,Babel的功能非常纯粹,以字符串的形式将源代码传给它,它就会返回一段新的代码字符串(以及sourcemap)。他既不会运行你的代码,也不会将多个代码打包到一起,它就是个编译器,输入语言是ES6+,编译目标语言是ES5。

在Babel官网,plugins菜单下藏着一个链接:thejameskyle/the-super-tiny-compiler。它已经解释了整个工作过程,有耐心者可以自己研究,当然也可以继续看我的文章。

Babel的编译过程跟绝大多数其他语言的编译器大致同理,分为三个阶段:

  1. 解析:将代码字符串解析成抽象语法树
  2. 变换:对抽象语法树进行变换操作
  3. 再建:根据变换后的抽象语法树再生成代码字符串

像我们在.babelrc里配置的presets和plugins都是在第2步工作的。

举个例子,首先你输入的代码如下:

if (1 > 0) {
    alert('hi');
}

经过第1步得到一个如下的对象:

{
  "type": "Program",                          // 程序根节点
  "body": [                                   // 一个数组包含所有程序的顶层语句
    {
      "type": "IfStatement",                  // 一个if语句节点
      "test": {                               // if语句的判断条件
        "type": "BinaryExpression",           // 一个双元运算表达式节点
        "operator": ">",                      // 运算表达式的运算符
        "left": {                             // 运算符左侧值
          "type": "Literal",                  // 一个常量表达式
          "value": 1                          // 常量表达式的常量值
        },
        "right": {                            // 运算符右侧值
          "type": "Literal",
          "value": 0
        }
      },
      "consequent": {                         // if语句条件满足时的执行内容
        "type": "BlockStatement",             // 用{}包围的代码块
        "body": [                             // 代码块内的语句数组
          {
            "type": "ExpressionStatement",    // 一个表达式语句节点
            "expression": {
              "type": "CallExpression",       // 一个函数调用表达式节点
              "callee": {                     // 被调用者
                "type": "Identifier",         // 一个标识符表达式节点
                "name": "alert"
              },
              "arguments": [                  // 调用参数
                {
                  "type": "Literal",
                  "value": "hi"
                }
              ]
            }
          }
        ]
      },
      "alternative": null                     // if语句条件未满足时的执行内容
    }
  ]
}

Babel实际生成的语法树还会包含更多复杂信息,这里只展示比较关键的部分,欲了解更多关于ES语言抽象语法树规范可阅读:The ESTree Spec。

用图像更简单地表达上面的结构:

第1步转换的过程中可以验证语法的正确性,同时由字符串变为对象结构后更有利于精准地分析以及进行代码结构调整。

第2步原理就很简单了,就是遍历这个对象所描述的抽象语法树,遇到哪里需要做一下改变,就直接在对象上进行操作,比如我把IfStatement给改成WhileStatement就达到了把条件判断改成循环的效果。

第3步也简单,递归遍历这颗语法树,然后生成相应的代码,大概的实现逻辑如下:

const types = {
  Program (node) {
    return node.body.map(child => generate(child));
  },
  IfStatement (node) {
    let code = `if (${generate(node.test)}) ${generate(node.consequent)}`;
    if (node.alternative) {
      code += `else ${generate(node.alternative)}`;
    }
    return code;
  },
  BlockStatement (node) {
    let code = node.body.map(child => generate(child));
    code = `{ ${code} }`;
    return code;
  },
  ......
};
function generate(node) {
  return types[node.type](node);
}
const ast = Babel.parse(...);            // 将代码解析成语法树
const generatedCode = generate(ast);     // 将语法树重新组合成代码

抽象语法树是如何产生的

第2、3步相信不用花多少篇幅大家自己都能理解,重点介绍的第一步来了。

解析这一步又分成两个步骤:

  1. 分词:将整个代码字符串分割成 语法单元 数组
  2. 语义分析:在分词结果的基础之上分析 语法单元之间的关系

我们一步步讲。

分词

首先解释一下什么是语法单元:语法单元是被解析语法当中具备实际意义的最小单元,通俗点说就是类似于自然语言中的词语。

看这句话“2020年奥运会将在东京举行”,不论词性及主谓关系等,人第一步会把这句话拆分成:2020年、奥运会、将、在、东京、举行。这就是分词:把整句话拆分成有意义的最小颗粒,这些小块不能再被拆分,否则就失去它所能表达的意义了。

那么回到代码的解析当中,JS代码有哪些语法单元呢?大致有以下这些(其他语言也许类似但通常都有区别):

分词的过过程从逻辑来讲并不难解释,但是这是个精细活,要考虑清楚所有的情况。还是以一个代码为例:

if (1 > 0) {
  alert("if "1 > 0"");
}

我们希望得到的分词是:

'if'     ' '       '('    '1'      ' '    '>'    ' '    ')'    ' '    '{'
'
  '   'alert'   '('    '"if "1 > 0""'    ')'    ';'    '
'   '}'

注意其中”if ”1 > 0””是作为一个语法单元存在,没有再查分成if、1、>、0这样,而且其中的转译符会阻止字符串早结束。

这拆分过程其实没啥可取巧的,就是简单粗暴地一个字符一个字符地遍历,然后分情况讨论,整个实现方法就是顺序遍历和大量的条件判断。我用一个简单的实现来解释,在关键的地方注释,我们只考虑上面那段代码里存在的语法单元类型。

function tokenizeCode (code) {
  const tokens = [];    // 结果数组
  for (let i = 0; i < code.length; i++) {
    // 从0开始,一个字符一个字符地读取
    let currentChar = code.charAt(i);

    if (currentChar === ';') {
      // 对于这种只有一个字符的语法单元,直接加到结果当中
      tokens.push({
        type: 'sep',
        value: ';',
      });
      // 该字符已经得到解析,不需要做后续判断,直接开始下一个
      continue;
    }
    
    if (currentChar === '(' || currentChar === ')') {
      // 与 ; 类似只是语法单元类型不同
      tokens.push({
        type: 'parens',
        value: currentChar,
      });
      continue;
    }

    if (currentChar === '}' || currentChar === '{') {
      // 与 ; 类似只是语法单元类型不同
      tokens.push({
        type: 'brace',
        value: currentChar,
      });
      continue;
    }

    if (currentChar === '>' || currentChar === '<') {
      // 与 ; 类似只是语法单元类型不同
      tokens.push({
        type: 'operator',
        value: currentChar,
      });
      continue;
    }

    if (currentChar === '"' || currentChar === ''') {
      // 引号表示一个字符传的开始
      const token = {
        type: 'string',
        value: currentChar,       // 记录这个语法单元目前的内容
      };
      tokens.push(token);

      const closer = currentChar;
      let escaped = false;        // 表示下一个字符是不是被转译的

      // 进行嵌套循环遍历,寻找字符串结尾
      for (i++; i < code.length; i++) {
        currentChar = code.charAt(i);
        // 先将当前遍历到的字符无条件加到字符串的内容当中
        token.value += currentChar;
        if (escaped) {
          // 如果当前转译状态是true,就将改为false,然后就不特殊处理这个字符
          escaped = false;
        } else if (currentChar === '') {
          // 如果当前字符是  ,将转译状态设为true,下一个字符不会被特殊处理
          escaped = true;
        } else if (currentChar === closer) {
          break;
        }
      }
      continue;
    }
    
    if (/[0-9]/.test(currentChar)) {
      // 数字是以0到9的字符开始的
      const token = {
        type: 'number',
        value: currentChar,
      };
      tokens.push(token);

      for (i++; i < code.length; i++) {
        currentChar = code.charAt(i);
        if (/[0-9.]/.test(currentChar)) {
          // 如果遍历到的字符还是数字的一部分(0到9或小数点)
          // 这里暂不考虑会出现多个小数点以及其他进制的情况
          token.value += currentChar;
        } else {
          // 遇到不是数字的字符就退出,需要把 i 往回调,
          // 因为当前的字符并不属于数字的一部分,需要做后续解析
          i--;
          break;
        }
      }
      continue;
    }

    if (/[a-zA-Z$_]/.test(currentChar)) {
      // 标识符是以字母、$、_开始的
      const token = {
        type: 'identifier',
        value: currentChar,
      };
      tokens.push(token);

      // 与数字同理
      for (i++; i < code.length; i++) {
        currentChar = code.charAt(i);
        if (/[a-zA-Z0-9$_]/.test(currentChar)) {
          token.value += currentChar;
        } else {
          i--;
          break;
        }
      }
      continue;
    }
    
    if (/s/.test(currentChar)) {
      // 连续的空白字符组合到一起
      const token = {
        type: 'whitespace',
        value: currentChar,
      };
      tokens.push(token);

      // 与数字同理
      for (i++; i < code.length; i++) {
        currentChar = code.charAt(i);
        if (/s]/.test(currentChar)) {
          token.value += currentChar;
        } else {
          i--;
          break;
        }
      }
      continue;
    }

    // 还可以有更多的判断来解析其他类型的语法单元

    // 遇到其他情况就抛出异常表示无法理解遇到的字符
    throw new Error('Unexpected ' + currentChar);
  }
  return tokens;
}

const tokens = tokenizeCode(`
if (1 > 0) {
  alert("if 1 > 0");
}
`);

以上代码是我个人的实现方式,与babel实际略有不同,但主要思路一样。

执行结果如下:

[
 { type: "whitespace", value: "
" },
 { type: "identifier", value: "if" },
 { type: "whitespace", value: " " },
 { type: "parens", value: "(" },
 { type: "number", value: "1" },
 { type: "whitespace", value: " " },
 { type: "operator", value: ">" },
 { type: "whitespace", value: " " },
 { type: "number", value: "0" },
 { type: "parens", value: ")" },
 { type: "whitespace", value: " " },
 { type: "brace", value: "{" },
 { type: "whitespace", value: "
 " },
 { type: "identifier", value: "alert" },
 { type: "parens", value: "(" },
 { type: "string", value: ""if 1 > 0"" },
 { type: "parens", value: ")" },
 { type: "sep", value: ";" },
 { type: "whitespace", value: "
" },
 { type: "brace", value: "}" },
 { type: "whitespace", value: "
" },
]

经过这一步的分词,这个数组就比摊开的字符串更方便进行下一步处理了。


昌邑网站推广【昌邑办理400电话】昌邑SEO优化、昌邑微信公众号APP客户端小程序开发、昌邑网站托管、昌邑APP开发


上往建站提供搭建网站,域名注册,官网备案服务,网店详情页设计,企业网店,专业网络店铺管理运营全托管公司咨询电话,服务器空间,微信公众号托管,网页美工排版,致力于域名申请,竞价托管,软文推广,全网营销,提供标准级专业技术保障,了却后顾之忧,主营:虚拟主机,网站推广,百度竞价托管,网站建设,上网建站推广服务,网络公司有哪些等业务,专业团队服务,效果好。

服务热线:400-111-6878 手机微信同号:18118153152(各城市商务人员可上门服务)


全国咨询热线:400-111-6878

地址:全国各地都有驻点商务

Copyright © 2021 通陆科技

网站建设:上往建站