bt365体育投注.主頁欢迎您!!

    <acronym id="zvmrr"></acronym>
    <td id="zvmrr"></td>
  • <tr id="zvmrr"><label id="zvmrr"></label></tr>
  • <acronym id="zvmrr"></acronym>
  • Eno_Yao

    Eno_Yao 查看完整档案

    填写现居城市  |  填写毕业院校公众号  |  前端遨游 编辑 github.com/wscats 编辑
    编辑

    分享和总结不易,确定不Follow一下?

    Github:https://github.com/wscats

    稀土掘金:https://juejin.im/user/584c7f...

    CSDN:https://blog.csdn.net/qq_2708...

    个人动态

    Eno_Yao 收藏了文章 · 9月10日

    try catch引发的性能优化深度思考

    image

    关键代码拆解成如下图所示(无关部分已省略):

    demo

    起初我认为可能是这个 getRowDataItemNumberFormat 函数里面某些方法执行太慢,从 formatData.replaceunescape(已废弃,官方建议使用 decodeURI 或者?decodeURIComponent 替代) 方法都怀疑了一遍,发现这些方法都不是该函数运行慢的原因。为了深究原因,我给 style.formatData 传入了不同的值,发现这个函数的运行效率出现不同的表现。开始有点疑惑为什么 style.formatData 的值导致这个函数的运行效率差别如此之大。

    进一步最终定位发现如果?style.formatData 为 undefined 的时候,效率骤降,如果?style.formatData 为合法的字符串的时候,效率是正常值。我开始意识到这个问题的原因在那里了,把目光转向了 try catch 代码块,这是一个很可疑的地方,在很早之前曾经听说过不合理的 try catch 是会影响性能的,但是之前从没遇到过,结合了一些资料,我发现比较少案例去探究这类代码片段的性能,我决定写代码去验证下:

    window.a = 'a';
    window.c = undefined;
    function getRowDataItemNumberFormatTryCatch() {
        console.time('getRowDataItemNumberFormatTryCatch');
        for (let i = 0; i < 3000; i++) {
            try {
                a.replace(/%022/g, '"');
            }
            catch (error) {
            }
        }
        console.timeEnd('getRowDataItemNumberFormatTryCatch');
    }

    我尝试把 try catch 放入一个 for 循环中,让它运行 3000 次,看看它的耗时为多少,我的电脑执行该代码的时间大概是 0.2 ms 左右,这是一个比较快的值,但是这里 a.replace 是正常运行的,也就是 a 是一个字符串能正常运行 replace 方法,所以这里的耗时是正常的。我对他稍微做了一下改变,如下:

    function getRowDataItemNumberFormatTryCatch2() {
        console.time('getRowDataItemNumberFormatTryCatch');
        for (let i = 0; i < 3000; i++) {
            try {
                c.replace(/%022/g, '"');
            }
            catch (error) {
            }
        }
        console.timeEnd('getRowDataItemNumberFormatTryCatch');
    }

    这段代码跟上面代码唯一的区别是,c.replace 此时应该是会报错的,因为 cundefined,这个错误会被 try catch 捕捉到,而上面的代码耗时出现了巨大的变化,上升到 40 ms,相差了将近 200 倍!并且上述代码和首图的?getRowDataItemNumberFormat 函数代码均出现了 Minor GC,注意这个 Minor GC 也是会耗时的。

    demo

    这可以解释一部分原因了,我们上面运行的代码是一个性能比较关键的部分,不应该使用 try catch 结构,因为该结构是相当独特的。与其他构造不同,它运行时会在当前作用域中创建一个新变量。每次 catch 执行该子句都会发生这种情况,将捕获的异常对象分配给一个变量。

    即使在同一作用域内,此变量也不存在于脚本的其他部分中。它在 catch 子句的开头创建,然后在子句末尾销毁。因为此变量是在运行时创建和销毁的(这些都需要额外的耗时!),并且这是 JavaScript 语言的一种特殊情况,所以某些浏览器不能非常有效地处理它,并且在捕获异常的情况下,将捕获处理程序放在性能关键的循环中可能会导致性能问题,这是我们为什么上面会出现 Minor GC 并且会有严重耗时的原因。

    如果可能,应在代码中的较高级别上进行异常处理,在这种情况下,异常处理可能不会那么频繁发生,或者可以通过首先检查是否允许所需的操作来避免。上面的 getRowDataItemNumberFormatTryCatch2 函数示例显示的循环,如果里面所需的属性不存在,则该循环可能引发多个异常,为此性能更优的写法应该如下:

    function getRowDataItemNumberFormatIf() {
        console.time('getRowDataItemNumberFormatIf');
        for (let i = 0; i < 3000; i++) {
            if (c) {
                c.replace(/%022/g, '"');
            }
        }
        console.timeEnd('getRowDataItemNumberFormatIf')
    }

    上面的这段代码语义上跟 try catch 其实是相似的,但运行效率迅速下降至 0.04ms,所以 try catch 应该通过检查属性或使用其他适当的单元测试来完全避免使用此构造,因为这些构造会极大地影响性能,因此应尽量减少使用它们。

    如果一个函数被重复调用,或者一个循环被重复求值,那么最好避免其中包含这些构造。它们最适合仅执行一次或仅执行几次且不在性能关键代码内执行的代码。尽可能将它们与其他代码隔离,以免影响其性能。

    例如,可以将它们放在顶级函数中,或者运行它们一次并存储结果,这样你以后就可以再次使用结果而不必重新运行代码。

    demo

    getRowDataItemNumberFormat 在经过上述思路改造后,运行效率得到了质的提升,在实测 300 多次循环中减少的时间如下图,足足优化了将近 2s 多的时间,如果是 3000 次的循环,那么它的优化比例会更高:

    demo
    demo

    由于上面的代码是从项目中改造出来演示的,可能并不够直观,所以我重新写了另外一个相似的例子,代码如下,这里面的逻辑和上面的?getRowDataItemNumberFormat 函数讲道理是一致的,但是我让其发生错误的时候进入 catch 逻辑执行任务。

    事实上 plus1plus2 函数的代码逻辑是一致的,只有代码语义是不相同,一个是返回 1,另一个是错误抛出1,一个求和方法在 try 片段完成,另一个求和方法再 catch 完成,我们可以粘贴这段代码在浏览器分别去掉不同的注释观察结果。

    我们发现 try 片段中的代码运行大约使用了 0.1 ms,而 catch 完成同一个求和逻辑却执行了大约 6 ms,这符合我们上面代码观察的预期,如果把计算范围继续加大,那么这个差距将会更加明显,实测如果计算 300000 次,那么将会由原来的 60 倍差距扩大到 500 倍,那就是说我们执行的 catch 次数越少折损效率越少,而如果我们执行的 catch 次数越多那么折损的效率也会越多。

    所以在不得已的情况下使用 try catch 代码块,也要尽量保证少进入到 catch 控制流分支中。

    const plus1 = () => 1;
    const plus2 = () => { throw 1 };
    console.time('sum');
    let sum = 0;
    for (let i = 0; i < 3000; i++) {
        try {
            // sum += plus1(); // 正确时候 约 0.1ms
            sum += plus2(); // 错误时候 约 6ms
        } catch (error) {
            sum += error;
        }
    }
    console.timeEnd('sum');

    上面的种种表现进一步引发了我对项目性能的一些思考,我搜了下我们这个项目至少存在 800 多个 try catch,糟糕的是我们无法保证所有的 try catch 是不损害代码性能并且有意义的,这里面肯定会隐藏着很多上述类的?try catch 代码块。

    从性能的角度来看,目前 V8 引擎确实在积极的通过 try catch 来优化这类代码片段,在以前浏览器版本中上面整个循环即使发生在 try catch 代码块内,它的速度也会变慢,因为以前浏览器版本会默认禁用 try catch 内代码的优化来方便我们调试异常。

    try catch 需要遍历某种结构来查找 catch 处理代码,并且通常以某种方式分配异常(例如:需要检查堆栈,查看堆信息,执行分支和回收堆栈)。尽管现在大部分浏览器已经优化了,我们也尽量要避免去写出上面相似的代码,比如以下代码:

    try {
        container.innerHTML = "I'm alloyteam";
    }
    catch (error) {
        // todo
    }

    上面这类代码我个人更建议写成如下形式,如果你实际上抛出并捕获了一个异常,它可能会变慢,但是由于在大多数情况下上面的代码是没有异常的,因此整体结果会比异常更快。

    这是因为代码控制流中没有分支会降低运行速度,换句话说就是这个代码执行没错误的时候,没有在 catch 中浪费你的代码执行时间,我们不应该编写过多的 try catch 这会在我们维护和检查代码的时候提升不必要的成本,有可能分散并浪费我们的注意力。

    当我们预感代码片段有可能出错,更应该是集中注意力去处理 successerror 的场景,而非使用 try catch 来保护我们的代码,更多时候 try catch 反而会让我们忽略了代码存在的致命问题。

    if (container) container.innerHTML = "I'm alloyteam";
    else // todo

    在简单代码中应当减少甚至不用 try catch ,我们可以优先考虑 if else 代替,在某些复杂不可测的代码中也应该减少 try catch(比如异步代码),我们看过很多 asyncawait 的示例代码都是结合 try catch 的,在很多性能场景下我认为它并不合理,个人觉得下面的写法应该是更干净,整洁和高效的。

    因为 JavaScript 是事件驱动的,虽然一个错误不会停止整个脚本,但如果发生任何错误,它都会出错,捕获和处理该错误几乎没有任何好处,代码主要部分中的 try catch 代码块是无法捕获事件回调中发生的错误。

    通常更合理的做法是在回调方法通过第一个参数传递错误信息,或者考虑使用 Promisereject() 来进行处理,也可以参考 node 中的常见写法如下:

    ;(async () => {
        const [err, data] = await readFile();
        if (err) {
            // todo
        };
    })()
    
    fs.readFile('<directory>', (err, data) => {
        if (err) {
            // todo
        }
    });

    结合了上面的一些分析,我自己做出一些浅显的总结:

      1. 如果我们通过完善一些测试,尽量确保不发生异常,则无需尝试使用 try catch 来捕获异常。
      1. 非异常路径不需要额外的 try catch,确保异常路径在需要考虑性能情况下优先考虑 if else,不考虑性能情况请君随意,而异步可以考虑回调函数返回 error 信息对其处理或者使用 Promse.reject()
      1. 应当适当减少?try catch 使用,也不要用它来保护我们的代码,其可读性和可维护性都不高,当你期望代码是异常时候,不满足上述1,2的情景时候可考虑使用。

    最后,笔者希望这篇文章能给到你我一些方向和启发吧,如有疏漏不妥之处,还请不吝赐教!

    附笔记链接,阅读往期更多优质文章可移步查看,希望对你有些许的帮助,你的点赞是对我最大的鼓励:

    查看原文

    Eno_Yao 发布了文章 · 9月10日

    try catch引发的性能优化深度思考

    image

    关键代码拆解成如下图所示(无关部分已省略):

    demo

    起初我认为可能是这个 getRowDataItemNumberFormat 函数里面某些方法执行太慢,从 formatData.replaceunescape(已废弃,官方建议使用 decodeURI 或者?decodeURIComponent 替代) 方法都怀疑了一遍,发现这些方法都不是该函数运行慢的原因。为了深究原因,我给 style.formatData 传入了不同的值,发现这个函数的运行效率出现不同的表现。开始有点疑惑为什么 style.formatData 的值导致这个函数的运行效率差别如此之大。

    进一步最终定位发现如果?style.formatData 为 undefined 的时候,效率骤降,如果?style.formatData 为合法的字符串的时候,效率是正常值。我开始意识到这个问题的原因在那里了,把目光转向了 try catch 代码块,这是一个很可疑的地方,在很早之前曾经听说过不合理的 try catch 是会影响性能的,但是之前从没遇到过,结合了一些资料,我发现比较少案例去探究这类代码片段的性能,我决定写代码去验证下:

    window.a = 'a';
    window.c = undefined;
    function getRowDataItemNumberFormatTryCatch() {
        console.time('getRowDataItemNumberFormatTryCatch');
        for (let i = 0; i < 3000; i++) {
            try {
                a.replace(/%022/g, '"');
            }
            catch (error) {
            }
        }
        console.timeEnd('getRowDataItemNumberFormatTryCatch');
    }

    我尝试把 try catch 放入一个 for 循环中,让它运行 3000 次,看看它的耗时为多少,我的电脑执行该代码的时间大概是 0.2 ms 左右,这是一个比较快的值,但是这里 a.replace 是正常运行的,也就是 a 是一个字符串能正常运行 replace 方法,所以这里的耗时是正常的。我对他稍微做了一下改变,如下:

    function getRowDataItemNumberFormatTryCatch2() {
        console.time('getRowDataItemNumberFormatTryCatch');
        for (let i = 0; i < 3000; i++) {
            try {
                c.replace(/%022/g, '"');
            }
            catch (error) {
            }
        }
        console.timeEnd('getRowDataItemNumberFormatTryCatch');
    }

    这段代码跟上面代码唯一的区别是,c.replace 此时应该是会报错的,因为 cundefined,这个错误会被 try catch 捕捉到,而上面的代码耗时出现了巨大的变化,上升到 40 ms,相差了将近 200 倍!并且上述代码和首图的?getRowDataItemNumberFormat 函数代码均出现了 Minor GC,注意这个 Minor GC 也是会耗时的。

    demo

    这可以解释一部分原因了,我们上面运行的代码是一个性能比较关键的部分,不应该使用 try catch 结构,因为该结构是相当独特的。与其他构造不同,它运行时会在当前作用域中创建一个新变量。每次 catch 执行该子句都会发生这种情况,将捕获的异常对象分配给一个变量。

    即使在同一作用域内,此变量也不存在于脚本的其他部分中。它在 catch 子句的开头创建,然后在子句末尾销毁。因为此变量是在运行时创建和销毁的(这些都需要额外的耗时!),并且这是 JavaScript 语言的一种特殊情况,所以某些浏览器不能非常有效地处理它,并且在捕获异常的情况下,将捕获处理程序放在性能关键的循环中可能会导致性能问题,这是我们为什么上面会出现 Minor GC 并且会有严重耗时的原因。

    如果可能,应在代码中的较高级别上进行异常处理,在这种情况下,异常处理可能不会那么频繁发生,或者可以通过首先检查是否允许所需的操作来避免。上面的 getRowDataItemNumberFormatTryCatch2 函数示例显示的循环,如果里面所需的属性不存在,则该循环可能引发多个异常,为此性能更优的写法应该如下:

    function getRowDataItemNumberFormatIf() {
        console.time('getRowDataItemNumberFormatIf');
        for (let i = 0; i < 3000; i++) {
            if (c) {
                c.replace(/%022/g, '"');
            }
        }
        console.timeEnd('getRowDataItemNumberFormatIf')
    }

    上面的这段代码语义上跟 try catch 其实是相似的,但运行效率迅速下降至 0.04ms,所以 try catch 应该通过检查属性或使用其他适当的单元测试来完全避免使用此构造,因为这些构造会极大地影响性能,因此应尽量减少使用它们。

    如果一个函数被重复调用,或者一个循环被重复求值,那么最好避免其中包含这些构造。它们最适合仅执行一次或仅执行几次且不在性能关键代码内执行的代码。尽可能将它们与其他代码隔离,以免影响其性能。

    例如,可以将它们放在顶级函数中,或者运行它们一次并存储结果,这样你以后就可以再次使用结果而不必重新运行代码。

    demo

    getRowDataItemNumberFormat 在经过上述思路改造后,运行效率得到了质的提升,在实测 300 多次循环中减少的时间如下图,足足优化了将近 2s 多的时间,如果是 3000 次的循环,那么它的优化比例会更高:

    demo
    demo

    由于上面的代码是从项目中改造出来演示的,可能并不够直观,所以我重新写了另外一个相似的例子,代码如下,这里面的逻辑和上面的?getRowDataItemNumberFormat 函数讲道理是一致的,但是我让其发生错误的时候进入 catch 逻辑执行任务。

    事实上 plus1plus2 函数的代码逻辑是一致的,只有代码语义是不相同,一个是返回 1,另一个是错误抛出1,一个求和方法在 try 片段完成,另一个求和方法再 catch 完成,我们可以粘贴这段代码在浏览器分别去掉不同的注释观察结果。

    我们发现 try 片段中的代码运行大约使用了 0.1 ms,而 catch 完成同一个求和逻辑却执行了大约 6 ms,这符合我们上面代码观察的预期,如果把计算范围继续加大,那么这个差距将会更加明显,实测如果计算 300000 次,那么将会由原来的 60 倍差距扩大到 500 倍,那就是说我们执行的 catch 次数越少折损效率越少,而如果我们执行的 catch 次数越多那么折损的效率也会越多。

    所以在不得已的情况下使用 try catch 代码块,也要尽量保证少进入到 catch 控制流分支中。

    const plus1 = () => 1;
    const plus2 = () => { throw 1 };
    console.time('sum');
    let sum = 0;
    for (let i = 0; i < 3000; i++) {
        try {
            // sum += plus1(); // 正确时候 约 0.1ms
            sum += plus2(); // 错误时候 约 6ms
        } catch (error) {
            sum += error;
        }
    }
    console.timeEnd('sum');

    上面的种种表现进一步引发了我对项目性能的一些思考,我搜了下我们这个项目至少存在 800 多个 try catch,糟糕的是我们无法保证所有的 try catch 是不损害代码性能并且有意义的,这里面肯定会隐藏着很多上述类的?try catch 代码块。

    从性能的角度来看,目前 V8 引擎确实在积极的通过 try catch 来优化这类代码片段,在以前浏览器版本中上面整个循环即使发生在 try catch 代码块内,它的速度也会变慢,因为以前浏览器版本会默认禁用 try catch 内代码的优化来方便我们调试异常。

    try catch 需要遍历某种结构来查找 catch 处理代码,并且通常以某种方式分配异常(例如:需要检查堆栈,查看堆信息,执行分支和回收堆栈)。尽管现在大部分浏览器已经优化了,我们也尽量要避免去写出上面相似的代码,比如以下代码:

    try {
        container.innerHTML = "I'm alloyteam";
    }
    catch (error) {
        // todo
    }

    上面这类代码我个人更建议写成如下形式,如果你实际上抛出并捕获了一个异常,它可能会变慢,但是由于在大多数情况下上面的代码是没有异常的,因此整体结果会比异常更快。

    这是因为代码控制流中没有分支会降低运行速度,换句话说就是这个代码执行没错误的时候,没有在 catch 中浪费你的代码执行时间,我们不应该编写过多的 try catch 这会在我们维护和检查代码的时候提升不必要的成本,有可能分散并浪费我们的注意力。

    当我们预感代码片段有可能出错,更应该是集中注意力去处理 successerror 的场景,而非使用 try catch 来保护我们的代码,更多时候 try catch 反而会让我们忽略了代码存在的致命问题。

    if (container) container.innerHTML = "I'm alloyteam";
    else // todo

    在简单代码中应当减少甚至不用 try catch ,我们可以优先考虑 if else 代替,在某些复杂不可测的代码中也应该减少 try catch(比如异步代码),我们看过很多 asyncawait 的示例代码都是结合 try catch 的,在很多性能场景下我认为它并不合理,个人觉得下面的写法应该是更干净,整洁和高效的。

    因为 JavaScript 是事件驱动的,虽然一个错误不会停止整个脚本,但如果发生任何错误,它都会出错,捕获和处理该错误几乎没有任何好处,代码主要部分中的 try catch 代码块是无法捕获事件回调中发生的错误。

    通常更合理的做法是在回调方法通过第一个参数传递错误信息,或者考虑使用 Promisereject() 来进行处理,也可以参考 node 中的常见写法如下:

    ;(async () => {
        const [err, data] = await readFile();
        if (err) {
            // todo
        };
    })()
    
    fs.readFile('<directory>', (err, data) => {
        if (err) {
            // todo
        }
    });

    结合了上面的一些分析,我自己做出一些浅显的总结:

      1. 如果我们通过完善一些测试,尽量确保不发生异常,则无需尝试使用 try catch 来捕获异常。
      1. 非异常路径不需要额外的 try catch,确保异常路径在需要考虑性能情况下优先考虑 if else,不考虑性能情况请君随意,而异步可以考虑回调函数返回 error 信息对其处理或者使用 Promse.reject()
      1. 应当适当减少?try catch 使用,也不要用它来保护我们的代码,其可读性和可维护性都不高,当你期望代码是异常时候,不满足上述1,2的情景时候可考虑使用。

    最后,笔者希望这篇文章能给到你我一些方向和启发吧,如有疏漏不妥之处,还请不吝赐教!

    附笔记链接,阅读往期更多优质文章可移步查看,希望对你有些许的帮助,你的点赞是对我最大的鼓励:

    查看原文

    赞 18 收藏 10 评论 4

    Eno_Yao 收藏了文章 · 5月14日

    12道MySQL常见的面试题

    photo-1589064090574-7be967916250.jpeg

    原文: https://github.com/lvCmx/study

    有关事务的面试题

    (1) 事务的特性

    • 原子性:是指事务包含所有操作要么全部成功,要么全部失败回滚。
    • 一致性:指事务必须使数据库从一个一致性状态变换成另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
      拿转账来说,假设用户 A 和用户 B 两者的钱加起来一共是 5000,那么不管 A 和 B 之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是 5000,这就是事务的一致性。
    • 隔离性:是当多个用户并发访问数据库时,比如操作同一张表时,数据表为每个用户开启的事务,不能被其他事务所干扰,多个并发事务之间要相互隔离。
    • 持久性:持久性是指一个事务一旦被提交,那么对数据库中的数据的改变就是永久的,即便是在数据库系统遇到故障的性况下也不会丢失提交事务的操作。

    (2) 并发操作问题

    • 脏读:脏读是指在一个事务处理过程中读取到了另外一个未提交事务中的数据。
    • 不可重复读:不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
    • 虚读(幻读):幻读发生在当两个完全相同的查询执行时,第二次查询所返回的结果集跟第一个查询不相同。
      比如两个事务操作,A 事务查询状态为 1 的记录时,这时 B 事务插入了一条状态为 1 的记录,A 事务再次查询返回的结果不一样。

    (3) 事务的隔离级别

    • Serializable(串行化):可避免脏读、不可重复读、幻读。(就是串行化读数据)
    • Repeatable read(可重复读):可避免脏读、不可重复读的发生。
    • Read committed(读已提交):可避免脏读的发生。
    • Read uncommitted(读未提交):最低级别,任何情况都无法保证。

    在 MySQL 数据库中,支持上面四种隔离级别,默认的为 Repeatable read (可重复读);而在 Oracle 数据库中,只支持 Serializable (串行化)级别和 Read committed (读已提交)这两种级别,其中默认的为 Read committed 级别。

    数据库索引

    索引是表的目录,在查找内容之前可以先在目录中查找索引位置,以此快速定位查询数据。对于索引,会保存在额外的文件中。
    索引是数据库中专门用于帮助用户快速查询数据的一种数据结构,类似于字典中的目录,查找字典内容时可以根据目录查找到数据的存放位置,然后直接获取即可。

    • 索引由数据库中一列或多列组合而成,其作用是提高对表中数据的查询速度。
    • 索引的优点是可以提高检索数据的速度。
    • 索引的缺点是创建和维护索引需要耗费时间。
    • 索引可以提高查询速度,会减慢写入速度。

    索引本身也很大,不可能全部存储在内存中,因此索引往往索引文件的形式存储在磁盘上。索引查找过程中就要产生磁盘 IO 消耗,相对于内存存取,IO 存取的消耗要高几个数量级。所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘 I/O 操作次数的渐进复杂度。(换句话说,索引的结构组织要尽量减少查找过程中磁盘 I/O 的存取次数。)

    (1) 索引的种类

    ? B-tree、Hash、R-Tree、全文索引、主键索引、普通索引等。在 mysql 中索引是在存储引擎层而不是服务器层实现的,所以没有统一的索引标准。

    • 普通索引:仅加速查询
    • 唯一索引:加速查询+列值唯一。(可以有 null)
    • 主键索引:加速查询+列值唯一+表中只有一个(不可以有 null)
    • 组合索引:多列值组成一个索引。专门用于组合搜索,其效率大于索引合并。
    • 全文索引:对文本的内容进行分词,进行搜索。

    B-Tree 索引:

    ? B-Tree 不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少,指针少的情况下要保存大量数据,只能增加树的高度,导致 IO 操作变多,查询性能变低。而每一个页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(一个页)能存储的 key 的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 IO 次数,进而影响查询效率。

    ? 在 B+Tree 中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只能存储 key 值信息,这样可以大大加大每个节点存储的 key 值数量,降低 B+Tree 的高度。

    ? 数据库中的 B+Tree 索引可以分为聚集索引(clustered index)和辅助索引(secondary index)。上面的 B+Tree 示例图在数据库中的实现即为聚集索引,聚集索引的 B+Tree 中的叶子节点存放的是整张表的行记录数据。辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的聚集索引键,即主键。当通过辅助索引来查询数据时,InnoDB 存储引擎会遍历辅助索引找到主键,然后再通过主键在聚集索引中找到完整的行记录数据。

    hash 索引

    在进行查询操作时,使用 hash 索引效率很高。因此,当使用一个语句去比较字符中,然后返回结果集。这样的操作使用 hash 索引是很快的。

    在字符串上创建 Hash 索引非常好,列值将插入到 Hash 表中和一个键对应,并和实际的数据行有一个映射关系,也就是该键是一个指向表中数据行的指针。Hash 表实际是基于关联数组,假如有一个这样的语句:“boyce = 0×28936”其中 0×28936 是关联到存储在内存中的 boyce。在 hash 表索引中查找 boyce 的值并返回内存中的数据,要比检索整个表的列值要快得多。

    Hash 表不能进行排序的数据结构,Hash 表擅长的是键值对,也就是说,检索语句检查相等性。在 Hash 表中键值是没有排序的,在存储的时候也没有任何的排序规则。因为 hash 索引不够灵活,所以,hash 索引不是默认的索引的数据结构。

    • 不是按照索引顺序存储,不能用于排序。
    • 不支持部分索引列匹配查找。
    • 支持等值查询,不支持范围查询。
    • 哈希值冲突多时,不适用。

      全文索引

    ? 全文索引是一种特殊类型的索引,它查找的是文本中的关键词,而不是直接比较索引中的值。全文索引和其他几种索引的匹配方式完全不一样,它更类似于搜索引擎做的事情,而不是简单的 where 条件匹配。可以在相同的列上,同时创建全文索引和 B-Tree 索引,全文索引适用于 Match Against 操作,而不是普通的 where 条件操作。

    ? 索引可以包含一个列(即字段)或多个列的值。如果索引包含多个列,一般会将其称作复合索引,此时,列的顺序就十分重要,因为 MySQL 只能高效的使用索引的最左前缀列。创建一个包含两个列的索引,和创建两个只包含一列的索引是大不相同的。

    (2) B+Tree 索引

    在 B+Tree 的每个叶子节点增加一个指向相邻叶子节点的指针,就形成了带有顺序访问指针的 B+Tree。做这个优化的目的是为了提高区间访问的性能,例如图中如果要查询 key 为从 18 到 49 的所有数据记录,当找到 18 后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提到了区间查询效率。

    • B+Tree 它的非叶子节点不存储数据,只存储索引,而数据会存放在叶子节点中。
    • B+Tree 规定:所有的叶子结点包含了全部关键字信息,以及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。

    在 InnoDB 存储引擎中,数据存放的方式是以页的方式进行存放,计算机在存储数据的时候,有最小存储单元,就是最小数据扇区,一个扇区的大小是 512 字节,而文件系统(ext4)他的最小单元是块,一个块的大小是 4k,而对于我们的 InnoDB 存储引擎也有自己的最小存储单元--页。一个页的大小是 16K。在 InnoDB 中默认的数据页的大小是 16K。

    数据表中的数据都是存储在页中的,所以一个页中能存储多少行数据呢?假设一个数据的大小是 1K,那么一个页可以存放 16 行这样的数据。

    • InnoDB 存储引擎的最小存储单元是页,页可以用于存放数据也可以用于存放键值+指针,在 B+树中叶子节点存放数据,非叶子节点存放键值+指针
    • 索引组织表通过非叶子节点的二分查找法以及指针确定数据在哪个页中,进页在云数据页中查找到需要的数据。

    例如:
    假设 B+树高为 2,即存在一个根节点和若干个叶子节点,那么这棵 B+树的存放总记录数为:根节点指针数*单个叶子节点记录行数。
    假设一行数据大小为:1KB,那么一页(16KB)中可以存放 16 行数据。
    假设主键 ID 为 bigint 类型,长度为 8 字节,而指针大小在 InnoDB 源码中设置为 6 字节,这样一共 14 个字节。(8+6 的意思是 B+Tree 有 key 和指针)
    16KB * 1024(化成字节) / 14 =1170(一个节点可以存放页的指针数据)
    那么可以算出一棵高度为 2 的 B+树,能存放 1170*16=18720 行 解释:1170 表示一个节点能够创建的指针数,而 16 表示,一个存放数据的页可以存放 16 行数据。*
    3 阶 B+树可以存放 1170117016=2 千万(左右)
    所在在 InnoDb 中 B+树高度一般为 1-3 层,它就能满足千万级的数据存储,在查找数据时一次页的查找代表一次 IO,所以通过主键索引查询通常只需要 1-3 次 IO 操作即可查找到数据。

    在没有加数据记录大小的情况下:
    InnoDB 存储引擎中页的大小为 16KB,一般表的主键类型为 INT(占用 4 个字节)或 BIGINT(占用 8 个字节),指针类型也一般为 4 或 8 个字节,也就是说一个页(B+Tree 中的一个节点)中大概存储 16KB/(8B+8B)=1K 个键值(因为是估值,为方便计算,这里的 K 取值为〖10〗^3)。也就是说一个深度为 3 的 B+Tree 索引可以维护 10^3 10^3 10^3 = 10 亿 条记录。

    (3) InnoDB 索引实现

    InnoDB 的数据文件本身就是索引文件,InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这棵树的叶节点 data 域保存了完整的数据记录,这个索引的 key 是数据表的主键。InnoDB 表都必须有主键,如果没有定义主键,会默认添加一个主键。

    一级索引(主键):

    • 如果一个主键被定义了,那么这个主键就是作为聚焦索引。
    • 如果没有主键被定义,那么该表的第一个唯一非空索引被作为聚焦索引。
    • 如果没有主键也没有合适的唯一索引,那么 InnoDB 内部会生成一个隐藏的主键作为聚焦索引,这个隐藏的主键是一个 6 个字节的列,该列的值会随着数据的插入自增。

    二级索引:

    • 以所在的列建立二级索引,然后二级索引的 data 域则为主键。

    (4) MyISAM 索引实现

    MyISAM 索引文件和数据文件是分离的,索引文件仅保存记录所在页的指针,通过这些地址来读取页,进而读取被索引的行。

    (5) InnoDB 与 MyISAM 在 B+Tree 索引的区别

    第一重大的区别是 InnoDB 的数据文件本身就是索引文件,MyISAM 索引文件和数据文件是分离的,索引文件仅仅保存数据记录的地址,而在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构。这棵树的叶节点 data 域保存了完整的数据记录,这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。因为 InnoDB 的数据文件本身要按主键聚集,所以 InnoDB 要求表必须有主键(MyISAM 可以没有)如果没有显示指定,则 MySQL 系统会自动选择一个可以唯一标识数据的列作为主键,如果不存在这种列,则 MySQL 自动为 InnoDB 表生成一个隐含字段作为主键,这个字段长度为 6 个字节,类型为长整型。

    第二个与 MyISAM 索引的不同是 InnoDB 的辅助索引 DATA 域存储相应记录主键的值而不是地址,InnoDB 的所有辅助索引都引用主键作为 data 域。

    聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。

    (6) 有一道 MySQL 的面试题,为什么 MySQL 的索引要使用 B+树而不是其它树形结构?比如 B 树?

    因为 B 树不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致 IO 操作变多,查询性能变低;红黑树 BST 的时间复杂度是依赖于树的高度,但是,红黑树的高度与 Btree 相比,高度更大。

    (7) 聚集索引与非聚集索引

    聚集索引:也叫聚簇索引,聚集索引表记录的排列顺序和索引的排列顺序一致,所以查询效率快,只要找到第一个索引值索引,其余就连续性记录在物理也一样连续存放,聚集索引对应的缺点就是修改慢,因为为了保证表中记录的物理和索引顺序一致,在记录插入的时候会对数据页重新排序(InnoDB 的 B+树)。

    非聚集索引制定了表中记录的逻辑顺序,但是记录的物理和索引不一定一致,两种索引都采用 B+Tree 结构,非聚集索引的叶子层并不和实际数据页相重叠,而采用叶子层包含一个指向表中的记录在数据页中的指针方式,非聚集索引层次多,不会造成数据重排。

    Mysql 存储引擎

    在 Mysql 将每个数据库(Schema)保存为数据目录下的一个子目录。创建表时,Mysql 会在数据库子目录下创建一个和表同名的.frm 文件保存表的定义。

    有关:show table status like 'user'查看 user 表的相关信息:

    (1) InnoDB 存储引擎:Mysql 的默认事务型引擎

    InnoDB 使用的是行级锁,但实际是有限制的,只有在你增删改查时匹配的条件字段带有索引时,InnoDB 才会使用行级锁,在你增删改查时匹配的条件字段不带有索引时。InnoDB 使用的将是表级锁。

    InnoDB 是新版本 mysql 的默认引擎,支持事务处理和外键,但是其缺点就是慢了些,存储方式分为两种

    1、共享表空间存储。这种方式创建的表的表结构保存在.frm 文件中,数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中,可以是多个文件。

    2、多表空间存储。(.frm 表结构和 idb 数据。) 这种方式创建的表的表结构仍然保存在.frm 文件中,但是每个表的数据和索引单独保存在.ibd 中。如果是个分区表,则每个分区对应单独的.ibd 文件,文件名是“表名+分区名”,可以在创建分区的时候指定每个分区的数据文件的位置,以此来将表的 IO 均匀分布在多个磁盘上。

    使用 engine=innodb default charset=utf-8;

    (2) MyISAM

    MyISAM 存储引擎是旧版本 mysql 的默认引擎,现在默认引擎是 InnoDB,MyISAM 引擎的主要特点就是快,没有事务处理操作,也不支持外键操作。适合 insert 与 select 的操作表。MyISAM 存储引擎的表在数据库中,每一个表都被存放为三个以表名命名的物理文件。定义表结构.frm,存放表数据.myd 和索引数据.myi。使用方法:engine=myisam default charset=utf-8;

    其中 MyISAM 支持以下三种类型的索引;

    • B-Tree 索引,就是所有的索引节点都按照 B-Tree 的数据结构来存储,所有的索引数据节点都在叶节点。
    • R-Tree 索引
    • Full-Text 索引,全文索引,它的存储结构也是 B-Tree。主要是为了解决在我们需要用 like 查询的低效问题。

    (3) MEMORY

    存储引擎使用存在内存中的内容来创建表,每个 Memory 表只实现对应一个磁盘文件,格式是.frm(只保存表结构,不保存内容)。Memory 类型的表访问非常快,因为它的数据是放在内存中的,并且默认使用 Hash 索引,但是一旦服务关闭,表中的数据就会丢失掉。

    CREATE TABLE tab_memory ENGINE=MEMORY

    给 Memory 表创建索引的时候,可以指定使用 Hash 索引还是 BTree 索引。

    Create index mem_hash using HASH on table_memory(city_id);

    (4) InnoDB 与 MyISAM 区别

    InnoDB 和 MyISAM 是许多人在使用 MySQL 时最常用的两个表类型,MyISAM 不支持事务处理等高级处理,而 InnoDB 类型支持。MyISAM 类型的强调的是性能。其执行速度比 InnoDB 类型更快,但是不提供事务类型支持。而 InnoDB 提供事务支持和外键。

    联合索引

    把联合索引单独拿出来是因为去滴滴面试的时候被问到过。

    (1) 联合索引的规则

    联合索引可以将多个字段组合创建一个索引,在查询 where 条件中使用组合索引时,它符合最左匹配规则。例如为 A,B,C 三列创建索引,则它支持 A/A,B/A,B,C 而 B,C 则无法使用组合索引。

    当一个列存在联合索引和单列索引时,mysql 会根据查询语句的成本来选择走哪条索引。

    (2) 查辅助索引时需要查几次索引

    需要两次:在前面的索引一节我们详细的介绍过辅助索引的结构,辅助索引的 B+Tree 结点的叶子结点存放的是一级索引的值,所以查到一级索引时,它会再查询一次一级索引。

    Mysql 读写分离

    (1)? 应用场景

    读写分离主要解决的是数据库的写操作是比较耗时的,而数据库的读取则是非常快的,所以读写分离来解决数据库的写入,影响了查询的效率。

    读写分离的好处

    1. 分摊服务器压力,提高机器的系统处理效率。读写分离适用于读远比写的场景,如果有一台服务器,当 select 很多时,update 和 delete 会被这些 select 访问中的数据堵塞,等待 select 结束,并发性能并不高,而主从只负责各自的写和读,极大程度的缓解 X 锁和 S 锁争用;
    2. 增加冗余,提高服务可用性,当一台数据库服务器宕机后可以调整另外一台从库以最快速度恢复服务

    (2) 原理分析

    主库将插入、更新或删除的记录写入到了 binlog 日志,然后从库连接到主库之后,从库有一个 IO 线程,将主库的 binlog 日志拷贝到自己本地,写入一个中继日志中。接着从库中有一个 SQL 线程会从中继日志读取 binlog,然后执行 binlog 日志中的内容,也就是在自己本地再次执行一遍 SQL,这样就可以保证自己跟主库的数据是一样的。

    这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行 SQL 的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。

    而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。

    mysql 半同步复制:

    这个所谓半同步复制,semi-sync 复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。

    并行复制

    所谓并行复制,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

    (3) 延迟问题解决方案

    在 Master 上增加一个自增表,这个表仅含有 1 个字段,当 master 接收到任何数据更新的请求时,均会触发这个触发器,该触发器更新自增表中的记录。

    MySQL_Poxy 解决方案:

    写数据时:由于 Count_table 也参与 Mysq 的主从同步,因此在 Master 上作的 Update 更新也会同步到 Slave 上。当 Client 通过 Proxy 进行数据读取时,Proxy 可以先向 Master 和 Slave 的 Count_table 表发送查询请求,当二者的数据相同时,Proxy 可以认定 Master 和 Slave 的数据状态是一致的,然后把 select 请求发送到 Slave 服务器上,否则就发送到 Master 上。如下图所示:

    读数据时:通过这种方式,就可以比较完美的结果 MySQL 的同步延迟不可控问题。之所以所“比较完美”,是因为这种方案 double 了查询请求,对 Master 和 Slave 构成了额外的压力。不过由于 Proxy 与真实的 Mysql Server 采用连接池的方式连接,因此额外的压力还是可以接受的

    Mysql 分库分表

    (1)? 为什么分库分表

    ? 比如一个项目单表都几千万数据了,mysql 数据库已经抗不住了,单表数据量太大,会极大影响你的 sql 执行的性能,会发遭受 sql 可能就跑的越来越慢。
    ? 分表就是把一个表的数据放到多个表中,然后项目查询数据的时候只查询一个表,比如按照用户 id 来分表,将一个用户的数据就放在一个用中。这样可以控制每个表的数据量在可控的范围内,比如每个表固定在 200 万以内。
    ? 分库就是你一个库最多支撑并发 2000,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大,那么你可以将一个库的数据拆分到多个库中,访问的时候访问一个库就好了。

    (2) 分库分表中间件

    数据库分库/分表中间件有两类:一类是 client 层,也就是直接在系统中使用的,另一类是 proxy 层,需要单独部署这种中间件。

    • sharding-jdbc 这种 client 层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合 sharding-jdbc 的依赖;
    • mycat 这种 proxy 层方案的缺点在于需要部署,自己及运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。

    (3)? 垂直拆分与水平拆分

    ? 水平拆分:就是把一个表的数据给拆分到多个库的多个表里面去,但是每个库的表结构都一样,只不过每个库下表放的数据是不同的,所有的不同库的表数据加起来就是全部数据,水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。
    ? 垂直拆分:就是把一个有很多字段的表给拆分成多个表,或者是多个库上去,每个库表的结构都不一样,每个库都都包含部分字段。一般来说会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。

    ? 你就得考虑一下,你的项目里该如何分库分表?一般来说,垂直拆分,你可以在表层面来做,对一些字段特别多的表做一下拆分;水平拆分,你可以说是并发承载不了,或者是数据量太大,容量承载不了,你给拆了,按什么字段来拆,你自己想好;分表,你考虑一下,你如果哪怕是拆到每个库里去,并发和容量都 ok 了,但是每个库的表还是太大了,那么你就分表,将这个表分开,保证每个表的数据量并不是很大。
    ? 而且这儿还有两种分库分表的方式,一种是按照 range 来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了;或者是按照某个字段 hash 一下均匀分散,这个较为常用。
    ? range 来分,好处在于说,后面扩容的时候,就很容易,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景,你的用户不是仅仅访问最新的数据,而是均匀的访问现在的数据以及历史的数据
    ? hash 分法,好处在于说,可以平均分配没给库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的这么一个过程。

    把系统从未分库分表动态切换到分库分表上

    (1)? 停机迁移方案

    停机迁移方案中,就是把系统在凌晨 12 点开始运维,系统停掉,然后提前写好一个导数据的一次性工具,此时直接跑起来,然后将单库单表的数据写到分库分表里面去。 导入数据完成了之后,修改系统的数据库连接配置,包括可能代码和 SQL 也许有修改,那你就用最新的代码,然后直接启动连到新的分库分表上去。

    (2)? 双写迁移方案

    此方案不用停机,比较常用。 简单来说,就是在线上系统里面,之前所有写库的地方,增删改操作,都除了对老库增删改,都加上对新库的增删改,这就是所谓的双写。同时写两个库,老库和新库。然后系统部署之后,新库数据差太远,用之前说的导数工具,路起来读老库数据写新库,写的时候要根据 gmt_modified 这类判断这条数据最后修改时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。

    8.分库分表之后生成全局唯一 ID

    数据库经过分库分表之后,系统在向数据库中插入数据的时候,需要生成一个唯一的数据库 id,它在分库分表中必须是全局唯一的。常用的生成方法如下:

    (1) 数据库自增主键

    在数据库中单独创建一张表,这张表只包含一个字段 id,每次要向分库分表中插入数据时,先从这张表中获取一个 id。
    缺点:这张生成 id 的表是单库单表的,要是高并发的话,就会产生瓶颈。
    适合的场景:你分库分表就俩原因,要不就是单库并发太高,要不就是单库数据量太大;除非是你并发不高,但是数据量太大导致的分库分表扩容,你可以用这个方案,因为可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。

    (2)?UUID

    好处就是本地生成,不需要基于数据库,不好之处就是,UUID 太长了,并且是字符串,作为主键性能太差了,不适合用于主键。
    适合的场景:如果你是要随机生成个什么文件名了,编号之类的,你可以用 uuid,但是作为主键是不能用 uuid 的。

    (3)? 获取系统当前时间

    这个就是获取当前时间即可,但是问题是,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的,基本就不用考虑了。
    适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一 id,如果业务上你觉得可以接受,那么是可以的,你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号,订单编号啊:时间戳 + 用户 id + 业务含义编码。

    (4) redis 生成唯一 ID

    redis 是部署在数据库集群之外,它不依赖于数据库,使用 redis 能够生成唯一的 id 这主要依赖于 Redis 是单线程的,所以也可以用生成全局唯一的 ID,可以用 redis 的原子操作 incr 和 incrby 来实现。
    也可以使用 redis 集群来获取更高的吞吐量,假如一个集群中有 5 台 Redis,可以初始化每台 Redis 的值分别是 1,2,3,4,5。然后步长都是 5,各个 redis 生成的 ID 为:
    A:1,6,11,16,21
    B:2,7,12,17,22
    C:3,8,13,18,23
    D:4,9,14,19,24
    E:5,10,15,20,25

    (5) snowflake 算法

    twitter 开源的分布式 id 生成算法,就是把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号

    • 1 bit:不用,为啥呢?因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0
    • 41 bit:表示的是时间戳,单位是毫秒。41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值,换算成年就是表示 69 年的时间。
    • 10 bit:记录工作机器 id,代表的是这个服务最多可以部署在 2^10 台机器上哪,也就是 1024 台机器。但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2 ^ 5 个机房(32 个机房),每个机房里可以代表 2 ^ 5 个机器(32 台机器)。
    • 12 bit:这个是用来记录同一个毫秒内产生的不同 id,12 bit 可以代表的最大正整数是 2 ^ 12 - 1 = 4096,也就是说可以用这个 12bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id

    例如:0 1011100111101101000110100010111 10001 00001 000000000001
    第 1 位 0:表示正数
    接着 41 位表示:1559661847--> 2019-6-4 23:24:7
    接着 5 位表示:17 机房
    接着 5 位表示:17 机房下的第 1 台机器
    最后 12 位表示:当前时间戳下并发的自增编号。

    参考代码:

    public class IdWorker{
        private long workerId;
        private long datacenterId;
        private long sequence;
    
        private long twepoch = 1288834974657L;
    
        private long workerIdBits = 5L;
        private long datacenterIdBits = 5L;
        private long maxWorkerId = -1L ^ (-1L << workerIdBits); // 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
        private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内
        private long sequenceBits = 12L;
    
        private long workerIdShift = sequenceBits;
        private long datacenterIdShift = sequenceBits + workerIdBits;
        private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
        private long sequenceMask = -1L ^ (-1L << sequenceBits);
    
        private long lastTimestamp = -1L;
    
        public IdWorker(long workerId, long datacenterId, long sequence){
            // 这儿不就检查了一下,要求就是你传递进来的机房id和机器id不能超过32,不能小于0
            if (workerId > maxWorkerId || workerId < 0) {
                throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
            }
            if (datacenterId > maxDatacenterId || datacenterId < 0) {
                throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
            }
            System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                    timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
    
            this.workerId = workerId;
            this.datacenterId = datacenterId;
            this.sequence = sequence;
        }
    
        public synchronized long nextId() {
        // 这儿就是获取当前时间戳,单位是毫秒
            long timestamp = timeGen();
    
            if (timestamp < lastTimestamp) {
                System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds",
                        lastTimestamp - timestamp));
            }
    
            // 在同一个毫秒内,又发送了一个请求生成一个id,0 -> 1
            if (lastTimestamp == timestamp) {
                sequence = (sequence + 1) & sequenceMask; // 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
                if (sequence == 0) {
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0;
            }
            // 这儿记录一下最近一次生成id的时间戳,单位是毫秒
            lastTimestamp = timestamp;
             // 这儿就是将时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后10 bit;最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型
            return ((timestamp - twepoch) << timestampLeftShift) |
                    (datacenterId << datacenterIdShift) |
                    (workerId << workerIdShift) |
                    sequence;
        }
        private long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }
        private long timeGen(){
            return System.currentTimeMillis();
        }
        public long getWorkerId(){
            return workerId;
        }
        public long getDatacenterId(){
            return datacenterId;
        }
        public long getTimestamp(){
            return System.currentTimeMillis();
        }
        //---------------测试---------------
        public static void main(String[] args) {
            IdWorker worker = new IdWorker(1,1,1);
            for (int i = 0; i < 30; i++) {
                System.out.println(worker.nextId());
            }
        }
    }
    查看原文

    Eno_Yao 回答了问题 · 5月13日

    解决react多次setState会对性能有影响吗?

    会的,可以做以下措施来优化:

    1. 合并相似的 setState 代码(不过其实 setState(null) 是不会触发 render 的)
    2. 增加一些比对的条件来优化 render 函数,减少多余的渲染逻辑
    3. 利用 shouldComponentUpdate 或者 PureComponent 来减少因父组件更新而触发子组件的 render
    4. 使用 React.memo 用来缓存组件的渲染(与 PureComponent 十分类似,但不同的是,React.memo 只能用于函数组件)
    5. 适当拆分组件其实也可以,因为粒度越小,就越小范围去 render 和 diff,也能提高性能

    关注 2 回答 2

    Eno_Yao 收藏了文章 · 5月13日

    36个工作中常用的JavaScript函数片段

    photo-1589127383386-270465a13dea.jpeg
    如果文章和笔记能带您一丝帮助或者启发,请不要吝啬你的赞和收藏,你的肯定是我前进的最大动力??

    数组 Array

    数组去重

    function noRepeat(arr) {
      return [...new Set(arr)];
    }

    查找数组最大

    function arrayMax(arr) {
      return Math.max(...arr);
    }

    查找数组最小

    function arrayMin(arr) {
      return Math.min(...arr);
    }

    返回已 size 为长度的数组分割的原数组

    function chunk(arr, size = 1) {
      return Array.from(
        {
          length: Math.ceil(arr.length / size),
        },
        (v, i) => arr.slice(i * size, i * size + size)
      );
    }

    检查数组中某元素出现的次数

    function countOccurrences(arr, value) {
      return arr.reduce((a, v) => (v === value ? a + 1 : a + 0), 0);
    }

    扁平化数组

    • 默认 depth 全部展开
    function flatten(arr, depth = -1) {
      if (depth === -1) {
        return [].concat(
          ...arr.map((v) => (Array.isArray(v) ? this.flatten(v) : v))
        );
      }
      if (depth === 1) {
        return arr.reduce((a, v) => a.concat(v), []);
      }
      return arr.reduce(
        (a, v) => a.concat(Array.isArray(v) ? this.flatten(v, depth - 1) : v),
        []
      );
    }

    对比两个数组并且返回其中不同的元素

    function diffrence(arrA, arrB) {
      return arrA.filter((v) => !arrB.includes(v));
    }

    返回两个数组中相同的元素

    function intersection(arr1, arr2) {
      return arr2.filter((v) => arr1.includes(v));
    }

    从右删除 n 个元素

    function dropRight(arr, n = 0) {
      return n < arr.length ? arr.slice(0, arr.length - n) : [];
    }

    截取第一个符合条件的元素及其以后的元素

    function dropElements(arr, fn) {
      while (arr.length && !fn(arr[0])) arr = arr.slice(1);
      return arr;
    }

    返回数组中下标间隔 nth 的元素

    function everyNth(arr, nth) {
      return arr.filter((v, i) => i % nth === nth - 1);
    }

    返回数组中第 n 个元素

    • 支持负数
    function nthElement(arr, n = 0) {
      return (n >= 0 ? arr.slice(n, n + 1) : arr.slice(n))[0];
    }

    返回数组头元素

    function head(arr) {
      return arr[0];
    }

    返回数组末尾元素

    function last(arr) {
      return arr[arr.length - 1];
    }

    数组乱排

    function shuffle(arr) {
      let array = arr;
      let index = array.length;
    
      while (index) {
        index -= 1;
        let randomInedx = Math.floor(Math.random() * index);
        let middleware = array[index];
        array[index] = array[randomInedx];
        array[randomInedx] = middleware;
      }
    
      return array;
    }

    浏览器对象 BOM

    判读浏览器是否支持 CSS 属性

    /**
     * 告知浏览器支持的指定css属性情况
     * @param {String} key - css属性,是属性的名字,不需要加前缀
     * @returns {String} - 支持的属性情况
     */
    function validateCssKey(key) {
      const jsKey = toCamelCase(key); // 有些css属性是连字符号形成
      if (jsKey in document.documentElement.style) {
        return key;
      }
      let validKey = "";
      // 属性名为前缀在js中的形式,属性值是前缀在css中的形式
      // 经尝试,Webkit 也可是首字母小写 webkit
      const prefixMap = {
        Webkit: "-webkit-",
        Moz: "-moz-",
        ms: "-ms-",
        O: "-o-",
      };
      for (const jsPrefix in prefixMap) {
        const styleKey = toCamelCase(`${jsPrefix}-${jsKey}`);
        if (styleKey in document.documentElement.style) {
          validKey = prefixMap[jsPrefix] + key;
          break;
        }
      }
      return validKey;
    }
    
    /**
     * 把有连字符号的字符串转化为驼峰命名法的字符串
     */
    function toCamelCase(value) {
      return value.replace(/-(\w)/g, (matched, letter) => {
        return letter.toUpperCase();
      });
    }
    
    /**
     * 检查浏览器是否支持某个css属性值(es6版)
     * @param {String} key - 检查的属性值所属的css属性名
     * @param {String} value - 要检查的css属性值(不要带前缀)
     * @returns {String} - 返回浏览器支持的属性值
     */
    function valiateCssValue(key, value) {
      const prefix = ["-o-", "-ms-", "-moz-", "-webkit-", ""];
      const prefixValue = prefix.map((item) => {
        return item + value;
      });
      const element = document.createElement("div");
      const eleStyle = element.style;
      // 应用每个前缀的情况,且最后也要应用上没有前缀的情况,看最后浏览器起效的何种情况
      // 这就是最好在prefix里的最后一个元素是''
      prefixValue.forEach((item) => {
        eleStyle[key] = item;
      });
      return eleStyle[key];
    }
    
    /**
     * 检查浏览器是否支持某个css属性值
     * @param {String} key - 检查的属性值所属的css属性名
     * @param {String} value - 要检查的css属性值(不要带前缀)
     * @returns {String} - 返回浏览器支持的属性值
     */
    function valiateCssValue(key, value) {
      var prefix = ["-o-", "-ms-", "-moz-", "-webkit-", ""];
      var prefixValue = [];
      for (var i = 0; i < prefix.length; i++) {
        prefixValue.push(prefix[i] + value);
      }
      var element = document.createElement("div");
      var eleStyle = element.style;
      for (var j = 0; j < prefixValue.length; j++) {
        eleStyle[key] = prefixValue[j];
      }
      return eleStyle[key];
    }
    
    function validCss(key, value) {
      const validCss = validateCssKey(key);
      if (validCss) {
        return validCss;
      }
      return valiateCssValue(key, value);
    }

    返回当前网页地址

    function currentURL() {
      return window.location.href;
    }

    获取滚动条位置

    function getScrollPosition(el = window) {
      return {
        x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft,
        y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop,
      };
    }

    获取 url 中的参数

    function getURLParameters(url) {
      return url
        .match(/([^?=&]+)(=([^&]*))/g)
        .reduce(
          (a, v) => (
            (a[v.slice(0, v.indexOf("="))] = v.slice(v.indexOf("=") + 1)), a
          ),
          {}
        );
    }

    页面跳转,是否记录在 history 中

    function redirect(url, asLink = true) {
      asLink ? (window.location.href = url) : window.location.replace(url);
    }

    滚动条回到顶部动画

    function scrollToTop() {
      const scrollTop =
        document.documentElement.scrollTop || document.body.scrollTop;
      if (scrollTop > 0) {
        window.requestAnimationFrame(scrollToTop);
        window.scrollTo(0, c - c / 8);
      } else {
        window.cancelAnimationFrame(scrollToTop);
      }
    }

    复制文本

    function copy(str) {
      const el = document.createElement("textarea");
      el.value = str;
      el.setAttribute("readonly", "");
      el.style.position = "absolute";
      el.style.left = "-9999px";
      el.style.top = "-9999px";
      document.body.appendChild(el);
      const selected =
        document.getSelection().rangeCount > 0
          ? document.getSelection().getRangeAt(0)
          : false;
      el.select();
      document.execCommand("copy");
      document.body.removeChild(el);
      if (selected) {
        document.getSelection().removeAllRanges();
        document.getSelection().addRange(selected);
      }
    }

    检测设备类型

    function detectDeviceType() {
      return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
        navigator.userAgent
      )
        ? "Mobile"
        : "Desktop";
    }

    Cookie

    function setCookie(key, value, expiredays) {
      var exdate = new Date();
      exdate.setDate(exdate.getDate() + expiredays);
      document.cookie =
        key +
        "=" +
        escape(value) +
        (expiredays == null ? "" : ";expires=" + exdate.toGMTString());
    }

    function delCookie(name) {
      var exp = new Date();
      exp.setTime(exp.getTime() - 1);
      var cval = getCookie(name);
      if (cval != null) {
        document.cookie = name + "=" + cval + ";expires=" + exp.toGMTString();
      }
    }

    function getCookie(name) {
      var arr,
        reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
      if ((arr = document.cookie.match(reg))) {
        return arr[2];
      } else {
        return null;
      }
    }

    日期 Date

    时间戳转换为时间

    • 默认为当前时间转换结果
    • isMs 为时间戳是否为毫秒
    function timestampToTime(timestamp = Date.parse(new Date()), isMs = true) {
      const date = new Date(timestamp * (isMs ? 1 : 1000));
      return `${date.getFullYear()}-${
        date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1
      }-${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
    }

    ? 文档对象 DOM

    固定滚动条

    /**
     * 功能描述:一些业务场景,如弹框出现时,需要禁止页面滚动,这是兼容安卓和 iOS 禁止页面滚动的解决方案
     */
    
    let scrollTop = 0;
    
    function preventScroll() {
      // 存储当前滚动位置
      scrollTop = window.scrollY;
    
      // 将可滚动区域固定定位,可滚动区域高度为 0 后就不能滚动了
      document.body.style["overflow-y"] = "hidden";
      document.body.style.position = "fixed";
      document.body.style.width = "100%";
      document.body.style.top = -scrollTop + "px";
      // document.body.style['overscroll-behavior'] = 'none'
    }
    
    function recoverScroll() {
      document.body.style["overflow-y"] = "auto";
      document.body.style.position = "static";
      // document.querySelector('body').style['overscroll-behavior'] = 'none'
    
      window.scrollTo(0, scrollTop);
    }

    判断当前位置是否为页面底部

    • 返回值为 true/false
    function bottomVisible() {
      return (
        document.documentElement.clientHeight + window.scrollY >=
        (document.documentElement.scrollHeight ||
          document.documentElement.clientHeight)
      );
    }

    判断元素是否在可视范围内

    • partiallyVisible 为是否为完全可见
    function elementIsVisibleInViewport(el, partiallyVisible = false) {
      const { top, left, bottom, right } = el.getBoundingClientRect();
    
      return partiallyVisible
        ? ((top > 0 && top < innerHeight) ||
            (bottom > 0 && bottom < innerHeight)) &&
            ((left > 0 && left < innerWidth) || (right > 0 && right < innerWidth))
        : top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth;
    }

    获取元素 css 样式

    function getStyle(el, ruleName) {
      return getComputedStyle(el, null).getPropertyValue(ruleName);
    }

    进入全屏

    function launchFullscreen(element) {
      if (element.requestFullscreen) {
        element.requestFullscreen();
      } else if (element.mozRequestFullScreen) {
        element.mozRequestFullScreen();
      } else if (element.msRequestFullscreen) {
        element.msRequestFullscreen();
      } else if (element.webkitRequestFullscreen) {
        element.webkitRequestFullScreen();
      }
    }
    
    launchFullscreen(document.documentElement);
    launchFullscreen(document.getElementById("id")); //某个元素进入全屏

    退出全屏

    function exitFullscreen() {
      if (document.exitFullscreen) {
        document.exitFullscreen();
      } else if (document.msExitFullscreen) {
        document.msExitFullscreen();
      } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen();
      } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen();
      }
    }
    
    exitFullscreen();

    全屏事件

    document.addEventListener("fullscreenchange", function (e) {
      if (document.fullscreenElement) {
        console.log("进入全屏");
      } else {
        console.log("退出全屏");
      }
    });

    数字 Number

    数字千分位分割

    function commafy(num) {
      return num.toString().indexOf(".") !== -1
        ? num.toLocaleString()
        : num.toString().replace(/(\d)(?=(?:\d{3})+$)/g, "$1,");
    }

    生成随机数

    function randomNum(min, max) {
      switch (arguments.length) {
        case 1:
          return parseInt(Math.random() * min + 1, 10);
        case 2:
          return parseInt(Math.random() * (max - min + 1) + min, 10);
        default:
          return 0;
      }
    }

    交流

    文章同步持续更新,可以微信搜索「 前端遨游 」关注公众号方便你往后阅读,往期文章收录在https://github.com/Wscats/art...
    欢迎您的关注和交流??

    截屏2020-03-15上午11.16.34.png

    查看原文

    Eno_Yao 发布了文章 · 5月13日

    36个工作中常用的JavaScript函数片段

    photo-1589127383386-270465a13dea.jpeg
    如果文章和笔记能带您一丝帮助或者启发,请不要吝啬你的赞和收藏,你的肯定是我前进的最大动力??

    数组 Array

    数组去重

    function noRepeat(arr) {
      return [...new Set(arr)];
    }

    查找数组最大

    function arrayMax(arr) {
      return Math.max(...arr);
    }

    查找数组最小

    function arrayMin(arr) {
      return Math.min(...arr);
    }

    返回已 size 为长度的数组分割的原数组

    function chunk(arr, size = 1) {
      return Array.from(
        {
          length: Math.ceil(arr.length / size),
        },
        (v, i) => arr.slice(i * size, i * size + size)
      );
    }

    检查数组中某元素出现的次数

    function countOccurrences(arr, value) {
      return arr.reduce((a, v) => (v === value ? a + 1 : a + 0), 0);
    }

    扁平化数组

    • 默认 depth 全部展开
    function flatten(arr, depth = -1) {
      if (depth === -1) {
        return [].concat(
          ...arr.map((v) => (Array.isArray(v) ? this.flatten(v) : v))
        );
      }
      if (depth === 1) {
        return arr.reduce((a, v) => a.concat(v), []);
      }
      return arr.reduce(
        (a, v) => a.concat(Array.isArray(v) ? this.flatten(v, depth - 1) : v),
        []
      );
    }

    对比两个数组并且返回其中不同的元素

    function diffrence(arrA, arrB) {
      return arrA.filter((v) => !arrB.includes(v));
    }

    返回两个数组中相同的元素

    function intersection(arr1, arr2) {
      return arr2.filter((v) => arr1.includes(v));
    }

    从右删除 n 个元素

    function dropRight(arr, n = 0) {
      return n < arr.length ? arr.slice(0, arr.length - n) : [];
    }

    截取第一个符合条件的元素及其以后的元素

    function dropElements(arr, fn) {
      while (arr.length && !fn(arr[0])) arr = arr.slice(1);
      return arr;
    }

    返回数组中下标间隔 nth 的元素

    function everyNth(arr, nth) {
      return arr.filter((v, i) => i % nth === nth - 1);
    }

    返回数组中第 n 个元素

    • 支持负数
    function nthElement(arr, n = 0) {
      return (n >= 0 ? arr.slice(n, n + 1) : arr.slice(n))[0];
    }

    返回数组头元素

    function head(arr) {
      return arr[0];
    }

    返回数组末尾元素

    function last(arr) {
      return arr[arr.length - 1];
    }

    数组乱排

    function shuffle(arr) {
      let array = arr;
      let index = array.length;
    
      while (index) {
        index -= 1;
        let randomInedx = Math.floor(Math.random() * index);
        let middleware = array[index];
        array[index] = array[randomInedx];
        array[randomInedx] = middleware;
      }
    
      return array;
    }

    浏览器对象 BOM

    判读浏览器是否支持 CSS 属性

    /**
     * 告知浏览器支持的指定css属性情况
     * @param {String} key - css属性,是属性的名字,不需要加前缀
     * @returns {String} - 支持的属性情况
     */
    function validateCssKey(key) {
      const jsKey = toCamelCase(key); // 有些css属性是连字符号形成
      if (jsKey in document.documentElement.style) {
        return key;
      }
      let validKey = "";
      // 属性名为前缀在js中的形式,属性值是前缀在css中的形式
      // 经尝试,Webkit 也可是首字母小写 webkit
      const prefixMap = {
        Webkit: "-webkit-",
        Moz: "-moz-",
        ms: "-ms-",
        O: "-o-",
      };
      for (const jsPrefix in prefixMap) {
        const styleKey = toCamelCase(`${jsPrefix}-${jsKey}`);
        if (styleKey in document.documentElement.style) {
          validKey = prefixMap[jsPrefix] + key;
          break;
        }
      }
      return validKey;
    }
    
    /**
     * 把有连字符号的字符串转化为驼峰命名法的字符串
     */
    function toCamelCase(value) {
      return value.replace(/-(\w)/g, (matched, letter) => {
        return letter.toUpperCase();
      });
    }
    
    /**
     * 检查浏览器是否支持某个css属性值(es6版)
     * @param {String} key - 检查的属性值所属的css属性名
     * @param {String} value - 要检查的css属性值(不要带前缀)
     * @returns {String} - 返回浏览器支持的属性值
     */
    function valiateCssValue(key, value) {
      const prefix = ["-o-", "-ms-", "-moz-", "-webkit-", ""];
      const prefixValue = prefix.map((item) => {
        return item + value;
      });
      const element = document.createElement("div");
      const eleStyle = element.style;
      // 应用每个前缀的情况,且最后也要应用上没有前缀的情况,看最后浏览器起效的何种情况
      // 这就是最好在prefix里的最后一个元素是''
      prefixValue.forEach((item) => {
        eleStyle[key] = item;
      });
      return eleStyle[key];
    }
    
    /**
     * 检查浏览器是否支持某个css属性值
     * @param {String} key - 检查的属性值所属的css属性名
     * @param {String} value - 要检查的css属性值(不要带前缀)
     * @returns {String} - 返回浏览器支持的属性值
     */
    function valiateCssValue(key, value) {
      var prefix = ["-o-", "-ms-", "-moz-", "-webkit-", ""];
      var prefixValue = [];
      for (var i = 0; i < prefix.length; i++) {
        prefixValue.push(prefix[i] + value);
      }
      var element = document.createElement("div");
      var eleStyle = element.style;
      for (var j = 0; j < prefixValue.length; j++) {
        eleStyle[key] = prefixValue[j];
      }
      return eleStyle[key];
    }
    
    function validCss(key, value) {
      const validCss = validateCssKey(key);
      if (validCss) {
        return validCss;
      }
      return valiateCssValue(key, value);
    }

    返回当前网页地址

    function currentURL() {
      return window.location.href;
    }

    获取滚动条位置

    function getScrollPosition(el = window) {
      return {
        x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft,
        y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop,
      };
    }

    获取 url 中的参数

    function getURLParameters(url) {
      return url
        .match(/([^?=&]+)(=([^&]*))/g)
        .reduce(
          (a, v) => (
            (a[v.slice(0, v.indexOf("="))] = v.slice(v.indexOf("=") + 1)), a
          ),
          {}
        );
    }

    页面跳转,是否记录在 history 中

    function redirect(url, asLink = true) {
      asLink ? (window.location.href = url) : window.location.replace(url);
    }

    滚动条回到顶部动画

    function scrollToTop() {
      const scrollTop =
        document.documentElement.scrollTop || document.body.scrollTop;
      if (scrollTop > 0) {
        window.requestAnimationFrame(scrollToTop);
        window.scrollTo(0, c - c / 8);
      } else {
        window.cancelAnimationFrame(scrollToTop);
      }
    }

    复制文本

    function copy(str) {
      const el = document.createElement("textarea");
      el.value = str;
      el.setAttribute("readonly", "");
      el.style.position = "absolute";
      el.style.left = "-9999px";
      el.style.top = "-9999px";
      document.body.appendChild(el);
      const selected =
        document.getSelection().rangeCount > 0
          ? document.getSelection().getRangeAt(0)
          : false;
      el.select();
      document.execCommand("copy");
      document.body.removeChild(el);
      if (selected) {
        document.getSelection().removeAllRanges();
        document.getSelection().addRange(selected);
      }
    }

    检测设备类型

    function detectDeviceType() {
      return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
        navigator.userAgent
      )
        ? "Mobile"
        : "Desktop";
    }

    Cookie

    function setCookie(key, value, expiredays) {
      var exdate = new Date();
      exdate.setDate(exdate.getDate() + expiredays);
      document.cookie =
        key +
        "=" +
        escape(value) +
        (expiredays == null ? "" : ";expires=" + exdate.toGMTString());
    }

    function delCookie(name) {
      var exp = new Date();
      exp.setTime(exp.getTime() - 1);
      var cval = getCookie(name);
      if (cval != null) {
        document.cookie = name + "=" + cval + ";expires=" + exp.toGMTString();
      }
    }

    function getCookie(name) {
      var arr,
        reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
      if ((arr = document.cookie.match(reg))) {
        return arr[2];
      } else {
        return null;
      }
    }

    日期 Date

    时间戳转换为时间

    • 默认为当前时间转换结果
    • isMs 为时间戳是否为毫秒
    function timestampToTime(timestamp = Date.parse(new Date()), isMs = true) {
      const date = new Date(timestamp * (isMs ? 1 : 1000));
      return `${date.getFullYear()}-${
        date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1
      }-${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
    }

    ? 文档对象 DOM

    固定滚动条

    /**
     * 功能描述:一些业务场景,如弹框出现时,需要禁止页面滚动,这是兼容安卓和 iOS 禁止页面滚动的解决方案
     */
    
    let scrollTop = 0;
    
    function preventScroll() {
      // 存储当前滚动位置
      scrollTop = window.scrollY;
    
      // 将可滚动区域固定定位,可滚动区域高度为 0 后就不能滚动了
      document.body.style["overflow-y"] = "hidden";
      document.body.style.position = "fixed";
      document.body.style.width = "100%";
      document.body.style.top = -scrollTop + "px";
      // document.body.style['overscroll-behavior'] = 'none'
    }
    
    function recoverScroll() {
      document.body.style["overflow-y"] = "auto";
      document.body.style.position = "static";
      // document.querySelector('body').style['overscroll-behavior'] = 'none'
    
      window.scrollTo(0, scrollTop);
    }

    判断当前位置是否为页面底部

    • 返回值为 true/false
    function bottomVisible() {
      return (
        document.documentElement.clientHeight + window.scrollY >=
        (document.documentElement.scrollHeight ||
          document.documentElement.clientHeight)
      );
    }

    判断元素是否在可视范围内

    • partiallyVisible 为是否为完全可见
    function elementIsVisibleInViewport(el, partiallyVisible = false) {
      const { top, left, bottom, right } = el.getBoundingClientRect();
    
      return partiallyVisible
        ? ((top > 0 && top < innerHeight) ||
            (bottom > 0 && bottom < innerHeight)) &&
            ((left > 0 && left < innerWidth) || (right > 0 && right < innerWidth))
        : top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth;
    }

    获取元素 css 样式

    function getStyle(el, ruleName) {
      return getComputedStyle(el, null).getPropertyValue(ruleName);
    }

    进入全屏

    function launchFullscreen(element) {
      if (element.requestFullscreen) {
        element.requestFullscreen();
      } else if (element.mozRequestFullScreen) {
        element.mozRequestFullScreen();
      } else if (element.msRequestFullscreen) {
        element.msRequestFullscreen();
      } else if (element.webkitRequestFullscreen) {
        element.webkitRequestFullScreen();
      }
    }
    
    launchFullscreen(document.documentElement);
    launchFullscreen(document.getElementById("id")); //某个元素进入全屏

    退出全屏

    function exitFullscreen() {
      if (document.exitFullscreen) {
        document.exitFullscreen();
      } else if (document.msExitFullscreen) {
        document.msExitFullscreen();
      } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen();
      } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen();
      }
    }
    
    exitFullscreen();

    全屏事件

    document.addEventListener("fullscreenchange", function (e) {
      if (document.fullscreenElement) {
        console.log("进入全屏");
      } else {
        console.log("退出全屏");
      }
    });

    数字 Number

    数字千分位分割

    function commafy(num) {
      return num.toString().indexOf(".") !== -1
        ? num.toLocaleString()
        : num.toString().replace(/(\d)(?=(?:\d{3})+$)/g, "$1,");
    }

    生成随机数

    function randomNum(min, max) {
      switch (arguments.length) {
        case 1:
          return parseInt(Math.random() * min + 1, 10);
        case 2:
          return parseInt(Math.random() * (max - min + 1) + min, 10);
        default:
          return 0;
      }
    }

    交流

    文章同步持续更新,可以微信搜索「 前端遨游 」关注公众号方便你往后阅读,往期文章收录在https://github.com/Wscats/art...
    欢迎您的关注和交流??

    截屏2020-03-15上午11.16.34.png

    查看原文

    赞 85 收藏 72 评论 4

    Eno_Yao 收藏了文章 · 4月28日

    给自己点时间再记记这200条Git命令

    photo-1519076651956-95f25edf6780.jpeg
    我平时使用 Git 的时候,很多的 Git 命令我都不是很常用,工作中一般我们会配合一些可视化工具,或者编辑器自带的一些插件去维护 Git 仓库,但是我们也要记得一些常用 Git 命令来应变一些特殊的场景,下面是我收录整理的常用和不常用的一些 Git 命令,希望能帮助到大家更好的掌握 Git 的使用,如果文章和笔记能带您一丝帮助或者启发,请不要吝啬你的赞和收藏,你的肯定是我前进的最大动力??

    新建

    创建一个新的 git 版本库。这个版本库的配置、存储等信息会被保存到.git 文件夹中

    # 初始化当前项目
    $ git init
    
    # 新建一个目录,将其初始化为Git代码库
    $ git init [project-name]
    
    # 在指定目录创建一个空的 Git 仓库。运行这个命令会创建一个名为 directory,只包含 .git 子目录的空目录。
    
    $ git init --bare <directory>
    
    # 下载一个项目和它的整个代码历史
    # 这个命令就是将一个版本库拷贝到另一个目录中,同时也将分支都拷贝到新的版本库中。这样就可以在新的版本库中提交到远程分支
    $ git clone [url]

    配置

    更改设置。可以是版本库的设置,也可以是系统的或全局的

    # 显示当前的Git配置
    $ git config --list
    
    # 编辑Git配置文件
    $ git config -e [--global]
    
    # 输出、设置基本的全局变量
    $ git config --global user.email
    $ git config --global user.name
    
    $ git config --global user.email "MyEmail@gmail.com"
    $ git config --global user.name "My Name"
    
    # 定义当前用户所有提交使用的作者邮箱。
    $ git config --global alias.<alias-name> <git-command>
    
    # 为Git命令创建一个快捷方式(别名)。
    $ git config --system core.editor <editor>

    帮助

    git 内置了对命令非常详细的解释,可以供我们快速查阅

    # 查找可用命令
    $ git help
    
    # 查找所有可用命令
    $ git help -a
    
    # 在文档当中查找特定的命令
    # git help <命令>
    $ git help add
    $ git help commit
    $ git help init

    状态

    显示索引文件(也就是当前工作空间)和当前的头指针指向的提交的不同

    # 显示分支,未跟踪文件,更改和其他不同
    $ git status
    
    # 查看其他的git status的用法
    $ git help status

    信息

    获取某些文件,某些分支,某次提交等 git 信息

    # 显示commit历史,以及每次commit发生变更的文件
    $ git log --stat
    
    # 搜索提交历史,根据关键词
    $ git log -S [keyword]
    
    # 显示某个commit之后的所有变动,每个commit占据一行
    $ git log [tag] HEAD --pretty=format:%s
    
    # 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
    $ git log [tag] HEAD --grep feature
    
    # 显示某个文件的版本历史,包括文件改名
    $ git log --follow [file]
    $ git whatchanged [file]
    
    # 显示指定文件相关的每一次diff
    $ git log -p [file]
    
    # 显示过去5次提交
    $ git log -5 --pretty --oneline
    
    # 显示所有提交过的用户,按提交次数排序
    $ git shortlog -sn
    
    # 显示指定文件是什么人在什么时间修改过
    $ git blame [file]
    
    # 显示暂存区和工作区的差异
    $ git diff
    
    # 显示暂存区和上一个commit的差异
    $ git diff --cached [file]
    
    # 显示工作区与当前分支最新commit之间的差异
    $ git diff HEAD
    
    # 显示两次提交之间的差异
    $ git diff [first-branch]...[second-branch]
    
    # 显示今天你写了多少行代码
    $ git diff --shortstat "@{0 day ago}"
    
    # 比较暂存区和版本库差异
    $ git diff --staged
    
    # 比较暂存区和版本库差异
    $ git diff --cached
    
    # 仅仅比较统计信息
    $ git diff --stat
    
    # 显示某次提交的元数据和内容变化
    $ git show [commit]
    
    # 显示某次提交发生变化的文件
    $ git show --name-only [commit]
    
    # 显示某次提交时,某个文件的内容
    $ git show [commit]:[filename]
    
    # 显示当前分支的最近几次提交
    $ git reflog
    
    # 查看远程分支
    $ git br -r
    
    # 创建新的分支
    $ git br <new_branch>
    
    # 查看各个分支最后提交信息
    $ git br -v
    
    # 查看已经被合并到当前分支的分支
    $ git br --merged
    
    # 查看尚未被合并到当前分支的分支
    $ git br --no-merged

    添加

    添加文件到当前工作空间中。如果你不使用 git add 将文件添加进去,那么这些文件也不会添加到之后的提交之中

    # 添加一个文件
    $ git add test.js
    
    # 添加一个子目录中的文件
    $ git add /path/to/file/test.js
    
    # 支持正则表达式
    $ git add ./*.js
    
    # 添加指定文件到暂存区
    $ git add [file1] [file2] ...
    
    # 添加指定目录到暂存区,包括子目录
    $ git add [dir]
    
    # 添加当前目录的所有文件到暂存区
    $ git add .
    
    # 添加每个变化前,都会要求确认
    # 对于同一个文件的多处变化,可以实现分次提交
    $ git add -p

    删除

    rm 和上面的 add 命令相反,从工作空间中去掉某个文件

    # 移除 HelloWorld.js
    $ git rm HelloWorld.js
    
    # 移除子目录中的文件
    $ git rm /pather/to/the/file/HelloWorld.js
    
    # 删除工作区文件,并且将这次删除放入暂存区
    $ git rm [file1] [file2] ...
    
    # 停止追踪指定文件,但该文件会保留在工作区
    $ git rm --cached [file]

    分支

    管理分支,可以通过下列命令对分支进行增删改查切换等

    # 查看所有的分支和远程分支
    $ git branch -a
    
    # 创建一个新的分支
    $ git branch [branch-name]
    
    # 重命名分支
    # git branch -m <旧名称> <新名称>
    $ git branch -m [branch-name] [new-branch-name]
    
    # 编辑分支的介绍
    $ git branch [branch-name] --edit-description
    
    # 列出所有本地分支
    $ git branch
    
    # 列出所有远程分支
    $ git branch -r
    
    # 新建一个分支,但依然停留在当前分支
    $ git branch [branch-name]
    
    # 新建一个分支,并切换到该分支
    $ git checkout -b [branch]
    
    # 新建一个分支,指向指定commit
    $ git branch [branch] [commit]
    
    # 新建一个分支,与指定的远程分支建立追踪关系
    $ git branch --track [branch] [remote-branch]
    
    # 切换到指定分支,并更新工作区
    $ git checkout [branch-name]
    
    # 切换到上一个分支
    $ git checkout -
    
    # 建立追踪关系,在现有分支与指定的远程分支之间
    $ git branch --set-upstream [branch] [remote-branch]
    
    # 合并指定分支到当前分支
    $ git merge [branch]
    
    # 选择一个commit,合并进当前分支
    $ git cherry-pick [commit]
    
    # 删除分支
    $ git branch -d [branch-name]
    
    # 删除远程分支
    $ git push origin --delete [branch-name]
    $ git branch -dr [remote/branch]
    
    # 切换到某个分支
    $ git co <branch>
    
    # 创建新的分支,并且切换过去
    $ git co -b <new_branch>
    
    # 基于branch创建新的new_branch
    $ git co -b <new_branch> <branch>
    
    # 把某次历史提交记录checkout出来,但无分支信息,切换到其他分支会自动删除
    $ git co $id
    
    # 把某次历史提交记录checkout出来,创建成一个分支
    $ git co $id -b <new_branch>
    
    # 删除某个分支
    $ git br -d <branch>
    
    # 强制删除某个分支 (未被合并的分支被删除的时候需要强制)
    $ git br -D <branch>

    检出

    将当前工作空间更新到索引所标识的或者某一特定的工作空间

    # 检出一个版本库,默认将更新到master分支
    $ git checkout
    # 检出到一个特定的分支
    $ git checkout branchName
    # 新建一个分支,并且切换过去,相当于"git branch <名字>; git checkout <名字>"
    $ git checkout -b newBranch

    远程同步

    远程同步的远端分支

    # 下载远程仓库的所有变动
    $ git fetch [remote]
    
    # 显示所有远程仓库
    $ git remote -v
    
    # 显示某个远程仓库的信息
    $ git remote show [remote]
    
    # 增加一个新的远程仓库,并命名
    $ git remote add [shortname] [url]
    
    # 查看远程服务器地址和仓库名称
    $ git remote -v
    
    # 添加远程仓库地址
    $ git remote add origin git@ github:xxx/xxx.git
    
    # 设置远程仓库地址(用于修改远程仓库地址)
    $ git remote set-url origin git@ github.com:xxx/xxx.git
    
    # 删除远程仓库
    $ git remote rm <repository>
    
    # 上传本地指定分支到远程仓库
    # 把本地的分支更新到远端origin的master分支上
    # git push <远端> <分支>
    # git push 相当于 git push origin master
    $ git push [remote] [branch]
    
    # 强行推送当前分支到远程仓库,即使有冲突
    $ git push [remote] --force
    
    # 推送所有分支到远程仓库
    $ git push [remote] --all

    撤销

    # 恢复暂存区的指定文件到工作区
    $ git checkout [file]
    
    # 恢复某个commit的指定文件到暂存区和工作区
    $ git checkout [commit] [file]
    
    # 恢复暂存区的所有文件到工作区
    $ git checkout .
    
    # 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
    $ git reset [file]
    
    # 重置暂存区与工作区,与上一次commit保持一致
    $ git reset --hard
    
    # 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
    $ git reset [commit]
    
    # 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
    $ git reset --hard [commit]
    
    # 重置当前HEAD为指定commit,但保持暂存区和工作区不变
    $ git reset --keep [commit]
    
    # 新建一个commit,用来撤销指定commit
    # 后者的所有变化都将被前者抵消,并且应用到当前分支
    $ git revert [commit]
    
    # 恢复最后一次提交的状态
    $ git revert HEAD
    
    # 暂时将未提交的变化移除,稍后再移入
    $ git stash
    $ git stash pop
    
    # 列所有stash
    $ git stash list
    
    # 恢复暂存的内容
    $ git stash apply
    
    # 删除暂存区
    $ git stash drop

    commit

    将当前索引的更改保存为一个新的提交,这个提交包括用户做出的更改与信息

    # 提交暂存区到仓库区附带提交信息
    $ git commit -m [message]
    
    # 提交暂存区的指定文件到仓库区
    $ git commit [file1] [file2] ... -m [message]
    
    # 提交工作区自上次commit之后的变化,直接到仓库区
    $ git commit -a
    
    # 提交时显示所有diff信息
    $ git commit -v
    
    # 使用一次新的commit,替代上一次提交
    # 如果代码没有任何新变化,则用来改写上一次commit的提交信息
    $ git commit --amend -m [message]
    
    # 重做上一次commit,并包括指定文件的新变化
    $ git commit --amend [file1] [file2] ...

    diff

    显示当前工作空间和提交的不同

    # 显示工作目录和索引的不同
    $ git diff
    
    # 显示索引和最近一次提交的不同
    $ git diff --cached
    
    # 显示工作目录和最近一次提交的不同
    $ git diff HEAD

    grep

    可以在版本库中快速查找

    可选配置:

    # 感谢Travis Jeffery提供的以下用法:
    # 在搜索结果中显示行号
    $ git config --global grep.lineNumber true
    
    # 是搜索结果可读性更好
    $ git config --global alias.g "grep --break --heading --line-number"
    # 在所有的java中查找variableName
    $ git grep 'variableName' -- '*.java'
    
    # 搜索包含 "arrayListName" 和, "add" 或 "remove" 的所有行
    $ git grep -e 'arrayListName' --and \( -e add -e remove \)

    log

    显示这个版本库的所有提交

    # 显示所有提交
    $ git log
    
    # 显示某几条提交信息
    $ git log -n 10
    
    # 仅显示合并提交
    $ git log --merges
    
    # 查看该文件每次提交记录
    $ git log <file>
    
    # 查看每次详细修改内容的diff
    $ git log -p <file>
    
    # 查看最近两次详细修改内容的diff
    $ git log -p -2
    
    #查看提交统计信息
    $ git log --stat

    merge

    合并就是将外部的提交合并到自己的分支中

    # 将其他分支合并到当前分支
    $ git merge branchName
    
    # 在合并时创建一个新的合并后的提交
    # 不要 Fast-Foward 合并,这样可以生成 merge 提交
    $ git merge --no-ff branchName

    mv

    重命名或移动一个文件

    # 重命名
    $ git mv test.js test2.js
    
    # 移动
    $ git mv test.js ./new/path/test.js
    
    # 改名文件,并且将这个改名放入暂存区
    $ git mv [file-original] [file-renamed]
    
    # 强制重命名或移动
    # 这个文件已经存在,将要覆盖掉
    $ git mv -f myFile existingFile

    tag

    # 列出所有tag
    $ git tag
    
    # 新建一个tag在当前commit
    $ git tag [tag]
    
    # 新建一个tag在指定commit
    $ git tag [tag] [commit]
    
    # 删除本地tag
    $ git tag -d [tag]
    
    # 删除远程tag
    $ git push origin :refs/tags/[tagName]
    
    # 查看tag信息
    $ git show [tag]
    
    # 提交指定tag
    $ git push [remote] [tag]
    
    # 提交所有tag
    $ git push [remote] --tags
    
    # 新建一个分支,指向某个tag
    $ git checkout -b [branch] [tag]

    pull

    从远端版本库合并到当前分支

    # 从远端origin的master分支更新版本库
    # git pull <远端> <分支>
    $ git pull origin master
    
    # 抓取远程仓库所有分支更新并合并到本地,不要快进合并
    $ git pull --no-ff

    ci

    $ git ci <file>
    $ git ci .
    # 将git add, git rm和git ci等操作都合并在一起做
    $ git ci -a
    $ git ci -am "some comments"
    # 修改最后一次提交记录
    $ git ci --amend

    rebase (谨慎使用)

    将一个分支上所有的提交历史都应用到另一个分支上
    _不要在一个已经公开的远端分支上使用 rebase_.

    # 将experimentBranch应用到master上面
    # git rebase <basebranch> <topicbranch>
    $ git rebase master experimentBranch

    reset (谨慎使用)

    将当前的头指针复位到一个特定的状态。这样可以使你撤销 merge、pull、commits、add 等
    这是个很强大的命令,但是在使用时一定要清楚其所产生的后果

    # 使 staging 区域恢复到上次提交时的状态,不改变现在的工作目录
    $ git reset
    
    # 使 staging 区域恢复到上次提交时的状态,覆盖现在的工作目录
    $ git reset --hard
    
    # 将当前分支恢复到某次提交,不改变现在的工作目录
    # 在工作目录中所有的改变仍然存在
    $ git reset dha78as
    
    # 将当前分支恢复到某次提交,覆盖现在的工作目录
    # 并且删除所有未提交的改变和指定提交之后的所有提交
    $ git reset --hard dha78as

    其他

    # 生成一个可供发布的压缩包
    $ git archive
    
    # 打补丁
    $ git apply ../sync.patch
    
    # 测试补丁能否成功
    $ git apply --check ../sync.patch
    
    # 查看Git的版本
    $ git --version

    参考文档

    查看原文

    Eno_Yao 发布了文章 · 4月28日

    给自己点时间再记记这200条Git命令

    photo-1519076651956-95f25edf6780.jpeg
    我平时使用 Git 的时候,很多的 Git 命令我都不是很常用,工作中一般我们会配合一些可视化工具,或者编辑器自带的一些插件去维护 Git 仓库,但是我们也要记得一些常用 Git 命令来应变一些特殊的场景,下面是我收录整理的常用和不常用的一些 Git 命令,希望能帮助到大家更好的掌握 Git 的使用,如果文章和笔记能带您一丝帮助或者启发,请不要吝啬你的赞和收藏,你的肯定是我前进的最大动力??

    新建

    创建一个新的 git 版本库。这个版本库的配置、存储等信息会被保存到.git 文件夹中

    # 初始化当前项目
    $ git init
    
    # 新建一个目录,将其初始化为Git代码库
    $ git init [project-name]
    
    # 在指定目录创建一个空的 Git 仓库。运行这个命令会创建一个名为 directory,只包含 .git 子目录的空目录。
    
    $ git init --bare <directory>
    
    # 下载一个项目和它的整个代码历史
    # 这个命令就是将一个版本库拷贝到另一个目录中,同时也将分支都拷贝到新的版本库中。这样就可以在新的版本库中提交到远程分支
    $ git clone [url]

    配置

    更改设置。可以是版本库的设置,也可以是系统的或全局的

    # 显示当前的Git配置
    $ git config --list
    
    # 编辑Git配置文件
    $ git config -e [--global]
    
    # 输出、设置基本的全局变量
    $ git config --global user.email
    $ git config --global user.name
    
    $ git config --global user.email "MyEmail@gmail.com"
    $ git config --global user.name "My Name"
    
    # 定义当前用户所有提交使用的作者邮箱。
    $ git config --global alias.<alias-name> <git-command>
    
    # 为Git命令创建一个快捷方式(别名)。
    $ git config --system core.editor <editor>

    帮助

    git 内置了对命令非常详细的解释,可以供我们快速查阅

    # 查找可用命令
    $ git help
    
    # 查找所有可用命令
    $ git help -a
    
    # 在文档当中查找特定的命令
    # git help <命令>
    $ git help add
    $ git help commit
    $ git help init

    状态

    显示索引文件(也就是当前工作空间)和当前的头指针指向的提交的不同

    # 显示分支,未跟踪文件,更改和其他不同
    $ git status
    
    # 查看其他的git status的用法
    $ git help status

    信息

    获取某些文件,某些分支,某次提交等 git 信息

    # 显示commit历史,以及每次commit发生变更的文件
    $ git log --stat
    
    # 搜索提交历史,根据关键词
    $ git log -S [keyword]
    
    # 显示某个commit之后的所有变动,每个commit占据一行
    $ git log [tag] HEAD --pretty=format:%s
    
    # 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
    $ git log [tag] HEAD --grep feature
    
    # 显示某个文件的版本历史,包括文件改名
    $ git log --follow [file]
    $ git whatchanged [file]
    
    # 显示指定文件相关的每一次diff
    $ git log -p [file]
    
    # 显示过去5次提交
    $ git log -5 --pretty --oneline
    
    # 显示所有提交过的用户,按提交次数排序
    $ git shortlog -sn
    
    # 显示指定文件是什么人在什么时间修改过
    $ git blame [file]
    
    # 显示暂存区和工作区的差异
    $ git diff
    
    # 显示暂存区和上一个commit的差异
    $ git diff --cached [file]
    
    # 显示工作区与当前分支最新commit之间的差异
    $ git diff HEAD
    
    # 显示两次提交之间的差异
    $ git diff [first-branch]...[second-branch]
    
    # 显示今天你写了多少行代码
    $ git diff --shortstat "@{0 day ago}"
    
    # 比较暂存区和版本库差异
    $ git diff --staged
    
    # 比较暂存区和版本库差异
    $ git diff --cached
    
    # 仅仅比较统计信息
    $ git diff --stat
    
    # 显示某次提交的元数据和内容变化
    $ git show [commit]
    
    # 显示某次提交发生变化的文件
    $ git show --name-only [commit]
    
    # 显示某次提交时,某个文件的内容
    $ git show [commit]:[filename]
    
    # 显示当前分支的最近几次提交
    $ git reflog
    
    # 查看远程分支
    $ git br -r
    
    # 创建新的分支
    $ git br <new_branch>
    
    # 查看各个分支最后提交信息
    $ git br -v
    
    # 查看已经被合并到当前分支的分支
    $ git br --merged
    
    # 查看尚未被合并到当前分支的分支
    $ git br --no-merged

    添加

    添加文件到当前工作空间中。如果你不使用 git add 将文件添加进去,那么这些文件也不会添加到之后的提交之中

    # 添加一个文件
    $ git add test.js
    
    # 添加一个子目录中的文件
    $ git add /path/to/file/test.js
    
    # 支持正则表达式
    $ git add ./*.js
    
    # 添加指定文件到暂存区
    $ git add [file1] [file2] ...
    
    # 添加指定目录到暂存区,包括子目录
    $ git add [dir]
    
    # 添加当前目录的所有文件到暂存区
    $ git add .
    
    # 添加每个变化前,都会要求确认
    # 对于同一个文件的多处变化,可以实现分次提交
    $ git add -p

    删除

    rm 和上面的 add 命令相反,从工作空间中去掉某个文件

    # 移除 HelloWorld.js
    $ git rm HelloWorld.js
    
    # 移除子目录中的文件
    $ git rm /pather/to/the/file/HelloWorld.js
    
    # 删除工作区文件,并且将这次删除放入暂存区
    $ git rm [file1] [file2] ...
    
    # 停止追踪指定文件,但该文件会保留在工作区
    $ git rm --cached [file]

    分支

    管理分支,可以通过下列命令对分支进行增删改查切换等

    # 查看所有的分支和远程分支
    $ git branch -a
    
    # 创建一个新的分支
    $ git branch [branch-name]
    
    # 重命名分支
    # git branch -m <旧名称> <新名称>
    $ git branch -m [branch-name] [new-branch-name]
    
    # 编辑分支的介绍
    $ git branch [branch-name] --edit-description
    
    # 列出所有本地分支
    $ git branch
    
    # 列出所有远程分支
    $ git branch -r
    
    # 新建一个分支,但依然停留在当前分支
    $ git branch [branch-name]
    
    # 新建一个分支,并切换到该分支
    $ git checkout -b [branch]
    
    # 新建一个分支,指向指定commit
    $ git branch [branch] [commit]
    
    # 新建一个分支,与指定的远程分支建立追踪关系
    $ git branch --track [branch] [remote-branch]
    
    # 切换到指定分支,并更新工作区
    $ git checkout [branch-name]
    
    # 切换到上一个分支
    $ git checkout -
    
    # 建立追踪关系,在现有分支与指定的远程分支之间
    $ git branch --set-upstream [branch] [remote-branch]
    
    # 合并指定分支到当前分支
    $ git merge [branch]
    
    # 选择一个commit,合并进当前分支
    $ git cherry-pick [commit]
    
    # 删除分支
    $ git branch -d [branch-name]
    
    # 删除远程分支
    $ git push origin --delete [branch-name]
    $ git branch -dr [remote/branch]
    
    # 切换到某个分支
    $ git co <branch>
    
    # 创建新的分支,并且切换过去
    $ git co -b <new_branch>
    
    # 基于branch创建新的new_branch
    $ git co -b <new_branch> <branch>
    
    # 把某次历史提交记录checkout出来,但无分支信息,切换到其他分支会自动删除
    $ git co $id
    
    # 把某次历史提交记录checkout出来,创建成一个分支
    $ git co $id -b <new_branch>
    
    # 删除某个分支
    $ git br -d <branch>
    
    # 强制删除某个分支 (未被合并的分支被删除的时候需要强制)
    $ git br -D <branch>

    检出

    将当前工作空间更新到索引所标识的或者某一特定的工作空间

    # 检出一个版本库,默认将更新到master分支
    $ git checkout
    # 检出到一个特定的分支
    $ git checkout branchName
    # 新建一个分支,并且切换过去,相当于"git branch <名字>; git checkout <名字>"
    $ git checkout -b newBranch

    远程同步

    远程同步的远端分支

    # 下载远程仓库的所有变动
    $ git fetch [remote]
    
    # 显示所有远程仓库
    $ git remote -v
    
    # 显示某个远程仓库的信息
    $ git remote show [remote]
    
    # 增加一个新的远程仓库,并命名
    $ git remote add [shortname] [url]
    
    # 查看远程服务器地址和仓库名称
    $ git remote -v
    
    # 添加远程仓库地址
    $ git remote add origin git@ github:xxx/xxx.git
    
    # 设置远程仓库地址(用于修改远程仓库地址)
    $ git remote set-url origin git@ github.com:xxx/xxx.git
    
    # 删除远程仓库
    $ git remote rm <repository>
    
    # 上传本地指定分支到远程仓库
    # 把本地的分支更新到远端origin的master分支上
    # git push <远端> <分支>
    # git push 相当于 git push origin master
    $ git push [remote] [branch]
    
    # 强行推送当前分支到远程仓库,即使有冲突
    $ git push [remote] --force
    
    # 推送所有分支到远程仓库
    $ git push [remote] --all

    撤销

    # 恢复暂存区的指定文件到工作区
    $ git checkout [file]
    
    # 恢复某个commit的指定文件到暂存区和工作区
    $ git checkout [commit] [file]
    
    # 恢复暂存区的所有文件到工作区
    $ git checkout .
    
    # 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
    $ git reset [file]
    
    # 重置暂存区与工作区,与上一次commit保持一致
    $ git reset --hard
    
    # 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
    $ git reset [commit]
    
    # 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
    $ git reset --hard [commit]
    
    # 重置当前HEAD为指定commit,但保持暂存区和工作区不变
    $ git reset --keep [commit]
    
    # 新建一个commit,用来撤销指定commit
    # 后者的所有变化都将被前者抵消,并且应用到当前分支
    $ git revert [commit]
    
    # 恢复最后一次提交的状态
    $ git revert HEAD
    
    # 暂时将未提交的变化移除,稍后再移入
    $ git stash
    $ git stash pop
    
    # 列所有stash
    $ git stash list
    
    # 恢复暂存的内容
    $ git stash apply
    
    # 删除暂存区
    $ git stash drop

    commit

    将当前索引的更改保存为一个新的提交,这个提交包括用户做出的更改与信息

    # 提交暂存区到仓库区附带提交信息
    $ git commit -m [message]
    
    # 提交暂存区的指定文件到仓库区
    $ git commit [file1] [file2] ... -m [message]
    
    # 提交工作区自上次commit之后的变化,直接到仓库区
    $ git commit -a
    
    # 提交时显示所有diff信息
    $ git commit -v
    
    # 使用一次新的commit,替代上一次提交
    # 如果代码没有任何新变化,则用来改写上一次commit的提交信息
    $ git commit --amend -m [message]
    
    # 重做上一次commit,并包括指定文件的新变化
    $ git commit --amend [file1] [file2] ...

    diff

    显示当前工作空间和提交的不同

    # 显示工作目录和索引的不同
    $ git diff
    
    # 显示索引和最近一次提交的不同
    $ git diff --cached
    
    # 显示工作目录和最近一次提交的不同
    $ git diff HEAD

    grep

    可以在版本库中快速查找

    可选配置:

    # 感谢Travis Jeffery提供的以下用法:
    # 在搜索结果中显示行号
    $ git config --global grep.lineNumber true
    
    # 是搜索结果可读性更好
    $ git config --global alias.g "grep --break --heading --line-number"
    # 在所有的java中查找variableName
    $ git grep 'variableName' -- '*.java'
    
    # 搜索包含 "arrayListName" 和, "add" 或 "remove" 的所有行
    $ git grep -e 'arrayListName' --and \( -e add -e remove \)

    log

    显示这个版本库的所有提交

    # 显示所有提交
    $ git log
    
    # 显示某几条提交信息
    $ git log -n 10
    
    # 仅显示合并提交
    $ git log --merges
    
    # 查看该文件每次提交记录
    $ git log <file>
    
    # 查看每次详细修改内容的diff
    $ git log -p <file>
    
    # 查看最近两次详细修改内容的diff
    $ git log -p -2
    
    #查看提交统计信息
    $ git log --stat

    merge

    合并就是将外部的提交合并到自己的分支中

    # 将其他分支合并到当前分支
    $ git merge branchName
    
    # 在合并时创建一个新的合并后的提交
    # 不要 Fast-Foward 合并,这样可以生成 merge 提交
    $ git merge --no-ff branchName

    mv

    重命名或移动一个文件

    # 重命名
    $ git mv test.js test2.js
    
    # 移动
    $ git mv test.js ./new/path/test.js
    
    # 改名文件,并且将这个改名放入暂存区
    $ git mv [file-original] [file-renamed]
    
    # 强制重命名或移动
    # 这个文件已经存在,将要覆盖掉
    $ git mv -f myFile existingFile

    tag

    # 列出所有tag
    $ git tag
    
    # 新建一个tag在当前commit
    $ git tag [tag]
    
    # 新建一个tag在指定commit
    $ git tag [tag] [commit]
    
    # 删除本地tag
    $ git tag -d [tag]
    
    # 删除远程tag
    $ git push origin :refs/tags/[tagName]
    
    # 查看tag信息
    $ git show [tag]
    
    # 提交指定tag
    $ git push [remote] [tag]
    
    # 提交所有tag
    $ git push [remote] --tags
    
    # 新建一个分支,指向某个tag
    $ git checkout -b [branch] [tag]

    pull

    从远端版本库合并到当前分支

    # 从远端origin的master分支更新版本库
    # git pull <远端> <分支>
    $ git pull origin master
    
    # 抓取远程仓库所有分支更新并合并到本地,不要快进合并
    $ git pull --no-ff

    ci

    $ git ci <file>
    $ git ci .
    # 将git add, git rm和git ci等操作都合并在一起做
    $ git ci -a
    $ git ci -am "some comments"
    # 修改最后一次提交记录
    $ git ci --amend

    rebase (谨慎使用)

    将一个分支上所有的提交历史都应用到另一个分支上
    _不要在一个已经公开的远端分支上使用 rebase_.

    # 将experimentBranch应用到master上面
    # git rebase <basebranch> <topicbranch>
    $ git rebase master experimentBranch

    reset (谨慎使用)

    将当前的头指针复位到一个特定的状态。这样可以使你撤销 merge、pull、commits、add 等
    这是个很强大的命令,但是在使用时一定要清楚其所产生的后果

    # 使 staging 区域恢复到上次提交时的状态,不改变现在的工作目录
    $ git reset
    
    # 使 staging 区域恢复到上次提交时的状态,覆盖现在的工作目录
    $ git reset --hard
    
    # 将当前分支恢复到某次提交,不改变现在的工作目录
    # 在工作目录中所有的改变仍然存在
    $ git reset dha78as
    
    # 将当前分支恢复到某次提交,覆盖现在的工作目录
    # 并且删除所有未提交的改变和指定提交之后的所有提交
    $ git reset --hard dha78as

    其他

    # 生成一个可供发布的压缩包
    $ git archive
    
    # 打补丁
    $ git apply ../sync.patch
    
    # 测试补丁能否成功
    $ git apply --check ../sync.patch
    
    # 查看Git的版本
    $ git --version

    参考文档

    查看原文

    赞 108 收藏 85 评论 9

    Eno_Yao 发布了文章 · 4月26日

    一些常见必备的MySQL知识

    photo-1587613754067-13e9a170b42f.jpeg
    最近在整理 sql 的时候发现一份优秀的笔记,是原作者学习 sql 所做的笔记,分享这份总结给大家,对大家对 sql 的可以来一次全方位的检漏和排查,感谢原作者 hjzCy 的付出,原文链接放在文章最下方,如果出现错误,希望大家共同指出!

    登录和退出 MySQL 服务器

    # 登录MySQL
    $ mysql -u root -p12345612
    
    # 退出MySQL数据库服务器
    exit;

    基本语法

    -- 显示所有数据库
    show databases;
    
    -- 创建数据库
    CREATE DATABASE test;
    
    -- 切换数据库
    use test;
    
    -- 显示数据库中的所有表
    show tables;
    
    -- 创建数据表
    CREATE TABLE pet (
        name VARCHAR(20),
        owner VARCHAR(20),
        species VARCHAR(20),
        sex CHAR(1),
        birth DATE,
        death DATE
    );
    
    -- 查看数据表结构
    -- describe pet;
    desc pet;
    
    -- 查询表
    SELECT * from pet;
    
    -- 插入数据
    INSERT INTO pet VALUES ('puffball', 'Diane', 'hamster', 'f', '1990-03-30', NULL);
    
    -- 修改数据
    UPDATE pet SET name = 'squirrel' where owner = 'Diane';
    
    -- 删除数据
    DELETE FROM pet where name = 'squirrel';
    
    -- 删除表
    DROP TABLE myorder;

    建表约束

    主键约束

    -- 主键约束
    -- 使某个字段不重复且不得为空,确保表内所有数据的唯一性。
    CREATE TABLE user (
        id INT PRIMARY KEY,
        name VARCHAR(20)
    );
    
    -- 联合主键
    -- 联合主键中的每个字段都不能为空,并且加起来不能和已设置的联合主键重复。
    CREATE TABLE user (
        id INT,
        name VARCHAR(20),
        password VARCHAR(20),
        PRIMARY KEY(id, name)
    );
    
    -- 自增约束
    -- 自增约束的主键由系统自动递增分配。
    CREATE TABLE user (
        id INT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(20)
    );
    
    -- 添加主键约束
    -- 如果忘记设置主键,还可以通过SQL语句设置(两种方式):
    ALTER TABLE user ADD PRIMARY KEY(id);
    ALTER TABLE user MODIFY id INT PRIMARY KEY;
    
    -- 删除主键
    ALTER TABLE user drop PRIMARY KEY;

    唯一主键

    -- 建表时创建唯一主键
    CREATE TABLE user (
        id INT,
        name VARCHAR(20),
        UNIQUE(name)
    );
    
    -- 添加唯一主键
    -- 如果建表时没有设置唯一建,还可以通过SQL语句设置(两种方式):
    ALTER TABLE user ADD UNIQUE(name);
    ALTER TABLE user MODIFY name VARCHAR(20) UNIQUE;
    
    -- 删除唯一主键
    ALTER TABLE user DROP INDEX name;

    非空约束

    -- 建表时添加非空约束
    -- 约束某个字段不能为空
    CREATE TABLE user (
        id INT,
        name VARCHAR(20) NOT NULL
    );
    
    -- 移除非空约束
    ALTER TABLE user MODIFY name VARCHAR(20);

    默认约束

    -- 建表时添加默认约束
    -- 约束某个字段的默认值
    CREATE TABLE user2 (
        id INT,
        name VARCHAR(20),
        age INT DEFAULT 10
    );
    
    -- 移除非空约束
    ALTER TABLE user MODIFY age INT;

    外键约束

    -- 班级
    CREATE TABLE classes (
        id INT PRIMARY KEY,
        name VARCHAR(20)
    );
    
    -- 学生表
    CREATE TABLE students (
        id INT PRIMARY KEY,
        name VARCHAR(20),
        -- 这里的 class_id 要和 classes 中的 id 字段相关联
        class_id INT,
        -- 表示 class_id 的值必须来自于 classes 中的 id 字段值
        FOREIGN KEY(class_id) REFERENCES classes(id)
    );
    
    -- 1. 主表(父表)classes 中没有的数据值,在副表(子表)students 中,是不可以使用的;
    -- 2. 主表中的记录被副表引用时,主表不可以被删除。

    数据库的三大设计范式

    1NF

    只要字段值还可以继续拆分,就不满足第一范式。

    范式设计得越详细,对某些实际操作可能会更好,但并非都有好处,需要对项目的实际情况进行设定。

    2NF

    在满足第一范式的前提下,其他列都必须完全依赖于主键列。如果出现不完全依赖,只可能发生在联合主键的情况下:

    -- 订单表
    CREATE TABLE myorder (
        product_id INT,
        customer_id INT,
        product_name VARCHAR(20),
        customer_name VARCHAR(20),
        PRIMARY KEY (product_id, customer_id)
    );

    实际上,在这张订单表中,product_name 只依赖于 product_idcustomer_name 只依赖于 customer_id 。也就是说,product_namecustomer_id 是没用关系的,customer_nameproduct_id 也是没有关系的。

    这就不满足第二范式:其他列都必须完全依赖于主键列!

    CREATE TABLE myorder (
        order_id INT PRIMARY KEY,
        product_id INT,
        customer_id INT
    );
    
    CREATE TABLE product (
        id INT PRIMARY KEY,
        name VARCHAR(20)
    );
    
    CREATE TABLE customer (
        id INT PRIMARY KEY,
        name VARCHAR(20)
    );

    拆分之后,myorder 表中的 product_idcustomer_id 完全依赖于 order_id 主键,而 productcustomer 表中的其他字段又完全依赖于主键。满足了第二范式的设计!

    3NF

    在满足第二范式的前提下,除了主键列之外,其他列之间不能有传递依赖关系。

    CREATE TABLE myorder (
        order_id INT PRIMARY KEY,
        product_id INT,
        customer_id INT,
        customer_phone VARCHAR(15)
    );

    表中的 customer_phone 有可能依赖于 order_idcustomer_id 两列,也就不满足了第三范式的设计:其他列之间不能有传递依赖关系。

    CREATE TABLE myorder (
        order_id INT PRIMARY KEY,
        product_id INT,
        customer_id INT
    );
    
    CREATE TABLE customer (
        id INT PRIMARY KEY,
        name VARCHAR(20),
        phone VARCHAR(15)
    );

    修改后就不存在其他列之间的传递依赖关系,其他列都只依赖于主键列,满足了第三范式的设计!

    查询练习

    准备数据

    -- 创建数据库
    CREATE DATABASE select_test;
    -- 切换数据库
    USE select_test;
    
    -- 创建学生表
    CREATE TABLE student (
        no VARCHAR(20) PRIMARY KEY,
        name VARCHAR(20) NOT NULL,
        sex VARCHAR(10) NOT NULL,
        birthday DATE, -- 生日
        class VARCHAR(20) -- 所在班级
    );
    
    -- 创建教师表
    CREATE TABLE teacher (
        no VARCHAR(20) PRIMARY KEY,
        name VARCHAR(20) NOT NULL,
        sex VARCHAR(10) NOT NULL,
        birthday DATE,
        profession VARCHAR(20) NOT NULL, -- 职称
        department VARCHAR(20) NOT NULL -- 部门
    );
    
    -- 创建课程表
    CREATE TABLE course (
        no VARCHAR(20) PRIMARY KEY,
        name VARCHAR(20) NOT NULL,
        t_no VARCHAR(20) NOT NULL, -- 教师编号
        -- 表示该 tno 来自于 teacher 表中的 no 字段值
        FOREIGN KEY(t_no) REFERENCES teacher(no)
    );
    
    -- 成绩表
    CREATE TABLE score (
        s_no VARCHAR(20) NOT NULL, -- 学生编号
        c_no VARCHAR(20) NOT NULL, -- 课程号
        degree DECIMAL,    -- 成绩
        -- 表示该 s_no, c_no 分别来自于 student, course 表中的 no 字段值
        FOREIGN KEY(s_no) REFERENCES student(no),
        FOREIGN KEY(c_no) REFERENCES course(no),
        -- 设置 s_no, c_no 为联合主键
        PRIMARY KEY(s_no, c_no)
    );
    
    -- 查看所有表
    SHOW TABLES;
    
    -- 添加学生表数据
    INSERT INTO student VALUES('101', '曾华', '男', '1977-09-01', '95033');
    INSERT INTO student VALUES('102', '匡明', '男', '1975-10-02', '95031');
    INSERT INTO student VALUES('103', '王丽', '女', '1976-01-23', '95033');
    INSERT INTO student VALUES('104', '李军', '男', '1976-02-20', '95033');
    INSERT INTO student VALUES('105', '王芳', '女', '1975-02-10', '95031');
    INSERT INTO student VALUES('106', '陆军', '男', '1974-06-03', '95031');
    INSERT INTO student VALUES('107', '王尼玛', '男', '1976-02-20', '95033');
    INSERT INTO student VALUES('108', '张全蛋', '男', '1975-02-10', '95031');
    INSERT INTO student VALUES('109', '赵铁柱', '男', '1974-06-03', '95031');
    
    -- 添加教师表数据
    INSERT INTO teacher VALUES('804', '李诚', '男', '1958-12-02', '副教授', '计算机系');
    INSERT INTO teacher VALUES('856', '张旭', '男', '1969-03-12', '讲师', '电子工程系');
    INSERT INTO teacher VALUES('825', '王萍', '女', '1972-05-05', '助教', '计算机系');
    INSERT INTO teacher VALUES('831', '刘冰', '女', '1977-08-14', '助教', '电子工程系');
    
    -- 添加课程表数据
    INSERT INTO course VALUES('3-105', '计算机导论', '825');
    INSERT INTO course VALUES('3-245', '操作系统', '804');
    INSERT INTO course VALUES('6-166', '数字电路', '856');
    INSERT INTO course VALUES('9-888', '高等数学', '831');
    
    -- 添加添加成绩表数据
    INSERT INTO score VALUES('103', '3-105', '92');
    INSERT INTO score VALUES('103', '3-245', '86');
    INSERT INTO score VALUES('103', '6-166', '85');
    INSERT INTO score VALUES('105', '3-105', '88');
    INSERT INTO score VALUES('105', '3-245', '75');
    INSERT INTO score VALUES('105', '6-166', '79');
    INSERT INTO score VALUES('109', '3-105', '76');
    INSERT INTO score VALUES('109', '3-245', '68');
    INSERT INTO score VALUES('109', '6-166', '81');
    
    -- 查看表结构
    SELECT * FROM course;
    SELECT * FROM score;
    SELECT * FROM student;
    SELECT * FROM teacher;

    1 到 10

    -- 查询 student 表的所有行
    SELECT * FROM student;
    
    -- 查询 student 表中的 name、sex 和 class 字段的所有行
    SELECT name, sex, class FROM student;
    
    -- 查询 teacher 表中不重复的 department 列
    -- department: 去重查询
    SELECT DISTINCT department FROM teacher;
    
    -- 查询 score 表中成绩在60-80之间的所有行(区间查询和运算符查询)
    -- BETWEEN xx AND xx: 查询区间, AND 表示 "并且"
    SELECT * FROM score WHERE degree BETWEEN 60 AND 80;
    SELECT * FROM score WHERE degree > 60 AND degree < 80;
    
    -- 查询 score 表中成绩为 85, 86 或 88 的行
    -- IN: 查询规定中的多个值
    SELECT * FROM score WHERE degree IN (85, 86, 88);
    
    -- 查询 student 表中 '95031' 班或性别为 '女' 的所有行
    -- or: 表示或者关系
    SELECT * FROM student WHERE class = '95031' or sex = '女';
    
    -- 以 class 降序的方式查询 student 表的所有行
    -- DESC: 降序,从高到低
    -- ASC(默认): 升序,从低到高
    SELECT * FROM student ORDER BY class DESC;
    SELECT * FROM student ORDER BY class ASC;
    
    -- 以 c_no 升序、degree 降序查询 score 表的所有行
    SELECT * FROM score ORDER BY c_no ASC, degree DESC;
    
    -- 查询 "95031" 班的学生人数
    -- COUNT: 统计
    SELECT COUNT(*) FROM student WHERE class = '95031';
    
    -- 查询 score 表中的最高分的学生学号和课程编号(子查询或排序查询)。
    -- (SELECT MAX(degree) FROM score): 子查询,算出最高分
    SELECT s_no, c_no FROM score WHERE degree = (SELECT MAX(degree) FROM score);
    
    --  排序查询
    -- LIMIT r, n: 表示从第r行开始,查询n条数据
    SELECT s_no, c_no, degree FROM score ORDER BY degree DESC LIMIT 0, 1;

    分组计算平均成绩

    查询每门课的平均成绩。

    -- AVG: 平均值
    SELECT AVG(degree) FROM score WHERE c_no = '3-105';
    SELECT AVG(degree) FROM score WHERE c_no = '3-245';
    SELECT AVG(degree) FROM score WHERE c_no = '6-166';
    
    -- GROUP BY: 分组查询
    SELECT c_no, AVG(degree) FROM score GROUP BY c_no;

    分组条件与模糊查询

    查询 score 表中至少有 2 名学生选修,并以 3 开头的课程的平均分数。

    SELECT * FROM score;
    -- c_no 课程编号
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 103  | 3-105 |     92 |
    | 103  | 3-245 |     86 |
    | 103  | 6-166 |     85 |
    | 105  | 3-105 |     88 |
    | 105  | 3-245 |     75 |
    | 105  | 6-166 |     79 |
    | 109  | 3-105 |     76 |
    | 109  | 3-245 |     68 |
    | 109  | 6-166 |     81 |
    +------+-------+--------+

    分析表发现,至少有 2 名学生选修的课程是 3-1053-2456-166 ,以 3 开头的课程是 3-1053-245 。也就是说,我们要查询所有 3-1053-245degree 平均分。

    -- 首先把 c_no, AVG(degree) 通过分组查询出来
    SELECT c_no, AVG(degree) FROM score GROUP BY c_no
    +-------+-------------+
    | c_no  | AVG(degree) |
    +-------+-------------+
    | 3-105 |     85.3333 |
    | 3-245 |     76.3333 |
    | 6-166 |     81.6667 |
    +-------+-------------+
    
    -- 再查询出至少有 2 名学生选修的课程
    -- HAVING: 表示持有
    HAVING COUNT(c_no) >= 2
    
    -- 并且是以 3 开头的课程
    -- LIKE 表示模糊查询,"%" 是一个通配符,匹配 "3" 后面的任意字符。
    AND c_no LIKE '3%';
    
    -- 把前面的SQL语句拼接起来,
    -- 后面加上一个 COUNT(*),表示将每个分组的个数也查询出来。
    SELECT c_no, AVG(degree), COUNT(*) FROM score GROUP BY c_no
    HAVING COUNT(c_no) >= 2 AND c_no LIKE '3%';
    +-------+-------------+----------+
    | c_no  | AVG(degree) | COUNT(*) |
    +-------+-------------+----------+
    | 3-105 |     85.3333 |        3 |
    | 3-245 |     76.3333 |        3 |
    +-------+-------------+----------+

    多表查询 - 1

    查询所有学生的 name,以及该学生在 score 表中对应的 c_nodegree

    SELECT no, name FROM student;
    +-----+-----------+
    | no  | name      |
    +-----+-----------+
    | 101 | 曾华      |
    | 102 | 匡明      |
    | 103 | 王丽      |
    | 104 | 李军      |
    | 105 | 王芳      |
    | 106 | 陆军      |
    | 107 | 王尼玛    |
    | 108 | 张全蛋    |
    | 109 | 赵铁柱    |
    +-----+-----------+
    
    SELECT s_no, c_no, degree FROM score;
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 103  | 3-105 |     92 |
    | 103  | 3-245 |     86 |
    | 103  | 6-166 |     85 |
    | 105  | 3-105 |     88 |
    | 105  | 3-245 |     75 |
    | 105  | 6-166 |     79 |
    | 109  | 3-105 |     76 |
    | 109  | 3-245 |     68 |
    | 109  | 6-166 |     81 |
    +------+-------+--------+

    通过分析可以发现,只要把 score 表中的 s_no 字段值替换成 student 表中对应的 name 字段值就可以了,如何做呢?

    -- FROM...: 表示从 student, score 表中查询
    -- WHERE 的条件表示为,只有在 student.no 和 score.s_no 相等时才显示出来。
    SELECT name, c_no, degree FROM student, score
    WHERE student.no = score.s_no;
    +-----------+-------+--------+
    | name      | c_no  | degree |
    +-----------+-------+--------+
    | 王丽      | 3-105 |     92 |
    | 王丽      | 3-245 |     86 |
    | 王丽      | 6-166 |     85 |
    | 王芳      | 3-105 |     88 |
    | 王芳      | 3-245 |     75 |
    | 王芳      | 6-166 |     79 |
    | 赵铁柱    | 3-105 |     76 |
    | 赵铁柱    | 3-245 |     68 |
    | 赵铁柱    | 6-166 |     81 |
    +-----------+-------+--------+

    多表查询 - 2

    查询所有学生的 no 、课程名称 ( course 表中的 name ) 和成绩 ( score 表中的 degree ) 列。

    只有 score 关联学生的 no ,因此只要查询 score 表,就能找出所有和学生相关的 nodegree

    SELECT s_no, c_no, degree FROM score;
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 103  | 3-105 |     92 |
    | 103  | 3-245 |     86 |
    | 103  | 6-166 |     85 |
    | 105  | 3-105 |     88 |
    | 105  | 3-245 |     75 |
    | 105  | 6-166 |     79 |
    | 109  | 3-105 |     76 |
    | 109  | 3-245 |     68 |
    | 109  | 6-166 |     81 |
    +------+-------+--------+

    然后查询 course 表:

    +-------+-----------------+
    | no    | name            |
    +-------+-----------------+
    | 3-105 | 计算机导论      |
    | 3-245 | 操作系统        |
    | 6-166 | 数字电路        |
    | 9-888 | 高等数学        |
    +-------+-----------------+

    只要把 score 表中的 c_no 替换成 course 表中对应的 name 字段值就可以了。

    -- 增加一个查询字段 name,分别从 score、course 这两个表中查询。
    -- as 表示取一个该字段的别名。
    SELECT s_no, name as c_name, degree FROM score, course
    WHERE score.c_no = course.no;
    +------+-----------------+--------+
    | s_no | c_name          | degree |
    +------+-----------------+--------+
    | 103  | 计算机导论      |     92 |
    | 105  | 计算机导论      |     88 |
    | 109  | 计算机导论      |     76 |
    | 103  | 操作系统        |     86 |
    | 105  | 操作系统        |     75 |
    | 109  | 操作系统        |     68 |
    | 103  | 数字电路        |     85 |
    | 105  | 数字电路        |     79 |
    | 109  | 数字电路        |     81 |
    +------+-----------------+--------+

    三表关联查询

    查询所有学生的 name 、课程名 ( course 表中的 name ) 和 degree

    只有 score 表中关联学生的学号和课堂号,我们只要围绕着 score 这张表查询就好了。

    SELECT * FROM score;
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 103  | 3-105 |     92 |
    | 103  | 3-245 |     86 |
    | 103  | 6-166 |     85 |
    | 105  | 3-105 |     88 |
    | 105  | 3-245 |     75 |
    | 105  | 6-166 |     79 |
    | 109  | 3-105 |     76 |
    | 109  | 3-245 |     68 |
    | 109  | 6-166 |     81 |
    +------+-------+--------+

    只要把 s_noc_no 替换成 studentsrouse 表中对应的 name 字段值就好了。

    首先把 s_no 替换成 student 表中的 name 字段:

    SELECT name, c_no, degree FROM student, score WHERE student.no = score.s_no;
    +-----------+-------+--------+
    | name      | c_no  | degree |
    +-----------+-------+--------+
    | 王丽      | 3-105 |     92 |
    | 王丽      | 3-245 |     86 |
    | 王丽      | 6-166 |     85 |
    | 王芳      | 3-105 |     88 |
    | 王芳      | 3-245 |     75 |
    | 王芳      | 6-166 |     79 |
    | 赵铁柱    | 3-105 |     76 |
    | 赵铁柱    | 3-245 |     68 |
    | 赵铁柱    | 6-166 |     81 |
    +-----------+-------+--------+

    再把 c_no 替换成 course 表中的 name 字段:

    -- 课程表
    SELECT no, name FROM course;
    +-------+-----------------+
    | no    | name            |
    +-------+-----------------+
    | 3-105 | 计算机导论      |
    | 3-245 | 操作系统        |
    | 6-166 | 数字电路        |
    | 9-888 | 高等数学        |
    +-------+-----------------+
    
    -- 由于字段名存在重复,使用 "表名.字段名 as 别名" 代替。
    SELECT student.name as s_name, course.name as c_name, degree
    FROM student, score, course
    WHERE student.NO = score.s_no
    AND score.c_no = course.no;

    子查询加分组求平均分

    查询 95031 班学生每门课程的平均成绩。

    score 表中根据 student 表的学生编号筛选出学生的课堂号和成绩:

    -- IN (..): 将筛选出的学生号当做 s_no 的条件查询
    SELECT s_no, c_no, degree FROM score
    WHERE s_no IN (SELECT no FROM student WHERE class = '95031');
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 105  | 3-105 |     88 |
    | 105  | 3-245 |     75 |
    | 105  | 6-166 |     79 |
    | 109  | 3-105 |     76 |
    | 109  | 3-245 |     68 |
    | 109  | 6-166 |     81 |
    +------+-------+--------+

    这时只要将 c_no 分组一下就能得出 95031 班学生每门课的平均成绩:

    SELECT c_no, AVG(degree) FROM score
    WHERE s_no IN (SELECT no FROM student WHERE class = '95031')
    GROUP BY c_no;
    +-------+-------------+
    | c_no  | AVG(degree) |
    +-------+-------------+
    | 3-105 |     82.0000 |
    | 3-245 |     71.5000 |
    | 6-166 |     80.0000 |
    +-------+-------------+

    子查询 - 1

    查询在 3-105 课程中,所有成绩高于 109 号同学的记录。

    首先筛选出课堂号为 3-105 ,在找出所有成绩高于 109 号同学的的行。

    SELECT * FROM score
    WHERE c_no = '3-105'
    AND degree > (SELECT degree FROM score WHERE s_no = '109' AND c_no = '3-105');

    子查询 - 2

    查询所有成绩高于 109 号同学的 3-105 课程成绩记录。

    -- 不限制课程号,只要成绩大于109号同学的3-105课程成绩就可以。
    SELECT * FROM score
    WHERE degree > (SELECT degree FROM score WHERE s_no = '109' AND c_no = '3-105');

    YEAR 函数与带 IN 关键字查询

    查询所有和 101108 号学生同年出生的 nonamebirthday 列。

    -- YEAR(..): 取出日期中的年份
    SELECT no, name, birthday FROM student
    WHERE YEAR(birthday) IN (SELECT YEAR(birthday) FROM student WHERE no IN (101, 108));

    多层嵌套子查询

    查询 '张旭' 教师任课的学生成绩表。

    首先找到教师编号:

    SELECT NO FROM teacher WHERE NAME = '张旭'

    通过 sourse 表找到该教师课程号:

    SELECT NO FROM course WHERE t_no = ( SELECT NO FROM teacher WHERE NAME = '张旭' );

    通过筛选出的课程号查询成绩表:

    SELECT * FROM score WHERE c_no = (
        SELECT no FROM course WHERE t_no = (
            SELECT no FROM teacher WHERE NAME = '张旭'
        )
    );

    多表查询

    查询某选修课程多于 5 个同学的教师姓名。

    首先在 teacher 表中,根据 no 字段来判断该教师的同一门课程是否有至少 5 名学员选修:

    -- 查询 teacher 表
    SELECT no, name FROM teacher;
    +-----+--------+
    | no  | name   |
    +-----+--------+
    | 804 | 李诚   |
    | 825 | 王萍   |
    | 831 | 刘冰   |
    | 856 | 张旭   |
    +-----+--------+
    
    SELECT name FROM teacher WHERE no IN (
        -- 在这里找到对应的条件
    );

    查看和教师编号有有关的表的信息:

    SELECT * FROM course;
    -- t_no: 教师编号
    +-------+-----------------+------+
    | no    | name            | t_no |
    +-------+-----------------+------+
    | 3-105 | 计算机导论      | 825  |
    | 3-245 | 操作系统        | 804  |
    | 6-166 | 数字电路        | 856  |
    | 9-888 | 高等数学        | 831  |
    +-------+-----------------+------+

    我们已经找到和教师编号有关的字段就在 course 表中,但是还无法知道哪门课程至少有 5 名学生选修,所以还需要根据 score 表来查询:

    -- 在此之前向 score 插入一些数据,以便丰富查询条件。
    INSERT INTO score VALUES ('101', '3-105', '90');
    INSERT INTO score VALUES ('102', '3-105', '91');
    INSERT INTO score VALUES ('104', '3-105', '89');
    
    -- 查询 score 表
    SELECT * FROM score;
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 101  | 3-105 |     90 |
    | 102  | 3-105 |     91 |
    | 103  | 3-105 |     92 |
    | 103  | 3-245 |     86 |
    | 103  | 6-166 |     85 |
    | 104  | 3-105 |     89 |
    | 105  | 3-105 |     88 |
    | 105  | 3-245 |     75 |
    | 105  | 6-166 |     79 |
    | 109  | 3-105 |     76 |
    | 109  | 3-245 |     68 |
    | 109  | 6-166 |     81 |
    +------+-------+--------+
    
    -- 在 score 表中将 c_no 作为分组,并且限制 c_no 持有至少 5 条数据。
    SELECT c_no FROM score GROUP BY c_no HAVING COUNT(*) > 5;
    +-------+
    | c_no  |
    +-------+
    | 3-105 |
    +-------+

    根据筛选出来的课程号,找出在某课程中,拥有至少 5 名学员的教师编号:

    SELECT t_no FROM course WHERE no IN (
        SELECT c_no FROM score GROUP BY c_no HAVING COUNT(*) > 5
    );
    +------+
    | t_no |
    +------+
    | 825  |
    +------+

    teacher 表中,根据筛选出来的教师编号找到教师姓名:

    SELECT name FROM teacher WHERE no IN (
        -- 最终条件
        SELECT t_no FROM course WHERE no IN (
            SELECT c_no FROM score GROUP BY c_no HAVING COUNT(*) > 5
        )
    );

    子查询 - 3

    查询 “计算机系” 课程的成绩表。

    思路是,先找出 course 表中所有 计算机系 课程的编号,然后根据这个编号查询 score 表。

    -- 通过 teacher 表查询所有 `计算机系` 的教师编号
    SELECT no, name, department FROM teacher WHERE department = '计算机系'
    +-----+--------+--------------+
    | no  | name   | department   |
    +-----+--------+--------------+
    | 804 | 李诚   | 计算机系     |
    | 825 | 王萍   | 计算机系     |
    +-----+--------+--------------+
    
    -- 通过 course 表查询该教师的课程编号
    SELECT no FROM course WHERE t_no IN (
        SELECT no FROM teacher WHERE department = '计算机系'
    );
    +-------+
    | no    |
    +-------+
    | 3-245 |
    | 3-105 |
    +-------+
    
    -- 根据筛选出来的课程号查询成绩表
    SELECT * FROM score WHERE c_no IN (
        SELECT no FROM course WHERE t_no IN (
            SELECT no FROM teacher WHERE department = '计算机系'
        )
    );
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 103  | 3-245 |     86 |
    | 105  | 3-245 |     75 |
    | 109  | 3-245 |     68 |
    | 101  | 3-105 |     90 |
    | 102  | 3-105 |     91 |
    | 103  | 3-105 |     92 |
    | 104  | 3-105 |     89 |
    | 105  | 3-105 |     88 |
    | 109  | 3-105 |     76 |
    +------+-------+--------+

    UNION 和 NOTIN 的使用

    查询 计算机系电子工程系 中的不同职称的教师。

    -- NOT: 代表逻辑非
    SELECT * FROM teacher WHERE department = '计算机系' AND profession NOT IN (
        SELECT profession FROM teacher WHERE department = '电子工程系'
    )
    -- 合并两个集
    UNION
    SELECT * FROM teacher WHERE department = '电子工程系' AND profession NOT IN (
        SELECT profession FROM teacher WHERE department = '计算机系'
    );

    ANY 表示至少一个 - DESC ( 降序 )

    查询课程 3-105 且成绩 <u>至少</u> 高于 3-245score 表。

    SELECT * FROM score WHERE c_no = '3-105';
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 101  | 3-105 |     90 |
    | 102  | 3-105 |     91 |
    | 103  | 3-105 |     92 |
    | 104  | 3-105 |     89 |
    | 105  | 3-105 |     88 |
    | 109  | 3-105 |     76 |
    +------+-------+--------+
    
    SELECT * FROM score WHERE c_no = '3-245';
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 103  | 3-245 |     86 |
    | 105  | 3-245 |     75 |
    | 109  | 3-245 |     68 |
    +------+-------+--------+
    
    -- ANY: 符合SQL语句中的任意条件。
    -- 也就是说,在 3-105 成绩中,只要有一个大于从 3-245 筛选出来的任意行就符合条件,
    -- 最后根据降序查询结果。
    SELECT * FROM score WHERE c_no = '3-105' AND degree > ANY(
        SELECT degree FROM score WHERE c_no = '3-245'
    ) ORDER BY degree DESC;
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 103  | 3-105 |     92 |
    | 102  | 3-105 |     91 |
    | 101  | 3-105 |     90 |
    | 104  | 3-105 |     89 |
    | 105  | 3-105 |     88 |
    | 109  | 3-105 |     76 |
    +------+-------+--------+

    表示所有的 ALL

    查询课程 3-105 且成绩高于 3-245score 表。

    -- 只需对上一道题稍作修改。
    -- ALL: 符合SQL语句中的所有条件。
    -- 也就是说,在 3-105 每一行成绩中,都要大于从 3-245 筛选出来全部行才算符合条件。
    SELECT * FROM score WHERE c_no = '3-105' AND degree > ALL(
        SELECT degree FROM score WHERE c_no = '3-245'
    );
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 101  | 3-105 |     90 |
    | 102  | 3-105 |     91 |
    | 103  | 3-105 |     92 |
    | 104  | 3-105 |     89 |
    | 105  | 3-105 |     88 |
    +------+-------+--------+

    复制表的数据作为条件查询

    查询某课程成绩比该课程平均成绩低的 score 表。

    -- 查询平均分
    SELECT c_no, AVG(degree) FROM score GROUP BY c_no;
    +-------+-------------+
    | c_no  | AVG(degree) |
    +-------+-------------+
    | 3-105 |     87.6667 |
    | 3-245 |     76.3333 |
    | 6-166 |     81.6667 |
    +-------+-------------+
    
    -- 查询 score 表
    SELECT degree FROM score;
    +--------+
    | degree |
    +--------+
    |     90 |
    |     91 |
    |     92 |
    |     86 |
    |     85 |
    |     89 |
    |     88 |
    |     75 |
    |     79 |
    |     76 |
    |     68 |
    |     81 |
    +--------+
    
    -- 将表 b 作用于表 a 中查询数据
    -- score a (b): 将表声明为 a (b),
    -- 如此就能用 a.c_no = b.c_no 作为条件执行查询了。
    SELECT * FROM score a WHERE degree < (
        (SELECT AVG(degree) FROM score b WHERE a.c_no = b.c_no)
    );
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 105  | 3-245 |     75 |
    | 105  | 6-166 |     79 |
    | 109  | 3-105 |     76 |
    | 109  | 3-245 |     68 |
    | 109  | 6-166 |     81 |
    +------+-------+--------+

    子查询 - 4

    查询所有任课 ( 在 course 表里有课程 ) 教师的 namedepartment

    SELECT name, department FROM teacher WHERE no IN (SELECT t_no FROM course);
    +--------+-----------------+
    | name   | department      |
    +--------+-----------------+
    | 李诚   | 计算机系        |
    | 王萍   | 计算机系        |
    | 刘冰   | 电子工程系      |
    | 张旭   | 电子工程系      |
    +--------+-----------------+

    条件加组筛选

    查询 student 表中至少有 2 名男生的 class

    -- 查看学生表信息
    SELECT * FROM student;
    +-----+-----------+-----+------------+-------+
    | no  | name      | sex | birthday   | class |
    +-----+-----------+-----+------------+-------+
    | 101 | 曾华      | 男  | 1977-09-01 | 95033 |
    | 102 | 匡明      | 男  | 1975-10-02 | 95031 |
    | 103 | 王丽      | 女  | 1976-01-23 | 95033 |
    | 104 | 李军      | 男  | 1976-02-20 | 95033 |
    | 105 | 王芳      | 女  | 1975-02-10 | 95031 |
    | 106 | 陆军      | 男  | 1974-06-03 | 95031 |
    | 107 | 王尼玛    | 男  | 1976-02-20 | 95033 |
    | 108 | 张全蛋    | 男  | 1975-02-10 | 95031 |
    | 109 | 赵铁柱    | 男  | 1974-06-03 | 95031 |
    | 110 | 张飞      | 男  | 1974-06-03 | 95038 |
    +-----+-----------+-----+------------+-------+
    
    -- 只查询性别为男,然后按 class 分组,并限制 class 行大于 1。
    SELECT class FROM student WHERE sex = '男' GROUP BY class HAVING COUNT(*) > 1;
    +-------+
    | class |
    +-------+
    | 95033 |
    | 95031 |
    +-------+

    NOTLIKE 模糊查询取反

    查询 student 表中不姓 "王" 的同学记录。

    -- NOT: 取反
    -- LIKE: 模糊查询
    mysql> SELECT * FROM student WHERE name NOT LIKE '王%';
    +-----+-----------+-----+------------+-------+
    | no  | name      | sex | birthday   | class |
    +-----+-----------+-----+------------+-------+
    | 101 | 曾华      | 男  | 1977-09-01 | 95033 |
    | 102 | 匡明      | 男  | 1975-10-02 | 95031 |
    | 104 | 李军      | 男  | 1976-02-20 | 95033 |
    | 106 | 陆军      | 男  | 1974-06-03 | 95031 |
    | 108 | 张全蛋    | 男  | 1975-02-10 | 95031 |
    | 109 | 赵铁柱    | 男  | 1974-06-03 | 95031 |
    | 110 | 张飞      | 男  | 1974-06-03 | 95038 |
    +-----+-----------+-----+------------+-------+

    YEAR 与 NOW 函数

    查询 student 表中每个学生的姓名和年龄。

    -- 使用函数 YEAR(NOW()) 计算出当前年份,减去出生年份后得出年龄。
    SELECT name, YEAR(NOW()) - YEAR(birthday) as age FROM student;
    +-----------+------+
    | name      | age  |
    +-----------+------+
    | 曾华      |   42 |
    | 匡明      |   44 |
    | 王丽      |   43 |
    | 李军      |   43 |
    | 王芳      |   44 |
    | 陆军      |   45 |
    | 王尼玛    |   43 |
    | 张全蛋    |   44 |
    | 赵铁柱    |   45 |
    | 张飞      |   45 |
    +-----------+------+

    MAX 与 MIN 函数

    查询 student 表中最大和最小的 birthday 值。

    SELECT MAX(birthday), MIN(birthday) FROM student;
    +---------------+---------------+
    | MAX(birthday) | MIN(birthday) |
    +---------------+---------------+
    | 1977-09-01    | 1974-06-03    |
    +---------------+---------------+

    多段排序

    classbirthday 从大到小的顺序查询 student 表。

    SELECT * FROM student ORDER BY class DESC, birthday;
    +-----+-----------+-----+------------+-------+
    | no  | name      | sex | birthday   | class |
    +-----+-----------+-----+------------+-------+
    | 110 | 张飞      | 男  | 1974-06-03 | 95038 |
    | 103 | 王丽      | 女  | 1976-01-23 | 95033 |
    | 104 | 李军      | 男  | 1976-02-20 | 95033 |
    | 107 | 王尼玛    | 男  | 1976-02-20 | 95033 |
    | 101 | 曾华      | 男  | 1977-09-01 | 95033 |
    | 106 | 陆军      | 男  | 1974-06-03 | 95031 |
    | 109 | 赵铁柱    | 男  | 1974-06-03 | 95031 |
    | 105 | 王芳      | 女  | 1975-02-10 | 95031 |
    | 108 | 张全蛋    | 男  | 1975-02-10 | 95031 |
    | 102 | 匡明      | 男  | 1975-10-02 | 95031 |
    +-----+-----------+-----+------------+-------+

    子查询 - 5

    查询 "男" 教师及其所上的课程。

    SELECT * FROM course WHERE t_no in (SELECT no FROM teacher WHERE sex = '男');
    +-------+--------------+------+
    | no    | name         | t_no |
    +-------+--------------+------+
    | 3-245 | 操作系统     | 804  |
    | 6-166 | 数字电路     | 856  |
    +-------+--------------+------+

    MAX 函数与子查询

    查询最高分同学的 score 表。

    -- 找出最高成绩(该查询只能有一个结果)
    SELECT MAX(degree) FROM score;
    
    -- 根据上面的条件筛选出所有最高成绩表,
    -- 该查询可能有多个结果,假设 degree 值多次符合条件。
    SELECT * FROM score WHERE degree = (SELECT MAX(degree) FROM score);
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 103  | 3-105 |     92 |
    +------+-------+--------+

    子查询 - 6

    查询和 "李军" 同性别的所有同学 name

    -- 首先将李军的性别作为条件取出来
    SELECT sex FROM student WHERE name = '李军';
    +-----+
    | sex |
    +-----+
    | 男  |
    +-----+
    
    -- 根据性别查询 name 和 sex
    SELECT name, sex FROM student WHERE sex = (
        SELECT sex FROM student WHERE name = '李军'
    );
    +-----------+-----+
    | name      | sex |
    +-----------+-----+
    | 曾华      | 男  |
    | 匡明      | 男  |
    | 李军      | 男  |
    | 陆军      | 男  |
    | 王尼玛    | 男  |
    | 张全蛋    | 男  |
    | 赵铁柱    | 男  |
    | 张飞      | 男  |
    +-----------+-----+

    子查询 - 7

    查询和 "李军" 同性别且同班的同学 name

    SELECT name, sex, class FROM student WHERE sex = (
        SELECT sex FROM student WHERE name = '李军'
    ) AND class = (
        SELECT class FROM student WHERE name = '李军'
    );
    +-----------+-----+-------+
    | name      | sex | class |
    +-----------+-----+-------+
    | 曾华      | 男  | 95033 |
    | 李军      | 男  | 95033 |
    | 王尼玛    | 男  | 95033 |
    +-----------+-----+-------+

    子查询 - 8

    查询所有选修 "计算机导论" 课程的 "男" 同学成绩表。

    需要的 "计算机导论" 和性别为 "男" 的编号可以在 coursestudent 表中找到。

    SELECT * FROM score WHERE c_no = (
        SELECT no FROM course WHERE name = '计算机导论'
    ) AND s_no IN (
        SELECT no FROM student WHERE sex = '男'
    );
    +------+-------+--------+
    | s_no | c_no  | degree |
    +------+-------+--------+
    | 101  | 3-105 |     90 |
    | 102  | 3-105 |     91 |
    | 104  | 3-105 |     89 |
    | 109  | 3-105 |     76 |
    +------+-------+--------+

    按等级查询

    建立一个 grade 表代表学生的成绩等级,并插入数据:

    CREATE TABLE grade (
        low INT(3),
        upp INT(3),
        grade char(1)
    );
    
    INSERT INTO grade VALUES (90, 100, 'A');
    INSERT INTO grade VALUES (80, 89, 'B');
    INSERT INTO grade VALUES (70, 79, 'C');
    INSERT INTO grade VALUES (60, 69, 'D');
    INSERT INTO grade VALUES (0, 59, 'E');
    
    SELECT * FROM grade;
    +------+------+-------+
    | low  | upp  | grade |
    +------+------+-------+
    |   90 |  100 | A     |
    |   80 |   89 | B     |
    |   70 |   79 | C     |
    |   60 |   69 | D     |
    |    0 |   59 | E     |
    +------+------+-------+

    查询所有学生的 s_noc_nograde 列。

    思路是,使用区间 ( BETWEEN ) 查询,判断学生的成绩 ( degree ) 在 grade 表的 lowupp 之间。

    SELECT s_no, c_no, grade FROM score, grade
    WHERE degree BETWEEN low AND upp;
    +------+-------+-------+
    | s_no | c_no  | grade |
    +------+-------+-------+
    | 101  | 3-105 | A     |
    | 102  | 3-105 | A     |
    | 103  | 3-105 | A     |
    | 103  | 3-245 | B     |
    | 103  | 6-166 | B     |
    | 104  | 3-105 | B     |
    | 105  | 3-105 | B     |
    | 105  | 3-245 | C     |
    | 105  | 6-166 | C     |
    | 109  | 3-105 | C     |
    | 109  | 3-245 | D     |
    | 109  | 6-166 | B     |
    +------+-------+-------+

    连接查询

    准备用于测试连接查询的数据:

    CREATE DATABASE testJoin;
    
    CREATE TABLE person (
        id INT,
        name VARCHAR(20),
        cardId INT
    );
    
    CREATE TABLE card (
        id INT,
        name VARCHAR(20)
    );
    
    INSERT INTO card VALUES (1, '饭卡'), (2, '建行卡'), (3, '农行卡'), (4, '工商卡'), (5, '邮政卡');
    SELECT * FROM card;
    +------+-----------+
    | id   | name      |
    +------+-----------+
    |    1 | 饭卡      |
    |    2 | 建行卡    |
    |    3 | 农行卡    |
    |    4 | 工商卡    |
    |    5 | 邮政卡    |
    +------+-----------+
    
    INSERT INTO person VALUES (1, '张三', 1), (2, '李四', 3), (3, '王五', 6);
    SELECT * FROM person;
    +------+--------+--------+
    | id   | name   | cardId |
    +------+--------+--------+
    |    1 | 张三   |      1 |
    |    2 | 李四   |      3 |
    |    3 | 王五   |      6 |
    +------+--------+--------+

    分析两张表发现,person 表并没有为 cardId 字段设置一个在 card 表中对应的 id 外键。如果设置了的话,personcardId 字段值为 6 的行就插不进去,因为该 cardId 值在 card 表中并没有。

    内连接

    要查询这两张表中有关系的数据,可以使用 INNER JOIN ( 内连接 ) 将它们连接在一起。

    -- INNER JOIN: 表示为内连接,将两张表拼接在一起。
    -- on: 表示要执行某个条件。
    SELECT * FROM person INNER JOIN card on person.cardId = card.id;
    +------+--------+--------+------+-----------+
    | id   | name   | cardId | id   | name      |
    +------+--------+--------+------+-----------+
    |    1 | 张三   |      1 |    1 | 饭卡      |
    |    2 | 李四   |      3 |    3 | 农行卡    |
    +------+--------+--------+------+-----------+
    
    -- 将 INNER 关键字省略掉,结果也是一样的。
    -- SELECT * FROM person JOIN card on person.cardId = card.id;
    注意:card 的整张表被连接到了右边。

    左外连接

    完整显示左边的表 ( person ) ,右边的表如果符合条件就显示,不符合则补 NULL

    -- LEFT JOIN 也叫做 LEFT OUTER JOIN,用这两种方式的查询结果是一样的。
    SELECT * FROM person LEFT JOIN card on person.cardId = card.id;
    +------+--------+--------+------+-----------+
    | id   | name   | cardId | id   | name      |
    +------+--------+--------+------+-----------+
    |    1 | 张三   |      1 |    1 | 饭卡      |
    |    2 | 李四   |      3 |    3 | 农行卡    |
    |    3 | 王五   |      6 | NULL | NULL      |
    +------+--------+--------+------+-----------+

    右外链接

    完整显示右边的表 ( card ) ,左边的表如果符合条件就显示,不符合则补 NULL

    SELECT * FROM person RIGHT JOIN card on person.cardId = card.id;
    +------+--------+--------+------+-----------+
    | id   | name   | cardId | id   | name      |
    +------+--------+--------+------+-----------+
    |    1 | 张三   |      1 |    1 | 饭卡      |
    |    2 | 李四   |      3 |    3 | 农行卡    |
    | NULL | NULL   |   NULL |    2 | 建行卡    |
    | NULL | NULL   |   NULL |    4 | 工商卡    |
    | NULL | NULL   |   NULL |    5 | 邮政卡    |
    +------+--------+--------+------+-----------+

    全外链接

    完整显示两张表的全部数据。

    -- MySQL 不支持这种语法的全外连接
    -- SELECT * FROM person FULL JOIN card on person.cardId = card.id;
    -- 出现错误:
    -- ERROR 1054 (42S22): Unknown column 'person.cardId' in 'on clause'
    
    -- MySQL全连接语法,使用 UNION 将两张表合并在一起。
    SELECT * FROM person LEFT JOIN card on person.cardId = card.id
    UNION
    SELECT * FROM person RIGHT JOIN card on person.cardId = card.id;
    +------+--------+--------+------+-----------+
    | id   | name   | cardId | id   | name      |
    +------+--------+--------+------+-----------+
    |    1 | 张三   |      1 |    1 | 饭卡      |
    |    2 | 李四   |      3 |    3 | 农行卡    |
    |    3 | 王五   |      6 | NULL | NULL      |
    | NULL | NULL   |   NULL |    2 | 建行卡    |
    | NULL | NULL   |   NULL |    4 | 工商卡    |
    | NULL | NULL   |   NULL |    5 | 邮政卡    |
    +------+--------+--------+------+-----------+

    事务

    在 MySQL 中,事务其实是一个最小的不可分割的工作单元。事务能够保证一个业务的完整性

    比如我们的银行转账:

    -- a -> -100
    UPDATE user set money = money - 100 WHERE name = 'a';
    
    -- b -> +100
    UPDATE user set money = money + 100 WHERE name = 'b';

    在实际项目中,假设只有一条 SQL 语句执行成功,而另外一条执行失败了,就会出现数据前后不一致。

    因此,在执行多条有关联 SQL 语句时,事务可能会要求这些 SQL 语句要么同时执行成功,要么就都执行失败。

    如何控制事务 - COMMIT / ROLLBACK

    在 MySQL 中,事务的自动提交状态默认是开启的。

    -- 查询事务的自动提交状态
    SELECT @@AUTOCOMMIT;
    +--------------+
    | @@AUTOCOMMIT |
    +--------------+
    |            1 |
    +--------------+

    自动提交的作用:当我们执行一条 SQL 语句的时候,其产生的效果就会立即体现出来,且不能回滚

    什么是回滚?举个例子:

    CREATE DATABASE bank;
    
    USE bank;
    
    CREATE TABLE user (
        id INT PRIMARY KEY,
        name VARCHAR(20),
        money INT
    );
    
    INSERT INTO user VALUES (1, 'a', 1000);
    
    SELECT * FROM user;
    +----+------+-------+
    | id | name | money |
    +----+------+-------+
    |  1 | a    |  1000 |
    +----+------+-------+

    可以看到,在执行插入语句后数据立刻生效,原因是 MySQL 中的事务自动将它提交到了数据库中。那么所谓回滚的意思就是,撤销执行过的所有 SQL 语句,使其回滚到最后一次提交数据时的状态。

    在 MySQL 中使用 ROLLBACK 执行回滚:

    -- 回滚到最后一次提交
    ROLLBACK;
    
    SELECT * FROM user;
    +----+------+-------+
    | id | name | money |
    +----+------+-------+
    |  1 | a    |  1000 |
    +----+------+-------+

    由于所有执行过的 SQL 语句都已经被提交过了,所以数据并没有发生回滚。那如何让数据可以发生回滚?

    -- 关闭自动提交
    SET AUTOCOMMIT = 0;
    
    -- 查询自动提交状态
    SELECT @@AUTOCOMMIT;
    +--------------+
    | @@AUTOCOMMIT |
    +--------------+
    |            0 |
    +--------------+

    将自动提交关闭后,测试数据回滚:

    INSERT INTO user VALUES (2, 'b', 1000);
    
    -- 关闭 AUTOCOMMIT 后,数据的变化是在一张虚拟的临时数据表中展示,
    -- 发生变化的数据并没有真正插入到数据表中。
    SELECT * FROM user;
    +----+------+-------+
    | id | name | money |
    +----+------+-------+
    |  1 | a    |  1000 |
    |  2 | b    |  1000 |
    +----+------+-------+
    
    -- 数据表中的真实数据其实还是:
    +----+------+-------+
    | id | name | money |
    +----+------+-------+
    |  1 | a    |  1000 |
    +----+------+-------+
    
    -- 由于数据还没有真正提交,可以使用回滚
    ROLLBACK;
    
    -- 再次查询
    SELECT * FROM user;
    +----+------+-------+
    | id | name | money |
    +----+------+-------+
    |  1 | a    |  1000 |
    +----+------+-------+

    那如何将虚拟的数据真正提交到数据库中?使用 COMMIT :

    INSERT INTO user VALUES (2, 'b', 1000);
    -- 手动提交数据(持久性),
    -- 将数据真正提交到数据库中,执行后不能再回滚提交过的数据。
    COMMIT;
    
    -- 提交后测试回滚
    ROLLBACK;
    
    -- 再次查询(回滚无效了)
    SELECT * FROM user;
    +----+------+-------+
    | id | name | money |
    +----+------+-------+
    |  1 | a    |  1000 |
    |  2 | b    |  1000 |
    +----+------+-------+

    总结

    1. 自动提交

      • 查看自动提交状态:SELECT @@AUTOCOMMIT
      • 设置自动提交状态:SET AUTOCOMMIT = 0
    2. 手动提交

      @@AUTOCOMMIT = 0 时,使用 COMMIT 命令提交事务。

    3. 事务回滚

      @@AUTOCOMMIT = 0 时,使用 ROLLBACK 命令回滚事务。

    事务的实际应用,让我们再回到银行转账项目:

    -- 转账
    UPDATE user set money = money - 100 WHERE name = 'a';
    
    -- 到账
    UPDATE user set money = money + 100 WHERE name = 'b';
    
    SELECT * FROM user;
    +----+------+-------+
    | id | name | money |
    +----+------+-------+
    |  1 | a    |   900 |
    |  2 | b    |  1100 |
    +----+------+-------+

    这时假设在转账时发生了意外,就可以使用 ROLLBACK 回滚到最后一次提交的状态:

    -- 假设转账发生了意外,需要回滚。
    ROLLBACK;
    
    SELECT * FROM user;
    +----+------+-------+
    | id | name | money |
    +----+------+-------+
    |  1 | a    |  1000 |
    |  2 | b    |  1000 |
    +----+------+-------+

    这时我们又回到了发生意外之前的状态,也就是说,事务给我们提供了一个可以反悔的机会。假设数据没有发生意外,这时可以手动将数据真正提交到数据表中:COMMIT

    手动开启事务 - BEGIN / START TRANSACTION

    事务的默认提交被开启 ( @@AUTOCOMMIT = 1 ) 后,此时就不能使用事务回滚了。但是我们还可以手动开启一个事务处理事件,使其可以发生回滚:

    -- 使用 BEGIN 或者 START TRANSACTION 手动开启一个事务
    -- START TRANSACTION;
    BEGIN;
    UPDATE user set money = money - 100 WHERE name = 'a';
    UPDATE user set money = money + 100 WHERE name = 'b';
    
    -- 由于手动开启的事务没有开启自动提交,
    -- 此时发生变化的数据仍然是被保存在一张临时表中。
    SELECT * FROM user;
    +----+------+-------+
    | id | name | money |
    +----+------+-------+
    |  1 | a    |   900 |
    |  2 | b    |  1100 |
    +----+------+-------+
    
    -- 测试回滚
    ROLLBACK;
    
    SELECT * FROM user;
    +----+------+-------+
    | id | name | money |
    +----+------+-------+
    |  1 | a    |  1000 |
    |  2 | b    |  1000 |
    +----+------+-------+

    仍然使用 COMMIT 提交数据,提交后无法再发生本次事务的回滚。

    BEGIN;
    UPDATE user set money = money - 100 WHERE name = 'a';
    UPDATE user set money = money + 100 WHERE name = 'b';
    
    SELECT * FROM user;
    +----+------+-------+
    | id | name | money |
    +----+------+-------+
    |  1 | a    |   900 |
    |  2 | b    |  1100 |
    +----+------+-------+
    
    -- 提交数据
    COMMIT;
    
    -- 测试回滚(无效,因为表的数据已经被提交)
    ROLLBACK;

    事务的 ACID 特征与使用

    事务的四大特征:

    • A 原子性:事务是最小的单位,不可以再分割;
    • C 一致性:要求同一事务中的 SQL 语句,必须保证同时成功或者失败;
    • I 隔离性:事务 1 和 事务 2 之间是具有隔离性的;
    • D 持久性:事务一旦结束 ( COMMIT ) ,就不可以再返回了 ( ROLLBACK ) 。

    事务的隔离性

    事务的隔离性可分为四种 ( 性能从低到高 )

    1. READ UNCOMMITTED ( 读取未提交 )

      如果有多个事务,那么任意事务都可以看见其他事务的未提交数据

    2. READ COMMITTED ( 读取已提交 )

      只能读取到其他事务已经提交的数据

    3. REPEATABLE READ ( 可被重复读 )

      如果有多个连接都开启了事务,那么事务之间不能共享数据记录,否则只能共享已提交的记录。

    4. SERIALIZABLE ( 串行化 )

      所有的事务都会按照固定顺序执行,执行完一个事务后再继续执行下一个事务的写入操作

    查看当前数据库的默认隔离级别:

    -- MySQL 8.x, GLOBAL 表示系统级别,不加表示会话级别。
    SELECT @@GLOBAL.TRANSACTION_ISOLATION;
    SELECT @@TRANSACTION_ISOLATION;
    +--------------------------------+
    | @@GLOBAL.TRANSACTION_ISOLATION |
    +--------------------------------+
    | REPEATABLE-READ                | -- MySQL的默认隔离级别,可以重复读。
    +--------------------------------+
    
    -- MySQL 5.x
    SELECT @@GLOBAL.TX_ISOLATION;
    SELECT @@TX_ISOLATION;

    修改隔离级别:

    -- 设置系统隔离级别,LEVEL 后面表示要设置的隔离级别 (READ UNCOMMITTED)。
    SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
    
    -- 查询系统隔离级别,发现已经被修改。
    SELECT @@GLOBAL.TRANSACTION_ISOLATION;
    +--------------------------------+
    | @@GLOBAL.TRANSACTION_ISOLATION |
    +--------------------------------+
    | READ-UNCOMMITTED               |
    +--------------------------------+

    脏读

    测试 READ UNCOMMITTED ( 读取未提交 ) 的隔离性:

    INSERT INTO user VALUES (3, '小明', 1000);
    INSERT INTO user VALUES (4, '淘宝店', 1000);
    
    SELECT * FROM user;
    +----+-----------+-------+
    | id | name      | money |
    +----+-----------+-------+
    |  1 | a         |   900 |
    |  2 | b         |  1100 |
    |  3 | 小明      |  1000 |
    |  4 | 淘宝店    |  1000 |
    +----+-----------+-------+
    
    -- 开启一个事务操作数据
    -- 假设小明在淘宝店买了一双800块钱的鞋子:
    START TRANSACTION;
    UPDATE user SET money = money - 800 WHERE name = '小明';
    UPDATE user SET money = money + 800 WHERE name = '淘宝店';
    
    -- 然后淘宝店在另一方查询结果,发现钱已到账。
    SELECT * FROM user;
    +----+-----------+-------+
    | id | name      | money |
    +----+-----------+-------+
    |  1 | a         |   900 |
    |  2 | b         |  1100 |
    |  3 | 小明      |   200 |
    |  4 | 淘宝店    |  1800 |
    +----+-----------+-------+

    由于小明的转账是在新开启的事务上进行操作的,而该操作的结果是可以被其他事务(另一方的淘宝店)看见的,因此淘宝店的查询结果是正确的,淘宝店确认到账。但就在这时,如果小明在它所处的事务上又执行了 ROLLBACK 命令,会发生什么?

    -- 小明所处的事务
    ROLLBACK;
    
    -- 此时无论对方是谁,如果再去查询结果就会发现:
    SELECT * FROM user;
    +----+-----------+-------+
    | id | name      | money |
    +----+-----------+-------+
    |  1 | a         |   900 |
    |  2 | b         |  1100 |
    |  3 | 小明      |  1000 |
    |  4 | 淘宝店    |  1000 |
    +----+-----------+-------+

    这就是所谓的脏读,一个事务读取到另外一个事务还未提交的数据。这在实际开发中是不允许出现的。

    读取已提交

    把隔离级别设置为 READ COMMITTED

    SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
    SELECT @@GLOBAL.TRANSACTION_ISOLATION;
    +--------------------------------+
    | @@GLOBAL.TRANSACTION_ISOLATION |
    +--------------------------------+
    | READ-COMMITTED                 |
    +--------------------------------+

    这样,再有新的事务连接进来时,它们就只能查询到已经提交过的事务数据了。但是对于当前事务来说,它们看到的还是未提交的数据,例如:

    -- 正在操作数据事务(当前事务)
    START TRANSACTION;
    UPDATE user SET money = money - 800 WHERE name = '小明';
    UPDATE user SET money = money + 800 WHERE name = '淘宝店';
    
    -- 虽然隔离级别被设置为了 READ COMMITTED,但在当前事务中,
    -- 它看到的仍然是数据表中临时改变数据,而不是真正提交过的数据。
    SELECT * FROM user;
    +----+-----------+-------+
    | id | name      | money |
    +----+-----------+-------+
    |  1 | a         |   900 |
    |  2 | b         |  1100 |
    |  3 | 小明      |   200 |
    |  4 | 淘宝店    |  1800 |
    +----+-----------+-------+
    
    
    -- 假设此时在远程开启了一个新事务,连接到数据库。
    $ mysql -u root -p12345612
    
    -- 此时远程连接查询到的数据只能是已经提交过的
    SELECT * FROM user;
    +----+-----------+-------+
    | id | name      | money |
    +----+-----------+-------+
    |  1 | a         |   900 |
    |  2 | b         |  1100 |
    |  3 | 小明      |  1000 |
    |  4 | 淘宝店    |  1000 |
    +----+-----------+-------+

    但是这样还有问题,那就是假设一个事务在操作数据时,其他事务干扰了这个事务的数据。例如:

    -- 小张在查询数据的时候发现:
    SELECT * FROM user;
    +----+-----------+-------+
    | id | name      | money |
    +----+-----------+-------+
    |  1 | a         |   900 |
    |  2 | b         |  1100 |
    |  3 | 小明      |   200 |
    |  4 | 淘宝店    |  1800 |
    +----+-----------+-------+
    
    -- 在小张求表的 money 平均值之前,小王做了一个操作:
    START TRANSACTION;
    INSERT INTO user VALUES (5, 'c', 100);
    COMMIT;
    
    -- 此时表的真实数据是:
    SELECT * FROM user;
    +----+-----------+-------+
    | id | name      | money |
    +----+-----------+-------+
    |  1 | a         |   900 |
    |  2 | b         |  1100 |
    |  3 | 小明      |  1000 |
    |  4 | 淘宝店    |  1000 |
    |  5 | c         |   100 |
    +----+-----------+-------+
    
    -- 这时小张再求平均值的时候,就会出现计算不相符合的情况:
    SELECT AVG(money) FROM user;
    +------------+
    | AVG(money) |
    +------------+
    |  820.0000  |
    +------------+

    虽然 READ COMMITTED 让我们只能读取到其他事务已经提交的数据,但还是会出现问题,就是在读取同一个表的数据时,可能会发生前后不一致的情况。这被称为不可重复读现象 ( READ COMMITTED )

    幻读

    将隔离级别设置为 REPEATABLE READ ( 可被重复读取 ) :

    SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
    SELECT @@GLOBAL.TRANSACTION_ISOLATION;
    +--------------------------------+
    | @@GLOBAL.TRANSACTION_ISOLATION |
    +--------------------------------+
    | REPEATABLE-READ                |
    +--------------------------------+

    测试 REPEATABLE READ ,假设在两个不同的连接上分别执行 START TRANSACTION :

    -- 小张 - 成都
    START TRANSACTION;
    INSERT INTO user VALUES (6, 'd', 1000);
    
    -- 小王 - 北京
    START TRANSACTION;
    
    -- 小张 - 成都
    COMMIT;

    当前事务开启后,没提交之前,查询不到,提交后可以被查询到。但是,在提交之前其他事务被开启了,那么在这条事务线上,就不会查询到当前有操作事务的连接。相当于开辟出一条单独的线程。

    无论小张是否执行过 COMMIT ,在小王这边,都不会查询到小张的事务记录,而是只会查询到自己所处事务的记录:

    SELECT * FROM user;
    +----+-----------+-------+
    | id | name      | money |
    +----+-----------+-------+
    |  1 | a         |   900 |
    |  2 | b         |  1100 |
    |  3 | 小明      |  1000 |
    |  4 | 淘宝店    |  1000 |
    |  5 | c         |   100 |
    +----+-----------+-------+

    这是因为小王在此之前开启了一个新的事务 ( START TRANSACTION ) ,那么在他的这条新事务的线上,跟其他事务是没有联系的,也就是说,此时如果其他事务正在操作数据,它是不知道的。

    然而事实是,在真实的数据表中,小张已经插入了一条数据。但是小王此时并不知道,也插入了同一条数据,会发生什么呢?

    INSERT INTO user VALUES (6, 'd', 1000);
    -- ERROR 1062 (23000): Duplicate entry '6' for key 'PRIMARY'

    报错了,操作被告知已存在主键为 6 的字段。这种现象也被称为幻读,一个事务提交的数据,不能被其他事务读取到

    串行化

    顾名思义,就是所有事务的写入操作全都是串行化的。什么意思?把隔离级别修改成 SERIALIZABLE :

    SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    SELECT @@GLOBAL.TRANSACTION_ISOLATION;
    +--------------------------------+
    | @@GLOBAL.TRANSACTION_ISOLATION |
    +--------------------------------+
    | SERIALIZABLE                   |
    +--------------------------------+

    还是拿小张和小王来举例:

    -- 小张 - 成都
    START TRANSACTION;
    
    -- 小王 - 北京
    START TRANSACTION;
    
    -- 开启事务之前先查询表,准备操作数据。
    SELECT * FROM user;
    +----+-----------+-------+
    | id | name      | money |
    +----+-----------+-------+
    |  1 | a         |   900 |
    |  2 | b         |  1100 |
    |  3 | 小明      |  1000 |
    |  4 | 淘宝店    |  1000 |
    |  5 | c         |   100 |
    |  6 | d         |  1000 |
    +----+-----------+-------+
    
    -- 发现没有 7 号王小花,于是插入一条数据:
    INSERT INTO user VALUES (7, '王小花', 1000);

    此时会发生什么呢?由于现在的隔离级别是 SERIALIZABLE ( 串行化 ) ,串行化的意思就是:假设把所有的事务都放在一个串行的队列中,那么所有的事务都会按照固定顺序执行,执行完一个事务后再继续执行下一个事务的写入操作 ( 这意味着队列中同时只能执行一个事务的写入操作 ) 。

    根据这个解释,小王在插入数据时,会出现等待状态,直到小张执行 COMMIT 结束它所处的事务,或者出现等待超时。

    推荐

    笔者再次墙裂推荐收藏这篇笔记,收录于hjzCy - sql学习笔记,希望对大家有所帮助。

    最后如果文章和笔记能带您一丝帮助或者启发,请不要吝啬你的赞和收藏,你的肯定是我前进的最大动力??

    查看原文

    赞 37 收藏 34 评论 0

    Eno_Yao 赞了文章 · 4月25日

    与 JavaScript 模块相关的所有知识点

    作者:Dixin

    翻译:疯狂的技术宅

    https://weblogs.asp.net/dixin...

    未经允许严禁转载

    JavaScript 语言最初是为简单的表单操作而发明的,没有诸如模块或命名空间之类的内置功能。多年以来发明了大量的术语、模式、库、语法和工具来模块化 JavaScript。本文讨论了 JavaScript 中的所有主流模块系统、格式、库和工具,包括:

    • JavaScript 模块格式和工具大全

      • IIFE 模块:JavaScript 模块模式

        • IIFE:立即调用的函数表达式
        • 混合导入
      • Revealing 模块:JavaScript 显示模块模式
      • CJS 模块:CommonJS 模块或 Node.js 模块
      • AMD 模块:异步模块定义或 RequireJS 模块

        • 动态加载
        • 来自 CommonJS 模块的 AMD 模块
      • UMD 模块:通用模块定义或 UmdJS 模块

        • 适用于AMD(RequireJS)和本机浏览器的 UMD
        • 适用于AMD(RequireJS)和CommonJS(Node.js)的UMD
      • ES 模块:ECMAScript 2015 或 ES6 模块
      • ES 动态模块:ECMAScript 2020 或 ES11 动态模块
      • 系统模块:SystemJS 模块

        • 动态模块加载
      • Webpack 模块:来自 CJS、AMD、ES 模块的捆绑软件
      • Babel 模块:从 ES 模块转换

        • Babel with SystemJS
      • TypeScript 模块:转换为 CJS、AMD、ES、系统模块

        • 内部模块和命名空间
      • 结论

    希望本文可以帮助你了解和使用 JavaScript/TypeScript 语言,RequireJS/SystemJS 库和 Webpack/Babel 工具等所有这些模式。

    IIFE 模块:JavaScript 模块模式

    在浏览器中,定义 JavaScript 变量就是定义全局变量,这会导致当前网页所加载的全部 JavaScript 文件之间的污染:

    // Define global variables.
    let count = 0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
        console.log("Count is reset.");
    };
    
    // Use global variables.
    increase();
    reset();

    为了避免全局污染,可以用匿名函数来包装代码:

    (() => {
        let count = 0;
        // ...
    });

    显然,这样就不再有任何全局变量。但是定义函数不会在函数内部执行代码。

    IIFE:立即调用的函数表达式

    为了执行函数 f 中的代码,语法是将函数调用 () 作为 f()。为了在匿名函数 (() => {}) 中执行代码,可以将相同的函数调用语法 () 用作 (() => {})

    (() => {
        let count = 0;
        // ...
    })();

    这称为 IIFE(立即调用的函数表达式)。因此,可以通过以下方式定义基本模块:

    // Define IIFE module.
    const iifeCounterModule = (() => {
        let count = 0;
        return {
            increase: () => ++count,
            reset: () => {
                count = 0;
                console.log("Count is reset.");
            }
        };
    })();
    
    // Use IIFE module.
    iifeCounterModule.increase();
    iifeCounterModule.reset();

    它将模块代码包装在 IIFE 中,返回一个对象,这个对象是导出的 API 的占位符。仅引入 1 个全局变量,这是模式名称。之后模块名可用于调用导出的模块 API。这称为 JavaScript 的模块模式。

    混合导入

    定义模块时,可能需要一些依赖关系。使用 IIFE 模块模式,其他所有模块都是全局变量。它们可以在匿名函数内部直接访问,也可以通过匿名函数的参数进行传递:

    // Define IIFE module with dependencies.
    const iifeCounterModule = ((dependencyModule1, dependencyModule2) => {
        let count = 0;
        return {
            increase: () => ++count,
            reset: () => {
                count = 0;
                console.log("Count is reset.");
            }
        };
    })(dependencyModule1, dependencyModule2);

    一些流行库(如 jQuery)的早期版本遵循这种模式。

    revealing module:JavaScript 揭示模块模式

    揭示模块模式由 Christian Heilmann 命名。此模式也是 IIFE,但它强调将所有 API 定义为匿名函数内的局部变量:

    // Define revealing module.
    const revealingCounterModule = (() => {
        let count = 0;
        const increase = () => ++count;
        const reset = () => {
            count = 0;
            console.log("Count is reset.");
        };
    
        return {
            increase,
            reset
        };
    })();
    
    // Use revealing module.
    revealingCounterModule.increase();
    revealingCounterModule.reset();

    用这种语法,当 API 需要相互调用时,将会变得更加容易。

    CJS 模块:CommonJS 模块或 Node.js 模块

    CommonJS(最初名为 ServerJS)是定义和使用模块的模式。它由 Node.js 实现。默认情况下,每个 .js 文件都是 CommonJS 模块。为模块提供了暴露 API 的模块变量和导出变量。并且提供了一个 require 函数来使用模块。以下代码以 CommonJS 语法定义了 counter 模块:

    // Define CommonJS module: commonJSCounterModule.js.
    const dependencyModule1 = require("./dependencyModule1");
    const dependencyModule2 = require("./dependencyModule2");
    
    let count = 0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
        console.log("Count is reset.");
    };
    
    exports.increase = increase;
    exports.reset = reset;
    // Or equivalently:
    module.exports = {
        increase,
        reset
    };

    以下例子使用了 counter 模块:

    // Use CommonJS module.
    const { increase, reset } = require("./commonJSCounterModule");
    increase();
    reset();
    // Or equivelently:
    const commonJSCounterModule = require("./commonJSCounterModule");
    commonJSCounterModule.increase();
    commonJSCounterModule.reset();

    在运行时,Node.js 通过将文件内的代码包装到一个函数中,然后通过参数传递 exports 变量、module 变量和 require 函数来实现这一目的。

    // Define CommonJS module: wrapped commonJSCounterModule.js.
    (function (exports, require, module, __filename, __dirname) {
        const dependencyModule1 = require("./dependencyModule1");
        const dependencyModule2 = require("./dependencyModule2");
    
        let count = 0;
        const increase = () => ++count;
        const reset = () => {
            count = 0;
            console.log("Count is reset.");
        };
    
        module.exports = {
            increase,
            reset
        };
    
        return module.exports;
    }).call(thisValue, exports, require, module, filename, dirname);
    
    // Use CommonJS module.
    (function (exports, require, module, __filename, __dirname) {
        const commonJSCounterModule = require("./commonJSCounterModule");
        commonJSCounterModule.increase();
        commonJSCounterModule.reset();
    }).call(thisValue, exports, require, module, filename, dirname);

    AMD 模块:异步模块定义或 RequireJS 模块

    AMD(Asynchronous Module Definition https://github.com/amdjs/amdj...)是一种定义和使用模块的模式。它由 RequireJS 库(https://requirejs.org/)实现。 AMD 提供了一个定义模块的定义函数,该函数接受模块名称、依赖模块的名称以及工厂函数:

    // Define AMD module.
    define("amdCounterModule", ["dependencyModule1", "dependencyModule2"], (dependencyModule1, dependencyModule2) => {
        let count = 0;
        const increase = () => ++count;
        const reset = () => {
            count = 0;
            console.log("Count is reset.");
        };
    
        return {
            increase,
            reset
        };
    });

    它还提供了 require 函数来使用模块:

    // Use AMD module.
    require(["amdCounterModule"], amdCounterModule => {
        amdCounterModule.increase();
        amdCounterModule.reset();
    });

    AMD 的 require 函数与 CommonJS 的 require 函数完全不同。 AMD 的 require 接受要使用的模块的名称,并将模块传递给函数参数。

    动态加载

    AMD 的 require 函数还有另一个重载。它接受一个回调函数,并将类似 CommonJS 的 require 函数传递给该回调。所以可以通过调用 require 来加载 AMD 模块:

    // Use dynamic AMD module.
    define(require => {
        const dynamicDependencyModule1 = require("dependencyModule1");
        const dynamicDependencyModule2 = require("dependencyModule2");
    
        let count = 0;
        const increase = () => ++count;
        const reset = () => {
            count = 0;
            console.log("Count is reset.");
        };
    
        return {
            increase,
            reset
        };
    });

    来自 CommonJS 模块的 AMD 模块

    上面的 define 函数有一个重载,它可以传递 require 函数,并将变量和模块变量导出到回调中,以便 CommonJS 代码可以在其内部工作:

    // Define AMD module with CommonJS code.
    define((require, exports, module) => {
        // CommonJS code.
        const dependencyModule1 = require("dependencyModule1");
        const dependencyModule2 = require("dependencyModule2");
    
        let count = 0;
        const increase = () => ++count;
        const reset = () => {
            count = 0;
            console.log("Count is reset.");
        };
    
        exports.increase = increase;
        exports.reset = reset;
    });
    
    // Use AMD module with CommonJS code.
    define(require => {
        // CommonJS code.
        const counterModule = require("amdCounterModule");
        counterModule.increase();
        counterModule.reset();
    });

    UMD 模块:通用模块定义或 UmdJS 模块

    UMD(Universal Module Definition,https://github.com/umdjs/umd)是一组棘手的模式,可以使你的代码文件在多种环境中工作。

    适用于 AMD(RequireJS)和本机浏览器的 UMD

    例如以下是一种 UMD 模式,能够使模块定义可用于 AMD(RequireJS)和本机浏览器:

    // Define UMD module for both AMD and browser.
    ((root, factory) => {
        // Detects AMD/RequireJS"s define function.
        if (typeof define === "function" && define.amd) {
            // Is AMD/RequireJS. Call factory with AMD/RequireJS"s define function.
            define("umdCounterModule", ["deependencyModule1", "dependencyModule2"], factory);
        } else {
            // Is Browser. Directly call factory.
            // Imported dependencies are global variables(properties of window object).
            // Exported module is also a global variable(property of window object)
            root.umdCounterModule = factory(root.deependencyModule1, root.dependencyModule2);
        }
    })(typeof self !== "undefined" ? self : this, (deependencyModule1, dependencyModule2) => {
        // Module code goes here.
        let count = 0;
        const increase = () => ++count;
        const reset = () => {
            count = 0;
            console.log("Count is reset.");
        };
    
        return {
            increase,
            reset
        };
    });

    它比较复杂,但仍然只是 IIFE。匿名函数会检测是否存在 AMD 的 define 函数,如果存在,请使用 AMD 的define 函数调用模块工厂。如果不是,它将直接调用模块工厂。目前,root 参数实际上是浏览器的 window 对象。它从全局变量( window 对象的属性)获取依赖项模块。当 factory 返回模块时,返回的模块也被分配给一个全局变量( window 对象的属性)。

    适用于 AMD(RequireJS)和 CommonJS(Node.js)的 UMD

    以下是使模块定义与 AMD(RequireJS)和 CommonJS(Node.js)一起工作的另一种 UMD 模式:

    (define => define((require, exports, module) => {
        // Module code goes here.
        const dependencyModule1 = require("dependencyModule1");
        const dependencyModule2 = require("dependencyModule2");
    
        let count = 0;
        const increase = () => ++count;
        const reset = () => {
            count = 0;
            console.log("Count is reset.");
        };
    
        module.export = {
            increase,
            reset
        };
    }))(// Detects module variable and exports variable of CommonJS/Node.js.
        // Also detect the define function of AMD/RequireJS.
        typeof module === "object" && module.exports && typeof define !== "function"
            ? // Is CommonJS/Node.js. Manually create a define function.
                factory => module.exports = factory(require, exports, module)
            : // Is AMD/RequireJS. Directly use its define function.
                define);

    别怕,这只是一个IIFE。调用IIFE时,将评估其参数。参数评估检测环境(CommonJS / Node.js的模块变量和exports 变量,以及 AMD/RequireJS 的 define 函数)。如果环境是 CommonJS/Node.js,则匿名函数的参数是手动创建的 define 函数。如果环境是 AMD/RequireJS,则匿名函数的参数就是 AMD 的 define 函数。因此,当执行匿名函数时,可以确保它具有有效的 define 函数。在匿名函数内部,它仅调用 define 函数来创建模块。

    ES 模块:ECMAScript 2015 或 ES6 模块

    在所有模块混乱之后,JavaScript 的规范第 6 版在 2015 年定义了完全不同的模块系统和语法。该规范称为ECMAScript 2015 或 ES2015,AKA ECMAScript 6 或 ES6。主要语法是 import 关键字和 export 关键字。以下例子使用新语法演示 ES 模块的命名 import/export 和默认 import/export:

    // Define ES module: esCounterModule.js or esCounterModule.mjs.
    import dependencyModule1 from "./dependencyModule1.mjs";
    import dependencyModule2 from "./dependencyModule2.mjs";
    
    let count = 0;
    // Named export:
    export const increase = () => ++count;
    export const reset = () => {
        count = 0;
        console.log("Count is reset.");
    };
    // Or default export:
    export default {
        increase,
        reset
    };

    要在浏览器中使用此模块文件,请添加 <script> 标签并指定它为模块:<script type="module" data-original="esCounterModule.js"></script>。要在 Node.js 中使用此模块文件,请将其扩展名 .js 改为 .mjs

    // Use ES module.
    // Browser: <script type="module" data-original="esCounterModule.js"></script> or inline.
    // Server: esCounterModule.mjs
    // Import from named export.
    import { increase, reset } from "./esCounterModule.mjs";
    increase();
    reset();
    // Or import from default export:
    import esCounterModule from "./esCounterModule.mjs";
    esCounterModule.increase();
    esCounterModule.reset();

    对于浏览器,可以将 <script>nomodule 属性用于后备:

    <script nomodule>
        alert("Not supported.");
    </script>

    ES 动态模块:ECMAScript 2020 或 ES11 动态模块

    在 2020 年,最新的 JavaScript 规范第 11 版引入了内置函数 import 以动态使用 ES 模块。 import 函数返回一个 promise,因此可以通过其 then 方法调用该模块:

    // Use dynamic ES module with promise APIs, import from named export:
    import("./esCounterModule.js").then(({ increase, reset }) => {
        increase();
        reset();
    });
    // Or import from default export:
    import("./esCounterModule.js").then(dynamicESCounterModule => {
        dynamicESCounterModule.increase();
        dynamicESCounterModule.reset();
    });

    通过返回一个 promise ,显然 import 函数也可以与 await 关键字一起使用:

    // Use dynamic ES module with async/await.
    (async () => {
    
        // Import from named export:
        const { increase, reset } = await import("./esCounterModule.js");
        increase();
        reset();
    
        // Or import from default export:
        const dynamicESCounterModule = await import("./esCounterModule.js");
        dynamicESCounterModule.increase();
        dynamicESCounterModule.reset();
    
    })();

    以下是来自 https://developer.mozilla.org... 的导入、动态导入、导出的兼容性列表:

    import 兼容性

    export 兼容性

    系统模块:SystemJS 模块

    SystemJS 是一个库,可以为较旧的 ES5 启用 ES6 模块语法。例如以下模块以 ES6 语法定义:

    // Define ES module.
    import dependencyModule1 from "./dependencyModule1.js";
    import dependencyModule2 from "./dependencyModule2.js";
    dependencyModule1.api1();
    dependencyModule2.api2();
    
    let count = 0;
    // Named export:
    export const increase = function () { return ++count };
    export const reset = function () {
        count = 0;
        console.log("Count is reset.");
    };
    // Or default export:
    export default {
        increase,
        reset
    }

    如果当前运行时(例如旧的浏览器)不支持 ES6 语法,则以上代码将无法正常工作。 SystemJS 可以将模块定义转换为对库 API 的调用——System.register

    // Define SystemJS module.
    System.register(["./dependencyModule1.js", "./dependencyModule2.js"], function (exports_1, context_1) {
        "use strict";
        var dependencyModule1_js_1, dependencyModule2_js_1, count, increase, reset;
        var __moduleName = context_1 && context_1.id;
        return {
            setters: [
                function (dependencyModule1_js_1_1) {
                    dependencyModule1_js_1 = dependencyModule1_js_1_1;
                },
                function (dependencyModule2_js_1_1) {
                    dependencyModule2_js_1 = dependencyModule2_js_1_1;
                }
            ],
            execute: function () {
                dependencyModule1_js_1.default.api1();
                dependencyModule2_js_1.default.api2();
                count = 0;
                // Named export:
                exports_1("increase", increase = function () { return ++count };
                exports_1("reset", reset = function () {
                    count = 0;
                    console.log("Count is reset.");
                };);
                // Or default export:
                exports_1("default", {
                    increase,
                    reset
                });
            }
        };
    });

    这样新的 ES6 语法 import/export 就消失了。

    动态模块加载

    SystemJS 还提供了用于动态导入的 import 函数:

    // Use SystemJS module with promise APIs.
    System.import("./esCounterModule.js").then(dynamicESCounterModule => {
        dynamicESCounterModule.increase();
        dynamicESCounterModule.reset();
    });

    Webpack 模块:来自 CJS,AMD,ES 模块的捆绑包

    Webpack 是模块的捆绑器。它使用将组合的 CommonJS 模块、AMD 模块和 ES 模块转换为和谐模块模式,并将所有代码捆绑到一个文件中。例如以下 3 个文件中用 3 种不同的语法定义了 3 个模块:

    // Define AMD module: amdDependencyModule1.js
    define("amdDependencyModule1", () => {
        const api1 = () => { };
        return {
            api1
        };
    });
    
    // Define CommonJS module: commonJSDependencyModule2.js
    const dependencyModule1 = require("./amdDependencyModule1");
    const api2 = () => dependencyModule1.api1();
    exports.api2 = api2;
    
    // Define ES module: esCounterModule.js.
    import dependencyModule1 from "./amdDependencyModule1";
    import dependencyModule2 from "./commonJSDependencyModule2";
    dependencyModule1.api1();
    dependencyModule2.api2();
    
    let count = 0;
    const increase = () => ++count;
    const reset = () => {
        count = 0;
        console.log("Count is reset.");
    };
    
    export default {
        increase,
        reset
    }

    以下文件使用了 counter 模块:

    // Use ES module: index.js
    import counterModule from "./esCounterModule";
    counterModule.increase();
    counterModule.reset();

    Webpack 可以将以上所有文件打包在一起,即使它们位于 3 个不同的模块系统中,也都能打包为一个文件 main.js

    • root

      • dist

        • main.js (捆绑 src 下的所有文件)
      • src

        • amdDependencyModule1.js
        • commonJSDependencyModule2.js
        • esCounterModule.js
        • index.js
      • webpack.config.js

    有趣的是,Webpack 本身使用 CommonJS 模块语法。在webpack.config.js 中:

    const path = require('path');
    
    module.exports = {
        entry: './src/index.js',
        mode: "none", // Do not optimize or minimize the code for readability.
        output: {
            filename: 'main.js',
            path: path.resolve(__dirname, 'dist'),
        },
    };

    现在运行以下命令以不同的语法转换和捆绑 4 个文件:

    npm install webpack webpack-cli --save-dev
    npx webpack --config webpack.config.js

    重新格式化了以下捆绑文件 main.js,并重命名了变量以提高可读性:

    (function (modules) { // webpackBootstrap
        // The module cache
        var installedModules = {};
        // The require function
        function require(moduleId) {
            // Check if module is in cache
            if (installedModules[moduleId]) {
                return installedModules[moduleId].exports;
    
            }
            // Create a new module (and put it into the cache)
            var module = installedModules[moduleId] = {
                i: moduleId,
                l: false,
                exports: {}
    
            };
            // Execute the module function
            modules[moduleId].call(module.exports, module, module.exports, require);
            // Flag the module as loaded
            module.l = true;
            // Return the exports of the module
            return module.exports;
        }
    
        // expose the modules object (__webpack_modules__)
        require.m = modules;
        // expose the module cache
        require.c = installedModules;
        // define getter function for harmony exports
        require.d = function (exports, name, getter) {
            if (!require.o(exports, name)) {
                Object.defineProperty(exports, name, { enumerable: true, get: getter });
    
            }
    
        };
        // define __esModule on exports
        require.r = function (exports) {
            if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
                Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    
            }
            Object.defineProperty(exports, '__esModule', { value: true });
    
        };
        // create a fake namespace object
        // mode & 1: value is a module id, require it
        // mode & 2: merge all properties of value into the ns
        // mode & 4: return value when already ns object
        // mode & 8|1: behave like require
        require.t = function (value, mode) {
            if (mode & 1) value = require(value);
            if (mode & 8) return value;
            if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
            var ns = Object.create(null);
            require.r(ns);
            Object.defineProperty(ns, 'default', { enumerable: true, value: value });
            if (mode & 2 && typeof value != 'string') for (var key in value) require.d(ns, key, function (key) { return value[key]; }.bind(null, key));
            return ns;
        };
        // getDefaultExport function for compatibility with non-harmony modules
        require.n = function (module) {
            var getter = module && module.__esModule ?
                function getDefault() { return module['default']; } :
                function getModuleExports() { return module; };
            require.d(getter, 'a', getter);
            return getter;
        };
        // Object.prototype.hasOwnProperty.call
        require.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
        // __webpack_public_path__
        require.p = "";
        // Load entry module and return exports
        return require(require.s = 0);
    })([
        function (module, exports, require) {
            "use strict";
            require.r(exports);
            // Use ES module: index.js.
            var esCounterModule = require(1);
            esCounterModule["default"].increase();
            esCounterModule["default"].reset();
        },
        function (module, exports, require) {
            "use strict";
            require.r(exports);
            // Define ES module: esCounterModule.js.
            var amdDependencyModule1 = require.n(require(2));
            var commonJSDependencyModule2 = require.n(require(3));
            amdDependencyModule1.a.api1();
            commonJSDependencyModule2.a.api2();
    
            let count = 0;
            const increase = () => ++count;
            const reset = () => {
                count = 0;
                console.log("Count is reset.");
            };
    
            exports["default"] = {
                increase,
                reset
            };
        },
        function (module, exports, require) {
            var result;
            !(result = (() => {
                // Define AMD module: amdDependencyModule1.js
                const api1 = () => { };
                return {
                    api1
                };
            }).call(exports, require, exports, module),
                result !== undefined && (module.exports = result));
        },
        function (module, exports, require) {
            // Define CommonJS module: commonJSDependencyModule2.js
            const dependencyModule1 = require(2);
            const api2 = () => dependencyModule1.api1();
            exports.api2 = api2;
        }
    ]);

    同样,它只是一个 IIFE。所有 4 个文件的代码都转换为 4 个函数中的代码。并且这 4 个函数作为参数传递给匿名函数。

    Babel 模块:从 ES 模块转换

    Babel 是另一个为旧版环境(如旧版浏览器)把 ES6 + JavaScript 代码转换为旧版语法的编译器。可以将 ES6 import/export 语法中的上述 counter 模块转换为以下替换了新语法的 babel 模块:

    // Babel.
    Object.defineProperty(exports, "__esModule", {
        value: true
    });
    exports["default"] = void 0;
    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
    
    // Define ES module: esCounterModule.js.
    var dependencyModule1 = _interopRequireDefault(require("./amdDependencyModule1"));
    var dependencyModule2 = _interopRequireDefault(require("./commonJSDependencyModule2"));
    dependencyModule1["default"].api1();
    dependencyModule2["default"].api2();
    
    var count = 0;
    var increase = function () { return ++count; };
    var reset = function () {
        count = 0;
        console.log("Count is reset.");
    };
    
    exports["default"] = {
        increase: increase,
        reset: reset
    };

    这是 index.js 中使用 counter 模块的代码:

    // Babel.
    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
    
    // Use ES module: index.js
    var esCounterModule = _interopRequireDefault(require("./esCounterModule.js"));
    esCounterModule["default"].increase();
    esCounterModule["default"].reset();

    这是默认的转译。 Babel 还可以与其他工具一起使用。

    Babel 与 SystemJS

    SystemJS 可以用作 Babel 的插件:

    npm install --save-dev @babel/plugin-transform-modules-systemjs

    并将其添加到 Babel 配置中:

    {
        "plugins": ["@babel/plugin-transform-modules-systemjs"],
        "presets": [
            [
                "@babel/env",
                {
                    "targets": {
                        "ie": "11"
                    }
                }
            ]
        ]
    }

    现在 Babel 可以与 SystemJS 一起使用以转换 CommonJS/Node.js 模块、AMD/RequireJS 模块和 ES 模块:

    npx babel src --out-dir lib

    结果是:

    root

    • lib

      • amdDependencyModule1.js (与 SystemJS 一起编译)
      • commonJSDependencyModule2.js (与 SystemJS 一起编译)
      • esCounterModule.js (与 SystemJS 一起编译)
      • index.js (与 SystemJS 一起编译)
    • src

      • amdDependencyModule1.js
      • commonJSDependencyModule2.js
      • esCounterModule.js
      • index.js
    • babel.config.json

    现在所有 ADM、CommonJS 和 ES 模块语法都被转换为 SystemJS 语法:

    // Transpile AMD/RequireJS module definition to SystemJS syntax: lib/amdDependencyModule1.js.
    System.register([], function (_export, _context) {
        "use strict";
        return {
            setters: [],
            execute: function () {
                // Define AMD module: src/amdDependencyModule1.js
                define("amdDependencyModule1", () => {
                    const api1 = () => { };
    
                    return {
                        api1
                    };
                });
            }
        };
    });
    
    // Transpile CommonJS/Node.js module definition to SystemJS syntax: lib/commonJSDependencyModule2.js.
    System.register([], function (_export, _context) {
        "use strict";
        var dependencyModule1, api2;
        return {
            setters: [],
            execute: function () {
                // Define CommonJS module: src/commonJSDependencyModule2.js
                dependencyModule1 = require("./amdDependencyModule1");
    
                api2 = () => dependencyModule1.api1();
    
                exports.api2 = api2;
            }
        };
    });
    
    // Transpile ES module definition to SystemJS syntax: lib/esCounterModule.js.
    System.register(["./amdDependencyModule1", "./commonJSDependencyModule2"], function (_export, _context) {
        "use strict";
        var dependencyModule1, dependencyModule2, count, increase, reset;
        return {
            setters: [function (_amdDependencyModule) {
                dependencyModule1 = _amdDependencyModule.default;
            }, function (_commonJSDependencyModule) {
                dependencyModule2 = _commonJSDependencyModule.default;
            }],
            execute: function () {
                // Define ES module: src/esCounterModule.js.
                dependencyModule1.api1();
                dependencyModule2.api2();
                count = 0;
    
                increase = () => ++count;
    
                reset = () => {
                    count = 0;
                    console.log("Count is reset.");
                };
    
                _export("default", {
                    increase,
                    reset
                });
            }
        };
    });
    
    // Transpile ES module usage to SystemJS syntax: lib/index.js.
    System.register(["./esCounterModule"], function (_export, _context) {
        "use strict";
        var esCounterModule;
        return {
            setters: [function (_esCounterModuleJs) {
                esCounterModule = _esCounterModuleJs.default;
            }],
            execute: function () {
                // Use ES module: src/index.js
                esCounterModule.increase();
                esCounterModule.reset();
            }
        };
    });

    TypeScript模块:转换为CJS、AMD、ES、系统模块

    TypeScript 支持 ES 模块语法(https://www.typescriptlang.or...),根据 tsconfig.json 中指定的 transpiler 选项,可以将其保留为 ES6 或转换为其他格式,包括 CommonJS/Node.js、AMD/RequireJS、UMD/UmdJS 或 System/SystemJS:

    {
        "compilerOptions": {
            "module": "ES2020", // None, CommonJS, AMD, System, UMD, ES6, ES2015, ES2020, ESNext.
        }
    }

    例如:

    // TypeScript and ES module.
    // With compilerOptions: { module: "ES6" }. Transpile to ES module with the same import/export syntax.
    import dependencyModule from "./dependencyModule";
    dependencyModule.api();
    let count = 0;
    export const increase = function () { return ++count };
    
    
    // With compilerOptions: { module: "CommonJS" }. Transpile to CommonJS/Node.js module:
    var __importDefault = (this && this.__importDefault) || function (mod) {
        return (mod && mod.__esModule) ? mod : { "default": mod };
    };
    exports.__esModule = true;
    
    var dependencyModule_1 = __importDefault(require("./dependencyModule"));
    dependencyModule_1["default"].api();
    var count = 0;
    exports.increase = function () { return ++count; };
    
    // With compilerOptions: { module: "AMD" }. Transpile to AMD/RequireJS module:
    var __importDefault = (this && this.__importDefault) || function (mod) {
        return (mod && mod.__esModule) ? mod : { "default": mod };
    };
    define(["require", "exports", "./dependencyModule"], function (require, exports, dependencyModule_1) {
        "use strict";
        exports.__esModule = true;
    
        dependencyModule_1 = __importDefault(dependencyModule_1);
        dependencyModule_1["default"].api();
        var count = 0;
        exports.increase = function () { return ++count; };
    });
    
    // With compilerOptions: { module: "UMD" }. Transpile to UMD/UmdJS module:
    var __importDefault = (this && this.__importDefault) || function (mod) {
        return (mod && mod.__esModule) ? mod : { "default": mod };
    };
    (function (factory) {
        if (typeof module === "object" && typeof module.exports === "object") {
            var v = factory(require, exports);
            if (v !== undefined) module.exports = v;
        }
        else if (typeof define === "function" && define.amd) {
            define(["require", "exports", "./dependencyModule"], factory);
        }
    })(function (require, exports) {
        "use strict";
        exports.__esModule = true;
    
        var dependencyModule_1 = __importDefault(require("./dependencyModule"));
        dependencyModule_1["default"].api();
        var count = 0;
        exports.increase = function () { return ++count; };
    });
    
    // With compilerOptions: { module: "System" }. Transpile to System/SystemJS module:
    System.register(["./dependencyModule"], function (exports_1, context_1) {
        "use strict";
        var dependencyModule_1, count, increase;
        var __moduleName = context_1 && context_1.id;
        return {
            setters: [
                function (dependencyModule_1_1) {
                    dependencyModule_1 = dependencyModule_1_1;
                }
            ],
            execute: function () {
                dependencyModule_1["default"].api();
                count = 0;
                exports_1("increase", increase = function () { return ++count; });
            }
        };
    });

    这在 TypeScript 中称为外部模块。

    内部模块和命名空间

    TypeScript还具有一个 module 关键字和一个 namespace 关键字。它们被称为内部模块:

    module Counter {
        let count = 0;
        export const increase = () => ++count;
        export const reset = () => {
            count = 0;
            console.log("Count is reset.");
        };
    }
    
    namespace Counter {
        let count = 0;
        export const increase = () => ++count;
        export const reset = () => {
            count = 0;
            console.log("Count is reset.");
        };
    }

    它们都被转换为 JavaScript 对象:

    var Counter;
    (function (Counter) {
        var count = 0;
        Counter.increase = function () { return ++count; };
        Counter.reset = function () {
            count = 0;
            console.log("Count is reset.");
        };
    })(Counter || (Counter = {}));

    通过支持 . 分隔符,TypeScript 模块和命名空间可以有多个级别:

    module Counter.Sub {
        let count = 0;
        export const increase = () => ++count;
    }
    
    namespace Counter.Sub {
        let count = 0;
        export const increase = () => ++count;
    }

    它们被转换为对象的属性:

    var Counter;
    (function (Counter) {
        var Sub;
        (function (Sub) {
            var count = 0;
            Sub.increase = function () { return ++count; };
        })(Sub = Counter.Sub || (Counter.Sub = {}));
    })(Counter|| (Counter = {}));

    TypeScript 模块和命名空间也可以在 export 语句中使用:

    module Counter {
        let count = 0;
        export module Sub {
            export const increase = () => ++count;
        }
    }
    
    module Counter {
        let count = 0;
        export namespace Sub {
            export const increase = () => ++count;
        }
    }

    编译后 sub 模块和 sub 命名空间相同:

    var Counter;
    (function (Counter) {
        var count = 0;
        var Sub;
        (function (Sub) {
            Sub.increase = function () { return ++count; };
        })(Sub = Counter.Sub || (Counter.Sub = {}));
    })(Counter || (Counter = {}));

    结论

    欢迎使用 JavaScript,它具有如此丰富的功能——仅用于模块化/命名空间的就有 10 多种系统和格式:

    1. IIFE module:JavaScript 模块模式
    2. 揭示模块:JavaScript 揭示模块模式
    3. CJS模块:CommonJS 模块或 Node.js 模块
    4. AMD 模块:异步模块定义或 RequireJS 模块
    5. UMD 模块:通用模块定义或 UmdJS 模块
    6. ES 模块:ECMAScript 2015 或 ES6 模块
    7. ES 动态模块:ECMAScript 2020 或 ES11 动态模块
    8. 系统模块:SystemJS 模块
    9. Webpack 模块:CJS、AMD、ES 模块的移植和捆绑
    10. Babel 模块:可移植 ES 模块
    11. TypeScript模块 和命名空间

    幸运的是,现在 JavaScript 有模块的标准内置语言功能,并且 Node.js 和所有最新的现代浏览器都支持它。对于较旧的环境,你仍然可以用新的 ES 模块语法进行编码,然后用 Webpack/Babel/SystemJS/TypeScript 转换为较旧或兼容的语法。


    本文首发微信公众号:前端先锋

    欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

    欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

    欢迎继续阅读本专栏其它高赞文章:


    查看原文

    赞 11 收藏 9 评论 0

    认证与成就

    • 获得 1640 次点赞
    • 获得 11 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 10 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    • Piano

      ??用键盘8个键演奏一首蒲公英的约定送给996的自己或月亮代表我的心给七夕的她,非常简单~

    • Omi Snippets

      编写React和Omi单文件组件的VSC语法高亮插件??

    • Omil

      基于.omi单文件组件的webpack模块加载器??

    • articles

      ??My Learning Notes and Memories - 分享我的学习片段和与你的回忆

    • compile-hero

      ??Chrome Extension For Automatic Evaluation And Visual Studio Code Extension For Compiling Language

    注册于 2016-02-18
    个人主页被 7.1k 人浏览

    bt365体育投注