• 博文
  • 归档
  • js遍历那些事儿

    2024-08-02

    次访问

    本文总结js中的各种常用遍历。

    基本用法

    for

    最基本的遍历写法,通过索引来遍历。

    1
    2
    3
    4
    5
    let arr = [1, 2, 3]
    for(let i = 0; i < arr.length; i++){
    console.log(i) // 索引,数组下标 0, 1, 2
    console.log(arr[i]) // 数组下标所对应的元素 1, 2, 3
    }

    forEach

    写法比for简便,遍历所有元素,不返回任何值。

    1
    2
    let arr = [1, 2, 3]
    arr.forEach(i => console.log(i)) // 1, 2, 3

    map

    映射,常用于基于原数组返回新数组(不改变原数组)。

    1
    2
    3
    4
    let arr = [1, 2, 3]
    const newArr = arr.map(i => i + 1)
    console.log(arr) // [1, 2, 3]
    console.log(newArr) // [2, 3, 4]

    filter

    筛选,常用于过滤原数组来产生新数组,不改变原数组。

    1
    2
    3
    4
    let arr = [1, 2, 3]
    const newArr = arr.filter(i => i > 1)
    console.log(arr) // [1, 2, 3]
    console.log(newArr) // [2, 3]

    以上4种遍历基本满足大部分需求,下面看一些用的较少的遍历方法。

    reduce

    将数组中的元素逐个进行处理,并将它们合并为一个值。功能十分强大,回调函数可以进行各种复杂的操作,包括条件判断、对象构建等。

    语法:

    1
    array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
    • callback (执行数组中每个值的函数,包含四个参数)
      • total- 必需 (初始值, 或者计算结束后的返回值)
      • currentValue - 必需 (数组中当前被处理的元素)
      • currentIndex - 可选 (当前元素在数组中的索引)
      • arr - 可选 (调用 reduce 的数组)
    • initialValue - 可选 (作为第一次调用 callback 的第一个参数,如果不提供,第一次回调会使用数组的第一个元素)

    示例(最简单的累加):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let arr = [1, 2, 3]
    const sum = arr.reduce(function(prev, cur, index, arr) {
    console.log(prev, cur, index)
    return prev + cur
    })
    console.log(arr, sum)

    // 输出:
    // 1 2 1
    // 3 3 2
    // [1, 2, 3] 6

    比如我们要统计字符串中每个字母出现的次数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const arrString = 'abcdaabc'

    arrString.split('').reduce(function(res, cur) {
    res[cur] = (res[cur] || 0) + 1
    return res
    }, {})

    // 输出:
    // {a: 3, b: 2, c: 2, d: 1}

    every

    条件都满足返回true。

    1
    2
    3
    4
    5
    let arr = [1, 2, 3]
    const res = arr.every((item, index) => {
    return item > 1
    })
    console.log(res) // false

    some

    有一个条件满足返回true。

    1
    2
    3
    4
    5
    let arr = [1, 2, 3]
    const res = arr.some((item, index) => {
    return item > 1
    })
    console.log(res) // true

    for…in

    主要用于遍历对象的可枚举属性(还会遍历原型链上的可枚举属性)。

    遍历数组(不推荐,会遍历数组的所有可枚举属性,包括非索引属性和原型链上的属性):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let arr = [1, 2, 3]
    for(let index in arr) {
    console.log(index, arr[index])
    }

    // 输出:
    // 0 1
    // 1 2
    // 2 3

    遍历对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let obj = { a: 1, b: 2, c: 3 }
    for(let key in obj) {
    console.log(key, obj[key])
    }

    // 输出:
    // a: 1
    // b: 2
    // c: 3

    for…of

    用于遍历可迭代对象(例如 Array, Map, Set, String, TypedArray,NodeList以及其他 DOM 集合,arguments 对象等)的可迭代属性(不可迭代属性会被忽略)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const arr = [1, 2, 3];

    for (const value of arr) {
    console.log(value);
    }

    // 输出:
    // 1
    // 2
    // 3

    for...in和for...of的区别:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Object.prototype.objCustom = function () {}
    Array.prototype.arrCustom = function () {}

    let iterable = [3, 5, 7]
    iterable.foo = "hello"

    for (let i in iterable) {
    console.log(i) // 0, 1, 2, "foo", "arrCustom", "objCustom"
    }

    for (let i of iterable) {
    console.log(i) // 3, 5, 7
    }

    可以看到,主要有3点不同:

    • 1.for...in遍历key,for...of遍历value
    • 2.for...in遍历的是可枚举属性,for...of遍历的是可迭代属性
    • 3.对于array的不可迭代元属性objCustom、arrCustom和实例属性foo,在for...of循环中都被忽略

    Object.keys,values,entries

    对于普通对象(需要注意和map的区别):

    • Object.keys(obj):返回一个包含该对象所有的键的数组。
    • Object.values(obj):返回一个包含该对象所有的值的数组。
    • Object.entries(obj):返回一个包含该对象所有 [key, value] 键值对的数组。
    1
    2
    3
    4
    5
    6
    7
    let user = {
    name: "John",
    age: 30,
    }
    console.log(Object.keys(user)) // ["name", "age"]
    console.log(Object.values(user)) // ["John", 30]
    console.log(Object.entries(user)) // [ ["name","John"], ["age",30] ]

    Object.entries把 obj 变成由键/值对组成的数组,然后使用 Object.fromEntries可以将结果转回成原来的对应

    1
    2
    3
    4
    5
    6
    7
    let user = {
    name: "John",
    age: 30,
    }

    console.log(Object.entries(user)) // [ ["name","John"], ["age",30] ]
    console.log(Object.fromEntries(Object.entries(user))) // { name: 'John', age: 30 }

    中断循环

    中断循环,推荐使用break,continue不退出循环,只跳过当前循环,if条件判断替换continue,可读性更高。
    for循环(for/for...in/for...of)可通过break退出循环。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    let arr = [1, 2, 3]
    for (let i = 0; i < arr.length; i++) {
    console.log(i)
    if(i === 1) break
    }

    // 输出:
    // 0
    // 1


    for (let value of arr) {
    console.log(value)
    if(value === 1) break
    }

    // 输出:
    // 1

    let obj = { a: 1, b: 2, c: 3 }
    for(let key in obj) {
    console.log(key, obj[key])
    if (key === 'a') break
    }

    // 输出:
    // a 1

    try {
    let arr = [1, 2, 3]
    arr.forEach(i => {
    console.log(i)
    if(i === 1) {
    throw new Error('End Iterative')
    }
    })
    } catch(e) {
    console.log(e)
    }

    // 输出:
    // 11
    // Error: End Iterative

    总结,有for关键字时,可以通过break退出循环,forEach可以通过throw Error的方式退出循环(但不推荐这样写,推荐用for)。

    异步

    当遍历碰到async、 await、 Promise时,又该怎么写呢?

    1
    2
    3
    4
    let arr = [20, 10, 30]
    const sleep = (ms) => {
    return new Promise((resolve) => setTimeout(resolve, ms))
    }

    上面有个数组arr和耗时操作sleep,如果没有耗时操作,就是下面同步的写法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const syncRes = arr.map((i) => {
    console.log('loop', i)
    console.log(i)
    return i
    })

    console.log('syncRes', syncRes)

    // 输出:
    // loop 20
    // 20
    // loop 10
    // 10
    // loop 30
    // 30
    // syncRes [20, 10, 30]

    如果有耗时操作,像下面这样,怎么保持输出还是[20, 10, 30]呢?

    1
    2
    await sleep(i)
    return i

    最简单的方法可以用for循环(for/for...in/for...of都一样):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let asyncRes1 = []
    for (let i of arr){
    console.log('loop', i)
    await sleep(i)
    console.log(i)
    asyncRes1.push(i)
    }
    console.log('asyncRes1', asyncRes1)

    // 输出:
    // loop 20
    // 20
    // loop 10
    // 10
    // loop 30
    // 30
    // asyncRes1 [20, 10, 30]

    map可以像这样Promise.all(arr.map(async (...) => ...)):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const asyncRes = await Promise.all(arr.map(async (i) => {
    console.log('loop', i)
    await sleep(i)
    console.log(i)
    return i
    }))

    console.log('asyncRes', asyncRes)

    // 输出:
    // loop 20
    // loop 10
    // loop 30
    // 10
    // 20
    // 30
    // asyncRes [20, 10, 30]

    如果不用Promise.all,就是下面的效果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const asyncRes = arr.map(async (i) => {
    console.log('loop', i)
    await sleep(i)
    console.log(i)
    return i
    })

    console.log('asyncRes', asyncRes)

    // 输出:
    // loop 20
    // loop 10
    // loop 30
    // asyncRes [Promise, Promise, Promise]
    // 10
    // 20
    // 30

    碰到reduce时async (prev, cur) => await prev:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const asyncRes = await arr.reduce(async (prev, cur) => {
    console.log('loop', cur)
    await sleep(cur)
    const res = (await prev) + cur
    console.log(res)
    return res
    }, 0)

    console.log('asyncRes', asyncRes)

    // 输出:
    // loop 20
    // loop 10
    // loop 30
    // 20
    // 30
    // 60
    // asyncRes 60

    在forEach、filter等其他循环中需要使用异步,先用map、reduce、for...of处理。

    总结

    • 基本用法要熟记特性,灵活选用
    • 需要中断就用for(for/for...in/for...of),使用break退出
    • 碰到异步用for(for/for...in/for...of)/map/reduce

    参考文档

    • Asynchronous array functions in Javascript
    ...more
  • 详解CSS Grid布局

    2022-11-18

    次访问

    原文地址

    CSS Grid 布局简介

    CSS Grid是一种二维的布局方式,和以前的布局方式完全不同,CSS常用来给页面布局,但做的总不够好,以前我们用table布局,后来用float、position、inline-block,这些方式本质上都是hack,缺少一些重要的特性(比如垂直居中),flex布局是一个很好的布局方式,但它基于轴的布局方式在其他方面用处更大,并且可以和Grid布局很好的配合使用,Grid布局是专门用来解决我们一直以来布局页面时所碰到的各种问题的。

    本指南围绕Grid布局的最新特性讲解,所以不会考虑老旧的浏览器兼容性。

    CSS Grid基础

    到2017年3月,大部分浏览器都支持Grid布局(无需前缀):Chrome(包括Android)、Firefox、Safari(包括IOS)、Opera。IE10和IE11也能通过一定的途径支持,所以现在是时候使用Grid布局了。

    首先你需要一个父元素,并设置dispaly: grid,通过grid-template-columns和grid-template-rows设置列和行的大小,然后向父元素中添加子元素,并设置grid-column和grid-row属性,和flex布局类似,子元素的原始顺序并不重要,CSS可以任意控制它们的顺序,这使得通过媒体查询排列元素变得超级简单。想象下你把整个页面设置成grid布局,然后对不同的屏幕宽度自适应时完全重新排列,只需要几行CSS。Grid是有史以来引入的最强大的CSS模块之一。

    重要的CSS Grid术语

    在深入学习Grid布局之前,先了解一些重要的术语,它们有些相似,如果不先了解,后面就比较容易混淆,不过不用担心,这些概念并不多。

    Grid Container

    应用display: grid的元素,是所有grid items的直接父级。下面的例子中,container就是grid container。

    1
    2
    3
    4
    5
    <div class="container">
    <div class="item item-1"> </div>
    <div class="item item-2"> </div>
    <div class="item item-3"> </div>
    </div>

    Grid Item

    Grid Container的子元素,下面的例子中,item元素就是grid item,但sub-item不是。

    1
    2
    3
    4
    5
    6
    7
    <div class="container">
    <div class="item"> </div>
    <div class="item">
    <p class="sub-item"> </p>
    </div>
    <div class="item"> </div>
    </div>

    Grid Line

    Grid Line是用来构成网格布局的分隔线。它们可以是垂直的(“列网格线”)或水平的(“行网格线”),位于行或列的任意一侧。这里的黄线是列网格线的一个例子。

    terms-grid-line

    Grid Cell

    Grid Cell是相邻两行和相邻两列之间的区域,称为网格单元,下图黄色部分是行网格线1和2,列网格线2和3之间的网格单元。
    terms-grid-cell

    Grid Track

    Grid Track是相邻网格线之间的区域,你可以理解成一行或一列网格。下图黄色部分是第2和第3行网格线之间的Grid Track。
    terms-grid-track

    Grid Area

    Grid Area是由4条网格线围起来的区域。一个Grid Area可以由若干个网格单元组成,下图黄色部分的是行网格线1和3,列网格线1和3围起来的Grid Area。
    terms-grid-area

    CSS Grid属性

    CSS Grid属性分为2类,分别是用在父元素和子元素上的。

    用在父元素Grid Container上的属性

    • display
    • grid-template-columns
    • grid-template-rows
    • grid-template-areas
    • grid-template
    • grid-column-gap
    • grid-row-gap
    • grid-gap
    • justify-items
    • align-items
    • place-items
    • justify-content
    • align-content
    • place-content
    • grid-auto-columns
    • grid-auto-rows
    • grid-auto-flow
    • grid

    1、display

    1
    2
    3
    .container {
    display: grid | inline-grid;
    }

    可取值:

    • grid——生成块级grid
    • inline-grid——生成行内grid

    2、grid-template-columns、grid-template-rows
    用一行空格分开的值来定义网格的行和列,这些值代表Grid Track的大小,空格代表分隔线Grid Line。

    • track-size —— 可以是一个长度、百分比、或者fr单位的值
    • line-name —— 你对网格线的命名
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    .container {
    grid-template-columns: ... ...;
    /* e.g.
    1fr 1fr
    minmax(10px, 1fr) 3fr
    repeat(5, 1fr)
    50px auto 100px 1fr
    */
    grid-template-rows: ... ...;
    /* e.g.
    min-content 1fr min-content
    100px 1fr max-content
    */
    }

    网格线自动取正值,-1是最后一行的备选值。
    template-columns-rows-01

    但是你也可以直接给网格线命名,注意名称时的括号语法:

    1
    2
    3
    4
    .container {
    grid-template-columns: [first] 40px [line2] 50px [line3] auto [col4-start] 50px [five] 40px [end];
    grid-template-rows: [row1-start] 25% [row1-end] 100px [third-line] auto [last-line];
    }

    template-columns-rows-02

    需要注意的是分隔线可以有多个名字。比如下面第二条线有2个名字:row1-end 和 row2-start

    1
    2
    3
    .container {
    grid-template-rows: [row1-start] 25% [row1-end row2-start] 25% [row2-end];
    }

    如果需要定义重复的部分,可以用repeat()来简化:

    1
    2
    3
    .container {
    grid-template-columns: repeat(3, 20px [col-start]);
    }

    上面的写法和下面的等价:

    1
    2
    3
    .container {
    grid-template-columns: 20px [col-start] 20px [col-start] 20px [col-start];
    }

    如果多条分割线使用同样的名字,则可以通过名字加计数来区分。

    1
    2
    3
    .item {
    grid-column-start: col-start 2;
    }

    fr单位可以把父元素空闲部分按比例划分给Grid Track。比如下面的写法会把Grid Track设为父元素宽度的三分之一。

    1
    2
    3
    .container {
    grid-template-columns: 1fr 1fr 1fr;
    }

    空闲部分是非自适应元素计算完毕后剩下的空间。比如下面的例子中,对fr可用的空间不包括50px:

    1
    2
    3
    .container {
    grid-template-columns: 1fr 50px 1fr 1fr;
    }

    3、grid-template-areas
    grid-template-areas用名字定义网格区域,重复网格名字使该区域包括覆盖的网格单元,句号代表空网格单元。语法本身让网格结构十分明了。

    可取值:

    • grid-template-areas —— 网格区域名字
    • . —— 空的网格单元
    • none —— 未定义网格区域
    1
    2
    3
    4
    5
    .container {
    grid-template-areas:
    "<grid-area-name> | . | none | ..."
    "...";
    }

    举个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    .item-a {
    grid-area: header;
    }
    .item-b {
    grid-area: main;
    }
    .item-c {
    grid-area: sidebar;
    }
    .item-d {
    grid-area: footer;
    }

    .container {
    display: grid;
    grid-template-columns: 50px 50px 50px 50px;
    grid-template-rows: auto;
    grid-template-areas:
    "header header header header"
    "main main . sidebar"
    "footer footer footer footer";
    }

    上面的代码会创建一个3行4列的网格区域,第一行是header区域,中间的一行由2块main区域、1块空单元和1块sidebar区域组成,最后一行是footer区域。

    dddgrid-template-areas

    每一行的网格单元数量要相同。可以用任意数量的紧挨着的句号来声明一个空单元格。只要句号之间没有空格,它们就代表一个空单元格。

    需要注意的是,grid-template-areas命名的是区域,当使用该属性时,区域两端的线会被自动命名。如果网格区域被命名成foo,该区域的第1行和第1列分隔线都会自动名称成foo-start,最后1行和最后1列的分隔线被命名成foo-end,这也意味着一些分隔线会有多个名字,比如上面的例子中,有一条分隔线有3个名字:header-start, main-start, 和 footer-start。

    4、grid-template
    grid-template可以把grid-template-rows,grid-template-columns和 grid-template-areas简写到一起。

    可取值:

    • none —— 3个属性都设为默认值。
    • <grid-template-rows> / <grid-template-columns> —— 分别设置grid-template-rows和grid-template-columns的值,并把grid-template-areas设为none。
    1
    2
    3
    .container {
    grid-template: none | <grid-template-rows> / <grid-template-columns>;
    }

    它的值还可以更复杂,但使用起来更方便:

    1
    2
    3
    4
    5
    6
    .container {
    grid-template:
    [row1-start] "header header header" 25px [row1-end]
    [row2-start] "footer footer footer" 25px [row2-end]
    / auto 50px auto;
    }

    上面的写法和下面的等价:

    1
    2
    3
    4
    5
    6
    7
    .container {
    grid-template-rows: [row1-start] 25px [row1-end row2-start] 25px [row2-end];
    grid-template-columns: auto 50px auto;
    grid-template-areas:
    "header header header"
    "footer footer footer";
    }

    由于grid-template不会重置隐式网格属性(grid-auto-columns, grid-auto-rows和grid-auto-flow),这可能是您在大多数情况下想要做的,因此建议使用grid属性而不是grid-template。

    5、column-gap、row-gap、grid-column-gap、grid-row-gap
    定义分隔线的粗细。可以理解成设置行或列的间距。

    • <line-size> —— 长度值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    .container {
    /* 新写法 */
    column-gap: <line-size>;
    row-gap: <line-size>;

    /* 旧写法 */
    grid-column-gap: <line-size>;
    grid-row-gap: <line-size>;
    }

    举个例子:

    1
    2
    3
    4
    5
    6
    .container {
    grid-template-columns: 100px 50px 100px;
    grid-template-rows: 80px auto 80px;
    column-gap: 10px;
    row-gap: 15px;
    }

    dddgrid-gap

    间距只在行或列中间存在,边缘部分不存在。

    需要注意的是,grid前缀会被移除,grid-column-gap 和 grid-row-gap 会被重命名成 column-gap 和 row-gap,无前缀的语法已经被Chrome 68+, Safari 11.2 Release 50+, and Opera 54+支持。

    6、gap、grid-gap
    row-gap 和 column-gap的简写形式。

    可取值:

    • <grid-row-gap> <grid-column-gap> —— 长度值
    1
    2
    3
    4
    5
    6
    7
    .container {
    /* 新写法 */
    gap: <grid-row-gap> <grid-column-gap>;

    /* 旧写法 */
    grid-gap: <grid-row-gap> <grid-column-gap>;
    }

    举个例子:

    1
    2
    3
    4
    5
    .container {
    grid-template-columns: 100px 50px 100px;
    grid-template-rows: 80px auto 80px;
    gap: 15px 10px;
    }

    如果没有定义row-gap的值,它会自动等于column-gap。

    7、justify-items
    grid items的水平对齐方式,适用container中所有的grid items。

    可取值:

    • start —— 向单元格的起始边对齐。
    • end —— 向单元格的尾部对齐。
    • center —— 单元格内居中。
    • stretch —— 宽度填满单元格(这是默认值)。
    1
    2
    3
    .container {
    justify-items: start | end | center | stretch;
    }

    举个例子:

    1
    2
    3
    .container {
    justify-items: start;
    }

    justify-items-start

    1
    2
    3
    .container {
    justify-items: end;
    }

    justify-items-end

    1
    2
    3
    .container {
    justify-items: center;
    }

    justify-items-center

    1
    2
    3
    .container {
    justify-items: stretch;
    }

    justify-items-stretch

    该对齐方式也可以用justify-self单独给grid items设置。

    8、align-items
    grid items的垂直对齐方式,值和用法和justify-items一样。该对齐方式也可以用align-self单独给grid items设置。

    9、place-items
    align-items 和 justify-items的简写方式。

    可取值:

    • <align-items> / <justify-items> —— 第1格值代表align-items,第2个值代表justify-items,如果第2个值省略了,第1个值会默认代表这2个属性。
    1
    2
    3
    4
    .center {
    display: grid;
    place-items: center;
    }

    10、justify-content
    有时整体网格的大小比容器小,比如所有的网格都是用px指定了大小,这时可以设置网格在容器中的对齐方式,justify-content设置网格的水平对齐方式。

    可取值:

    • start —— 向容器的头部对齐。
    • end —— 向容器的尾部对齐。
    • center —— 在容器中水平居中。
    • stretch —— 重新适配items的大小把容器的宽度填满。
    • space-around —— 使每个网格的列间距相等,两边的留白是列间距的一半。
    • space-between —— 使每个网格的列间距相等,两边不留白。
    • space-evenly —— 使每个网格的列间距相等,两边的留白等于列间距。
    1
    2
    3
    .container {
    justify-content: start | end | center | stretch | space-around | space-between | space-evenly;
    }

    举个例子:

    1
    2
    3
    .container {
    justify-content: start;
    }

    justify-content-start

    1
    2
    3
    .container {
    justify-content: end;
    }

    justify-content-end

    1
    2
    3
    .container {
    justify-content: center;
    }

    justify-content-center

    1
    2
    3
    .container {
    justify-content: stretch;
    }

    justify-content-stretch

    1
    2
    3
    .container {
    justify-content: space-around;
    }

    justify-content-space-around

    1
    2
    3
    .container {
    justify-content: space-between;
    }

    justify-content-space-between

    1
    2
    3
    .container {
    justify-content: space-evenly;
    }

    justify-content-space-evenly

    11、align-content
    值和用法和justify-content一样,区别是align-content用来设置垂直方向的对齐方式。

    12、place-content
    justify-content和align-content的简写方式。

    可取值:

    • <align-content> / <justify-content>
      13、grid-auto-columns、grid-auto-rows
      指定任何自动生成的grid tracks的大小。

    用法:

    1
    2
    3
    4
    .container {
    grid-auto-columns: <track-size> ...;
    grid-auto-rows: <track-size> ...;
    }

    举个例子,来看看隐式grid tracks是怎么创建的。

    1
    2
    3
    4
    .container {
    grid-template-columns: 60px 60px;
    grid-template-rows: 90px 90px;
    }

    grid-auto-columns-rows-01

    上面的代码创建了一个2x2的网格。
    接下来用grid-column和grid-row设置item的位置:

    1
    2
    3
    4
    5
    6
    7
    8
    .item-a {
    grid-column: 1 / 2;
    grid-row: 2 / 3;
    }
    .item-b {
    grid-column: 5 / 6;
    grid-row: 2 / 3;
    }

    grid-auto-columns-rows-02

    这时item-b就超出了2x2的网格范围,超出部分会默认创建隐式的grid tracks,但是这些隐式的grid tracks的宽度是0,这时我们就可以通过grid-auto-columns和grid-auto-rows为它们设置宽度:

    1
    2
    3
    .container {
    grid-auto-columns: 60px;
    }

    grid-auto-columns-rows-03

    14、grid-auto-flow
    grid-auto-flow控制item的自动排列方式。

    可取值:

    • row —— 按顺序填充每一行,必要时新增行。
    • column —— 按顺序填充每一列,必要时新增列。
    • dense —— 较小的item排在前面。
    1
    2
    3
    .container {
    grid-auto-flow: row | column | row dense | column dense;
    }

    注意dense只是虚拟的改变items的顺序,这可能导致顺序混乱。

    举个例子:

    1
    2
    3
    4
    5
    6
    7
    <section class="container">
    <div class="item-a">item-a</div>
    <div class="item-b">item-b</div>
    <div class="item-c">item-c</div>
    <div class="item-d">item-d</div>
    <div class="item-e">item-e</div>
    </section>

    接着你定义了一个2x5的网格,并把grid-auto-flow设为row(row也是默认值):

    1
    2
    3
    4
    5
    6
    .container {
    display: grid;
    grid-template-columns: 60px 60px 60px 60px 60px;
    grid-template-rows: 30px 30px;
    grid-auto-flow: row;
    }

    在给item定位的时候,你只设置了2个:

    1
    2
    3
    4
    5
    6
    7
    8
    .item-a {
    grid-column: 1;
    grid-row: 1 / 3;
    }
    .item-e {
    grid-column: 5;
    grid-row: 1 / 3;
    }

    这时我们的网格看起来就是这样的:

    grid-auto-flow-01

    如果我们把grid-auto-flow设为column,item-b, item-c 和 item-d 就会按顺序沿着列来排:

    1
    2
    3
    4
    5
    6
    .container {
    display: grid;
    grid-template-columns: 60px 60px 60px 60px 60px;
    grid-template-rows: 30px 30px;
    grid-auto-flow: column;
    }

    grid-auto-flow-02

    15、grid
    可以把 grid-template-rows, grid-template-columns, grid-template-areas, grid-auto-rows, grid-auto-columns, 和 grid-auto-flow 写到一起。

    可取值:

    • none —— 所有属性设为默认值。
    • <grid-template> —— 跟 grid-template 一样。
    • <grid-template-rows> / [ auto-flow && dense? ] <grid-auto-columns>?
    • [ auto-flow && dense? ] <grid-auto-rows>? / <grid-template-columns>
      比如下面两种写法等价(<grid-template-rows> / <grid-auto-columns>):
    1
    2
    3
    4
    5
    6
    7
    8
    .container {
    grid: 100px 300px / 3fr 1fr;
    }

    .container {
    grid-template-rows: 100px 300px;
    grid-template-columns: 3fr 1fr;
    }

    下面的两种写法等价(auto-flow <grid-auto-rows> / <grid-template-columns>):

    1
    2
    3
    4
    5
    6
    7
    8
    .container {
    grid: auto-flow / 200px 1fr;
    }

    .container {
    grid-auto-flow: row;
    grid-template-columns: 200px 1fr;
    }

    下面的两种写法等价(auto-flow dense <grid-auto-rows> / <grid-template-columns>):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    .container {
    grid: auto-flow dense 100px / 1fr 2fr;
    }

    .container {
    grid-auto-flow: row dense;
    grid-auto-rows: 100px;
    grid-template-columns: 1fr 2fr;
    }

    下面的两种写法等价(<grid-template-rows> / auto-flow <grid-auto-columns>):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    .container {
    grid: 100px 300px / auto-flow 200px;
    }

    .container {
    grid-template-rows: 100px 300px;
    grid-auto-flow: column;
    grid-auto-columns: 200px;
    }

    还有更复杂但更简洁的写法,下面的两种写法等价:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    .container {
    grid: [row1-start] "header header header" 1fr [row1-end]
    [row2-start] "footer footer footer" 25px [row2-end]
    / auto 50px auto;
    }

    .container {
    grid-template-areas:
    "header header header"
    "footer footer footer";
    grid-template-rows: [row1-start] 1fr [row1-end row2-start] 25px [row2-end];
    grid-template-columns: auto 50px auto;
    }

    用在子元素Grid Item上的属性

    • grid-column-start
    • grid-column-end
    • grid-row-start
    • grid-row-end
    • grid-column
    • grid-row
    • grid-area
    • justify-self
    • align-self
    • place-self

    1、grid-column-start、grid-column-end、grid-row-start、grid-row-end
    通过指定分隔线的方式定义网格单元在网格中的位置, grid-column-start/grid-row-start是网格单元开始的地方,grid-column-end/grid-row-end是结束的地方。

    可取值:

    • <line> —— 代表分隔线的数字或者名字。
    • span <number> —— 包括指定数量的网格单元。
    • span <name> —— 包括单元格直到碰到指定名字的分隔线。
    • auto —— 自动放置,默认1个单元格。
      举个例子:
    1
    2
    3
    4
    5
    6
    .item-a {
    grid-column-start: 2;
    grid-column-end: five;
    grid-row-start: row1-start;
    grid-row-end: 3;
    }

    grid-column-row-start-end-01

    1
    2
    3
    4
    5
    6
    .item-b {
    grid-column-start: 1;
    grid-column-end: span col4-start;
    grid-row-start: 2;
    grid-row-end: span 2;
    }

    grid-column-row-start-end-02

    如果没有指定grid-column-end/grid-row-end,item默认包含1个单元。

    item还能彼此覆盖,可以用z-index控制层级。

    2、grid-column、grid-row
    grid-column-start、grid-column-end、grid-row-start、grid-row-end的简写形式。

    可取值:

    • <start-line> / <end-line> —— 写法和非简写值一样。
    1
    2
    3
    4
    .item {
    grid-column: <start-line> / <end-line> | <start-line> / span <value>;
    grid-row: <start-line> / <end-line> | <start-line> / span <value>;
    }

    举个例子:

    1
    2
    3
    4
    .item-c {
    grid-column: 3 / span 2;
    grid-row: third-line / 4;
    }

    grid-column-row

    3、grid-area
    grid-area给item命名,这样使用grid-template-areas可以直接引用item的名字。并且此属性还可以作为 grid-row-start+grid-column-start+grid-row-end+grid-column-end的简写形式。

    可取值:

    • <name> —— item的名字。
    • <row-start> / <column-start> / <row-end> / <column-end> —— 代表分隔线的数字或名字。
    1
    2
    3
    .item {
    grid-area: <name> | <row-start> / <column-start> / <row-end> / <column-end>;
    }

    举个例子:

    给item命名:

    1
    2
    3
    .item-d {
    grid-area: header;
    }

    简写 grid-row-start+grid-column-start+grid-row-end+grid-column-end:

    1
    2
    3
    .item-d {
    grid-area: 1 / col4-start / last-line / 6;
    }

    grid-area

    4、justify-self
    设置item在单元格内的水平对齐方式。

    1
    2
    3
    .item {
    justify-self: start | end | center | stretch;
    }

    举个例子:

    1
    2
    3
    .item-a {
    justify-self: start;
    }

    justify-self-start

    1
    2
    3
    .item-a {
    justify-self: end;
    }

    justify-self-end

    1
    2
    3
    .item-a {
    justify-self: center;
    }

    justify-self-center

    1
    2
    3
    .item-a {
    justify-self: stretch;
    }

    justify-self-stretch

    5、align-self
    值和用法和justify-self一样,区别是align-self用来设置垂直方向的对齐方式。

    6、place-self
    place-self是 align-self 和 justify-self的简写形式。

    可取值:

    • auto —— 默认对齐模式。
    • <align-self> / <justify-self> —— 第1个值是align-self,第2个值是justify-self,如果只有一个值,这个值会赋值给align-self和justify-self。

    特殊的单位和函数

    单位fr

    你可能会在grid布局中使用很多分数单位,比如fr,来代表剩余空间的一部分,就像这样用,表示25%和75%:

    1
    grid-template-columns: 1fr 3fr;

    这种写法比%更可靠,比如你增加了列数,就会破坏百分比的宽度,但是分数单位fr可以和其他单位更好的组合使用:

    1
    grid-template-columns: 50px min-content 1fr;

    大小关键字

    在设置行或列的大小时,可以用各种单位,比如px、rem、%等等,还可以用一些关键字:

    • min-content: 内容的最小宽度。比如一行文字E pluribus unum,会占用的最小宽度是单词pluribus的宽度。
    • max-content: 内容的最大宽度。比如一行文字E pluribus unum,能占用的最大宽度就是一整行句子。
    • auto: 和fr类似,但优先级低于fr。
    • fit-content: 使用可用空间,但不小于min-content,不大于max-content。
    • fractional units: 分数单位fr。

    大小函数

    • minmax() —— 设置大小范围,minmax(最小值,最大值)。
    • min()
    • max()

    repeat()函数和关键字

    repeat()函数可以使写法更简洁:

    1
    2
    3
    4
    5
    6
    7
    grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;

    /* 简洁的写法: */
    grid-template-columns: repeat(8, 1fr);

    /* 下面的情况更新凸显简洁: */
    grid-template-columns: repeat(8, minmax(10px, 1fr));

    repeat()和关键字结合时,会更加奇特:

    • auto-fill: 在一行中放入尽可能多的列,即使它们是空的。
    • auto-fit: 把所有列都放进去,增加列来填充空间即使是空的列。
      这就有了CSS网格中最著名的写法,也是有史以来最伟大的CSS技巧之一:
    1
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));

    masonry

    著名的瀑布流布局:

    1
    2
    3
    4
    5
    .container {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    grid-template-rows: masonry;
    }

    详情可以看这篇文章native-css-masonry-layout-css-grid

    subgrid

    grid-template-columns: subgrid;目前还只有 Firefox 支持,不做详细说明,有兴趣的可以看原文。

    ...more
  • Shutterstock Editor关于Canvas和WebGL图片滤镜的实现

    2022-08-11

    次访问

    原文地址

    Shutterstock Editor是一个简单易用且专业的图片设计工具,对于这类工具来说,图片滤镜都是一个很重要的功能,Shutterstock Editor也提供了强大的支持,本文主要介绍Shutterstock Editor如何在各种浏览器和硬件的限制下实现图片滤镜。

    最终效果

    0430-01

    Shutterstock Editor有2种滤镜,一种是应用预设的滤镜效果,我们称为filters,另一种是应用一系列的滤镜,我们称为effects,比如对比度、亮度。filters通常由多个effects组成,用户可以在任何时候用filters和effects,所以处理步骤可能多达20步。
    0430-06

    effects效果是通过滑杆控制,滤镜性能很影响用户体验。所以我们要尽量以60帧/秒的速度来处理图片,1000ms分成60帧后,每帧大概有16ms的时间,再把这16ms分给多个effects,每一步effects就只剩下不到1ms。JavaScript速度很快,但是用户速度更快,所以我们使用CPU来处理图片,才能尽可能达到这个速度,GPU不可用时,再降级到CPU来处理。

    使用CPU

    为了说明图片滤镜是怎么实现的,我们从CPU是如何处理的说起。
    Shutterstock Editor的图片是绘制在canvas上的,这意味着图片数据可以通过getImageData得到,图片像素点以r(红)、g(绿)、b(蓝)、a(透明度)的顺序存在一维数组里,于是,使用CPU处理图片滤镜,我们要这样做:

    • 通过getImageData获取图片数据。
    • 遍历上一步获取到的像素点数组,对每个像素点应用滤镜算法。一个简单的滤镜算法,比如灰度,只是取颜色通道r、g、b的加权平均值,把均值再赋值给r、g、b。
    • 再次遍历像素点,应用下一个滤镜算法。
    • 通过putImageData把处理结果更新到canvas上。

    比较好的电脑上(比如MacBook Pro i7、 Chrome 56),CPU处理1500px分辨率的图片,应用一次滤镜算法耗时约40到120ms,这些滤镜处理程序很快就会堆积起来,由于运行在UI线程,这会阻塞页面操作,所以使用CPU处理图片滤镜只是个备选,使用GPU来处理是更好的选择。

    使用GPU

    同样是canvas,我们可以通过WebGL来使用GPU处理图片:

    • 为图片创建texture对象,存储到GPU中。
    • 第一次使用滤镜时,GPU把代码编译成二进制格式。
    • GPU对texture运行编译后的代码,同时处理大量像素。
    • 对每个滤镜效果执行一次处理。
    • 把最后的处理结果texture更新到canvas上。

    跟CPU处理滤镜类似,但是速度快得多。同样的电脑,对同样的图片应用一次滤镜处理的时间变成0.2到0.4ms,时间更多的是消耗在canvas和GPU之间的图片数据传输上,甚至连续处理多个滤镜效果也不会影响到60fps的渲染目标。

    WebGL的缺点

    WebGL的缺点如下:

    • WebGL的实现因浏览器、操作系统的硬件驱动和硬件而异。
    • 错误处理有限。
    • 学习曲线陡峭。
    • Texture的尺寸有限制,所以处理大尺寸的图片需要一些特殊处理。

    因此,当WebGL不可用,或者不能实现我们的滤镜效果,或者抛出错误时,我们再用CPU来处理。

    WebGL滤镜原理

    我们用一个例子来说明WebGL是怎么处理图片的,上图要变成下图,需要的设置如右图:

    0430-10

    首先,把原图的texture(我们命名为originalTexture)传到GPU,再创建两个和原图宽高一样的texture(命名为textureA 和 textureB),当我们对图片应用不同的滤镜处理程序时,把处理结果在textureA 和 textureB之间来回传,直到所有的处理步骤都执行完。

    0430-07

    最后一个滤镜程序一执行完,我们就把textureB的内容写到页面的canvas上,如果上图中的Brightness或者其他某个设置变了,我们就再运行一次所有的滤镜程序。出于内存的考虑,为了提升性能,可能不会每次都从第一个滤镜程序开始,但对于GPU来说,滤镜几乎不会有性能问题。

    处理大图

    Shutterstock处理的图片尺寸通常都大于8000x5000px,有的用户的图片会更大,为了保持处理速度,我们通常预览图用小图,只有最终效果图会用原图,所以处理大图也很重要,对此,我们先了解到一些限制:

    • 不同的浏览器,对canvas的尺寸限制从5000px到12000px不等;
    • 不同的硬件,WebGL的texture尺寸限制从2048px到16384px不等。

    所以,对于大图,我们把它分成小片,每片是2048px x 2048px,这个尺寸在大多数硬件和浏览器上都没问题,并且可以让预览的小图在显示在一个小片上,这样除了最终的原始尺寸图片渲染,我们不需要再分片。

    分片处理

    分片后是怎么处理图片滤镜的呢?你的第一反应可能是这样的:
    0430-08

    大图被分成挨着的小图,每个小图都可以用作WebGL的texture,但是这对Shutterstock Editor来说还不行,因为有的滤镜效果,比如模糊就有问题。
    类似模糊这种滤镜效果,需要每个像素点周围的像素点一起做运算,上面这种分片方法,导致分片后小图周围的像素点丢失,对这些小图分别应用滤镜程序后,它们的拼接处会有明显的分割线:
    0430-09

    为了能实现类似模糊的这种需要周围的像素点来运算的滤镜效果,我们让分片的小图重叠,就像下面这样:

    0430-11

    这样分片有两个重要规则:
    1、重叠部分是滤镜程序需要范围的2倍。比如模糊滤镜需要周围的10个像素点,那么重叠部分须大于20px;
    2、切片没有与其他片重叠的边,须在原图的边上。这确保了切片中没有空白部分,使每个切片的处理程序更简单。

    处理后,切分再拼接到一起,每个切片提供重叠部分的一半数据,这就是为什么重叠部分得是滤镜程序需要的2倍:在切片重叠的中心,两个切片的数据要一样,从而避免出现分割线。

    总结

    图片滤镜很简单,但对各种不同的尺寸都保持良好的性能,就比较困难,Shutterstock Editor通过使用WebGL,并尽可能复用GPU的texture,把大图分成重叠的小片,从而在任何硬件和浏览器上都可以表现得不错。

    ...more
  • javascript异步error

    2022-05-19

    次访问

    本文主要总结javascript异步error的抛出和捕获。

    首先我们有一个耗时的操作wait:

    1
    2
    3
    const wait = (ms) => {
    return new Promise((resolve) => setTimeout(resolve, ms))
    }

    有一个函数foo调用了上面的wait:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const foo = async () => {
    console.log(1)
    await wait(2000)
    console.log(2)
    }

    console.log(3)
    await foo()
    console.log(4)

    // 3
    // 1
    // 等待约2秒
    // 2
    // 4

    async函数foo发生异常的话,可以通过throw一个Error来抛出:

    1
    2
    3
    4
    const foo = async () => {
    await wait(2000)
    throw new Error('Woops!')
    }

    error2

    或者返回一个Promise.reject,不过throw error的方式更常用。

    1
    2
    3
    4
    const foo = async () => {
    await wait(2000)
    return Promise.reject(new Error('Whoops!'))
    }

    foo发生异常后,可以通过catch来捕获,这样就不会报Uncaught错误。

    1
    2
    3
    4
    5
    const bar = async () => {
    await foo().catch((e) => {
    console.log(e) // caught
    })
    }

    因为foo是一个async函数,返回的是一个Promise,所以可以像Promise捕获异常一样,直接在函数foo后面跟catch。

    或者用try-catch来捕获:

    1
    2
    3
    4
    5
    6
    7
    const bar = async () => {
    try {
    await foo() // caught
    } catch(e) {
    console.log(e)
    }
    }

    对于异步函数,比如上面的foo,尽量在执行的时候用await,可以避免很多问题。比如用try-catch捕获异常时,如果没有await,就捕获不到:

    1
    2
    3
    4
    5
    6
    7
    const bar = async () => {
    try {
    foo() // uncaught
    } catch(e) {
    console.log(e)
    }
    }

    上面这些操作基本就能捕获到异常,然后做一些异常处理逻辑,比如提示操作失败,隐藏处理中的提示等等。
    编码时注意对异步逻辑的处理,最后就都能通过上面的方式捕获异常,更复杂的问题,可以试试Promise.all。

    ...more
  • win10编译opencvjs

    2022-02-22

    次访问

    前言

    本文主要讲win10系统怎么编译出opencv.js。
    主要编译过程跟官网一样,先安装Emscripten,再获取opencv源码,再编译opencv源码。
    本文主要解决的问题是在win10上怎么完成编译。

    WSL

    先说一下背景,公司的电脑是win10,构建opencv.js需要用到Emscripten,而Emscripten官网不推荐直接在windows系统上运行,对windows用户推荐了windows的Linux子系统。

    emscripten_note

    一开始我不了解什么是window的Linux子系统,即WSL,于是我直接在windows中运行了(我不想装虚拟机,也不想重装系统,也不想把mac带到公司来),结果捣腾了2天也没把编译时的各种报错解决完,报错一个接一个,解决完一个又出现一个……

    最后我决定去看一下WSL,发现非常好用,编译opencv.js一举成功!

    安装WSL

    1、勾选适用于Linux的windows子系统
    路径是「控制面板」-「程序」-「启用或关闭Windows功能」
    wsl_1

    2、打开 Microsoft Store,搜索「WSL」,选1个安装,比如我装的第一个Ubantu 20.04。
    wsl_2

    3、安装完成后自动打开终端,没有自动打开就手动打开,跟linux系统一样,设置好用户名和密码,就进入linux系统了。

    编译opencvjs

    1、确保安装了git、cmake、python,没有安装的话运行下列命令安装:

    1
    2
    3
    sudo apt install git
    sudo apt install cmake
    sudo apt install python

    安装后可以通过下列命令查看安装的版本:
    git_cmake_python

    2、安装Emscripten

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 创建customopencv目录
    mkdir customopencv

    # 进入customopencv目录
    cd customopencv/

    # 拉取emscripten源码
    git clone https://github.com/emscripten-core/emsdk.git

    # 进入emsdk目录
    cd emsdk/
    # 安装emsdk
    ./emsdk install latest
    # 激活emsdk
    ./emsdk activate latest
    # 设置环境变量
    source ./emsdk_env.sh

    emscripten_1
    emscripten_2

    3、获取opencv

    回到上级目录,clone opencv源码:

    1
    2
    # 拉取opencv源码
    git clone https://github.com/opencv/opencv.git

    menu

    4、编译opencvjs

    运行下面的命令来编译:

    1
    python platforms/js/build_js.py build_out --emscripten_dir /home/lovelyun/customopencv/emsdk/upstream/emscripten --build_wasm --clean_build_dir

    接下来去喝杯水,然后就可以看到编译成功了:

    1
    2
    3
    4
    =====
    ===== Build finished
    =====
    OpenCV.js location: /home/lovelyun/customopencv/opencv/build_out/bin/opencv.js

    进入到上面的bin文件夹,运行ls -l可以看到文件详情:

    build_out

    或者用du -sh *查看它们占用的空间:
    build_out_2

    这里的opencv.js就是我们最后需要的文件,现在在Linux子系统中,怎么传到windows系统中呢?

    最简单的是在文件资源管理器的地址栏输入\\wsl$,回车就可以看到所有的子系统。
    wsl_3

    点进去就可以看到上面的文件,比如我的路径是\\wsl$\Ubuntu-20.04\home\lovelyun\customopencv\opencv\build_out\bin。
    wsl_4

    自定义opencv构建模块

    接下来我们去掉DNN模块,首先用vscode打开子系统中的opencv文件夹,打开/platforms/js/build_js.py文件,把get_cmake_cmd(self)函数中的-DBUILD_opencv_dnn=ON改为-DBUILD_opencv_dnn=OFF。
    remove_dnn

    重新构建后可以看到,opencv.js从一开始的8.6M减小到了5.5M。
    build_out_3

    我们还可以修改opencv_js.config.py,去掉没用到的函数,比如只保留core和imgproc。
    opencv_js_config

    此时编译出来的opencv.js就只有3.7M。
    build_out_4

    或许你觉得3.7M也很大,当然大啦,但是core和imgproc中没用到的函数还可以接着删除呀。

    demo

    直接把bin目录中的opencv.js复制到项目中,比如下面这样引用:

    1
    <script src="js/opencv.js" onload="onOpenCvReady();" type="text/javascript"></script>

    然后发现调用cv的api会报错,比如cv.imread is not a function。
    打印发现cv是一个promise,这里我们简单的处理一下,把cv重新赋值为promise返回的结果,就可以运行起来了。

    1
    2
    3
    async function onOpenCvReady() {
    window.cv = await window.cv
    }

    按照官网的说法,应该是可以直接使用编译出来的opencv.js的,即直接替换官网编译的opencv.js。这里实测直接替换有问题,那就解决它,我们暂时不纠结为什么会有问题了,

    总结

    需要Linux环境的问题,就用Linux,不要用windows环境瞎折腾。

    这次编译的大部分时间都在解决windows上的报错问题,虽然windows上安装都是成功的,校验是否安装成功的结果都是ok的,但编译时各种报错,最后用了WSL,一开始为了省时间直接把windows上下载的emsdk文件夹cp过去用,而且WSL中的python环境默认是python3,这些因素也导致了编译报错,最后我想完全重新来一次,在WSL中重新下载emscripten,重新安装python,最后用的python2,编译一次性成功。

    用Linux编译,感觉就是超幸运,干什么都是一次成功。

    虽然觉得这种环境问题导致的报错解决起来浪费时间还没什么意义,但是编译成功的那一刻还是挺兴奋的,哈哈哈……

    ...more
  • 前端包管理器

    2022-01-26

    次访问

    前言

    本文将从前端包管理器的发展开始说起,然后对比npm、yarn和pnpm。

    没有包管理器

    依赖(dependency)是别人为了解决一些问题而写好的代码,即我们常说的第三方包或三方库。
    一个项目或多或少的会有一些依赖,而你安装的依赖又可能有它自己的依赖。

    比如,你需要写一个base64编解码的功能,你可以自己写,但为什么要自己造轮子呢?大多数情况下,一个可靠的第三方依赖经过多方测试,兼容性和健壮性会比你自己写的更好。

    项目中的依赖,可以是一个完整的库或者框架,比如react或vue;可以是一个很小的功能,比如日期格式化;也可以是一个命令行工具,比如eslint。

    如果没有现代化的构建工具,即包管理器,你需要用<script>标签来引入依赖。

    此外,如果你发现了一个比当前使用的依赖更好的库,或者你使用的依赖发布了更新,而你想用最新版本,在一个大的项目中,这些版本管理、依赖升级将是让人头疼的问题。

    于是包管理器诞生了,用来管理项目的依赖。
    它提供方法给你安装依赖(即安装一个包),管理包的存储位置,而且你可以发布自己写的包。

    npm v1-v2

    初代npm(Node.js Package Manager)随着Node.js的发布出现了。

    它的文件结构是嵌套的:

    npm_v1

    这会导致3个问题:

    1、node_modules体积过大(大量重复的包被安装)
    2、node_modules嵌套层级过深(会导致文件路径过长的问题)
    3、模块实例不能共享

    yarn & npm v3

    这个版本yarn和npm v3带来了扁平化依赖管理:
    yarn

    扁平化处理时,比如安装A,A依赖B和C,C依赖D和E,就把A~E全部放到node_modules目录下,从而解决上个版本中node_modules嵌套层级过深的问题。
    在install安装时,会不停的往上级node_modules中寻找,如果找到同样的包,就不再重复安装,从而解决了大量包被重复安装的问题。

    但是扁平化带来了新的问题:
    1、依赖结构的不确定性
    2、扁平化算法本身复杂性很高,耗时较长
    3、项目中仍然可以非法访问没有声明过依赖的包

    对于问题1,比如B和C都依赖了F,但是依赖的F版本不一样:
    f

    依赖结构的不确定性表现是扁平化的结果不确定,以下2种情况都有可能,取决于package.json中B和C的位置。
    unkown

    于是出现yarn.lock(npm5才有package-lock.json),来保证install后产生确定的依赖结构。但这并不能完全解决问题,node_modules中依然存在各种不同版本的F,而这可能导致各种情况的编译报错,以及安装满,占磁盘空间。

    对于问题3,package.json中我们只声明了A,BF都是因为扁平化处理才放到和A同级的node_modules下,理论上在项目中写代码时只可以使用A,但实际上BF也可以使用,由于扁平化将没有直接依赖的包提升到node_modules一级目录,Node.js没有校验是否有直接依赖,所以项目中可以非法访问没有声明过依赖的包。

    这会产生两个问题:

    B~F中的包升级后,项目可能出问题
    额外的管理成本(比如协作时别人运行一次npm install后项目依旧跑不起来)

    pnmp

    pnpm(Performance npm)的作者Zoltan Kochan发现 yarn 并没有打算去解决上述的这些问题,于是另起炉灶,写了全新的包管理器。

    pnpm复刻了npm所有的命令,所以使用方法和npm一样,并且在安装目录结构上做了优化,特点是善用链接,且由于链接的优势,大多数情况下pnpm的安装速度比yarn和npm更快。

    比如安装A,A依赖了B:
    pnpm1

    1、安装依赖
    A和B一起放到.pnpm中(和上面相比,这里没有耗时的扁平化算法)。

    另外A@1.0.0下面是node_modules,然后才是A,这样做有两点好处:

    允许包引用自身
    把包和它的依赖摊平,避免循环结构
    2、处理间接依赖
    A平级目录创建B,B指向B@1.0.0下面的B。

    3、处理直接依赖
    顶层node_modules目录下创建A,指向A@1.0.0下的A。

    对于更深的依赖,比如A和B都依赖了C:
    pnpm2

    总结

    如果你想更快的速度,更小的空间,你应该选择pnpm;
    如果你要用Monorepo,你可以用yarn或pnpm;
    如果是node项目,你应该用npm,因为这是node官方推荐的,而且yarn不支持node5+;
    对于npm项目,如果你担心项目的安全性,你可以考虑用yarn替换npm。

    参考

    • 为什么现在我更推荐 pnpm 而不是 npm/yarn?
    • Node.js 包管理器发展史
    • JavaScript package managers compared: Yarn, npm, or pnpm?
    • pnpm官网
    ...more
  • 跟着尤雨溪学Vite

    2022-01-21

    次访问

    前言

    本文是翻译的尤雨溪在油管上发布的Learn Vite with Evan You视频。
    下面开始正文部分。

    正文

    尤雨溪在这里欢迎大家,今天的视频我们会讲讲Vite。

    Vite是一个我从去年开始开发的新构建工具,并且我们认为它会成为下一代的前端工具,所以,这个视频我们会谈谈,什么是Vite,为什么是Vite,而且我们还会展示怎么开始用它,以及Vite没有提供的一些很棒的特性。接下来就开始吧。

    首先,什么是Vite?

    如果你去官网,会看到Vite是个法语单词,意思是“快”,有些人把它读成/vait/,但它实际上是表示“快”的法语单词,所以我们读/vit/。

    Vite是一个构建工具,用在开发环境,也可以为生产环境打包。如果你用过Vue CLI,你可以认为它们差不多。

    Vite要解决的问题是更新速度,如果你在一个很大的项目中用过Vue CLI,你可能要等半分钟,甚至一分钟,本地环境才会更新,随着项目变大,更新就变慢,只修改了一个文件,你就不得不等好几秒,等着屏幕上的东西更新。

    变慢主要是因为我们要打包,我们为什么需要打包呢?
    因为过去浏览器不支持ES Modules,无法加载模块化的代码,所以早期我们发明了模块化规范,比如AMD和CommonJS,我们写完代码后,用一些工具,比如webpack、Parcel、Browserfy,把代码打包成一个浏览器上可以运行的文件。

    幸运的是,如今大部分的现代浏览器都能支持ES Modules,这意味着开发阶段的大量工作我们不用做了,浏览器会帮我们处理,所以用Vite的前提是我们把代码写成ES Modules,编译文件时找到import申明,并对每个模块发一个HTTP请求到开发服务器,由于大部分工作浏览器做了,所以我们需要做点额外的工作。

    另一方面,如果你有很多依赖,Vite会很智能的先用esbuild打包来减少请求。

    Esbuild是Evan Wallace用Go写的一个工具,把代码编译成浏览器可执行的程序,所以要快几个数量级,然后Javascript有了同样的工具。

    因为上面这些原因,Vite可以提供一个极快的开发体验。所以,这是为什么是Vite。

    对于快字,我认为Vite有多快你用一下就知道了,那么,我们现在就用用看。

    首先确保你装了Node.js和npm,然后就可以简单的运行命令:

    1
    npm init @vitejs/app

    回车后会问你项目名,这里我就叫它hello-vite吧,然后会让你选一个框架,Vite对框架没有限制,所以即使你用react或者preact,你也可以用Vite,不过,作为第一个demo,我根本不想用框架,所以我们就选vanilla模板,Vite也可以支持Typescript,不过现在我们只选择vanilla,接下来我们进入hello-vite目录,开始前要先用npm安装依赖。

    init

    这时我们先看看我们的项目,有一个index.html,里面有一个script标签,type属性是module,src指向main.js,这个语法需要浏览器支持ESM。
    index

    接下来我们看看package.json,你可以看到,Vite是唯一的依赖,此外,还有一些npm scripts,有dev,build和serve的scripts,所以,让我们直接npm install吧。
    package

    我要开始运行npm install了,你可以看到安装过程非常的快,一共安装了13个包,就是这么简洁。而且我认为Vite没有本地缓存,从注册中心安装,花了不到1秒。
    install

    接下来我运行npm run dev启动服务,同样很快,本地服务就跑起来了,接着打开页面,就看到了Hello Vite,这就是一个普通的本地开发服务器。
    serve

    我们再看回代码,index.html和main.js都没什么变化,如果我们把main.js改成main.ts,重启服务,一样能跑起来,在Vite项目中,你可以直接引进ts文件,Vite会自动转译,仅此而已,这意味着Vite不会做类型检查。
    ts

    我们这样做的第一个原因是,我们用esbuild来编译ts文件,esbuild是用Go写的,比起用javascript写的ts自身的编译要快30倍,所以当我们用esbuild来编译,由于没有类型检查,如果你import一个type,确保是从另一个文件中import的,这是一个坑。

    1
    import type { } from './'

    但这对于非常大的typescript项目中开发体验得到极大改善来说,只是一个很小很小的代价。

    另一个原因是如果对你的编辑器,比如VS Code,做了TS相关的合适设置,可能你的IDE就会对ts文件做类型检查,ts已经在你的IDE中运行了。所以Vite就说:“好吧,就把这部分给IDE和打包程序吧”,在开发阶段Vite对ts的类型检查是不确定的,因为这部分交给其他工具了。关于typescript的就这么多。

    接下来我们花1分钟讲下CSS。
    可以看到我们能直接import CSS,接着我准备把这里的color改成red,你可以看到页面立刻就更新了,注意看浏览器地址栏,我们再把color改成green,地址栏没有重新加载,接着我打开控制台的Elements栏,这时我们再次改变color,你可以看到,页面没有重载,这里热更新了,加载的CSS局部更新。
    css

    过去我们通过npm来import、install、使用依赖,比如我想用lodash,从lodash-es中import debounce,浏览器并不能处理,因为它不知道什么是npm,不知道怎么处理依赖,但是Vite知道怎么处理。

    接下来我要install lodsah-es,然后npm run dev重启服务,你可以看到IDE已经判断出我们安装了lodash-es,所以它现在有类型定义,我加个console.log(debounce),打开浏览器控制台,可以看到我们能够从lodsah-es中拿到debounce函数,这点很棒,Vite能够为你处理npm依赖。

    还有一点很棒,如果我们去network中查看下debounce,可以发现Vite把lodash-es的众多模块组合成了一个文件,所以它比一般情况下加载地更快,如果你通过ES Modules加载loadsh-es,那就是另外一个表现了,嗯,npm依赖地加载就这么多。

    总结

    以上就是视频的主要内容了,视频是2021年9月15日发布,从Vite名字的由来,讲到为什么需要Vite,能解决什么问题,需要付出什么代价,然后带大家初步体验Vite的使用,体验启动及热更新速度,处理typescript和npm依赖。

    想了解更多,建议看看官网。

    ...more
  • js模块化

    2022-01-20

    次访问

    前言

    本文主要理理js模块化相关知识。
    涉及到内联脚本、外联脚本、动态脚本、阻塞、defer、async、CommonJS、AMD、CMD、UMD、ES Module。顺带探究下Vite。

    内联脚本

    假设你是一个前端新手,现在入门,那么我们创建一个html页面,需要新建一个index.html文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!DOCTYPE html>
    <html>
    <head>
    <title>test</title>
    </head>
    <body>
    <p id="content">hello world</p>
    </body>
    </html>

    如果需要在页面中执行javascript代码,我们就需要在 HTML 页面中插入 <script> 标签。

    有2种插入方式:
    1、放在<head>中
    2、放在<body>中

    比如,点击hello world之后,在hello world后面加3个感叹号的功能,我们在head中加入script标签,并给hello world绑定点击事件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!DOCTYPE html>
    <html>
    <head>
    <title>test</title>
    <script>
    function myFunction() {
    document.getElementById('content').innerHTML = 'hello world!!!'
    }
    </script>
    </head>

    <body>
    <p id="content" onclick="myFunction()">hello world</p>
    </body>
    </html>

    如果加在body中,一般放在body的最后面:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!DOCTYPE html>
    <html>
    <head>
    <title>test</title>
    </head>

    <body>
    <p id="content" onclick="myFunction()">hello world</p>
    <script>
    function myFunction() {
    document.getElementById('content').innerHTML = 'hello world!!!'
    }
    </script>
    </body>
    </html>

    简单的逻辑我们可以用这2种方式写,这种方式叫做内联脚本。

    外联脚本

    当逻辑复杂时,我们可以把上面的script标签中的代码抽取出来,比如在html的同级目录创建一个js文件夹,里面新建一个a.js的文件。

    a.js中写上面script标签中的代码:

    1
    2
    3
    function myFunction() {
    document.getElementById('content').innerHTML = 'hello world!!!'
    }

    上面的script标签则可以改成:

    1
    <script src="./js/a.js"></script>

    阻塞

    上面的2种写法,浏览器在加载html时,遇到script标签,会停止解析html。
    内联脚本会立刻执行;外联脚本会先下载再立刻执行。
    等脚本执行完毕才会继续解析html。
    (html解析到哪里,页面就能显示到哪里,用户也能看到哪里)

    比如下面的代码:

    1
    2
    3
    4
    5
    <p>...content before script...</p>

    <script src="./js/a.js"></script>

    <p>...content after script...</p>

    解析到第一个p标签,我们能看到...content before script...显示在了页面中,然后浏览器遇到script标签,会停止解析html,而去下载a.js并执行,执行完a.js才会继续解析html,然后页面中才会出现...content after script...。

    我们可以通过Chrome的Developer Tools分析一下index.html加载的时间线:

    timeline1

    这会导致2个问题:
    1、脚本无法访问它下面的dom;
    2、如果页面顶部有个笨重的脚本,在它执行完之前,用户都看不到完整的页面。

    对于问题2,我们可以把脚本放在页面底部,这样它可以访问到上面的dom,且不会阻塞页面的显示:

    1
    2
    3
    4
    5
    <body>
    ...all content is above the script...

    <script src="./js/a.js"></script>
    </body>

    但这不是最好的办法,我们接着往下看。

    defer

    我们给script标签加defer属性,就像下面这样:

    1
    2
    3
    4
    5
    <p>...content before script...</p>

    <script defer src="./js/a.js"></script>

    <p>...content after script...</p>

    defer 特性告诉浏览器不要等待脚本。于是,浏览器将继续解析html,脚本会并行下载,然后等 DOM 构建完成后,脚本才会执行。

    这样script标签不再阻塞html的解析。

    这时再看时间线:

    timeline2

    需要注意的是,具有 defer 特性的脚本保持其相对顺序。

    比如:

    1
    2
    <script defer src="./js/a.js"></script>
    <script defer src="./js/b.js"></script>

    上面的2个脚本会并行下载,但是不论哪个先下载完成,都是先执行a.js,a.js执行完才会执行b.js。
    这时,如果b.js依赖a.js,这种写法将很有用。

    另外需要注意的是,defer 特性仅适用于外联脚本,即如果 script标签没有 src属性,则会忽略 defer 特性。

    async

    我们可以给script标签加async属性,就像下面这样:

    1
    <script async src="./js/a.js"></script>

    这会告诉浏览器,该脚本完全独立。
    独立的意思是,DOM 和其他脚本不会等待它,它也不会等待其它东西。async 脚本就是一个会在加载完成时立即执行的完全独立的脚本。

    这时再看时间线:

    timeline3

    可以看到,虽然下载a.js不阻塞html的解析,但是执行a.js会阻塞。

    还需要注意多个async时的执行顺序,比如下面这段代码:

    1
    2
    3
    4
    5
    6
    <p>...content before script...</p>

    <script async src="./js/a.js"></script>
    <script async src="./js/b.js"></script>

    <p>...content after script...</p>

    两个p标签的内容会立刻显示出来,a.js和b.js则并行下载,且下载成功后立刻执行,所以多个async时的执行顺序是谁先下载成功谁先执行。
    一些比较独立的脚本,比如性能监控,就很适合用这种方式加载。

    另外,和defer一样,async 特性也仅适用于外联脚本。

    动态脚本

    我们可以动态的创建一个script标签并append到文档中。

    1
    2
    3
    let script = document.createElement('script')
    script.src = '/js/a.js'
    document.body.append(script)

    append后脚本就会立刻开始加载,表现默认和加了async属性一致。
    我们可以显示的设置script.async = false来改变这个默认行为,那么这时表现就和加了defer属性一致。

    上面的这些写法,当script标签变多时,容易导致全局作用域污染,还要维护书写顺序,要解决这个问题,需要一种将 JavaScript 程序拆分为可按需导入的单独模块的机制,即js模块化,我们接着往下看。

    CommonJS

    很长一段时间 JavaScript 没有模块化的概念,直到 Node.js 的诞生,把 JavaScript 带到服务端,这时,CommonJS诞生了。

    CommonJS定义了三个全局变量:

    1
    require,exports,module

    require 读入并执行一个 js 文件,然后返回其 exports 对象;
    exports 对外暴露模块的接口,可以是任何类型,指向 module.exports;
    module 是当前模块,exports 是 module 上的一个属性。

    Node.js 使用了CommonJS规范。

    比如:

    1
    2
    3
    4
    5
    6
    7
    // a.js
    let name = 'Lily'
    export.name = name

    // b.js
    let a = require('a.js')
    console.log(a.name) // Lily

    由于CommonJS不适合浏览器端,于是出现了AMD和CMD规范。

    AMD

    AMD(Asynchronous Module Definition) 是 RequireJS 在推广过程中对模块定义的规范化产出。

    基本思想是,通过 define 方法,将代码定义为模块。当这个模块被 require 时,开始加载依赖的模块,当所有依赖的模块加载完成后,开始执行回调函数,返回该模块导出的值。

    使用时,需要先引入require.js:

    1
    2
    <script src="require.js"></script>
    <script src="a.js"></script>

    然后可以这样写:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // a.js
    define(function() {
    let name = 'Lily'
    return {
    name
    }
    })
    // b.js
    define(['a.js'], function(a) {
    let name = 'Bob'
    console.log(a.name) // Lily
    return {
    name
    }
    })

    CMD

    CMD(Common Module Definition) 是 Sea.js 在推广过程中对模块定义的规范化产出。

    使用时,需要先引入sea.js:

    1
    2
    <script src="sea.js"></script>
    <script src="a.js"></script>

    然后可以这样写:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // a.js
    define(function(require, exports, module) {
    var name = 'Lily'
    exports.name = name
    })

    // b.js
    define(function(require, exports, module) {
    var name = 'Bob'
    var a = require('a.js')
    console.log(a.name) // 'Lily'
    exports.name = name
    })

    UMD

    UMD (Universal Module Definition) 目的是提供一个前后端跨平台的解决方案(兼容全局变量、AMD、CMD和CommonJS)。

    实现很简单,判断不同的环境,然后以不同的方式导出模块:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    (function (root, factory) {
    if (typeof define === 'function' && (define.amd || define.cmd)) {
    // AMD、CMD
    define([], factory);
    } else if (typeof module !== 'undefined' && typeof exports === 'object') {
    // Node、CommonJS
    module.exports = factory();
    } else {
    // 浏览器全局变量
    root.moduleName = factory();
    }
    }(this, function () {
    // 只需要返回一个值作为模块的export
    // 这里我们返回了一个空对象
    // 你也可以返回一个函数
    return {};
    }));

    ES Module

    AMD 和 CMD 是社区的开发者们制定的模块加载方案,并不是语言层面的标准。从 ES6 开始,在语言标准的层面上,实现了模块化功能,而且实现得相当简单,完全可以取代上文的规范,成为浏览器和服务器通用的模块解决方案。

    ES6 的模块自动采用严格模式。模块功能主要由两个命令构成:export和import。

    export命令用于规定模块的对外接口;
    import命令用于输入其他模块提供的功能。

    比如上面的代码,我们可以这样写:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // a.js
    const name = 'Lily'

    export {
    name
    }

    // 等价于
    export const name = 'Lily'

    // b.js
    import { name } from 'a.js'
    console.log(name) // Lily

    // b.js
    import * as a from 'a.js'
    console.log(a.name) // Lily

    此外,还可以用export default默认导出的写法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // a.js
    const name = 'Lily'

    export default {
    name
    }

    // b.js
    import a from 'a.js'
    console.log(a.name) // Lily

    如果只想运行a.js,可以只import:

    1
    2
    // b.js
    import 'a.js'

    我们可以给script标签加type=module让浏览器以 ES Module 的方式加载脚本:

    1
    <script type="module" src="./js/b.js"></script>

    这时,script标签会默认有defer属性(也可以设置成async),支持内联和外联脚本。

    这时我们运行打开index.html,会发现浏览器报错了:

    error1

    这是因为 type=module 的 script 标签加强了安全策略,浏览器加载不同域的脚本资源时,如果服务器未返回有效的 Allow-Origin 相关 CORS 头,会禁止加载改脚本。而这里启动的index.html是一个本地文件(地址是file://路径),将会遇到 CORS 错误,需要通过一个服务器来启动 HTML 文件。

    Vite

    在浏览器支持 ES Module 之前,我们用工具实现JavaScript模块化的开发,比如webpack、Rollup 和 Parcel 。但是当项目越来越大后,本地热更新越来越慢,而 Vite 旨在利用ESM解决上述问题。
    bundler
    esm

    Vite使用简单,可以去官网看看。

    总结

    老的规范了解即可,未来是ES Module的,用Vite可以极大的提升开发时的体验,生产环境用Rollup打包。

    ...more
  • 梯度下降法

    2022-01-05

    次访问

    本文主要解释什么是梯度,什么是梯度下降法,可以应用在哪里,如何用python实现。

    梯度

    梯度是微分中一个很重要的概念:

    • 在单变量的函数中,梯度其实就是函数的微分
    • 在多变量函数中,梯度是一个向量
      先说说单变量函数。

    梯度是函数的微分,那微分又是什么呢?

    下面我们看一下维基百科对微分的描述。

    函数的微分是对函数局部变化的一种线性描述。微分可以近似地描述当函数自变量的取值作足够小的改变时,函数的值是怎样改变的。

    在几何上,设 ∆x 是曲线 y=f(x) 上的点 P 在横坐标上的增量,∆y 是曲线在点 P 对应 ∆x 在纵坐标上的增量,dy 是曲线在点 P 的切线对应 ∆x 在纵坐标上的增量。当 |∆x| 很小时,|∆y−dy| 比 |∆x| 要小得多(高阶无穷小),因此在点P附近,我们可以用切线段来近似代替曲线段。

    dydx

    下面看一个例子。

    设函数f(x) = x²,回顾一下微分的定义,函数自变量取足够小的改变时,函数值是怎么改变的,假设函数自变量 x 变到 x + dx ,那么函数值的改变是 f(x + dx) - f(x),进一步计算可以看到:

    1
    2
    3
    4
    5
    f(x + dx) - f(x)
    = (x + dx)² - x²
    = x² + 2x * dx + (dx)² - x²
    = 2x * dx + (dx)²
    = Adx + o(dx)

    其中的线性主部:Adx = 2xdx ,高阶无穷小是 o(dx) = (dx)²。
    因此,该函数在 x 处的微分是 dy = 2xdx,函数的微分与自变量的微分之商 dy / dx = 2x = f’(x),等于函数的导数。

    那么现在我们再看看一元微分的定义:

    设函数 y = f(x) 在某区间内有定义,对于区间内的一点 x0 ,变动到附近的 x0 + ∆x (也在此区间内)时,如果函数的增量 ∆y = f(x0 + ∆x) - f(x) 可表示为 ∆y = A∆x + o(∆x) ,其中A是不依赖于 ∆x 的常数, o(∆x)是比 ∆x高阶的无穷小,那么称函数 f(x)在 x0 处是可微的,且 A∆x 称作函数在点 x0 相应于自变量增量 ∆x 的微分,记作 dy, 即 dy = A∆X, dy 是 ∆y 的线性主部,通常把自变量 x 的增量 ∆x 称为自变量的微分,记作 dx, 即 dx = ∆x。

    当一个函数有多个变量的时候,就有了多变量的微分,即分别对每个变量求微分。

    这里,我们先了解一下什么是偏导。

    偏导: 一个多变量的函数(或称多元函数),对其中一个变量(导数)微分,而保持其他变量恒定。
    函数 f 关于变量 x 的偏导数写为 fx’ 或 ∂f / ∂x。偏导数符号 ∂ 是全导数符号 d 的变体。

    比如函数 z = f(x, y) = x²y² ,对变量 x 微分,即 ∂f / ∂x = ∂(x²y²) / ∂x = 2xy²。

    复合函数则需要用到链式法则,比如f(x) = (x² + 1)³ 的导数 f’(x) = 3(x² + 1)² * 2x = 6x(x² + 1)²。

    梯度实际上就是多变量微分的一般化。

    比如 f(x, y, z) = 0.55 - (5x + 2y -12z), 那么该函数的梯度为 ▽f = <∂f / ∂x, ∂f / ∂y, ∂f / ∂z> = <-5, -2, 12>。

    我们可以看到,梯度其实是一个向量,这个向量指出了函数在给定点变化最快的方向。

    现在我们再看看什么是梯度下降法。

    理解梯度下降法,有一个很经典的例子:

    一个人被困在山上,需要下山。但山上有浓雾,可视度很低,下山的路无法确定。他必须利用自己周围的信息去找下山的路。
    这时,他就可以利用梯度下降算法。
    具体来说,以他当前的所处的位置为基准,寻找这个位置最陡峭的地方,然后朝着山的高度下降的地方走;
    同理,如果我们的目标是上山,也就是爬到山顶,那么此时应该是朝着最陡峭的方向往上走。
    然后每走一段距离,都反复采用同一个方法,最后就能成功的抵达山谷。

    同时可以假设最陡峭的地方无法一眼看出来,而是需要一个复杂的工具来测量。
    所以,此人每走一段距离,都需要一段时间来测量所在位置最陡峭的方向,这是比较耗时的。
    那么为了在太阳下山之前到达山底,就要尽可能的减少测量次数。
    问题是,如果测量频繁,可以保证下山的方向是绝对正确的,但又非常耗时,如果测量过少,又有偏离方向的风险。
    所以需要一个合适的测量频率,来确保下山的方向不错误,同时又不至于耗时太多。

    总结一下就是:

    下山 目标函数
    起点 随机参数
    方向 梯度下降
    步长 学习率

    梯度下降法的实现

    下面我们就用python来实现使用梯度下降法模拟求解 y=(2x + 4)² + 1 最小值,x范围[-10, 6],起点随机选取。

    首先引入 numpy 和 matplotlib.pyplot, numpy是一个数学函数库, matplotlib.pyplot用来画图。

    1
    2
    import numpy as np
    import matplotlib.pyplot as plt

    然后定义目标函数:

    1
    2
    # 目标函数: y = (2 * x + 4)^2 + 1
    def func(x): return np.square(2 * x + 4) + 1

    接着定义目标函数的导数:

    1
    2
    # 目标函数的一阶导数: dy / dx = 8 * x + 16
    def dfunc(x): return 8 * x + 16

    下面实现梯度下降法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 梯度下降法:给定起始点目标函数的一阶导函数,求在 iterations 次迭代中x的最新值
    # param x_start: x 的起始点
    # param df: 目标函数的一阶导函数
    # param iterations: 迭代次数
    # param lr: 学习率
    # return: x在每次迭代后的位置(包括起始点),长度为 iterations + 1
    def gradient_descent(x_start, df, lr = 0.01, iterations = 1000):
    xs = np.zeros(iterations + 1)
    x = x_start
    xs[0] = x
    print(f'起点:x={x}, y={func(x)}')
    for i in range(iterations):
    # diff表示x要改变的幅度
    diff = - df(x) * lr
    if np.all(np.abs(diff) <= stopping_threshold):
    print('停止')
    break
    x += diff
    xs[i + 1] = x
    print (f'迭代次数: {i + 1},结果:x={x}, y = {func(x)}')
    return xs

    接下来就是调用梯度下降函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 起始点
    x_start = np.random.uniform(-10, 6)
    # 迭代次数
    iterations = 1000
    # 学习率
    lr = 0.1
    # 停止迭代阈值
    stopping_threshold = 1e-5

    # 梯度下降法
    y = gradient_descent(x_start, dfunc, lr, iterations)

    最后画图。

    1
    2
    3
    4
    5
    6
    7
    8
    color = 'r'
    x = np.arange(-10.0, 6.0, 0.01)
    plt.plot(x, func(x), c = 'g')
    plt.plot(y, func(y), c = color, label='lr={}'.format(lr))
    plt.scatter(y, func(y), c = color)
    plt.legend()

    plt.show()

    我们运行3次就可以看到结果:

    gd1

    把学习率改为0.01时,迭代次数明显增加:
    gd2

    我们再运行3次,可以看到:

    第1次,起点 x = 8.80086, y = 186.00666,迭代131次,结果x = -2.00012, y = 1.00000
    第2次,起点 x = 7.31667, y = 114.0679,迭代128次,结果x = -2.00012, y = 1.00000
    第3次,起点 x = 0.84411, y = 6.34431,迭代110次,结果x = -1.99988, y = 1.00000

    把学习率改为0.001时,迭代次数高达1071次:
    gd3

    我们再运行3次,可以看到:

    第1次,起点 x = -8.81178, y =186.60116,迭代1000次,结果x = -2.00221, y = 1.00002
    第2次,起点 x = 2.98931, y = 100.57297,迭代1000次,结果x = -1.99838, y = 1.00001
    第3次,起点 x = 1.50069, y = 1.99721,迭代746次,结果x = -199875, y = 1.00001

    下面我们把学习率调大,比如0.2,可以看到迭代结果在最低点左右徘徊,27次后找到最低点。

    gd4

    再把学习率调到0.3时,迭代1000次后还没找到最低点:
    gd5

    参考

    • 微分-wiki
    • 深入浅出–梯度下降法及其实现
    • Gradient Descent - Problem of Hiking Down a Mountain
    ...more
  • conda建立及管理虚拟环境

    2021-12-14

    次访问

    前文我们安装了Anaconda,本文主要介绍如何利用conda建立及管理Python虚拟环境。

    前言

    开发python时,经常会需要不同的python版本以及不同的packages,如果你只需要使用特定的包,或者想要尝试不同的开发环境,但又不想彼此的开发环境受到影响,那么 Anaconda 的管理系统conda将是一个不错的方案。

    conda命令是管理不同package时使用的,可以建立(create)、输出(export)、罗列(list)、删除(remove)和更新(update)环境中的包,还可以分享你的虚拟环境。

    下面将通过5个步骤来说明conda如何建立及管理虚拟环境。
    conda_step

    安装及更新

    安装部分可参考前文《AI入门之环境安装》或官网,从开始菜单中打开Anaconda Prompt后,可以通过下列命令查看当前版本:

    1
    2
    conda -V
    conda --version

    conda_v

    通过下列命令更新:

    1
    conda update conda

    建立虚拟环境

    查看当前已安装的虚拟环境:

    1
    conda env list

    conda_env_list

    假设我们要建立一个叫myenv的虚拟环境,并且安装python 3.5的版本,我们可以执行下面的命令:

    1
    conda create --name myenv python=3.5

    安装完成后会出现下面的提示,提醒启动和关闭该环境的命令:
    env_down

    这时,我们conda env list可以看到多了一个刚刚建立的虚拟环境myenv。
    conda_myenv

    启动虚拟环境

    1
    activate myenv

    conda_env_activate

    当前环境已经切换到myenv。
    我们可以看到命令行最前面的括号内显示myenv,而且conda env list可以看到myenv后面有星号。

    如果是Linux或者macOS,启动虚拟环境的命令将是:

    1
    source activate myenv

    还可以通过下列命令看当前的虚拟环境安装了哪些东西:

    1
    conda list

    conda_list

    如果要在当前环境安装新的包,比如安装numpy,那么只需要执行下列命令:

    1
    conda install numpy

    离开虚拟环境

    windows中可以执行下列命令关闭虚拟环境:

    1
    conda deactivate

    conda_deactivate

    Linux或者macOS则是:

    1
    source deactivate

    删除虚拟环境或package

    删除myenv环境中的numpy包:

    1
    conda remove --name myenv numpy

    如果要删除整个虚拟环境,比如删除上面创建的myenv,需要先deactivate关闭该环境,再执行下列命令:

    1
    conda env remove --name myenv

    conda_env_remove

    总结

    为不同的需求建立独立的虚拟环境是个很好的习惯。
    因为它不会影响其它的系统配置,如果某个版本出现了问题,可以很轻易的删除某个package,或者重新搭建虚拟环境。

    ...more

分类归档

  • ai3
  • css2
  • doc2
  • git1
  • js15
  • node2
  • 动效6
  • 图像处理13

标签云

最近文章

  • js遍历那些事儿
  • 详解CSS Grid布局
  • Shutterstock Editor关于Canvas和WebGL图片滤镜的实现
  • javascript异步error
  • win10编译opencvjs
NEXT

© 2018 - 2024 Lovelyun