tangyanjie123

tangyanjie123 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

tangyanjie123 赞了回答 · 2020-09-04

解决vue element-ui select中的输入框的值怎么显示为value值?

模板写法,详情见官网自定义模板

关注 2 回答 1

tangyanjie123 收藏了文章 · 2019-12-16

js 正则匹配(去掉html标签)

1,得到网页上的链接地址:

string matchString = @"<a[^>]+href=\s*(?:'(?<href>^']+)'|""(?<href>[^""]+)""|(?<href>[^>\s]+))\s*[^>]*>";

2,得到网页的标题:

string matchString = @"<title>(?<title>.*)</title>";

3,去掉网页中的所有的html标记:

string temp = Regex.Replace(html, "<[^>]*>", ""); //html是一个要去除html标记的文档

4, string matchString = @"<title>([\S\s\t]*?)</title>";
5, js去掉所有html标记的函数:

function delHtmlTag(str)
{
      return str.replace(/<[^>]+>/g,"");//去掉所有的html标记
}

6. 统计字数

t = $('.remarktext').html().replace(/<[^>]+>/g,"").length;
查看原文

tangyanjie123 赞了文章 · 2019-12-16

js 正则匹配(去掉html标签)

1,得到网页上的链接地址:

string matchString = @"<a[^>]+href=\s*(?:'(?<href>^']+)'|""(?<href>[^""]+)""|(?<href>[^>\s]+))\s*[^>]*>";

2,得到网页的标题:

string matchString = @"<title>(?<title>.*)</title>";

3,去掉网页中的所有的html标记:

string temp = Regex.Replace(html, "<[^>]*>", ""); //html是一个要去除html标记的文档

4, string matchString = @"<title>([\S\s\t]*?)</title>";
5, js去掉所有html标记的函数:

function delHtmlTag(str)
{
      return str.replace(/<[^>]+>/g,"");//去掉所有的html标记
}

6. 统计字数

t = $('.remarktext').html().replace(/<[^>]+>/g,"").length;
查看原文

赞 8 收藏 11 评论 4

tangyanjie123 赞了文章 · 2019-10-17

用CSS新属性实现特殊的图片显示效果

1 概述

1.1 前言

使用一个或多个图像相关的CSS属性(background-blend-mode, mix-blend-mode, or filter)可以实现许多特殊的图片显示效果。本文转载自Bennett Feely的个人网站,文中共列举了20种图片显示效果。

详细代码及英文原文请访问Bennett Feely的主页

2 效果列表

2.1 铅笔画效果

效果示例

铅笔画效果

SCSS代码

.pencil-effect {
    $url : url(photo.jpg);
    background-image: $url;
    background-size: cover;
    background-position: center;
    @supports (filter: invert(1)) and (background-blend-mode: difference) {
        background-image: $url, $url;
        background-blend-mode: difference;
        background-position:
            calc(50% - 1px) calc(50% - 1px),
            calc(50% + 1px) calc(50% + 1px);
        filter: brightness(2) invert(1) grayscale(1);
        box-shadow: inset 0 0 0 1px black;
    }
}

查看示例程序

2.2 水彩效果

效果示例

水彩效果

SCSS代码

.watercolor-effect {
    $url : url(photo.jpg);
    background-image: $url;
    background-size: cover;
    background-position: center;
    @supports (filter: blur(2px)) and (mix-blend-mode: multiply) {
        position: relative;
        overflow: hidden;
        &:before, &:after {
            display: block;
            content: "";
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-size: cover;
        }
        &:before {
            background-image: $url, $url;
            background-blend-mode: difference;
            background-position:
                calc(50% - 1px) calc(50% - 1px),
                calc(50% + 1px) calc(50% + 1px);
            filter: brightness(2) invert(1) grayscale(1);
            box-shadow: inset 0 0 0 1px black;
        }
        &:after {
            background-image: $url;
            background-position: center;
            mix-blend-mode: multiply;
            filter: brightness(1.3) blur(2px) contrast(2);
        }
    }
}

查看示例程序

2.3 浮雕效果

效果示例

浮雕效果

SCSS代码

.emboss-effect {
    $url : url(photo.jpg);
    background-image: $url;
    background-size: cover;
    background-position: center;
    @supports (filter: invert(1)) and (background-blend-mode: difference, screen) {
        background-image: $url, $url, $url;
        background-blend-mode: difference, screen;
        background-position:
            calc(50% - 1px) calc(50% - 1px),
            calc(50% + 1px) calc(50% + 1px),
            center;
        filter: brightness(2) invert(1) grayscale(1);
    }
}

查看示例程序

2.4 彩铅效果

效果示例

彩铅效果

SCSS代码

.colored-pencil-effect {
    $url : url(photo.jpg);
    background-image: $url;
    background-size: cover;
    background-position: center;
    @supports (filter: invert(1)) and (mix-blend-mode: color) {
        position: relative;
        &:before,
        &:after {
            display: block;
            content: "";
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-size: cover;
            box-shadow: inset 0 0 0 1px black;
        }
        &:before {
            background-image: $url, $url;
            background-blend-mode: difference;
            background-position:
                calc(50% - 1px) calc(50% - 1px),
                calc(50% + 1px) calc(50% + 1px);
            filter: brightness(2) invert(1) grayscale(1);
        }
        &:after {
            background: inherit;
            mix-blend-mode: color;
        }
    }
}

查看示例程序

2.5 黑板效果

效果示例

黑板效果

SCSS代码

.chalkboard-effect {
    $url : url(photo.jpg);
    background-image: $url;
    background-size: cover;
    background-position: center;
    @supports (filter: grayscale(1)) and (background-blend-mode: difference) {
        background-image: $url, $url;
        background-blend-mode: difference;
        background-position:
            calc(50% - 1px) calc(50% - 1px),
            calc(50% + 1px) calc(50% + 1px);
        filter: brightness(1.5) grayscale(1);
    }
}

查看示例程序

2.6 彩色黑板效果

效果示例

彩色黑板效果

SCSS代码

.colored-chalkboard-effect {
    $url : url(photo.jpg);
    background-image: $url;
    background-size: cover;
    background-position: center;
    @supports (filter: brightness(2)) and (background-blend-mode: color, difference) {
        background-image: $url, $url, $url;
        background-size: cover;
        background-position:
            calc(50% - 1px) calc(50% - 1px),
            calc(50% + 1px) calc(50% + 1px),
            center;
        background-blend-mode: color, difference;
        filter: brightness(2);
    }
}

查看示例程序

2.7 喷枪效果

效果示例

喷枪效果

SCSS代码

.airbrush-effect {
  $url : url(photo.jpg);
  background-image: $url;
    background-size: cover;
    background-position: center;
  @supports (filter: blur(5px) contrast(5)) and (mix-blend-mode: multiply) {
        position: relative;
        overflow: hidden;
        &:after {
            display: block;
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: inherit;
            filter: brightness(1.5) saturate(100) blur(5px) contrast(5);
            mix-blend-mode: multiply;
        }
    }
}

查看示例程序

2.8 绚烂效果

效果示例

绚烂效果

SCSS代码

.hallucination-effect {
  $url : url(photo.jpg);
  $offset : 5px;
  background-image: $url;
  background-size: cover;
  background-position: center;
  @supports (mix-blend-mode: multiply) {
    position: relative;
    overflow: hidden;
    background-color: magenta;
    background-blend-mode: screen;
    &:before, &:after {
      display: block;
      content: "";
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: inherit;
      mix-blend-mode: multiply;
      transform: scale(1.05);
    }
    &:before {
      background-color: yellow;
      background-blend-mode: screen;
      transform-origin: top left;
    }
    &:after {
      background-color: cyan;
      background-blend-mode: screen;
      transform-origin: bottom right;
    }
  }
}

查看示例程序

2.9 绒布效果

效果示例

绒布效果

SCSS代码

.flannel-effect {
    $url : url(photo.jpg);
    background-image: $url;
    background-size: cover;
    background-position: center;
    @supports (background-blend-mode: overlay) {
        background-image: $url, $url, $url;
      background-position: center;
      background-size: 100%, 100000% 100%, 100% 100000%;
      background-blend-mode: overlay;
    }
}

查看示例程序

2.10 水平低墨

效果示例

水平低墨

SCSS代码

.low-ink-x-effect {
    $url : url(photo.jpg);
    background-image: $url;
    background-size: cover;
    background-position: center;
    @supports (background-blend-mode: screen, overlay) {
        background-image:     $url, $url, $url;
        background-size: 100% 100%, 10000% 100%;
        background-blend-mode: screen, overlay;
    }
}

查看示例程序

2.11 垂直低墨效果

效果示例

垂直低墨效果

SCSS代码

.low-ink-y-effect {
    $url : url(photo.jpg);
    background-image: $url;
    background-size: cover;
    background-position: center;
    @supports (background-blend-mode: screen, overlay) {
        background-image:     $url, $url, $url;
        background-size: 100% 100%, 100% 1000%;
        background-blend-mode: screen, overlay;
    }
}

查看示例程序

2.12 拼贴效果

效果示例

拼贴效果

SCSS代码

.collage-effect {
    $url : url(photo.jpg);
    background-image: $url;
    background-size: cover;
    background-position: center;
    @supports (background-blend-mode: overlay) {
        background-image: $url, $url, $url, $url, $url, $url;
        background-size: 200%, 80%, 60%, 50%, 40%, 100%;
        background-position: 50%, 80%, 30%, 0;
        background-blend-mode: overlay;
        background-repeat: no-repeat;
    }
}

查看示例程序

2.13 马赛克效果

效果示例

马赛克效果

SCSS代码

.mosaic-effect {
    $url : url(photo.jpg);
    background-image: $url, $url;
    background-size: cover, 5% 5%;
    background-position: center;
  background-blend-mode: overlay;
}

查看示例程序

2.14 图片边框效果

效果示例

图片边框效果

SCSS代码

.photo-border-effect {
  $url : url(photo.jpg);
  background-image: $url, $url;
  background-position: center;
  background-size: 60%, 20%;
  background-repeat: no-repeat, repeat;
}

查看示例程序

2.15 红外效果

效果示例

红外效果

SCSS代码

.infrared-effect {
  $url : url(photo.jpg);
  background-image: $url;
  background-size: cover;
  background-position: center;
  filter: hue-rotate(180deg) saturate(2);
}

查看示例程序

2.16 夜视效果

效果示例

夜视效果

SCSS代码

.night-vision-effect {
  $url : url(photo.jpg);
  $line-width: 5px;
  background-image:
    $url , radial-gradient(
      #0F0,
      #000
    ),
    repeating-linear-gradient(
      transparent 0,
      rgba(0,0,0,0.1) $line-width/2,
      transparent $line-width
    );
  background-size: cover;
  background-position: center;
  background-blend-mode: overlay;
}

查看示例程序

2.17 沃霍尔效果

效果示例

沃霍尔效果

SCSS代码

.warhol-effect {
  $url : url(photo.jpg);
  background-image: $url;
    background-size: cover;
    background-position: center;
  @supports (background-blend-mode: color) {
        background-image:
        linear-gradient(
          #14EBFF 0,
          #14EBFF 50%,
          #FFFF70 50%,
          #FFFF70 100%
        ),
        linear-gradient(
          #FF85DA 0,
          #FF85DA 50%,
          #AAA 50%,
          #AAA 100%
        ),
        $url;
        background-size: 50% 100%, 50% 100%, 50% 50%;
      background-position: top left, top right;
      background-repeat: no-repeat, no-repeat, repeat;
      background-blend-mode: color;
    }
}

查看示例程序

2.18 颜色校正效果

效果示例

颜色校正效果

SCSS代码

.selective-color-effect {
  $url : url(photo.jpg);
  background-image: $url;
  background-size: cover;
  background-position: center;
  @supports (filter: brightness(3)) and (mix-blend-mode: color) {
    position: relative;
    &:before, &:after {
      display: block;
      content: "";
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: inherit;
      background-color: red;
      background-blend-mode: screen;
      mix-blend-mode: color;
      filter: brightness(3);
    }
  }
}

查看示例程序

2.19 水平镜像效果

效果示例

水平镜像效果

SCSS代码

.mirror-x-effect {
    $url : url(photo.jpg);
    background-image: $url;
    background-size: cover;
    background-position: center;
    @supports (transform: scaleX(-1)) {
        position: relative;
        &:before, &:after {
            display: block;
            content: "";
            position: absolute;
            top: 0;
            bottom: 0;
            background: inherit;
        }
        &:before {
            left: 0;
            right: 50%;
            transform: scaleX(-1);
        }
        &:after {
            left: 50%;
            right: 0;
        }
    }
}

查看示例程序

2.20 垂直镜像效果

效果示例

垂直镜像效果

SCSS代码

.mirror-y-effect {
    $url : url(photo.jpg);
    background-image: $url;
    background-size: cover;
    background-position: center;
    @supports (transform: scaleY(-1)) {
        position: relative;
        &:before, &:after {
            display: block;
            content: "";
            position: absolute;
            left: 0;
            right: 0;
            background: inherit;
        }
        &:before {
            top: 0;
            bottom: 50%;
            transform: scaleY(-1);
        }
        &:after {
            top: 50%;
            bottom: 0;
        }
    }
}

查看示例程序

3 结尾

3.1 结语

详细代码及英文原文请访问Bennett Feely的主页

本文转载自Bennett Feely的个人网站,只做学习和交流使用。

查看原文

赞 78 收藏 57 评论 4

tangyanjie123 赞了文章 · 2019-10-16

8张图帮你一步步看清 async/await 和 promise 的执行顺序

8张图让你一步步看清 async/await 和 promise 的执行顺序

  • 为什么写这篇文章?
  • 测试一下自己有没有必要看
  • 需要具备的前置基础知识
  • 主要内容

    • 对于async await的理解
    • 画图一步步看清宏任务、微任务的执行过程

为什么写这篇文章?

说实话,关于js的异步执行顺序,宏任务、微任务这些,或者async/await这些慨念已经有非常多的文章写了。

但是怎么说呢,简单来说,业务中很少用async,不太懂async呢,

研究了一天,感觉懂了,所手痒想写一篇 ,哈哈

毕竟自己学会的知识,如果连表达清楚都做不到,怎么能指望自己用好它呢?

测试一下自己有没有必要看

所以我写这个的文章,主要还是交流学习,如果您已经清楚了eventloop/async/await/promise这些东西呢,可以 break 啦

有说的不对的地方,欢迎留言讨论,

那么还是先通过一道题自我检测一下,是否有必要继续看下去把。

其实呢,这是去年一道烂大街的「今日头条」的面试题

我觉得这道题的关键,不仅是说出正确的打印顺序,更重要的能否说清楚每一个步骤,为什么这样执行。


    async function async1() {
        console.log( 'async1 start' )
        await async2()
        console.log( 'async1 end' )
    }
    
    async function async2() {
        console.log( 'async2' )
    }
    
    console.log( 'script start' )
    
    setTimeout( function () {
        console.log( 'setTimeout' )
    }, 0 )
    
    async1();
    
    new Promise( function ( resolve ) {
        console.log( 'promise1' )
        resolve();
    } ).then( function () {
        console.log( 'promise2' )
    } )
    
    console.log( 'script end' )
注:因为是一道前端面试题,所以答案是以浏览器的eventloop机制为准的,在node平台上运行会有差异。
     script start
     async1 start
     async2
     promise1
     script end
     promise2
     async1 end
     setTimeout

如果你发现运行结果跟自己想的一样,可以选择跳过这篇文章啦,

或者如果你有兴趣看看俺俩的理解有没有区别,可以跳到后面的 「画图讲解的部分」

需要具备的前置知识

  • promise的使用经验
  • 浏览器端的eventloop

不过如果是对 ES7 的 async 不太熟悉,是没关系的哈,因为这篇文章会详解 async。

那么如果不具备这些知识呢,推荐几篇我觉得讲得比较清楚的文章

主要内容

第1部分:对于async await的理解

我推荐的那篇文章,对 async/await 讲得更详细。不过我希望自己能更加精炼的帮你理解它们

这部分,主要会讲解 3 点内容

  • 1.async 做一件什么事情?
  • 2.await 在等什么?
  • 3.await 等到之后,做了一件什么事情?
  • 4.补充: async/await 比 promise有哪些优势?(回头补充)
1.async 做一件什么事情?

一句话概括: 带 async 关键字的函数,它使得你的函数的返回值必定是 promise 对象

也就是

如果async关键字函数返回的不是promise,会自动用Promise.resolve()包装

如果async关键字函数显式地返回promise,那就以你返回的promise为准

这是一个简单的例子,可以看到 async 关键字函数和普通函数的返回值的区别

async function fn1(){
    return 123
}

function fn2(){
    return 123
}

console.log(fn1())
console.log(fn2())
Promise {<resolved>: 123}

123

所以你看,async 函数也没啥了不起的,以后看到带有 async 关键字的函数也不用慌张,你就想它无非就是把return值包装了一下,其他就跟普通函数一样。

关于async关键字还有那些要注意的?

  • 在语义上要理解,async表示函数内部有异步操作
  • 另外注意,一般 await 关键字要在 async 关键字函数的内部,await 写在外面会报错。
2.await 在等什么?

一句话概括: await等的是右侧「表达式」的结果

也就是说,

右侧如果是函数,那么函数的return值就是「表达式的结果」

右侧如果是一个 'hello' 或者什么值,那表达式的结果就是 'hello'

async function async1() {
    console.log( 'async1 start' )
    await async2()
    console.log( 'async1 end' )
}
async function async2() {
    console.log( 'async2' )
}
async1()
console.log( 'script start' )

这里注意一点,可能大家都知道await会让出线程,阻塞后面的代码,那么上面例子中, 'async2' 和 'script start' 谁先打印呢?

是从左向右执行,一旦碰到await直接跳出, 阻塞async2()的执行?

还是从右向左,先执行async2后,发现有await关键字,于是让出线程,阻塞代码呢?

实践的结论是,从右向左的。先打印async2,后打印的script start

之所以提一嘴,是因为我经常看到这样的说法,「一旦遇到await就立刻让出线程,阻塞后面的代码」

这样的说法,会让我误以为,await后面那个函数, async2()也直接被阻塞呢。

3.await 等到之后,做了一件什么事情?

那么右侧表达式的结果,就是await要等的东西。

等到之后,对于await来说,分2个情况

  • 不是promise对象
  • 是promise对象

如果不是 promise , await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果

如果它等到的是一个 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。

第2部分:画图一步步看清宏任务、微任务的执行过程

我们以开篇的经典面试题为例,分析这个例子中的宏任务和微任务。

        async function async1() {
            console.log( 'async1 start' )
            await async2()
            console.log( 'async1 end' )
        }
        async function async2() {
            console.log( 'async2' )
        }
        console.log( 'script start' )
        setTimeout( function () {
            console.log( 'setTimeout' )
        }, 0 )
        async1();
        new Promise( function ( resolve ) {
            console.log( 'promise1' )
            resolve();
        } ).then( function () {
            console.log( 'promise2' )
        } )
        console.log( 'script end' )

先分享一个我个人理解的宏任务和微任务的慨念,在我脑海中宏任务和为微任务如图所示

clipboard.png

也就是「宏任务」、「微任务」都是队列。

一段代码执行时,会先执行宏任务中的同步代码,

  • 如果执行中遇到setTimeout之类宏任务,那么就把这个setTimeout内部的函数推入「宏任务的队列」中,下一轮宏任务执行时调用。
  • 如果执行中遇到promise.then()之类的微任务,就会推入到「当前宏任务的微任务队列」中,在本轮宏任务的同步代码执行都完成后,依次执行所有的微任务1、2、3

下面就以面试题为例子,分析这段代码的执行顺序.

每次宏任务和微任务发生变化,我都会画一个图来表示他们的变化。

直接打印同步代码 console.log('script start')
首先是2个函数声明,虽然有async关键字,但不是调用我们就不看。然后首先是打印同步代码 console.log('script start')

clipboard.png

将setTimeout放入宏任务队列
默认<script></script>所包裹的代码,其实可以理解为是第一个宏任务,所以这里是宏任务2

clipboard.png

调用async1,打印 同步代码 console.log( 'async1 start' )
我们说过看到带有async关键字的函数,不用害怕,它的仅仅是把return值包装成了promise,其他并没有什么不同的地方。所以就很普通的打印 console.log( 'async1 start' )

clipboard.png

分析一下 await async2()
前文提过await,1.它先计算出右侧的结果,2.然后看到await后,中断async函数

- 先得到await右侧表达式的结果。执行async2(),打印同步代码console.log('async2'), 并且return Promise.resolve(undefined)
- await后,中断async函数,先执行async外的同步代码

目前就直接打印 console.log('async2')

clipboard.png

被阻塞后,要执行async之外的代码

执行new Promise(),Promise构造函数是直接调用的同步代码,所以 console.log( 'promise1' )

clipboard.png

代码运行到promise.then()
代码运行到promise.then(),发现这个是微任务,所以暂时不打印,只是推入当前宏任务的微任务队列中。

注意:这里只是把promise2推入微任务队列,并没有执行。微任务会在当前宏任务的同步代码执行完毕,才会依次执行

clipboard.png

打印同步代码 console.log( 'script end' )
没什么好说的。执行完这个同步代码后,「async外的代码」终于走了一遍

下面该回到 await 表达式那里,执行await Promise.resolve(undefined)了

clipboard.png

回到async内部,执行await Promise.resolve(undefined)

这部分可能不太好理解,我尽量表达我的想法。

对于 await Promise.resolve(undefined) 如何理解呢?

https://developer.mozilla.org...

根据 MDN 原话我们知道

如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果。

在我们这个例子中,就是Promise.resolve(undefined)正常处理完成,并返回其处理结果。那么await async2()就算是执行结束了。

目前这个promise的状态是fulfilled,等其处理结果返回就可以执行await下面的代码了。

那何时能拿到处理结果呢?

回忆平时我们用promise,调用resolve后,何时能拿到处理结果?是不是需要在then的第一个参数里,才能拿到结果。

(调用resolve时,会把then的参数推入微任务队列,等主线程空闲时,再调用它)

所以这里的 await Promise.resolve() 就类似于

Promise.resolve(undefined).then((undefined) => {

})

把then的第一个回调参数 (undefined) => {} 推入微任务队列。

then执行完,才是await async2()执行结束。

await async2()执行结束,才能继续执行后面的代码

如图

clipboard.png

此时当前宏任务1都执行完了,要处理微任务队列里的代码。

微任务队列,先进先出的原则,

  • 执行微任务1,打印promise2
  • 执行微任务2,没什么内容..

但是微任务2执行后,await async2()语句结束,后面的代码不再被阻塞,所以打印

console.log( 'async1 end' )

宏任务1执行完成后,执行宏任务2

宏任务2的执行比较简单,就是打印

console.log('setTimeout')

补充在不同浏览器上的测试结果

谷歌浏览器,目前是版本是「版本 71.0.3578.80(正式版本) (64 位)」 Mac操作系统

clipboard.png

Safari浏览器的测试结果

clipboard.png

火狐浏览器的测试结果

clipboard.png

如果不理解可以留言,有错误的话也欢迎指正。

关于执行顺序

评论区有指出

  • Chrome72 dev版本的执行顺序是Promise2后打印,
  • 或者是babel编译过后的代码是promise2后打印。

我自己也实践了一下babel编译后的代码执行顺序的确是promise2后打印的..

原因是ESMA最新规范的有修改,然后这一点的详情,说实话我目前也不是很清楚,评论区有给出资料,可供参考讨论。

https://github.com/rhinel/blo...

查看原文

赞 306 收藏 228 评论 67

tangyanjie123 回答了问题 · 2019-10-07

immutable中Map中嵌套的List怎么更新?

let listData = List([

{id: 1, name: "first", count: 2},
{id: 2, name: "second", count: 1},
{id: 3, name: "third", count: 2},
{id: 4, name: "fourth", count: 1}

])
想要更新 name=first 中的Count中的值

let list = state.getIn(['listData']);
list = list.update(list.findIndex(function(item){

return item.get('name') ==='third'; 
}),function(item){
    return item.set('count',400) 
} 

);

可是会报错 "item.get is not a function"

关注 4 回答 3

tangyanjie123 收藏了文章 · 2019-10-07

9102了,你还不会移动端真机调试?

移动端调试困难

很多时候,我们在进行移动端开发时,都是先在PC端使用手机模拟器进行调试,没有问题后,我们才会在手机端的浏览器进行测试,这个时候,如果没有出现问题,皆大欢喜。但是一旦出现问题,我们就很难解决,因为缺乏可视化的界面。不似在PC端,我们能直观的去改变样式,或者是进行断点调试。有时,在移动端我们不得不借助于alert来调试,但是这样的调试方法效率极其低下,很多时候,都是靠经验,或者是靠排除法。甚至,我们不得不归结为是浏览器的实现问题。

那么,有什么什么方法,能够让我们调试移动端的适配的时候,像调试PC端一样直观呢?本文旨在为你提供移动端的调试方法,希望能够为你打开新的一扇门。

本文会给出三种真机调试方法,你可以选择自己最喜欢的一款~

移动端真机调试方法

  1. chrome真机调试
  2. weinre调试
  3. spy-debugger调试

简单说明一下每一种方式的优缺点:

第一种: chrome真机调试,有一个很大的局限性就是,只能调试手机端的chrome浏览器,对于UC,QQ这些浏览器均不适用,因此在调试兼容问题时,帮助不大,但是最大的优点是: 简单快捷。

第二种: weinre调试方式,安装和适用不复杂,适用于全平台的调试,即任何手机的任何浏览器皆可以调试,不过需要手机和电脑在同一个网段下。

第三种:spy-debugger,安装稍微复杂一点,spy-debugger集成了weinre,不过还增加了抓包工具,使用最为方便。

下面我们开始具体介绍如何使用这三种调试方法:

1.chrome真机调试

手机端下载好chrome浏览器,使用USB连接到PC,打开手机的USB调试模式。
然后在PC端打开chrome浏览器,在地址栏输入: chrome://inspect. 勾选"discovery usb device"。然后在手机端浏览网页,就可以看到如下的页面,点击inspect,进行调试。(鉴于我的工作电脑是加了域的,因为并不能使用这个方式,如果有和我一样情况的童鞋,可以考虑使用另外两种调试方式)

clipboard.png

2.weinre真机调试

Weinre(WebInspector Remote)是一款基于Web Inspector(Webkit)的远程调试工具,借助于网络,可以在PC上直接调试运行在移动设备上的远程页面。

clipboard.png

本地服务器: 可以使用http-server、tomcat等,也可以使用编译器集成的服务

weinre安装

全局安装: npm install –g weinre

局部安装: npm install weinre

启动: weinre --httpPort 8090 --boundHost -all-

如果是局部安装的话,需要在前面加上 node_modules/.bin/

相信前端的童鞋都会用npm包管理工具,对于这个工具,我就不展开了,如果没有安装npm的,自行安装。

weinre启动参数说明:

  • httpPort: 设置Wninre使用的端口号,默认是8080
  • boundHost: [hostname | Ip | -all-]: 默认是 ‘localhost’.
  • debug [true | false] : 这个选项与–verbose类似, 会输出更多的信息。默认为false。
  • readTimeout [seconds] : Server发送信息到Target/Client的超时时间, 默认为5s。
  • deathTimeout [seconds] : 默认为3倍的readTimeout, 如果页面超过这个时间都没有任何响应, 那么就会断开连接。

8080端口使用情况较多,所以我选择了指定8090端口。

启动了weinre之后,我们在浏览器中输入localhost:8090.显示如下界面,表示已经启动成功。

clipboard.png

点击debug client user interface,进入调试页面。

clipboard.png

当前的targets中内容为空。

现在,我们需要做另外一点操作,在我们要调试的页面中,增加一个脚本。

<script data-original="http://localhost:8090/target/target-script-min.js#anonymous"></script>

记住将localhost换成你的IP地址.

然后,我们在本地启动一个服务器,可以是IDE集成的服务器,或者是http-server,我使用的是http-server.启动之后,我们在手机端访问要调试的网页。然后就会发现targets下面增加了记录。

这时,我们就可以点击Elements进行调试。

clipboard.png

修改样式时,会在手机端即时生效,并且也可以查看控制台信息,唯一一点就是,不能进行断点调试。

最后,在调试结束之后,别忘记删除嵌入的script。

除了这种方法之后,还介绍了在手机端保存一段Js代码,在需要调试某个页面时,点击执行JS,但是现在浏览器为了安全起见,已经不再支持此方法。默认的方法是搜索,而非执行,所以不可取。

3.spy-debugger真机调试

最后,再介绍一下spy-debugger方法。用这个方法,我们不再需要自己增加和删除脚本。

Spy-debugger内部集成了weinre,通过代理的方式拦截所有html自动注入weinre所需的js代码。简化了weinre需要给每个调试的页面添加js代码。spy-debugger原理是拦截所有html页面请求注入weinre所需要的js代码。让页面调试更加方便。

特性:

  1. 页面调试+抓包
  2. 操作简单
  3. 支持HTTPS。
  4. spy-debugger内部集成了weinre、node-mitmproxy、AnyProxy。
  5. 自动忽略原生App发起的https请求,只拦截webview发起的https请求。对使用了SSL pinning技术的原生App不造成任何影响。
  6. 可以配合其它代理工具一起使用(默认使用AnyProxy)
Spydebugger安装与使用
  1. 安装: 全局安装 npm install –g spy-debugger
  2. 启动: spy-debugger
  3. 设置手机的HTTP代理

    代理的地址为 PC的IP地址 ,代理的端口为spy-debugger的启动端口(默认端口为:9888)默认端口是 9888。

    如果要指定端口: spy-debugger –p 8888

    Android设置步骤:设置 - WLAN - 长按选中网络 - 修改网络 - 高级 - 代理设置 - 手动

    iOS设置代理步骤:设置 - 无线局域网 - 选中网络 - HTTP代理手动

  4. 手机安装证书(node-mitmproxy CA根证书)

    第一步:生成证书:

    生成CA根证书,根证书生成在 /Users/XXX/node-mitmproxy/ 目录下(Mac)。

    spy-debugger initCA

    第二步:安装证书:

    把node-mitmproxy文件夹下的 node-mitmproxy.ca.crt 传到手机上,点击安装即可。

    Spy-debugger启动界面,同样,在手机端刷新页面之后,targets中会有记录

clipboard.png

以我曾经做的京豆游戏的页面展示一下效果,当我们在手机上选中一个元素时,可以在电脑上看到相应的信息,这样我们就可以看出有可能是什么样式不兼容导致了UI的异常了,同样,还可以在控制台中看到JS的log信息,对于移动端调试来说非常有帮助。

clipboard.png
]

总结:

  1. chrome inspect应用场景有限
  2. weinre安装简单,使用过程中需要增加和删除script,如果调试页面很多的情况下,不适用。
  3. spy-debugger安装略复杂,但是使用过程非常愉快。

谢谢您花费宝贵的时间阅读本文,如果本文给了您一点帮助或者是启发,那么不要吝啬你的赞和Star哈,您的肯定是我前进的最大动力。https://github.com/YvetteLau/...

推荐关注本人公众号:

clipboard.png

查看原文

tangyanjie123 赞了文章 · 2019-10-05

React 中常见的动画实现方式

现在,用户对于前端页面的要求已经不能满足于实现功能,更要有颜值,有趣味。除了整体 UI 的美观,在合适的地方添加合适的动画效果往往比静态页面更具有表现力,达到更自然的效果。比如,一个简单的 loading 动画或者页面切换效果不仅能缓解用户的等待情绪,甚至通过使用品牌 logo 等形式,默默达到品牌宣传的效果。

React 作为最近几年比较流行的前端开发框架,提出了虚拟 DOM 概念,所有 DOM 的变化都先发生在虚拟 DOM 上,通过 DOM diff 来分析网页的实际变化,然后反映在真实 DOM 上,从而极大地提升网页性能。然而,在动画实现方面,React 作为框架并不会直接给组件提供动画效果,需要开发者自行实现,而传统 web 动画大多数都通过直接操作实际 DOM 元素来实现,这在 React 中显然是不被提倡的。那么,在 React 中动画都是如何实现的呢?

所有动画的本质都是连续修改 DOM 元素的一个或者多个属性,使其产生连贯的变化效果,从而形成动画。在 React 中实现动画本质上与传统 web 动画一样,仍然是两种方式: 通过 css3 动画实现和通过 js 修改元素属性。只不过在具体实现时,要更为符合 React 的框架特性,可以概括为几类:

  1. 基于定时器或 requestAnimationFrame(RAF) 的间隔动画;
  2. 基于 css3 的简单动画;
  3. React 动画插件 CssTransitionGroup
  4. 结合 hook 实现复杂动画;
  5. 其他第三方动画库。

一、基于定时器或 RAF 的间隔动画

最早,动画的实现都是依靠定时器 setIntervalsetTimeout 或者 requestAnimationFrame(RAF) 直接修改 DOM 元素的属性。不熟悉 React 特性的开发者可能会习惯性地通过 ref 或者 findDOMNode() 获取真实的 DOM 节点,直接修改其样式。然而,通过 ref 直接获取真实 DOM 并对其操作是是不被提倡使用,应当尽量避免这种操作。

因此,我们需要将定时器或者 RAF 等方法与 DOM 节点属性通过 state 联系起来。首先,需要提取出与变化样式相关的属性,替换为 state,然后在合适的生命周期函数中添加定时器或者 requestAnimationFrame 不断修改 state,触发组件更新,从而实现动画效果。

示例

以一个进度条为例,代码如下所示:

// 使用requestAnimationFrame改变state
import React, { Component } from 'react';

export default class Progress extends Component {
    constructor(props) {
        super(props);
        this.state = {
            percent: 10
        };
    }

    increase = () => {
        const percent = this.state.percent;
        const targetPercent = percent >= 90 ? 100 : percent + 10;
        const speed = (targetPercent - percent) / 400;
        let start = null;
        const animate = timestamp => {
            if (!start) start = timestamp;
            const progress = timestamp - start;
            const currentProgress = Math.min(parseInt(speed * progress + percent, 10), targetPercent);
            this.setState({
                percent: currentProgress
            });
            if (currentProgress < targetPercent) {
                window.requestAnimationFrame(animate);
            }
        };
        window.requestAnimationFrame(animate);
    }

    decrease = () => {
        const percent = this.state.percent;
        const targetPercent = percent < 10 ? 0 : percent - 10;
        const speed = (percent - targetPercent) / 400;
        let start = null;
        const animate = timestamp => {
            if (!start) start = timestamp;
            const progress = timestamp - start;
            const currentProgress = Math.max(parseInt(percent - speed * progress, 10), targetPercent);
            this.setState({
                    percent: currentProgress
                });
            if (currentProgress > targetPercent) {
                window.requestAnimationFrame(animate);
            }
        };
        window.requestAnimationFrame(animate);
    }

    render() {
        const { percent } = this.state;

        return (
            <div>
                <div className="progress">
                    <div className="progress-wrapper" >
                        <div className="progress-inner" style = {{width: `${percent}%`}} ></div>
                    </div>
                    <div className="progress-info" >{percent}%</div>
                </div>
                <div className="btns">
                    <button onClick={this.decrease}>-</button>
                    <button onClick={this.increase}>+</button>
                </div>
            </div>
        );
    }
}

在示例中,我们在 increasedecrease 函数中构建线性过渡函数 animationrequestAnimationFrame 在浏览器每次重绘前执行会执行过渡函数,计算当前进度条width 属性并更新该 state,使得进度条重新渲染。该示例的效果如下所示:

RAF实现进度条效果

这种实现方式在使用 requestAnimationFrame 时性能不错,完全使用纯 js 实现,不依赖于 css,使用定时器时可能出现掉帧卡顿现象。此外,还需要开发者根据速度函数自己计算状态,比较复杂。

二、基于 css3 的简单动画

当 css3 中的 animationtransition 出现和普及后,我们可以轻松地利用 css 实现元素样式的变化,而不用通过人为计算实时样式。

示例

我们仍以上面的进度条为例,使用 css3 实现进度条动态效果,代码如下所示:

import React, { Component } from 'react';

export default class Progress extends Component {
    constructor(props) {
        super(props);
        this.state = {
            percent: 10
        };
    }

    increase = () => {
        const percent = this.state.percent + 10;
        this.setState({
            percent: percent > 100 ? 100 : percent,
        })
    }

    decrease = () => {
        const percent = this.state.percent - 10;
        this.setState({
            percent: percent < 0 ? 0 : percent,
        })
    }

    render() {
        // 同上例, 省略
        ....
    }
}
.progress-inner {
  transition: width 400ms cubic-bezier(0.08, 0.82, 0.17, 1);
  // 其他样式同上,省略
  ...
}

在示例中,increasedecrease 函数中不再计算 width,而是直接设置增减后的宽度。需要注意的是,在 css 样式中设置了 transition 属性,当 width 属性发生变化时自动实现样式的动态变化效果,并且可以设置不同的速度效果的速度曲线。该示例的效果如下图所示,可以发现,与上一个例子不同的是,右侧的进度数据是直接变化为目标数字,没有具体的变化过程,而进度条的动态效果因为不再是线性变化,效果更为生动。

进度条效果

基于 css3 的实现方式具有较高的性能,代码量少,但是只能依赖于 css 效果,对于复杂动画也很难实现。此外,通过修改 state 实现动画效果,只能作用于已经存在于 DOM 树中的节点。如果想用这种方式为组件添加入场和离场动画,需要维持至少两个 state 来实现入场和离场动画,其中一个 state 用于控制元素是否显示,另一个 state 用于控制元素在动画中的变化属性。在这种情况下,开发者需要花费大量精力来维护组件的动画逻辑,十分复杂繁琐。

三、React 动画插件 CssTransitionGroup

React 曾为开发者提供过动画插件 react-addons-css-transition-group,后交由社区维护,形成现在的 react-transition-group,该插件可以方便地实现组件的入场和离场动画,使用时需要开发者额外安装。react-transition-group 包含 CSSTransitionGroupTransitionGroup 两个动画插件,其中,后者是底层 api,前者是后者的进一步封装,可以较为便捷地实现 css 动画。

示例

以一个动态增加tab的为例,代码如下:

import React, { Component } from 'react';
import { CSSTransitionGroup } from 'react-transition-group';

let uid = 2;
export default class Tabs extends Component {
    constructor(props) {
        super(props);
        this.state = {
            activeId: 1,
            tabData: [{
                id: 1,
                panel: '选项1'
            }, {
                id: 2,
                panel: '选项2'
            }]
        };
    }

    addTab = () => {
        // 添加tab代码
        ...
    }

    deleteTab = (id) => {
        // 删除tab代码
        ...
    }

    render() {
        const { tabData, activeId } = this.state;

        const renderTabs = () => {
            return tabData.map((item, index) => {
                return (
                    <div
                        className={`tab-item${item.id === activeId ? ' tab-item-active' : ''}`}
                        key={`tab${item.id}`}
                    >
                        {item.panel}
                        <span className="btns btn-delete" onClick={() => this.deleteTab(item.id)}>✕</span>
                    </div>
                );
            })
        }

        return (
            <div>
                <div className="tabs" >
                    <CSSTransitionGroup
                      transitionName="tabs-wrap"
                      transitionEnterTimeout={500}
                      transitionLeaveTimeout={500}
                    >
                      {renderTabs()}
                    </CSSTransitionGroup>
                    <span className="btns btn-add" onClick={this.addTab}>+</span>
                </div>
                <div className="tab-cont">
                    cont
                </div>
            </div>
        );
    }
}
/* tab动态增加动画 */
.tabs-wrap-enter {
  opacity: 0.01;
}

.tabs-wrap-enter.tabs-wrap-enter-active {
  opacity: 1;
  transition: all 500ms ease-in;
}

.tabs-wrap-leave {
  opacity: 1;
}

.tabs-wrap-leave.tabs-wrap-leave-active {
  opacity: 0.01;
  transition: all 500ms ease-in;
}

CSSTransitionGroup 可以为其子节点添加额外的 css 类,然后通过 css 动画达到入场和离场动画效果。为了给每个 tab 节点添加动画效果,需要先将它们包裹在 CSSTransitionGroup 组件中。 当设定 transitionName 属性为 'tabs-wrapper'transitionEnterTimeout 为400毫秒后,一旦 CSSTransitionGroup 中新增节点,该新增节点会在出现时被添加上 css 类 'tabs-wrapper-enter',然后在下一帧时被添加上 css 类 'tabs-wrapper-enter-active'。由于这两个 css 类中设定了不同的透明度和 css3 transition 属性,所以节点实现了透明度由小到大的入场效果。400毫秒后 css 类 'tabs-wrapper-enter''tabs-wrapper-enter-active' 将会同时被移除,节点完成整个入场动画过程。离场动画的实现类似于入场动画,只不过被添加的 css 类名为 'tabs-wrapper-leave''tabs-wrapper-leave-active'。该示例效果如下图所示:

动态增加tab效果

CSSTransitionGroup 支持以下7个属性:
CSSTransitionGroup 属性

其中,入场和离场动画是默认开启的,使用时需要设置 transitionEnterTimeouttransitionLeaveTimeout。值得注意的是,CSSTransitionGroup 还提供出现动画(appear),使用时需要设置 transitionAppearTimeout。那么,出现动画和入场动画有什么区别呢?当设定 transitionAppeartrue 时,CSSTransitionGroup初次渲染时,会添加一个出现阶段。在该阶段中,CSSTransitionGroup 的已有子节点都会被相继添加 css 类 'tabs-wrapper-appear''tabs-wrapper-appear-active',实现出现动画效果。因此,出现动画仅适用于 CSSTransitionGroup 在初次渲染时就存在的子节点,一旦 CSSTransitionGroup 完成渲染,其子节点就只可能有入场动画(enter),不可能有出现动画(appear)。

此外,使用 CSSTransitionGroup 需要注意以下几点:

  • CSSTransitionGroup 默认在 DOM 树中生成一个 span 标签包裹其子节点,如果想要使用其他 html 标签,可设定 CSSTransitionGroupcomponent 属性;
  • CSSTransitionGroup 的子元素必须添加 key 值才会在节点发生变化时,准确地计算出哪些节点需要添加入场动画,哪些节点需要添加离场动画;
  • CSSTransitionGroup 的动画效果只作用于直接子节点,不作用于其孙子节点;
  • 动画的结束时间不以 css 中 transition-duration 为准,而是以 transitionEnterTimeouttransitionLeaveTimeoutTransitionAppearTimeout 为准,因为某些情况下 transitionend 事件不会被触发,详见MDN transitionend

CSSTransitionGroup 实现动画的优点是:

  • 简单易用,可以方便快捷地实现元素的入场和离场动画;
  • 与 React 结合,性能比较好。

CSSTransitionGroup 缺点也十分明显:

  • 局限于出现动画,入场动画和离场动画;
  • 由于需要制定 transitionName,灵活性不够;
  • 只能依靠 css 实现简单的动画。

四、结合 hook 实现复杂动画

在实际项目中,可能需要一些更炫酷的动画效果,这些效果仅依赖于 css3 往往较难实现。此时,我们不妨借助一些成熟的第三方库,如 jQuery 或 GASP,结合 React 组件中的生命周期钩子方法 hook 函数,实现复杂动画效果。除了 React 组件正常的生命周期外,CSSTransitionGroup 的底层 api TransitonGroup 还为其子元素额外提供了一系列特殊的生命周期 hook 函数,在这些 hook 函数中结合第三方动画库可以实现丰富的入场、离场动画效果。

TransisitonGroup 分别提供一下六个生命周期 hook 函数:

  • componentWillAppear(callback)
  • componentDidAppear()
  • componentWillEnter(callback)
  • componentDidEnter()
  • componentWillLeave(callback)
  • componentDidLeave()

它们的触发时机如图所示:
TransitionGroup组件生命周期与自组件生命周期的关系

示例

GASP 是一个 flash 时代发展至今的动画库,借鉴视频帧的概念,特别适合做长时间的序列动画效果。本文中,我们用 TransitonGroupreact-gsap-enhancer(一个可以将 GSAP 应用于 React 的增强库)完成一个图片画廊,代码如下:

import React, { Component } from 'react';
import { TransitionGroup } from 'react-transition-group';
import GSAP from 'react-gsap-enhancer'
import { TimelineMax, Back, Sine } from 'gsap';

class Photo extends Component {
    constructor(props) {
        super(props);
    }

    componentWillEnter(callback) {
        this.addAnimation(this.enterAnim, {callback: callback})
    }

    componentWillLeave(callback) {
        this.addAnimation(this.leaveAnim, {callback: callback})
    }

    enterAnim = (utils) => {
        const { id } = this.props;
        return new TimelineMax()
            .from(utils.target, 1, {
                x: `+=${( 4 - id ) * 60}px`,
                autoAlpha: 0,
                onComplete: utils.options.callback,
            }, id * 0.7);
    }

    leaveAnim = (utils) => {
        const { id } = this.props;
        return new TimelineMax()
            .to(utils.target, 0.5, {
                scale: 0,
                ease: Sine.easeOut,
                onComplete: utils.options.callback,
            }, (4 - id) * 0.7);
    }

    render() {
        const { url } = this.props;
        return (
            <div className="photo">
                <img data-original={url} />
            </div>
        )
    }
}

const WrappedPhoto = GSAP()(Photo);

export default class Gallery extends Component {
    constructor(props) {
        super(props);
        this.state = {
            show: false,
            photos: [{
                id: 1,
                url: 'http://img4.imgtn.bdimg.com/it/u=1032683424,3204785822&fm=214&gp=0.jpg'
            }, {
                id: 2,
                url: 'http://imgtu.5011.net/uploads/content/20170323/7488001490262119.jpg'
            }, {
                id: 3,
                url: 'http://tupian.enterdesk.com/2014/lxy/2014/12/03/18/10.jpg'
            }, {
                id: 4,
                url: 'http://img4.imgtn.bdimg.com/it/u=360498760,1598118672&fm=27&gp=0.jpg'
            }]
        };
    }

    toggle = () => {
        this.setState({
            show: !this.state.show
        })
    }

    render() {
        const { show, photos } = this.state;

        const renderPhotos = () => {
            return photos.map((item, index) => {
                return <WrappedPhoto id={item.id} url={item.url} key={`photo${item.id}`} />;
            })
        }

        return (
            <div>
                <button onClick={this.toggle}>toggle</button>
                <TransitionGroup component="div">
                    {show && renderPhotos()}
                </TransitionGroup>
            </div>
        );
    }
}

在该示例中,我们在子组件 PhotocomponentWillEntercomponentWillLeave 两个 hook 函数中为每个子组件添加了入场动画 enterAnim 和 离场动画 LeaveAnim。在入场动画中,使用 TimeLineMax.from(target, duration, vars, delay) 方式建立时间轴动画,指定了每个子组件的动画移动距离随 id 增大而减小,延期时间随着 id 增大而增大,离场动画中每个子组件的延期时间随着 id 增大而减小,从而实现根据组件 id 不同具有不同的动画效果。实际使用时,你可以根据需求对任一子组件添加不同的效果。该示例的效果如下图所示:

图片画廊效果

在使用 TransitionGroup 时,在 componentnWillAppear(callback)componentnWillEntercallback)componentnWillLeave(callback) 函数中一定要在函数逻辑结束后调用 callback,以保证 TransitionGroup 能正确维护子节点的状态序列。关于 GASP 的详细使用方法可参考GASP官方文档和博文GSAP,专业的Web动画库,本文不再赘述。

结合 hook 实现动画可以支持各种复杂动画,如时间序列动画等,由于依赖第三方库,往往动画效果比较流畅,用户体验较好。但是第三方库的引入,需要开发者额外学习对应的 api,也提升了代码复杂度。

五、其他第三方动画库

此外,还有很多优秀的第三方动画库,如 react-motion,Animated,velocity-react等,这些动画库在使用时也各有千秋。

Animated

Animated 是一个跨平台的动画库,兼容 React 和 React Native。由于在动画过程中,我们只关心动画的初始状态、结束状态和变化函数,并不关心每个时刻元素属性的具体值,所以 Animated 采用声明式的动画,通过它提供的特定方法计算 css 对象,并传入 Animated.div 实现动画效果。

示例

我们使用 Animated 实现一个图片翻转的效果,代码如下。

import React, { Component } from 'react';
import Animated from 'animated/lib/targets/react-dom';

export default class PhotoPreview extends Component {
    constructor(props) {
        super(props);
        this.state = {
            anim: new Animated.Value(0)
        };
    }

    handleClick = () => {
        const { anim } = this.state;
        anim.stopAnimation(value => {
            Animated.spring(anim, {
                toValue: Math.round(value) + 1
            }).start();
        });
    }

    render() {
        const { anim } = this.state;

        const rotateDegree = anim.interpolate({
            inputRange: [0, 4],
            outputRange: ['0deg', '360deg']
        });

        return (
            <div>
                <button onClick={this.handleClick}>向右翻转</button>
                <Animated.div
                    style={{
                        transform: [{
                            rotate: rotateDegree
                        }]
                    }}
                    className="preivew-wrapper"
                >
                    <img
                        alt="img"
                        data-original="http://img4.imgtn.bdimg.com/it/u=1032683424,3204785822&fm=214&gp=0.jpg"
                    />
                </Animated.div>
            </div>
        );
    }
}

在该示例中,我们希望实现每点击一次按钮,图片向右旋转90°。在组件初始化时新建了一个初始值为 0 的 Animated 对象 this.state.animAnimated 对象中有插值函数 interpolate,当设定输入区间 inputRange 和输出区间 outputRange 后,插值函数可以根据 Animated 对象的当前值进行线性插值,计算得到对应的映射值。

在本例中,我们假设每点击一次按钮,this.state.anim 的值加 1,图像需要转动90°。在 render 函数中,我们设置插值函数 this.state.anim.interpolate 的输入区间为[0, 4],输出区间为['0deg', '360deg']。当执行动画时,this.state.anim 的值发生变化,插值函数根据 this.state.anim 当前值,计算得到旋转角度 rotateDegree,触发组件的重新渲染。因此,如果 Animated 对象当前值为 2,对应的旋转角度就是 180deg。在组件渲染结构中,需要使用 Animated.div 包裹动画节点,并将 rotateDegree 封装为 css 对象作为 stlye 传入 Animated.div 中,实现节点 css 属性的变化。

在点击事件中,考虑到按钮可能连续多次点击,我们首先使用 stopAnimation 停止当前正在进行的动画,该函数会在回调函数中返回一个 {value : number} 对象,value 对应最后一刻的动画属性值。根据获取的 value 值,随后使用 Animated.spring 函数开启一次新的弹簧动画过程,从而实现一个流畅的动画效果。由于每次转动停止时,我们希望图片的翻转角度都是90°的整数倍,所以需要对 Animated.spring 的终止值进行取整。最终我们实现了如下效果:

image

使用时需要注意一下几点:

  • Animated 对象的值和其插值结果只能作用于 Animated.div 节点;
  • interpolate 默认会根据输入区间和输出区间进行线性插值,如果输入值超出输入区间不受影响,插值结果默认会根据输出区间向外延展插值,可以通过设置 extrapolate 属性限制插值结果区间。

Animated 在动画过程中不直接修改组件 state,而是通过其新建对象的组件和方法直接修改元素的属性,不会重复触发 render 函数,是 React Native 中非常稳定的动画库。但是在 React 中存在低版本浏览器兼容问题,且具有一定学习成本。

结语

当我们在 React 中实现动画时,首先要考量动画的难易程度和使用场景,对于简单动画,优先使用 css3 实现,其次是基于 js 的时间间隔动画。如果是元素入场动画和离场动画,则建议结合 CSSTransitionGroup 或者 TransitionGroup 实现。当要实现的动画效果较为复杂时,不妨尝试一些优秀的第三方库,打开精彩的动效大门。

Ps. 本文所有示例代码可访问 github 查看

参考资料:

react-transition-group

react-gsap-enhancer

A Comparison of Animation Technologies

React Animations in Depth

本文首发于有赞技术博客
查看原文

赞 34 收藏 50 评论 0

tangyanjie123 收藏了文章 · 2019-10-05

React 中常见的动画实现方式

现在,用户对于前端页面的要求已经不能满足于实现功能,更要有颜值,有趣味。除了整体 UI 的美观,在合适的地方添加合适的动画效果往往比静态页面更具有表现力,达到更自然的效果。比如,一个简单的 loading 动画或者页面切换效果不仅能缓解用户的等待情绪,甚至通过使用品牌 logo 等形式,默默达到品牌宣传的效果。

React 作为最近几年比较流行的前端开发框架,提出了虚拟 DOM 概念,所有 DOM 的变化都先发生在虚拟 DOM 上,通过 DOM diff 来分析网页的实际变化,然后反映在真实 DOM 上,从而极大地提升网页性能。然而,在动画实现方面,React 作为框架并不会直接给组件提供动画效果,需要开发者自行实现,而传统 web 动画大多数都通过直接操作实际 DOM 元素来实现,这在 React 中显然是不被提倡的。那么,在 React 中动画都是如何实现的呢?

所有动画的本质都是连续修改 DOM 元素的一个或者多个属性,使其产生连贯的变化效果,从而形成动画。在 React 中实现动画本质上与传统 web 动画一样,仍然是两种方式: 通过 css3 动画实现和通过 js 修改元素属性。只不过在具体实现时,要更为符合 React 的框架特性,可以概括为几类:

  1. 基于定时器或 requestAnimationFrame(RAF) 的间隔动画;
  2. 基于 css3 的简单动画;
  3. React 动画插件 CssTransitionGroup
  4. 结合 hook 实现复杂动画;
  5. 其他第三方动画库。

一、基于定时器或 RAF 的间隔动画

最早,动画的实现都是依靠定时器 setIntervalsetTimeout 或者 requestAnimationFrame(RAF) 直接修改 DOM 元素的属性。不熟悉 React 特性的开发者可能会习惯性地通过 ref 或者 findDOMNode() 获取真实的 DOM 节点,直接修改其样式。然而,通过 ref 直接获取真实 DOM 并对其操作是是不被提倡使用,应当尽量避免这种操作。

因此,我们需要将定时器或者 RAF 等方法与 DOM 节点属性通过 state 联系起来。首先,需要提取出与变化样式相关的属性,替换为 state,然后在合适的生命周期函数中添加定时器或者 requestAnimationFrame 不断修改 state,触发组件更新,从而实现动画效果。

示例

以一个进度条为例,代码如下所示:

// 使用requestAnimationFrame改变state
import React, { Component } from 'react';

export default class Progress extends Component {
    constructor(props) {
        super(props);
        this.state = {
            percent: 10
        };
    }

    increase = () => {
        const percent = this.state.percent;
        const targetPercent = percent >= 90 ? 100 : percent + 10;
        const speed = (targetPercent - percent) / 400;
        let start = null;
        const animate = timestamp => {
            if (!start) start = timestamp;
            const progress = timestamp - start;
            const currentProgress = Math.min(parseInt(speed * progress + percent, 10), targetPercent);
            this.setState({
                percent: currentProgress
            });
            if (currentProgress < targetPercent) {
                window.requestAnimationFrame(animate);
            }
        };
        window.requestAnimationFrame(animate);
    }

    decrease = () => {
        const percent = this.state.percent;
        const targetPercent = percent < 10 ? 0 : percent - 10;
        const speed = (percent - targetPercent) / 400;
        let start = null;
        const animate = timestamp => {
            if (!start) start = timestamp;
            const progress = timestamp - start;
            const currentProgress = Math.max(parseInt(percent - speed * progress, 10), targetPercent);
            this.setState({
                    percent: currentProgress
                });
            if (currentProgress > targetPercent) {
                window.requestAnimationFrame(animate);
            }
        };
        window.requestAnimationFrame(animate);
    }

    render() {
        const { percent } = this.state;

        return (
            <div>
                <div className="progress">
                    <div className="progress-wrapper" >
                        <div className="progress-inner" style = {{width: `${percent}%`}} ></div>
                    </div>
                    <div className="progress-info" >{percent}%</div>
                </div>
                <div className="btns">
                    <button onClick={this.decrease}>-</button>
                    <button onClick={this.increase}>+</button>
                </div>
            </div>
        );
    }
}

在示例中,我们在 increasedecrease 函数中构建线性过渡函数 animationrequestAnimationFrame 在浏览器每次重绘前执行会执行过渡函数,计算当前进度条width 属性并更新该 state,使得进度条重新渲染。该示例的效果如下所示:

RAF实现进度条效果

这种实现方式在使用 requestAnimationFrame 时性能不错,完全使用纯 js 实现,不依赖于 css,使用定时器时可能出现掉帧卡顿现象。此外,还需要开发者根据速度函数自己计算状态,比较复杂。

二、基于 css3 的简单动画

当 css3 中的 animationtransition 出现和普及后,我们可以轻松地利用 css 实现元素样式的变化,而不用通过人为计算实时样式。

示例

我们仍以上面的进度条为例,使用 css3 实现进度条动态效果,代码如下所示:

import React, { Component } from 'react';

export default class Progress extends Component {
    constructor(props) {
        super(props);
        this.state = {
            percent: 10
        };
    }

    increase = () => {
        const percent = this.state.percent + 10;
        this.setState({
            percent: percent > 100 ? 100 : percent,
        })
    }

    decrease = () => {
        const percent = this.state.percent - 10;
        this.setState({
            percent: percent < 0 ? 0 : percent,
        })
    }

    render() {
        // 同上例, 省略
        ....
    }
}
.progress-inner {
  transition: width 400ms cubic-bezier(0.08, 0.82, 0.17, 1);
  // 其他样式同上,省略
  ...
}

在示例中,increasedecrease 函数中不再计算 width,而是直接设置增减后的宽度。需要注意的是,在 css 样式中设置了 transition 属性,当 width 属性发生变化时自动实现样式的动态变化效果,并且可以设置不同的速度效果的速度曲线。该示例的效果如下图所示,可以发现,与上一个例子不同的是,右侧的进度数据是直接变化为目标数字,没有具体的变化过程,而进度条的动态效果因为不再是线性变化,效果更为生动。

进度条效果

基于 css3 的实现方式具有较高的性能,代码量少,但是只能依赖于 css 效果,对于复杂动画也很难实现。此外,通过修改 state 实现动画效果,只能作用于已经存在于 DOM 树中的节点。如果想用这种方式为组件添加入场和离场动画,需要维持至少两个 state 来实现入场和离场动画,其中一个 state 用于控制元素是否显示,另一个 state 用于控制元素在动画中的变化属性。在这种情况下,开发者需要花费大量精力来维护组件的动画逻辑,十分复杂繁琐。

三、React 动画插件 CssTransitionGroup

React 曾为开发者提供过动画插件 react-addons-css-transition-group,后交由社区维护,形成现在的 react-transition-group,该插件可以方便地实现组件的入场和离场动画,使用时需要开发者额外安装。react-transition-group 包含 CSSTransitionGroupTransitionGroup 两个动画插件,其中,后者是底层 api,前者是后者的进一步封装,可以较为便捷地实现 css 动画。

示例

以一个动态增加tab的为例,代码如下:

import React, { Component } from 'react';
import { CSSTransitionGroup } from 'react-transition-group';

let uid = 2;
export default class Tabs extends Component {
    constructor(props) {
        super(props);
        this.state = {
            activeId: 1,
            tabData: [{
                id: 1,
                panel: '选项1'
            }, {
                id: 2,
                panel: '选项2'
            }]
        };
    }

    addTab = () => {
        // 添加tab代码
        ...
    }

    deleteTab = (id) => {
        // 删除tab代码
        ...
    }

    render() {
        const { tabData, activeId } = this.state;

        const renderTabs = () => {
            return tabData.map((item, index) => {
                return (
                    <div
                        className={`tab-item${item.id === activeId ? ' tab-item-active' : ''}`}
                        key={`tab${item.id}`}
                    >
                        {item.panel}
                        <span className="btns btn-delete" onClick={() => this.deleteTab(item.id)}>✕</span>
                    </div>
                );
            })
        }

        return (
            <div>
                <div className="tabs" >
                    <CSSTransitionGroup
                      transitionName="tabs-wrap"
                      transitionEnterTimeout={500}
                      transitionLeaveTimeout={500}
                    >
                      {renderTabs()}
                    </CSSTransitionGroup>
                    <span className="btns btn-add" onClick={this.addTab}>+</span>
                </div>
                <div className="tab-cont">
                    cont
                </div>
            </div>
        );
    }
}
/* tab动态增加动画 */
.tabs-wrap-enter {
  opacity: 0.01;
}

.tabs-wrap-enter.tabs-wrap-enter-active {
  opacity: 1;
  transition: all 500ms ease-in;
}

.tabs-wrap-leave {
  opacity: 1;
}

.tabs-wrap-leave.tabs-wrap-leave-active {
  opacity: 0.01;
  transition: all 500ms ease-in;
}

CSSTransitionGroup 可以为其子节点添加额外的 css 类,然后通过 css 动画达到入场和离场动画效果。为了给每个 tab 节点添加动画效果,需要先将它们包裹在 CSSTransitionGroup 组件中。 当设定 transitionName 属性为 'tabs-wrapper'transitionEnterTimeout 为400毫秒后,一旦 CSSTransitionGroup 中新增节点,该新增节点会在出现时被添加上 css 类 'tabs-wrapper-enter',然后在下一帧时被添加上 css 类 'tabs-wrapper-enter-active'。由于这两个 css 类中设定了不同的透明度和 css3 transition 属性,所以节点实现了透明度由小到大的入场效果。400毫秒后 css 类 'tabs-wrapper-enter''tabs-wrapper-enter-active' 将会同时被移除,节点完成整个入场动画过程。离场动画的实现类似于入场动画,只不过被添加的 css 类名为 'tabs-wrapper-leave''tabs-wrapper-leave-active'。该示例效果如下图所示:

动态增加tab效果

CSSTransitionGroup 支持以下7个属性:
CSSTransitionGroup 属性

其中,入场和离场动画是默认开启的,使用时需要设置 transitionEnterTimeouttransitionLeaveTimeout。值得注意的是,CSSTransitionGroup 还提供出现动画(appear),使用时需要设置 transitionAppearTimeout。那么,出现动画和入场动画有什么区别呢?当设定 transitionAppeartrue 时,CSSTransitionGroup初次渲染时,会添加一个出现阶段。在该阶段中,CSSTransitionGroup 的已有子节点都会被相继添加 css 类 'tabs-wrapper-appear''tabs-wrapper-appear-active',实现出现动画效果。因此,出现动画仅适用于 CSSTransitionGroup 在初次渲染时就存在的子节点,一旦 CSSTransitionGroup 完成渲染,其子节点就只可能有入场动画(enter),不可能有出现动画(appear)。

此外,使用 CSSTransitionGroup 需要注意以下几点:

  • CSSTransitionGroup 默认在 DOM 树中生成一个 span 标签包裹其子节点,如果想要使用其他 html 标签,可设定 CSSTransitionGroupcomponent 属性;
  • CSSTransitionGroup 的子元素必须添加 key 值才会在节点发生变化时,准确地计算出哪些节点需要添加入场动画,哪些节点需要添加离场动画;
  • CSSTransitionGroup 的动画效果只作用于直接子节点,不作用于其孙子节点;
  • 动画的结束时间不以 css 中 transition-duration 为准,而是以 transitionEnterTimeouttransitionLeaveTimeoutTransitionAppearTimeout 为准,因为某些情况下 transitionend 事件不会被触发,详见MDN transitionend

CSSTransitionGroup 实现动画的优点是:

  • 简单易用,可以方便快捷地实现元素的入场和离场动画;
  • 与 React 结合,性能比较好。

CSSTransitionGroup 缺点也十分明显:

  • 局限于出现动画,入场动画和离场动画;
  • 由于需要制定 transitionName,灵活性不够;
  • 只能依靠 css 实现简单的动画。

四、结合 hook 实现复杂动画

在实际项目中,可能需要一些更炫酷的动画效果,这些效果仅依赖于 css3 往往较难实现。此时,我们不妨借助一些成熟的第三方库,如 jQuery 或 GASP,结合 React 组件中的生命周期钩子方法 hook 函数,实现复杂动画效果。除了 React 组件正常的生命周期外,CSSTransitionGroup 的底层 api TransitonGroup 还为其子元素额外提供了一系列特殊的生命周期 hook 函数,在这些 hook 函数中结合第三方动画库可以实现丰富的入场、离场动画效果。

TransisitonGroup 分别提供一下六个生命周期 hook 函数:

  • componentWillAppear(callback)
  • componentDidAppear()
  • componentWillEnter(callback)
  • componentDidEnter()
  • componentWillLeave(callback)
  • componentDidLeave()

它们的触发时机如图所示:
TransitionGroup组件生命周期与自组件生命周期的关系

示例

GASP 是一个 flash 时代发展至今的动画库,借鉴视频帧的概念,特别适合做长时间的序列动画效果。本文中,我们用 TransitonGroupreact-gsap-enhancer(一个可以将 GSAP 应用于 React 的增强库)完成一个图片画廊,代码如下:

import React, { Component } from 'react';
import { TransitionGroup } from 'react-transition-group';
import GSAP from 'react-gsap-enhancer'
import { TimelineMax, Back, Sine } from 'gsap';

class Photo extends Component {
    constructor(props) {
        super(props);
    }

    componentWillEnter(callback) {
        this.addAnimation(this.enterAnim, {callback: callback})
    }

    componentWillLeave(callback) {
        this.addAnimation(this.leaveAnim, {callback: callback})
    }

    enterAnim = (utils) => {
        const { id } = this.props;
        return new TimelineMax()
            .from(utils.target, 1, {
                x: `+=${( 4 - id ) * 60}px`,
                autoAlpha: 0,
                onComplete: utils.options.callback,
            }, id * 0.7);
    }

    leaveAnim = (utils) => {
        const { id } = this.props;
        return new TimelineMax()
            .to(utils.target, 0.5, {
                scale: 0,
                ease: Sine.easeOut,
                onComplete: utils.options.callback,
            }, (4 - id) * 0.7);
    }

    render() {
        const { url } = this.props;
        return (
            <div className="photo">
                <img data-original={url} />
            </div>
        )
    }
}

const WrappedPhoto = GSAP()(Photo);

export default class Gallery extends Component {
    constructor(props) {
        super(props);
        this.state = {
            show: false,
            photos: [{
                id: 1,
                url: 'http://img4.imgtn.bdimg.com/it/u=1032683424,3204785822&fm=214&gp=0.jpg'
            }, {
                id: 2,
                url: 'http://imgtu.5011.net/uploads/content/20170323/7488001490262119.jpg'
            }, {
                id: 3,
                url: 'http://tupian.enterdesk.com/2014/lxy/2014/12/03/18/10.jpg'
            }, {
                id: 4,
                url: 'http://img4.imgtn.bdimg.com/it/u=360498760,1598118672&fm=27&gp=0.jpg'
            }]
        };
    }

    toggle = () => {
        this.setState({
            show: !this.state.show
        })
    }

    render() {
        const { show, photos } = this.state;

        const renderPhotos = () => {
            return photos.map((item, index) => {
                return <WrappedPhoto id={item.id} url={item.url} key={`photo${item.id}`} />;
            })
        }

        return (
            <div>
                <button onClick={this.toggle}>toggle</button>
                <TransitionGroup component="div">
                    {show && renderPhotos()}
                </TransitionGroup>
            </div>
        );
    }
}

在该示例中,我们在子组件 PhotocomponentWillEntercomponentWillLeave 两个 hook 函数中为每个子组件添加了入场动画 enterAnim 和 离场动画 LeaveAnim。在入场动画中,使用 TimeLineMax.from(target, duration, vars, delay) 方式建立时间轴动画,指定了每个子组件的动画移动距离随 id 增大而减小,延期时间随着 id 增大而增大,离场动画中每个子组件的延期时间随着 id 增大而减小,从而实现根据组件 id 不同具有不同的动画效果。实际使用时,你可以根据需求对任一子组件添加不同的效果。该示例的效果如下图所示:

图片画廊效果

在使用 TransitionGroup 时,在 componentnWillAppear(callback)componentnWillEntercallback)componentnWillLeave(callback) 函数中一定要在函数逻辑结束后调用 callback,以保证 TransitionGroup 能正确维护子节点的状态序列。关于 GASP 的详细使用方法可参考GASP官方文档和博文GSAP,专业的Web动画库,本文不再赘述。

结合 hook 实现动画可以支持各种复杂动画,如时间序列动画等,由于依赖第三方库,往往动画效果比较流畅,用户体验较好。但是第三方库的引入,需要开发者额外学习对应的 api,也提升了代码复杂度。

五、其他第三方动画库

此外,还有很多优秀的第三方动画库,如 react-motion,Animated,velocity-react等,这些动画库在使用时也各有千秋。

Animated

Animated 是一个跨平台的动画库,兼容 React 和 React Native。由于在动画过程中,我们只关心动画的初始状态、结束状态和变化函数,并不关心每个时刻元素属性的具体值,所以 Animated 采用声明式的动画,通过它提供的特定方法计算 css 对象,并传入 Animated.div 实现动画效果。

示例

我们使用 Animated 实现一个图片翻转的效果,代码如下。

import React, { Component } from 'react';
import Animated from 'animated/lib/targets/react-dom';

export default class PhotoPreview extends Component {
    constructor(props) {
        super(props);
        this.state = {
            anim: new Animated.Value(0)
        };
    }

    handleClick = () => {
        const { anim } = this.state;
        anim.stopAnimation(value => {
            Animated.spring(anim, {
                toValue: Math.round(value) + 1
            }).start();
        });
    }

    render() {
        const { anim } = this.state;

        const rotateDegree = anim.interpolate({
            inputRange: [0, 4],
            outputRange: ['0deg', '360deg']
        });

        return (
            <div>
                <button onClick={this.handleClick}>向右翻转</button>
                <Animated.div
                    style={{
                        transform: [{
                            rotate: rotateDegree
                        }]
                    }}
                    className="preivew-wrapper"
                >
                    <img
                        alt="img"
                        data-original="http://img4.imgtn.bdimg.com/it/u=1032683424,3204785822&fm=214&gp=0.jpg"
                    />
                </Animated.div>
            </div>
        );
    }
}

在该示例中,我们希望实现每点击一次按钮,图片向右旋转90°。在组件初始化时新建了一个初始值为 0 的 Animated 对象 this.state.animAnimated 对象中有插值函数 interpolate,当设定输入区间 inputRange 和输出区间 outputRange 后,插值函数可以根据 Animated 对象的当前值进行线性插值,计算得到对应的映射值。

在本例中,我们假设每点击一次按钮,this.state.anim 的值加 1,图像需要转动90°。在 render 函数中,我们设置插值函数 this.state.anim.interpolate 的输入区间为[0, 4],输出区间为['0deg', '360deg']。当执行动画时,this.state.anim 的值发生变化,插值函数根据 this.state.anim 当前值,计算得到旋转角度 rotateDegree,触发组件的重新渲染。因此,如果 Animated 对象当前值为 2,对应的旋转角度就是 180deg。在组件渲染结构中,需要使用 Animated.div 包裹动画节点,并将 rotateDegree 封装为 css 对象作为 stlye 传入 Animated.div 中,实现节点 css 属性的变化。

在点击事件中,考虑到按钮可能连续多次点击,我们首先使用 stopAnimation 停止当前正在进行的动画,该函数会在回调函数中返回一个 {value : number} 对象,value 对应最后一刻的动画属性值。根据获取的 value 值,随后使用 Animated.spring 函数开启一次新的弹簧动画过程,从而实现一个流畅的动画效果。由于每次转动停止时,我们希望图片的翻转角度都是90°的整数倍,所以需要对 Animated.spring 的终止值进行取整。最终我们实现了如下效果:

image

使用时需要注意一下几点:

  • Animated 对象的值和其插值结果只能作用于 Animated.div 节点;
  • interpolate 默认会根据输入区间和输出区间进行线性插值,如果输入值超出输入区间不受影响,插值结果默认会根据输出区间向外延展插值,可以通过设置 extrapolate 属性限制插值结果区间。

Animated 在动画过程中不直接修改组件 state,而是通过其新建对象的组件和方法直接修改元素的属性,不会重复触发 render 函数,是 React Native 中非常稳定的动画库。但是在 React 中存在低版本浏览器兼容问题,且具有一定学习成本。

结语

当我们在 React 中实现动画时,首先要考量动画的难易程度和使用场景,对于简单动画,优先使用 css3 实现,其次是基于 js 的时间间隔动画。如果是元素入场动画和离场动画,则建议结合 CSSTransitionGroup 或者 TransitionGroup 实现。当要实现的动画效果较为复杂时,不妨尝试一些优秀的第三方库,打开精彩的动效大门。

Ps. 本文所有示例代码可访问 github 查看

参考资料:

react-transition-group

react-gsap-enhancer

A Comparison of Animation Technologies

React Animations in Depth

本文首发于有赞技术博客
查看原文

tangyanjie123 关注了专栏 · 2019-10-05

有赞前端团队

有赞前端团队官方专栏

关注 291

认证与成就

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

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-08-22
个人主页被 287 人浏览