语义化版本控制规范(SemVer)

摘要

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。
    先行版本号及版本编译元数据可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

规范

以下关键词 MUST、MUST NOT、REQUIRED、SHALL、SHALL NOT、SHOULD、SHOULD NOT、 RECOMMENDED、MAY、OPTIONAL 依照 RFC 2119 的叙述解读。(译注:为了保持语句顺畅, 以下文件遇到的关键词将依照整句语义进行翻译,在此先不进行个别翻译。)

  1. 使用语义化版本控制的软件必须(MUST)定义公共 API。该 API 可以在代码中被定义或出现于严谨的文件内。无论何种形式都应该力求精确且完整。

  2. 标准的版本号必须(MUST)采用 X.Y.Z 的格式,其中 X、Y 和 Z 为非负的整数,且禁止(MUST NOT)在数字前方补零。X 是主版本号、Y 是次版本号、而 Z 为修订号。每个元素必须(MUST)以数值来递增。例如:1.9.1 -> 1.10.0 -> 1.11.0。

  3. 标记版本号的软件发行后,禁止(MUST NOT)改变该版本软件的内容。任何修改都必须(MUST)以新版本发行。

  4. 主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。

  5. 1.0.0 的版本号用于界定公共 API 的形成。这一版本之后所有的版本号更新都基于公共 API 及其修改内容。

  6. 修订号 Z(x.y.Z | x > 0)必须(MUST)在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改。

  7. 次版本号 Y(x.Y.z | x > 0)必须(MUST)在有向下兼容的新功能出现时递增。在任何公共 API 的功能被标记为弃用时也必须(MUST)递增。也可以(MAY)在内部程序有大量新功能或改进被加入时递增,其中可以(MAY)包括修订级别的改变。每当次版本号递增时,修订号必须(MUST)归零。

  8. 主版本号 X(X.y.z | X > 0)必须(MUST)在有任何不兼容的修改被加入公共 API 时递增。其中可以(MAY)包括次版本号及修订级别的改变。每当主版本号递增时,次版本号和修订号必须(MUST)归零。

  9. 先行版本号可以(MAY)被标注在修订版之后,先加上一个连接号再加上一连串以句点分隔的标识符来修饰。标识符必须(MUST)由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成,且禁止(MUST NOT)留白。数字型的标识符禁止(MUST NOT)在前方补零。先行版的优先级低于相关联的标准版本。被标上先行版本号则表示这个版本并非稳定而且可能无法满足预期的兼容性需求。范例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。

  10. 版本编译元数据可以(MAY)被标注在修订版或先行版本号之后,先加上一个加号再加上一连串以句点分隔的标识符来修饰。标识符必须(MUST)由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成,且禁止(MUST NOT)留白。当判断版本的优先层级时,版本编译元数据可(SHOULD)被忽略。因此当两个版本只有在版本编译元数据有差别时,属于相同的优先层级。范例:1.0.0-alpha+001、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。

  11. 版本的优先层级指的是不同版本在排序时如何比较。
    判断优先层级时,必须(MUST)把版本依序拆分为主版本号、次版本号、修订号及先行版本号后进行比较(版本编译元数据不在这份比较的列表中)。
    由左到右依序比较每个标识符,第一个差异值用来决定优先层级:主版本号、次版本号及修订号以数值比较,例如:1.0.0 < 2.0.0 < 2.1.0 < 2.1.1。
    当主版本号、次版本号及修订号都相同时,改以优先层级比较低的先行版本号决定。例如:1.0.0-alpha < 1.0.0。
    有相同主版本号、次版本号及修订号的两个先行版本号,其优先层级必须(MUST)透过由左到右的每个被句点分隔的标识符来比较,直到找到一个差异值后决定:
    只有数字的标识符以数值高低比较,有字母或连接号时则逐字以 ASCII 的排序来比较。
    数字的标识符比非数字的标识符优先层级低。
    若开头的标识符都相同时,栏位比较多的先行版本号优先层级比较高。
    范例:1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0。

为什么要使用语义化的版本控制?

这并不是一个新的或者革命性的想法。实际上,你可能已经在做一些近似的事情了。问题在于只是“近似”还不够。如果没有某个正式的规范可循,版本号对于依赖的管理并无实质意义。将上述的想法命名并给予清楚的定义,让你对软件使用者传达意向变得容易。一旦这些意向变得清楚,弹性(但又不会太弹性)的依赖规范就能达成。

举个简单的例子就可以展示语义化的版本控制如何让依赖地狱成为过去。假设有个名为“救火车”的函式库,它需要另一个名为“梯子”并已经有使用语义化版本控制的包。当救火车创建时,梯子的版本号为 3.1.0。因为救火车使用了一些版本 3.1.0 所新增的功能, 你可以放心地指定依赖于梯子的版本号大等于 3.1.0 但小于 4.0.0。这样,当梯子版本 3.1.1 和 3.2.0 发布时,你可以将直接它们纳入你的包管理系统,因为它们能与原有依赖的软件兼容。

作为一位负责任的开发者,你理当确保每次包升级的运作与版本号的表述一致。现实世界是复杂的,我们除了提高警觉外能做的不多。你所能做的就是让语义化的版本控制为你提供一个健全的方式来发行以及升级包,而无需推出新的依赖包,节省你的时间及烦恼。
如果你对此认同,希望立即开始使用语义化版本控制,你只需声明你的函式库正在使用它并遵循这些规则就可以了。请在你的 README 文件中保留此页连结,让别人也知道这些规则并从中受益。

FAQ

在 0.y.z 初始开发阶段,我该如何进行版本控制?

最简单的做法是以 0.1.0 作为你的初始化开发版本,并在后续的每次发行时递增次版本号。

如何判断发布 1.0.0 版本的时机?

当你的软件被用于正式环境,它应该已经达到了 1.0.0 版。如果你已经有个稳定的 API 被使用者依赖,也会是 1.0.0 版。如果你很担心向下兼容的问题,也应该算是 1.0.0 版了。

这不会阻碍快速开发和迭代吗?

主版本号为零的时候就是为了做快速开发。如果你每天都在改变 API,那么你应该仍在主版本号为零的阶段(0.y.z),或是正在下个主版本的独立开发分支中。

对于公共 API,若即使是最小但不向下兼容的改变都需要产生新的主版本号,岂不是很快就达到 42.0.0 版?

这是开发的责任感和前瞻性的问题。不兼容的改变不应该轻易被加入到有许多依赖代码的软件中。升级所付出的代价可能是巨大的。要递增主版本号来发行不兼容的改版,意味着你必须为这些改变所带来的影响深思熟虑,并且评估所涉及的成本及效益比。

为整个公共 API 写文件太费事了!

为供他人使用的软件编写适当的文件,是你作为一名专业开发者应尽的职责。保持专案高效一个非常重要的部份是掌控软件的复杂度,如果没有人知道如何使用你的软件或不知道哪些函数的调用是可靠的,要掌控复杂度会是困难的。长远来看,使用语义化版本控制以及对于公共 API 有良好规范的坚持,可以让每个人及每件事都运行顺畅。

万一不小心把一个不兼容的改版当成了次版本号发行了该怎么办?

一旦发现自己破坏了语义化版本控制的规范,就要修正这个问题,并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况,也不能去修改已发行的版本。可以的话,将有问题的版本号记录到文件中,告诉使用者问题所在,让他们能够意识到这是有问题的版本。

如果我更新了自己的依赖但没有改变公共 API 该怎么办?

由于没有影响到公共 API,这可以被认定是兼容的。若某个软件和你的包有共同依赖,则它会有自己的依赖规范,作者也会告知可能的冲突。要判断改版是属于修订等级或是次版等级,是依据你更新的依赖关系是为了修复问题或是加入新功能。对于后者,我经常会预期伴随着更多的代码,这显然会是一个次版本号级别的递增。

如果我变更了公共 API 但无意中未遵循版本号的改动怎么办呢?(意即在修订等级的发布中,误将重大且不兼容的改变加到代码之中)

自行做最佳的判断。如果你有庞大的使用者群在依照公共 API 的意图而变更行为后会大受影响,那么最好做一次主版本的发布,即使严格来说这个修复仅是修订等级的发布。记住, 语义化的版本控制就是透过版本号的改变来传达意义。若这些改变对你的使用者是重要的,那就透过版本号来向他们说明。

我该如何处理即将弃用的功能?

弃用现存的功能是软件开发中的家常便饭,也通常是向前发展所必须的。当你弃用部份公共 API 时,你应该做两件事:(1)更新你的文件让使用者知道这个改变,(2)在适当的时机将弃用的功能透过新的次版本号发布。在新的主版本完全移除弃用功能前,至少要有一个次版本包含这个弃用信息,这样使用者才能平顺地转移到新版 API。

语义化版本对于版本的字串长度是否有限制呢?

没有,请自行做适当的判断。举例来说,长到 255 个字元的版本已过度夸张。再者,特定的系统对于字串长度可能会有他们自己的限制。

第1章 Emscripten 快速入门

1.1 使用emsdk命令行工具安装Emscripten

Emscripten包含了将C/C++代码编译为WebAssembly所需的完整工具集(LLVM、Node.js、Python、Java等),它不依赖于任何其他的编译器环境。

emsdk是一组基于Python 2的脚本,Mac系统自带Python,无需安装

1.1.1 新建一个demo文件夹,然后通过git下载安装工具包

git clone https://github.com/juj/emsdk.git

1.1.2 安装并激活

进入到安装包的文件夹,分别执行以下命令

git pull
./emsdk install latest
./emsdk activate latest

把emcc命令添加至环境变量,新建一个控制台,切换至emsdk所在的目录,执行以下命令:

source ./emsdk_env.sh

注意:每次重新登陆或者新建 Shell 窗口,都要执行一次这行命令source ./emsdk_env.sh,然后本控制台窗口才能使用emcc命令。

1.1.3 验证安装是否成功

emcc是Emscripten的核心命令,正确安装激活并设置环境变量后,执行emcc -v可以查看版本信息:

> emcc -v
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 2.0.17
clang version 13.0.0 (/opt/s/w/ir/cache/git/chromium.googlesource.com-external-github.com-llvm-llvm--project 3b677b81cec7b3c5132aee8fccc30252d87deb69)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /Users/liuxiaofan/WorkSpace/vipkid/emscripten/emsdk/upstream/bin

1.2 Hello, wolrd!

先跑一个最基本的demo

1.2.1 生成wasm

新建一个名为hello.cc的C源文件,为了正确标识中文字符串,将其保存为UTF8编码:

//hello.cc
#include <stdio.h>

int main() {
    printf("Hello, wolrd!");
    return 0;
}

执行编译命令生成wasm文件:

emcc hello.cc

目录下将得到两个文件:a.out.wasm以及a.out.js。其中a.out.wasm为C源文件编译后形成的WebAssembly汇编文件;a.out.js是Emscripten生成的胶水代码。

使用-o选项可以指定emcc的输出文件,执行下列命令:

emcc hello.cc -o hello.js

注意:现在如果改变了hello.cc文件再次编译的话会报错。因为编译出的wasm默认情况下不会退出运行时,所以每次编译的时候加上-s EXIT_RUNTIME=1 即可。如下:

emcc index.cc -o index.js -s EXIT_RUNTIME=1

1.2.2 在网页中测试
新建一个index.html的网页文件:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Emscripten:你好,世界!</title>
  </head>
  <body>
    <script src="hello.js"></script>
  </body>
</html>

本地起一个web服务,让localhost的目录指向本demo的目录,然后打开控制发现成功打印出来Hello, wolrd!
例如全局安装live-server,然后执行live-server --port=8888启动一个本地服务

npm i -g live-server
live-server --port=8888

1.2.3 在Node.js中测试

Emscripten自带了Node.js环境,因此我们可以直接使用node来测试刚才的程序:

> node hello.js
Hello, wolrd!

1.3 胶水代码初探

简单了解一下Emscripten生成的JavaScript胶水代码hello.js

1.3.1 WebAssembly汇编模块载入

WebAssembly汇编模块(既.wasm文件)的载入后经过一系列的操作,最终被实例化,然后赋值了给全局对象Module的子对象asm。

function receiveInstance(instance, module) {
      ... ...
      Module['asm'] = exports;
      console.log(Module['asm']);  //我们在这添加一行Log在控制台里面打印出来看看这个对象里面都有哪些属性和方法
      ... ...

我当前的版本打印出来的结果是这样的,以后随着版本的升级可能会有变化。注意里面有个main方法

1.3.2 导出函数封装

在Emscripten中,C函数导出时,函数名前会添加下划线“_”,由此可知上述代码分别提供了main()以及malloc()函数的封装;我们可以在浏览器控制台中手动执行_main()以及Module._main()对此进行检验:如果报错,请忽略,接着往下看就明白了。

1.3.3 异步加载

经过我的测验,直接在控制台中执行_main()以及Module._main()会报错

Assertion failed: you need to wait for the runtime to be ready (e.g. wait for main() to be called)

解决的基本思路是在Module初始化前,向Module中注入一个名为onRuntimeInitialized的方法,Emscripten的Runtime就绪后,将会回调该方法。

<body>
    <script>
      Module = {};
      Module.onRuntimeInitialized = function() {
        //do sth.
        Module._main();
      }
    </script>
    <script src="hello.js"></script>
  </body>

如上面的代码,先监听RuntimeInitialized,再执行Module._main()就好了。

mac下使用n来切换指定Node版本

目前市面上有两种工具可以切换Node版本,我强烈推荐使用n,因为安装和使用更方便。

安装方法:

sudo npm install -g n 

或者

brew install n 

使用方法:

安装最新的node版本

sudo n latest 

安装指定的node版本,例如12.18.2

sudo n 12.18.2 

切换,控制台输入n然后选择要切换的版本即可

sudo n 

package-lock.json的作用

网上搜索package-lock.json的作用,有大把的文章介绍,我这里就不详细介绍了。
我要说的这个文件要解决什么问题?

当我们clone下来一个项目的时候,在执行了npm install之后发现各种报错,项目跑不起来,有没有?是不是装完依赖包之后发现版本都变高了?
那么怎么锁住版本呢?
package-lock.json就是用来锁依赖包版本用的。
如果项目中有这个文件,那么执行npm install 时改为 npm install --no-save 就不会升级版本了。

No Xcode or CLT version detected 解决办法

Mac系统,在用npm安装依赖包的时候总会报这个错误:gyp: No Xcode or CLT version detected!
原因是Xcode命令行工具有问题,需要删掉重装。

1 查看安装路径

xcode-select --print-path

默认的路径应该是/Library/Developer/CommandLineTools

2 删除

sudo rm -r -f /Library/Developer/CommandLineTools

3 重新安装

xcode-select --install

这个工具包大小400多m很快就能装完

排列组合算法公式JS版

之前做过的彩票项目中用到了排列和组合公式,分享给需要的人。

/**
         * 排列结果  包括重复内容
         */
        arrCPermute: function (arr, num) {
            var r = [];
            (function f(t, a, n) {
                if (n == 0) return r.push(t);
                for (var i = 0, l = a.length - n; i <= l; i++) {
                    f(t.concat(a[i]), a.slice(i + 1), n - 1);
                }
            })([], arr, num);
            return r;
        },
        /**
         *  排列结果  不包括重复内容
         */
        arrCPermute1: function (arr, num) {
            var r = [];
            (function f(t, a, n) {
                if (n == 0) return r.push(t);
                for (var i = 0, l = a.length; i < l; i++) {
                    f(t.concat(a[i]), a.slice(0, i).concat(a.slice(i + 1)), n - 1);
                }
            })([], arr, num);
            return r;
        },
        /**
         * 排列公式(从n个不同元素中取出m个元素的组合数)
         * @param n
         * @param m
         * @returns {number}
         */
        mathP: function (n, m) {
            var c = 1;
            for (var i = n - m; i < n; c *= ++i);
            return c;
        },
        /**
         * 组合公式(从n个不同元素中取出m个元素的组合数)
         * @param n
         * @param m
         * @returns {number}
         */
        mathC: function (n, m) {
            var n1 = 1,
                n2 = 1;
            for (var i = n, j = 1; j <= m; n1 *= i--, n2 *= j++);
            return n1 / n2;
        },

双色球计算组合时可以这样使用

lotNum = util.mathC(redNum, 5) * util.mathC(blueNum, 2); //红球的组合*篮球的组合

 

微信小程序手写签名组件

业务需求:在微信小程序内的商家入驻协议页面的下面设置一个空白区域,可以让用户在手机屏幕上进行手指滑动,从而实现手写签名的功能。

设计思路:
1 创建一个canvas对象
2 监听用户手指触摸事件bindtouchstart="start"
3 开始触摸时保存坐标x,y
4 触摸移动时bindtouchmove="move" 执行绘制操作ctx.stroke
5 保存图片通过wx.canvasToTempFilePath把临时路径返回出去用户上传接口使用

全部代码:

WXML

<view>
   <canvas 
    class="c-autograph"
    style="width:{{canvasW}}px;height:{{canvasH}}px;"
    canvas-id="autograph" 
    bindtouchcancel="cancel" 
    bindtouchmove="move" 
    bindtouchstart="start" 
    bindtouchend="end" 
    bindlongtap="longtap" 
    disable-scroll="true" 
    binderror="error"
    ></canvas>
</view>

JS

/*
 * @Author: liuxiaofan
 * @Description: v2.0.0
 * @Date: 2020-03-10 15:56:34
 * @LastEditors: liuxiaofan
 * @LastEditTime: 2020-04-20 15:40:55
 */
// 采坑: 如果html标签里面写了 type="2d",会导致真机空白
var ctx = null; //canvas实例
var touchs = [];
Component({
    properties: {

    },
    data: {
        canvasW: 140,
        canvasH: 56,
        lineColor: '#333', // 笔划的颜色
        LineWidth: 3, //笔划粗细
        count: 0 //画了几笔
    },
    ready() {
        this.autoWidth()
        this.init()
    },
    methods: {
        autoWidth() {
            const dpr = wx.getSystemInfoSync().pixelRatio // 获取设备的屏幕分辨率是2x还是3x
            let canvasW, canvasH;
            if (dpr >= 2) {
                canvasW = this.data.canvasW * 2
                canvasH = this.data.canvasH * 2
            }
            if (dpr >= 3) {
                canvasW = this.data.canvasW * 2.21
                canvasH = this.data.canvasH * 2.21
            }
            
            // 最近小高度100
            if (canvasH < 100) {
                canvasH = 100
            }
            
            if (canvasW < 280) {
                canvasW = 280
            }

            this.setData({
                canvasW,
                canvasH,
            })
        },
        init: function (options) {
            ctx = wx.createCanvasContext('autograph', this); // 如果是组件的形式,第2个参数要写this
            ctx.setStrokeStyle(this.data.lineColor);
            ctx.setLineWidth(this.data.LineWidth);
            ctx.setLineCap('round'); // 笔触形态
            ctx.setLineJoin('round'); // 交叉点形态
        },
        // 画布的触摸移动开始手势响应
        start: function (event) {
            // console.log("触摸开始" + event.changedTouches[0].x);
            // console.log("触摸开始" + event.changedTouches[0].y);
            //获取触摸开始的 x,y
            let point = { x: event.changedTouches[0].x, y: event.changedTouches[0].y }
            touchs.push(point);
        },
        // 画布的触摸移动手势响应
        move: function (e) {
            let point = { x: e.touches[0].x, y: e.touches[0].y }
            touchs.push(point);
            if (touchs.length >= 2) {
                this.draw(touchs);
            }
        },
        // 画布的触摸移动结束手势响应
        end: function (e) {
            this.setData({
                count: this.data.count + 1
            })
            // console.info("触摸结束" + this.data.count);
            //清空轨迹数组
            for (let i = 0; i < touchs.length; i++) {
                touchs.pop();
            }

        },
        // 画布的长按手势响应
        longtap: function (e) {
            // console.log("长按手势" + e);
        },
        cancel() { },
        error: function (e) {
            // console.log("画布触摸错误" + e);
        },
        //绘制
        draw: function (touchs) {
            let point1 = touchs[0];
            let point2 = touchs[1];
            // console.info(point1, point2)
            touchs.shift();
            ctx.moveTo(point1.x, point1.y);
            ctx.lineTo(point2.x, point2.y);
            ctx.stroke();
            ctx.draw(true);
        },
        //清除操作
        clearClick: function () {
            //清除画布
            ctx.clearRect(0, 0, 750, 700);
            ctx.draw(true);
            this.setData({
                count: 0
            })
        },
        //保存图片
        saveClick: function (callback) {
            if (this.data.count < 3) {
                wx.showModal({
                    title: '提示',
                    content: '签名不符合规范,请重新签名',
                    showCancel: false
                })
            } else {
                wx.canvasToTempFilePath({
                    canvasId: 'autograph',
                    fileType: 'jpg',
                    destWidth: 800,
                    destHeight: 500,
                    success: function (res) {
                        callback && callback(res.tempFilePath) //把图片路径回调出去
                    },
                    fail(res) {
                        console.info('保存失败', res)
                    }
                }, this)
            }

        }
    }
})

在父组件内的使用方式

this.selectComponent("#autograph").saveClick(function (tempFilePath) {
           that.creatMerchant(tempFilePath)

       })

 

粘性定位

postion 属性值共有6个:

  1. position: static // 默认值
  2. position: inherit // 继承父元素的postion值
  3. position: relative // 相对定位,遵循文档流
  4. position: absolute // 绝对定位,脱离文档流
  5. position: fixed // 固定定位
  6. position: sticky // 粘性定位

sticky是CSS规范中新加入的一个属性值。
这个属性值的作用表现为当一个列表元素位于一个可滚动区域内时,当滚动到特定的位置,例如顶部的时候,就会触发fixed特性,固定在顶部这样。
粘性定位的应用场景随处可见,例如segmentfault的文章页面中,顶部的导航条就使用了sticky。或者很多新闻类的app里面的粘性标题栏等等。

代码示例:

HTML

<main>
       <header>标题一</header>
       <div class="content">
       </div>
       <header>标题二</header>
       <div class="content">
       </div>
       <header>标题三</header>
       <div class="content">
       </div>
       <header>标题四</header>
       <div class="content">
       </div>
   </main>

CSS

main {
    width: 400px;
    height: 300px;
    margin: 200px auto;
    overflow: auto;
}
header {
    position: sticky;
    top: 0;
    height: 30px;
    background-color: blanchedalmond;
}

JS

let num = 20
let html = ''
for (var i = 0; i < num; i++) {
    html += `<p>${i + 1}</p>`
}
let contentsDom = document.getElementsByClassName('content')
let contentsArr = [...contentsDom]
contentsArr.forEach(item => {
    item.innerHTML = html
})

 

将NodeList(类数组)转换为数组(Array)

原生DOM元素通常使用document下的querySelectorAll、getElementsByClassName等方法获取。
这样的DOM对象被称为NodeList。不能使用数组的方法进行操作,例如forEach()。

NodeList 不是一个数组,是一个类似数组的对象(Like Array Object)。虽然 NodeList 不是一个数组,但是可以使用 Array.from() 将其转换为数组。
除了Array.from之外还可以使用下面3种方式转换

  • Array.apply(null,contentsDom)
  • Array.prototype.slice.call(contentsDom)
  • [...contentsDom]

代码示例:

let num = 20
let html = ''
for (var i = 0; i < num; i++) {
    html += `<p>${i + 1}</p>`
}
let contentsDom = document.getElementsByClassName('content')
let contentsArr=Arry.from(contentsDom)
let contentsArr=Array.apply(null,contentsDom)
let contentsArr=Array.prototype.slice.call(contentsDom)
let contentsArr = [...contentsDom]

contentsArr.forEach(item => {
    item.innerHTML = html
})

LeetCode: 206 反转链表

示例:
1->2->3->4->5->null
5->4->3->2->1->null

解题思路:
1. 先思考如何反转2个节点,将n+1的next指向n
2. 双指针遍历链表,然后重复上面的动作

解题步骤:
1. 双指针遍历
2. 反转双指针

var reverseList = function (head) {
    let p1 = head
    let p2 = null
    while (p1) {
        const tmp = p1.next
        p1.next = p2
        p2 = p1
        p1 = tmp
    }
    return p2
};