五月的星空

五月的星空 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织 www.justrockit.top/ 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

五月的星空 赞了文章 · 5月14日

Window.print()打印整个网站中的部分内容,打印后,原网页保持不变

想要达到打印整个网站中的部分内容且完成打印后原网页保持不变的效果,搜到以下资料:
window.print打印指定网页区域的方法
JavaScript页面打印(只打印指定部分)
JS打印指定页面且去样式后的内容,window.print
都能达到打印整个网站中的部分内容,但是,打印后,网页就剩下了打印的内容,其他内容全部被隐藏了,这不是我想要的,最终找到Javascript Print iframe contents only这个链接里的被采纳回答的例子,结合自己的需要,达到了想要的效果。

以上资料里各种方法的利弊都说得很清楚了,我就不废话了,直接看demo吧。

demo

html代码:

<table align="center" class="ml70">
        <tbody>
            <tr>
                <td colspan="8">
                    <div align="center">
                        <b><span >不打印的内容部分//////////////////////</span></b></div>
                    <div>
            </tr>
        </tbody>
    </table>
    <a href="#" onclick="javascript:history.back(1); event.returnValue=false">
        << 返回</a><br />
            <!--startprint-->
            <br />
            <table align="center" class="ml70">
                <tbody>
                    <tr>
                        <td colspan="8">
                            <div align="center">
                                <b><span >//////////////////////要打印的内容部分//////////////////////</span></b></div>
                            <div>
                    </tr>
                </tbody>
            </table>
            <!--endprint-->
            <input type="button" value="打印" onClick="printPage()" />

            <iframe id="printf" data-original="" width="0" height="0" frameborder="0"></iframe>

js代码:

 <script type="text/javascript">  
        function printPage() {  
            //获取当前页的html代码  
            var bodyhtml = window.document.body.innerHTML;  
            //设置打印开始区域、结束区域  
            var startFlag = "<!--startprint-->";  
            var endFlag = "<!--endprint-->";  
            // 要打印的部分  
            var printhtml = bodyhtml.substring(bodyhtml.indexOf(startFlag),   
                    bodyhtml.indexOf(endFlag));  
            // 生成并打印ifrme  
            var f = document.getElementById('printf'); 
            f.contentDocument.write(printhtml);
            f.contentDocument.close();
            f.contentWindow.print();
        }  
    </script>

效果截图

图片描述

图片描述

打印取消后,页面还是第一张图那样子的。

查看原文

赞 12 收藏 8 评论 6

五月的星空 赞了文章 · 2月10日

React Axios 解决前端跨域 代理 Proxy

XMLHttpRequest cannot load http://www.xxx.com/v1. Response to
preflight request doesn't pass access control check: No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://localhost:3000' is therefore not allowed
access.

前端人员在调接口数据的时候遇到这个问题是不是感觉到脑瓜疼,跨域跨域有又跨域,让后台们用CORS...办法,然后自己就等呀等呢!
其实前端也能解决...

我用 create-react-app 创建的项目,执行了npm run eject 之后 webpack 暴露了...但是不着急

还可以在项目 -- config -- webpackDevServer.config.js 下更改

    // 原数据
    module.exports = function (proxy, allowedHost) {
       return {
           ...
           proxy,
           ...  
      }
    }
    // 更改
     module.exports = function (proxy, allowedHost) {
       return {
           ...
           proxy: {
              '/v1': {   
                target: 'http://www.xxx.com',
                changeOrigin: true,
                secure: false,
              },
            },
           ...  
      }
    }
    
    // AXIOS地址也要进行下修改
     $.ajax({
        url:'/v1/code',   // 地址不需要写域名啦
        type:'POST',
        dataType:'json',
        success:function(data){
          console.log(data);
        },
        error:function(error){
          console.log(error)
        }
      })

然后就可以继续写代码啦♪(^∇^*)!

最后附赠两幅图
一个是webpackDevServer.config.js更改前
一个是webpackDevServer.config.js更改后的...
有没有感觉很贴心
O(∩_∩)O哈哈~

clipboard.png

clipboard.png

查看原文

赞 2 收藏 2 评论 1

五月的星空 赞了文章 · 2019-11-21

Javascript自动化文档工具:YUI Doc, JSDoc 3, JSDuck等比较

最近随着写Node以及独立的CommonJS模块越来越多,我发现有一份好的文档不仅可以帮助自己在应用这些接口的时候不至于迷糊,而且对于共同开发的情况下,能够省去大量团队的交流和Debug的时间。

本文比较了5种较为主流的Javascript注释文档生成工具。需要指出的一点是,Javascript是一个极为灵活的语言,文档生成并不像Java那样具有绝对统一的规范,还要根据使用场景确定哪个工具更加适合。


文中涉及的工具


比较标准

  • 生成文档的易读性
  • 工具的可扩展性
  • 注释语法标准
  • 注释语义的丰富程度

长话短说

对没有兴趣了解细节比较的你,可以快速浏览下面的总结来了解这些工具。

YUIDoc是YUI的附属项目。YUI团队希望这个文档工具不仅仅可以支持Javascript,而是更多的脚本语言,因此它并不考虑实际的代码实施细节,而只是对注释部分进行了扫描。从好的一面来说,如果你同时使用了Ruby/PHP/Python等等,YUI或许是一个比较适合的工具。从反面来说,因为它没有考虑实施细节,你需要额外的变量声明、同时生成的文档有可能会和实际的实施细节不吻合。

JSDoc 3的前身是JSDoc Toolkit。它会对代码的具体实施进行扫描,因此你如果不按照符合JSDoc的注释语法来进行注释的话,可能生成的文档一个字也没有。虽然它的学习曲线很高,但是一旦掌握了之后,由于它提供了完整的模版开发,事件触发等等接口,其灵活性也是不容小觑的。

Dox是一个非常轻量级和可以定制化的工具,它使用和JSDoc 3兼容的注释语法标准,并将其转化成JSON格式。因此在呈现方式上具有比JSDoc 3更强的可定制性,当然也意味着你初期的麻烦会比较多。

Docco是一种行间注释的方式。比起API注释,它更适合于注释代码的实施逻辑,以便于后期程序员能够快速捕捉到原作者在此处的实施意图。应该说是非常适合开源项目、多个作者共同维护的一个文档工具。比如最经典的Backbone Annotated Source就是使用Docco进行注释的。

JSDuck由Sencha开发,因此对于自家extJS具有非常强大的支持功能,包括令人咋舌的实时代码修改。但即使你不是使用extJS进行开发,JSDuck仍然是一个非常强大的工具,一方面它的语法非常灵活,描述支持Markdown语法,上手难度远远低于JSDoc 3;另一方面它生成的文档可读性堪比YUIDoc,同时支持源码的对照,学习起来也非常的容易。要说JSDuck有什么不好的地方,估计就是它把一切都Handle太完美了以致于没有给你提供什么可以自己定制的余地。

最后我选择了JSDuck作为文档生成的工具。从目的上说,我需要生成的是提供给Q&A和其他团队成员参考的API文档,考虑到毕竟写代码才是我们的主要工作,注释和文档越简单能够表达意思越好用,而JSDuck恰好十分符合我的需求。但是你选择使用哪种工具还需要根据使用场景来具体考虑,还请参考下面的细节比较


细节比较(JSDoc 3, YUI, JSDuck)

生成文档的易读性(YUIDoc、JSDuck)

  • YUIDoc
    YUIDoc提供了非常清晰的文档格式。不仅对象内部的内容非常清晰,而且相互引用的link也工作的非常好。同时YUIDoc提供了全局搜索的功能,你可以容易根据关键词找到对应的内容。
    图片描述

  • JSDuck
    JSDuck生成的文档绝对不输给YUIDoc。构造方法参数、属性、方法都非常清晰的列在文档之中。它的link也非常的好用,能够准确的定位到不同模块中的内容。JSDuck同样提供了全局搜索的方法,而且还在你敲下关键词的同时给你相关的提示。除了这些以外,JSDuck还提供了对于依赖(dependency)、以及查看源码(View source)的方法。
    图片描述

  • JSDoc 3
    JSDoc生成的文档使用了Bootstrap样式。默认的样式非常非常的糟糕,不过JSDoc 3在自己的介绍页面里推荐了一个第三方开发的模版系统“Docstrap”。这个模版虽然比JSDoc 3的默认模版好上很多,但是与YUIDoc和JSDuck生成的内容相比就差强人意了。其中的link的锚点也会偶尔不能正常工作。此外,它并不支持全局对于变量的搜索,你可以在Docstrap Github的issue中找到他们相关的开发计划。
    除了Docstrap以外,它还有一些推荐的模版系统,例如Jaguar,你也可以在这个基础上开发自己的模版。
    图片描述

工具的可扩展性(JSDoc 3)

  • YUIDoc
    由于YUIDoc是Yahoo下属YUI项目的一个部分,它并不像JSDoc 3提供了那么多可定制的功能。能够修改的大概就只有Logo,基本的CSS样式而已。

  • JSDuck
    JSDuck和YUIDoc的情况非常相似,属于Sencha下属的项目。样式上能够修改的也只有Logo而已。不过JSDuck灵活的地方在于,你可以定制文档顶部的Tag:你可以除了文档以外进一步包含视频、教程等等多种内容。

  • JSDoc 3
    JSDoc 3属于完全开源的项目,因此如果你等不及社区的更新,你完全可以自己对JSDoc进行深度的开发。JSDoc对外提供的开发接口有3个:

    • 模版Template
    • 事件Event
      其中最有意思的我认为是事件功能。JSDoc几乎对载入文件、分析注释和生成文档的每一步都提供了事件的hook:parseBegin, fileBegin, beforeParse, jsdocCommentFound……通过这些事件,你甚至可以进一步定义自己需要的comment tag和解析规则。
    • 插件Plugins

    更加详细的内容则可以在JSDoc的使用说明上找到详细的讲解。

注释语法标准(JSDuck)

最早这一票是投给JSDoc的,但是经过实际的测试之后发现JSDoc的语法标准非常严格,稍微不符合它的解析规则JSDoc便不能够正确的生成文档,其中最让我不能接受的事情是它对于立刻执行函数IIFE的支持实在是%……&*&*……%(里面定义的内容大多被认为是不暴露在全局中的,非常不方便)
下面的说明中我给出了注释一个函数的详细示例。

    function exampleName(config, extra){
        extra = !!extra;   //set the default value of extra to false
        this.callback = config.callback;

        return this;
    }

从这个简单的例子上几乎不太能看出来三者实现的差别,但是当你要注释命名空间、AMD或者CommonJS模块的时候,三者的差别就会凸显出来了。此外要声明的一点是,三者的语法几乎是不能通用的。我曾经试着将用YUIDoc注释的文件使用JSDoc解析,结果非常悲剧,反之亦然。

  • YUIDoc
    YUIDoc为了支持多种语言,它仅对注释块内部的内容进行解析。这意味着你需要对于函数、类、命名空间等的名称和更多的内容进行显性的声明。此外,就像前文提到的,它可能会造成文档和代码实现的不一致,甚至对于接口的使用造成不好的影响。
    YUIDoc对于注释内容要求严格的两层结构:Primary Tag和Secondary Tag
    详细的YUIDoc支持的语法标签可以参考这里

    /**
    * My method description.  Like other pieces of your comment blocks, 
    * this can span multiple lines.
    *
    * @method exampleName  此处必须显性声明method名称
    * @param {Object} config A config object
    * @param {Function} config.callback A callback function on the config object
    * @param {Boolean} [extra=false] Do extra, optional work
    * @example
    *     new exampleName(function(){console.log("Hello World")})
    * @return {Object} Returns the constructed target object
    */
    
  • JSDuck
    JSDuck在这点上恰好处在YUIDoc和JSDoc 3之间。它既对代码的实现进行了最基本的解析,从中获取对应的名称、变量,有效的减少了Tag的使用。同时又不像JSDoc 3那样严格,我尝试了CommonJS、AMD和IIFE都能够非常自然的生成对应的文档。
    详细的JSDuck支持的语法标签可以参考这里

    /**
    * My method description.  Like other pieces of your comment blocks, 
    * this can span multiple lines.
    *
    *     new exampleName(function(){console.log("Hello World")})  JSDuck支持Markdown格式,插入代码示例非常简单
    *
    * @param {Object} config A config object
    * @param {Function} config.callback A callback function on the config object
    * @param {Boolean} [extra=false] Do extra, optional work
    *     
    * @return {Object} The constructed target object
    * @return {Function} return.callback the callback function of the object  JSDuck可以支持返回对象的详细结构注释
    */
    
  • JSDoc 3
    JSDoc 3很大程度上参考了Javadoc的实现。它有非常详细的语法规则,并且你只有当和代码实现完全一致的时候,它才能正确的生成文档。但是对于Javascript这样一门灵活的语言而言,这似乎并不是最适合的方式。虽然代码有推荐的最佳实践,但是为了让文档生成工具能够正确识别而要对原本的代码进行修改就有点僭越本份的意味了。

    /**
    * My method description.  Like other pieces of your comment blocks, 
    * this can span multiple lines.
    *
    * @param {Object} config A config object
    * @param {Function} config.callback A callback function on the config object
    * @param {Boolean} [extra=false] Do extra, optional work
    * @example
    * new exampleName(function(){console.log("Hello World")}) 
    * @returns {Object} The constructed target object
    */
    

注释语义的丰富程度(JSDoc 3、JSDuck)

YUIDoc并非针对Javascript量身定制,因此它有一些用法会让你使用的时候感到比较别扭。比如最明显的一个例子就是,YUIDoc并没有一个专门的方法用来注释namespace,而是必须使用@class来对namespace进行注释。
相比之下JSDoc 3和JSDuck都是针对Javascript而设计的,虽然支持的标签略有差别,但是两者都能够很好的支持常见的设计模式。另外,JSDoc 3由于是从JSDoc Toolkit发展而来的,悠久的历史让它在Stackoverflow上有很多不错的例子可以参考。

查看原文

赞 14 收藏 51 评论 10

五月的星空 赞了文章 · 2019-10-12

JS一维数组与二维数组互相转换的方法

一维数组转化为二维数组

  let baseArray = [1, 2, 3, 4, 5, 6, 7, 8];
  let len = baseArray.length;
  let n = 4; //假设每行显示4个
  let lineNum = len % n === 0 ? len / n : Math.floor( (len / n) + 1 );
  let res = [];
  for (let i = 0; i < lineNum; i++) {
    // slice() 方法返回一个从开始到结束(不包括结束)选择的数组的一部分浅拷贝到一个新数组对象。且原始数组不会被修改。
    let temp = baseArray.slice(i*n, i*n+n);
    res.push(temp);
  }
  console.log(res);

结果:

[[1, 2, 3, 4],[5, 6, 7, 8]]

二维数组转化为一维数组

const arr=[[1,2,3],[3,4],[5]];
console.log([].concat.apply([],arr));

结果:

[1, 2, 3, 3, 4, 5]
查看原文

赞 7 收藏 4 评论 1

五月的星空 收藏了文章 · 2019-09-25

Emoji的编码以及常见问题处理

我在虎嗅上看过一篇关于Emoji的趣闻, 特别有意思, 在这里跟大家分享一下。里面提到了Emoji是怎么诞生的。

1999年前后,日本一个名叫栗田穰崇的年轻人,和许多直男一样, 给女友发的短信经常会被误解。比如,“知道了”被解读成“生气了”、“不耐烦了”,随后引发冷战。 于是少年栗田想:“如果能在文字里插入一些表情符号来表达感情,大家应该会需要吧!”
原始的Emoji就这么诞生了。

Emoji极大地丰富了我们的生活和通讯交流。Emoji诞生自程序员,但反过来对程序员也造成过一些困扰。
尤其对于面向C端的产品开发者, 用户越来越习惯于输入Emoji, 因此处理字符时遇到Emoji也只会越来越频繁。

Emoji的编码

Emoji字符是Unicode字符集中一部分. 特定形象的Emoji表情符号对应到特定的Unicode字节。
常见的Emoji表情符号在Unicode字符集中的范围和具体的字节映射关系, 可通过Emoji Unicode Tables查看到。

有意思的是, 在Emoji Unicode Tables表中,还给出了同一个Emoji表情在不同系统中的字体(是字体没错, Emoji的样式可通过字体文件改变)。

image

关于Emoji的最权威资料, 可以在Unicode® Emoji Charts上查阅到。
截止我写这篇文章的时刻, Emoji Charts 的最新版本是v3.0, v4.0还只是处于Beta阶段。

题外话补充一点: Unicode是一种字符编码方法,它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。
我们所知道的UTF-8、UTF-16等编码, 是对Unicode的不同实现方式。
如果要深入了解更多关于ASCII、Unicode、UTF-8、gb2312、gbk等编码的相关知识,在这里强烈推荐几篇文章,讲得非常好。

一些特殊的Emoji

在众多Emoji中, 有一些特殊的Emoji 并没有显示的样式, 只是起到了控制的作用。这些控制型的Emoji 与基础Emoji 出现在一起, 可以展示更多的样式。

比如 "变量选择器-15"(VARIATION SELECTOR-15, 简写VS-15): <U+FE0E>, 作用是让基础Emoji 变成更接近文本样式(text-style);
而 "变量选择器-16"(VARIATION SELECTOR-16, 简写VS-16): <U+FE0F>, 作用则是让基础Emoji 变成更接近Emoji样式(emoji-style).

VS-15 和 VS-16 加在基础Emoji字符的后面, 可以起到控制作用(前提是必须系统支持, 否则会被忽略)。

image

用一段Python代码来演示该例子:

# -*- coding: utf-8 -*-
# more info to see https://en.wikipedia.org/wiki/Emoji
# 符号分别是上图(截图自wiki)中的符号, 最后再加上一个“狗”的Emoji
sample_list = [u'\u2139', u'\u231B', u'\u26A0', u'\u2712', u'\u2764', u'\U0001F004', u'\U0001F21A', u'\U0001f436', ]

# 输出原样式
for code in sample_list:
    print code,
print
print '-' * 20
# 后面加上VS-15
for code in sample_list:
    print (code + u'\uFE0E'),
print
print '-' * 20
# 后面加上VS-16
for code in sample_list:
    print (code + u'\uFE0F'),

其输出如下图, 第一行是原样式,第二行是加上VS-15后的样式,第三行是加上VS-16后的样式:

image

另外, 还有一些控制型的Emoji, 可以对人体肤色进行改变,改变对象仅限于"表示人身体部位的Emoji".
它们分别是: <U+1F3FB><U+1F3FF> 共五个, 分别简称为: FITZ-1-2, FITZ-3, FITZ-4, FITZ-5, FITZ-6.

image

还有一个特殊的控制符: <U+200D> (ZERO WIDTH JOINER, 简写ZWJ), 起到了连接Emoji的作用, 从而将多个Emoji变成一个Emoji来显示. 同样,前提是必须系统支持, 否则会被忽略.

使用Python代码演示 FITZ-*ZWJ:

# -*- coding: utf-8 -*-
# more info to see https://en.wikipedia.org/wiki/Emoji

# man_list 分别是: 男孩  女孩  男人  女人
man_list = [u'\U0001F466', u'\U0001F467', u'\U0001F468', u'\U0001F469']
# skin_color_list 分别是: 空字符串,表示默认  白种人 -->(不断加深肤色)  黑种人
skin_color_list = ['', u'\U0001F3FB', u'\U0001F3FC', u'\U0001F3FD', u'\U0001F3FE', u'\U0001F3FF', ]
for man in man_list:
    for color in skin_color_list:
        print (man + color),
    print
    print '-' * 20

# Emoji的连接符<U+200D>  (英文名为: ZERO WIDTH JOINER, 简写ZWJ )
# 如果系统支持: 连接(男人 + ZWJ + 女人 + ZWJ + 女孩)
print u'\U0001F468' + u'\u200D' + u'\U0001F469' + u'\u200D' + u'\U0001F467'
# 如果系统不支持: 连接(狗 + ZWJ + 猫 + ZWJ + 老鼠)
print u'\U0001f436' + u'\u200D' + u'\U0001f431' + u'\u200D' + u'\U0001f42d'

其输出如下图:

image

以上内容参考自维基百科

对Emoji 的介绍到该小节结束, 下面内容是一些关于实际中可能遇到的技术问题的解决方法。

MySQL存储Emoji

使用MySQL存储Emoji, 只需要数据表的字符集为utf8mb4即可, 即CHARSET=utf8mb4.

如果想要知道你的MySQL数据库是否支持utf8mb4编码, 可通过show charset; 输出当前安装的MySQL所支持的所有字符集, 查看输出中是否包含有utf8mb4.

另外, 有一些比较老的业务, 可能一开始设计时没考虑到需要支持Emoji, 那就需要修改数据库或数据表的字符集.

查看MySQL说支持的所有字符集
mysql> show charset;

查看某张表当前的字符集
mysql> show create table <table_name>;

创建默认字符集为utf8mb4的数据库.在该数据库中,如果创建表时是不指明字符集,则默认utf8mb4.
mysql> create database default charset utf8mb4;

创建字符集为utf8mb4的表, 数据库的默认字符集非utf8mb4也没问题.
mysql> create table `<table_name>` (Column定义, Column定义, ...) DEFAULT CHARSET=utf8mb4;

修改已存在的数据库的字符集
mysql> alter database <db_name> default charset = utf8mb4;

修改已存在的表的字符集
mysql> alter table <table_name> default charset = utf8mb4;


使用正则表达式匹配Emoji

很可惜, Emoji的范围并没有明确的定义。正如上面提到了,Emoji Charts目前最新版本是v3.0, 未来Emoji的范围还会不断扩大。而且Emoji 在Unicode的分配中并不是连续的区间。

所以, 在这里我只能给出一个可行的匹配区间, 尽可能涵盖了基本常见的Emoj。
该匹配区间中会包含一些未定义的字符, 可能在某些系统会有定义,但是在另外的系统中并没有定义。毕竟Emoji是商业的产物。

该匹配规则区间参考了emoji-data.txtUnicode® Technical Report #51, 如下:

<U+1F300> - <U+1F5FF>      # symbols & pictographs
<U+1F600> - <U+1F64F>      # emoticons
<U+1F680> - <U+1F6FF>      # transport & map symbols
<U+2600>  - <U+2B55>       # other

下面使用Python代码来演示如何使用正则表达式替换(或找出)字符串中的Emoji:

# -*- coding: utf-8 -*-
import re
try:
    # Wide UCS-4 build
    myre = re.compile(u'['
        u'\U0001F300-\U0001F64F'
        u'\U0001F680-\U0001F6FF'
        u'\u2600-\u2B55]+',
        re.UNICODE)
except re.error:
    # Narrow UCS-2 build
    myre = re.compile(u'('
        u'\ud83c[\udf00-\udfff]|'
        u'\ud83d[\udc00-\ude4f\ude80-\udeff]|'
        u'[\u2600-\u2B55])+',
        re.UNICODE)

sss = u'I have a dog \U0001f436 . You have a cat \U0001f431 ! I smile \U0001f601 to you!'
print myre.sub('[Emoji]', sss)  # 替换字符串中的Emoji
print myre.findall(sss)         # 找出字符串中的Emoji

输出如下:

I have a dog [Emoji] . You have a cat [Emoji] ! I smile [Emoji] to you!
[u'\U0001f436', u'\U0001f431', u'\U0001f601']

上面例子中, 之所以使用try...except...来处理代码, 是考虑到 UCS-2 (Narrow UCS-2 build) 和 UCS-4 (Wide UCS-4 build) 的区别.
该Demo例子参考了stackoverflow上的精彩回答, 解答了我对此的困惑。

关于UCS-2和UCS-4的区别, 在上面提到的扩展阅读程序员趣味读物:谈谈Unicode编码中有提到, 值得一看.

本文中使用到的示例代码,可以在我的github下载到。

带有Emoji的字符串截取

在Python、JavaScript 这类编程语言中, 一个中文字符的长度为1,但是对大部分的Emoji(并非全部), 取长度则是2。下面使用Python做演示。

以中文的"汉"字取长度为例,取长度为1:

>>>len(u'汉')
1

而对于Emoji,以<U+1f436>(该Emoji是一只萌萌的狗)为例,取长度为2:

>>>len(u'\U0001f436')
2

那么, 这就存在一个隐患, 在对字符串进行截断时可能从中间截断, 导致该字符显示为乱码, 甚至引发报错。

下面例子中, 对字符串进行截取时,正好从<U+1f436>的中间截断了,出现了乱码:

>>>u'这是一只可爱的狗狗\U0001f436'.__len__()
11
>>>u'这是一只可爱的狗狗\U0001f436'[0:10]
这是一只可爱的狗狗???

实际场景中,对字符串进行截断是非常常见的需求,而且字符串往往可能是用户高度自由的输入内容, 那么包含Emoji的可能性其实是很高的。
一个具体的场景就是: 你正在开发了一款社交APP, 允许用户保存文字记录, 然后在应用的某个地方, 又需要显示这些文字记录的摘要,摘要只显示用户输入的前100个字符, 超出部分用省略号表示。
这种情况下,就不可避免的可能发生Emoji在中间被截断的问题。

解决方案也有多种:

  • 全文进行正则匹配, 去掉大部分Emoji, 但是文本长度过长的情况消耗太大, 不值得.
  • 先截取前200个字符, 匹配去掉Emoji再截取100个字符. 貌似可行. 但如果极端条件下前200个字符都是Emoji怎么办? 管他的.
  • 运用上面提到的扩展阅读: 字符编码笔记:ASCII,Unicode和UTF-8中提到的UTF-8的编码规则, 对截断后字符串的最后字符进行检查, 发现是截断的字符即进行剔除。该方案可行, 不过你需要自己去实现了。
  • 允许一定概率出现乱码, 乱码就乱码吧,概率不高,不影响主要体验。将更多精力放在避免其他bug上吧。
查看原文

五月的星空 赞了文章 · 2019-09-25

Emoji的编码以及常见问题处理

我在虎嗅上看过一篇关于Emoji的趣闻, 特别有意思, 在这里跟大家分享一下。里面提到了Emoji是怎么诞生的。

1999年前后,日本一个名叫栗田穰崇的年轻人,和许多直男一样, 给女友发的短信经常会被误解。比如,“知道了”被解读成“生气了”、“不耐烦了”,随后引发冷战。 于是少年栗田想:“如果能在文字里插入一些表情符号来表达感情,大家应该会需要吧!”
原始的Emoji就这么诞生了。

Emoji极大地丰富了我们的生活和通讯交流。Emoji诞生自程序员,但反过来对程序员也造成过一些困扰。
尤其对于面向C端的产品开发者, 用户越来越习惯于输入Emoji, 因此处理字符时遇到Emoji也只会越来越频繁。

Emoji的编码

Emoji字符是Unicode字符集中一部分. 特定形象的Emoji表情符号对应到特定的Unicode字节。
常见的Emoji表情符号在Unicode字符集中的范围和具体的字节映射关系, 可通过Emoji Unicode Tables查看到。

有意思的是, 在Emoji Unicode Tables表中,还给出了同一个Emoji表情在不同系统中的字体(是字体没错, Emoji的样式可通过字体文件改变)。

image

关于Emoji的最权威资料, 可以在Unicode® Emoji Charts上查阅到。
截止我写这篇文章的时刻, Emoji Charts 的最新版本是v3.0, v4.0还只是处于Beta阶段。

题外话补充一点: Unicode是一种字符编码方法,它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。
我们所知道的UTF-8、UTF-16等编码, 是对Unicode的不同实现方式。
如果要深入了解更多关于ASCII、Unicode、UTF-8、gb2312、gbk等编码的相关知识,在这里强烈推荐几篇文章,讲得非常好。

一些特殊的Emoji

在众多Emoji中, 有一些特殊的Emoji 并没有显示的样式, 只是起到了控制的作用。这些控制型的Emoji 与基础Emoji 出现在一起, 可以展示更多的样式。

比如 "变量选择器-15"(VARIATION SELECTOR-15, 简写VS-15): <U+FE0E>, 作用是让基础Emoji 变成更接近文本样式(text-style);
而 "变量选择器-16"(VARIATION SELECTOR-16, 简写VS-16): <U+FE0F>, 作用则是让基础Emoji 变成更接近Emoji样式(emoji-style).

VS-15 和 VS-16 加在基础Emoji字符的后面, 可以起到控制作用(前提是必须系统支持, 否则会被忽略)。

image

用一段Python代码来演示该例子:

# -*- coding: utf-8 -*-
# more info to see https://en.wikipedia.org/wiki/Emoji
# 符号分别是上图(截图自wiki)中的符号, 最后再加上一个“狗”的Emoji
sample_list = [u'\u2139', u'\u231B', u'\u26A0', u'\u2712', u'\u2764', u'\U0001F004', u'\U0001F21A', u'\U0001f436', ]

# 输出原样式
for code in sample_list:
    print code,
print
print '-' * 20
# 后面加上VS-15
for code in sample_list:
    print (code + u'\uFE0E'),
print
print '-' * 20
# 后面加上VS-16
for code in sample_list:
    print (code + u'\uFE0F'),

其输出如下图, 第一行是原样式,第二行是加上VS-15后的样式,第三行是加上VS-16后的样式:

image

另外, 还有一些控制型的Emoji, 可以对人体肤色进行改变,改变对象仅限于"表示人身体部位的Emoji".
它们分别是: <U+1F3FB><U+1F3FF> 共五个, 分别简称为: FITZ-1-2, FITZ-3, FITZ-4, FITZ-5, FITZ-6.

image

还有一个特殊的控制符: <U+200D> (ZERO WIDTH JOINER, 简写ZWJ), 起到了连接Emoji的作用, 从而将多个Emoji变成一个Emoji来显示. 同样,前提是必须系统支持, 否则会被忽略.

使用Python代码演示 FITZ-*ZWJ:

# -*- coding: utf-8 -*-
# more info to see https://en.wikipedia.org/wiki/Emoji

# man_list 分别是: 男孩  女孩  男人  女人
man_list = [u'\U0001F466', u'\U0001F467', u'\U0001F468', u'\U0001F469']
# skin_color_list 分别是: 空字符串,表示默认  白种人 -->(不断加深肤色)  黑种人
skin_color_list = ['', u'\U0001F3FB', u'\U0001F3FC', u'\U0001F3FD', u'\U0001F3FE', u'\U0001F3FF', ]
for man in man_list:
    for color in skin_color_list:
        print (man + color),
    print
    print '-' * 20

# Emoji的连接符<U+200D>  (英文名为: ZERO WIDTH JOINER, 简写ZWJ )
# 如果系统支持: 连接(男人 + ZWJ + 女人 + ZWJ + 女孩)
print u'\U0001F468' + u'\u200D' + u'\U0001F469' + u'\u200D' + u'\U0001F467'
# 如果系统不支持: 连接(狗 + ZWJ + 猫 + ZWJ + 老鼠)
print u'\U0001f436' + u'\u200D' + u'\U0001f431' + u'\u200D' + u'\U0001f42d'

其输出如下图:

image

以上内容参考自维基百科

对Emoji 的介绍到该小节结束, 下面内容是一些关于实际中可能遇到的技术问题的解决方法。

MySQL存储Emoji

使用MySQL存储Emoji, 只需要数据表的字符集为utf8mb4即可, 即CHARSET=utf8mb4.

如果想要知道你的MySQL数据库是否支持utf8mb4编码, 可通过show charset; 输出当前安装的MySQL所支持的所有字符集, 查看输出中是否包含有utf8mb4.

另外, 有一些比较老的业务, 可能一开始设计时没考虑到需要支持Emoji, 那就需要修改数据库或数据表的字符集.

查看MySQL说支持的所有字符集
mysql> show charset;

查看某张表当前的字符集
mysql> show create table <table_name>;

创建默认字符集为utf8mb4的数据库.在该数据库中,如果创建表时是不指明字符集,则默认utf8mb4.
mysql> create database default charset utf8mb4;

创建字符集为utf8mb4的表, 数据库的默认字符集非utf8mb4也没问题.
mysql> create table `<table_name>` (Column定义, Column定义, ...) DEFAULT CHARSET=utf8mb4;

修改已存在的数据库的字符集
mysql> alter database <db_name> default charset = utf8mb4;

修改已存在的表的字符集
mysql> alter table <table_name> default charset = utf8mb4;


使用正则表达式匹配Emoji

很可惜, Emoji的范围并没有明确的定义。正如上面提到了,Emoji Charts目前最新版本是v3.0, 未来Emoji的范围还会不断扩大。而且Emoji 在Unicode的分配中并不是连续的区间。

所以, 在这里我只能给出一个可行的匹配区间, 尽可能涵盖了基本常见的Emoj。
该匹配区间中会包含一些未定义的字符, 可能在某些系统会有定义,但是在另外的系统中并没有定义。毕竟Emoji是商业的产物。

该匹配规则区间参考了emoji-data.txtUnicode® Technical Report #51, 如下:

<U+1F300> - <U+1F5FF>      # symbols & pictographs
<U+1F600> - <U+1F64F>      # emoticons
<U+1F680> - <U+1F6FF>      # transport & map symbols
<U+2600>  - <U+2B55>       # other

下面使用Python代码来演示如何使用正则表达式替换(或找出)字符串中的Emoji:

# -*- coding: utf-8 -*-
import re
try:
    # Wide UCS-4 build
    myre = re.compile(u'['
        u'\U0001F300-\U0001F64F'
        u'\U0001F680-\U0001F6FF'
        u'\u2600-\u2B55]+',
        re.UNICODE)
except re.error:
    # Narrow UCS-2 build
    myre = re.compile(u'('
        u'\ud83c[\udf00-\udfff]|'
        u'\ud83d[\udc00-\ude4f\ude80-\udeff]|'
        u'[\u2600-\u2B55])+',
        re.UNICODE)

sss = u'I have a dog \U0001f436 . You have a cat \U0001f431 ! I smile \U0001f601 to you!'
print myre.sub('[Emoji]', sss)  # 替换字符串中的Emoji
print myre.findall(sss)         # 找出字符串中的Emoji

输出如下:

I have a dog [Emoji] . You have a cat [Emoji] ! I smile [Emoji] to you!
[u'\U0001f436', u'\U0001f431', u'\U0001f601']

上面例子中, 之所以使用try...except...来处理代码, 是考虑到 UCS-2 (Narrow UCS-2 build) 和 UCS-4 (Wide UCS-4 build) 的区别.
该Demo例子参考了stackoverflow上的精彩回答, 解答了我对此的困惑。

关于UCS-2和UCS-4的区别, 在上面提到的扩展阅读程序员趣味读物:谈谈Unicode编码中有提到, 值得一看.

本文中使用到的示例代码,可以在我的github下载到。

带有Emoji的字符串截取

在Python、JavaScript 这类编程语言中, 一个中文字符的长度为1,但是对大部分的Emoji(并非全部), 取长度则是2。下面使用Python做演示。

以中文的"汉"字取长度为例,取长度为1:

>>>len(u'汉')
1

而对于Emoji,以<U+1f436>(该Emoji是一只萌萌的狗)为例,取长度为2:

>>>len(u'\U0001f436')
2

那么, 这就存在一个隐患, 在对字符串进行截断时可能从中间截断, 导致该字符显示为乱码, 甚至引发报错。

下面例子中, 对字符串进行截取时,正好从<U+1f436>的中间截断了,出现了乱码:

>>>u'这是一只可爱的狗狗\U0001f436'.__len__()
11
>>>u'这是一只可爱的狗狗\U0001f436'[0:10]
这是一只可爱的狗狗???

实际场景中,对字符串进行截断是非常常见的需求,而且字符串往往可能是用户高度自由的输入内容, 那么包含Emoji的可能性其实是很高的。
一个具体的场景就是: 你正在开发了一款社交APP, 允许用户保存文字记录, 然后在应用的某个地方, 又需要显示这些文字记录的摘要,摘要只显示用户输入的前100个字符, 超出部分用省略号表示。
这种情况下,就不可避免的可能发生Emoji在中间被截断的问题。

解决方案也有多种:

  • 全文进行正则匹配, 去掉大部分Emoji, 但是文本长度过长的情况消耗太大, 不值得.
  • 先截取前200个字符, 匹配去掉Emoji再截取100个字符. 貌似可行. 但如果极端条件下前200个字符都是Emoji怎么办? 管他的.
  • 运用上面提到的扩展阅读: 字符编码笔记:ASCII,Unicode和UTF-8中提到的UTF-8的编码规则, 对截断后字符串的最后字符进行检查, 发现是截断的字符即进行剔除。该方案可行, 不过你需要自己去实现了。
  • 允许一定概率出现乱码, 乱码就乱码吧,概率不高,不影响主要体验。将更多精力放在避免其他bug上吧。
查看原文

赞 40 收藏 62 评论 3

五月的星空 赞了回答 · 2019-09-11

点击按钮能不能不触发input框失去焦点事件?

完全可以。
只要给按钮设置 mousedown 事件,并在其中 event.preventDefault() 就可以了

// html
<input type="text" autofocus="autofocus">
<button>点击我文本输入框不会失去焦点</button>

// javascript
var btn = document.querySelector('button')
btn.onmousedown = function(event) {event.preventDefault()}

题主想要点击按钮,触发按钮的 click 事件,但又不想触发 input 的 blur 事件。 这里面的问题就在于,当我们点击按钮的时候,文本框失焦,这是浏览器的默认事件。当你点击按钮的时候,会触发按钮的 mousedown 事件,mousedown 事件的默认行为是使除了你点击的对象之外的有焦点的对象失去焦点。所以只要在 mousedown 事件中阻止默认事件发生就可以了!

关注 5 回答 4

五月的星空 收藏了文章 · 2019-04-14

Redux入门教程(快速上手)

满满的干货,耐心看完,你会发现redux原来这么可爱。

典型的Web应用程序通常由共享数据的多个UI组件组成。通常,多个组件的任务是负责展示同一对象的不同属性。这个对象表示可随时更改的状态。在多个组件之间保持状态的一致性会是一场噩梦,特别是如果有多个通道用于更新同一个对象。

举个小栗子,一个带有购物车的网站。在顶部,我们用一个UI组件显示购物车中的商品数量。我们还可以用另一个UI组件,显示购物车中商
品的总价。如果用户点击添加到购物车按钮,则这两个组件应立即更新当前的数据。如果用户从购物车中删除商品、更改数目、使用优惠券或者更改送货地点,则相关的UI组件都应该更新出正确的信息。
可以看到,随着功能范围的扩大,一个简单的购物车将会很难保持数据同步。

在这篇文章中,我将介绍Redux框架,它可以帮助你以简单易用的方式构建复杂项目并进行维护。为了使学习更容易,我们将使用一个简化的购物车项目来学习Redux的工作原理。你需要至少熟悉React库,因为你以后需要将其与Redux集成。

学习前提

在我们开始以前,确保你熟悉以下知识:

同时,确保你的设备已经安装:

什么是Redux

Redux是一个流行的JavaScript框架,为应用程序提供一个可预测的状态容器。Redux基于简化版本的Flux框架,Flux是Facebook开发的一个框架。在标准的MVC框架中,数据可以在UI组件和存储之间双向流动,而Redux严格限制了数据只能在一个方向上流动。 见下图:

图片描述

在Redux中,所有的数据(比如state)被保存在一个被称为store的容器中 → 在一个应用程序中只能有一个。store本质上是一个状态树,保存了所有对象的状态。任何UI组件都可以直接从store访问特定对象的状态。要通过本地或远程组件更改状态,需要分发一个action分发在这里意味着将可执行信息发送到store。当一个store接收到一个action,它将把这个action代理给相关的reducerreducer是一个纯函数,它可以查看之前的状态,执行一个action并且返回一个新的状态。

理解不变性(Immutability)

在我们开始实践之前,需要先了解JavaScript中的不变性意味着什么。在编码中,我们编写的代码一直在改变变量的值。这是可变性。但是可变性常常会导致意外的错误。如果代码只处理原始数据类型(numbers, strings, booleans),那么你不用担心。但是,如果在处理Arrays和Objects时,则需要小心执行可变操作。
接下来演示不变性

  • 打开终端并启动node(输入node)。
  • 创建一个数组,并将其赋值给另一个变量。
> let a = [1, 2, 3]
> let b = a
> b.push(8)
> b
[1, 2, 3, 8]
> a
[1, 2, 3, 8]

可以看到,更新数组b也会同时改变数组a。这是因为对象和数组是引用数据类型 → 这意味着这样的数据类型实际上并不保存值,而是存储指向存储单元的指针。
将a赋值给b,其实我们只是创建了第二个指向同一存储单元的指针。要解决这个问题,我们需要将引用的值复制到一个新的存储单元。在Javascript中,有三种不同的实现方式:

  1. 使用Immutable.js创建不可变的数据结构。
  2. 使用JavaScript库(如UnderscoreLodash)来执行不可变的操作。
  3. 使用ES6方法执行不可变操作。

本文将使用ES6方法,因为它已经在NodeJS环境中可用了,在终端中,执行以下操作:

> a = [1,2,3]
[ 1, 2, 3 ]
> b = Object.assign([],a)
[ 1, 2, 3 ]
> b.push(8)
> b
[ 1, 2, 3, 8 ] // b output
> a
[ 1, 2, 3 ] // a output

在上面的代码中,修改数组b将不会影响数组a。我们使用Object.assign()创建了一个新的副本,由数组b指向。我们也可以使用操作符(...)执行不可变操作:

> a = [1,2,3]
[ 1, 2, 3 ]
> b = [...a, 4, 5, 6]
[ 1, 2, 3, 4, 5, 6 ]
> a
[ 1, 2, 3 ]

我不会深入这个主题,但是这里还有一些额外的ES6功能,我们可以用它们执行不可变操作:

配置Redux

配置Redux开发环境的最快方法是使用create-react-app工具。在开始之前,确保已经安装并更新了nodejsnpmyarn。我们生成一个redux-shopping-cart项目并安装Redux

create-react-app redux-shopping-cart

cd redux-shopping-cart
yarn add redux # 或者npm install redux

首先,删除src文件夹中除index.js以外的所有文件。打开index.js,删除所有代码,键入以下内容:

import { createStore } from "redux";

const reducer = function(state, action) {
  return state;
}

const store = createStore(reducer);

让我解释一下上面的代码:

  1. 首先,我们从redux包中引入createStore()方法。
  2. 我们创建了一个名为reducer的方法。第一个参数state是当前保存在store中的数据,第二个参数action是一个容器,用于:

    • type - 一个简单的字符串常量,例如ADD, UPDATE, DELETE等。
    • payload - 用于更新状态的数据。
  3. 我们创建一个Redux存储区,它只能使用reducer作为参数来构造。存储在Redux存储区中的数据可以被直接访问,但只能通过提供的reducer进行更新。

注意到,我在第二点中所提到state。目前,state为undefined或null。要解决这个问题,需要分配一个默认的值给state,使其成为一个空数组:

const reducer = function(state=[], action) {
  return state;
}

让我们更进一步。目前我们创建的reducer是通用的。它的名字没有描述它的用途。那么我们如何使用多个reducer呢?我们将用到Redux包中提供的combineReducers函数。修改代码如下:

// src/index.js

import { createStore } from "redux";
import { combineReducers } from 'redux';

const productsReducer = function(state=[], action) {
  return state;
}

const cartReducer = function(state=[], action) {
  return state;
}

const allReducers = {
  products: productsReducer,
  shoppingCart: cartReducer
}

const rootReducer = combineReducers(allReducers);

let store = createStore(rootReducer);

在上面的代码中,我们将通用的reducer修改为productReducercartReducer。创建这两个空的reducer是为了展示如何在一个store中使用combineReducers函数组合多个reducer。

接下来,我们将为reducer定义一些测试数据。修改代码如下:

// src/index.js

…

const initialState = {
  cart: [
    {
      product: 'bread 700g',
      quantity: 2,
      unitCost: 90
    },
    {
      product: 'milk 500ml',
      quantity: 1,
      unitCost: 47
    }
  ]
}

const cartReducer = function(state=initialState, action) {
  return state;
}

…

let store = createStore(rootReducer);

console.log("initial state: ", store.getState());

我们使用store.getState()在控制台中打印出当前的状态。你可以在终端中执行npm start或者yarn start来运行dev服务器。并在控制台中查看state

图片描述

现在,我们的cartReducer什么也没做,但它应该在Redux的存储区中管理购物车商品的状态。我们需要定义添加、更新和删除商品的操作(action)。我们首先定义ADD_TO_CART的逻辑:

// src/index.js

…

const ADD_TO_CART = 'ADD_TO_CART';

const cartReducer = function(state=initialState, action) {
  switch (action.type) {
    case ADD_TO_CART: {
      return {
        ...state,
        cart: [...state.cart, action.payload]
      }
    }

    default:
      return state;
  }
}

…

我们继续来分析一下代码。一个reducer需要处理不同的action类型,因此我们需要一个SWITCH语句。当一个ADD_TO_CART类型的action在应用程序中分发时,switch中的代码将处理它。
正如你所看到的,我们将action.payload中的数据与现有的state合并以创建一个新的state。

接下来,我们将定义一个action,作为store.dispatch()的一个参数。action是一个Javascript对象,有一个必须的type和可选的payload。我们在cartReducer函数后定义一个:

…
function addToCart(product, quantity, unitCost) {
  return {
    type: ADD_TO_CART,
    payload: { product, quantity, unitCost }
  }
}
…

在这里,我们定义了一个函数,返回一个JavaScript对象。在我们分发消息之前,我们添加一些代码,让我们能够监听store事件的更改。

…
let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

unsubscribe();

接下来,我们通过分发消息到store来向购物车中添加商品。将下面的代码添加在unsubscribe()之前:

…
store.dispatch(addToCart('Coffee 500gm', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));

下面是整个index.js文件:

// src/index.js

import { createStore } from "redux";
import { combineReducers } from 'redux';

const productsReducer = function(state=[], action) {
  return state;
}

const initialState = {
  cart: [
    {
      product: 'bread 700g',
      quantity: 2,
      unitCost: 90
    },
    {
      product: 'milk 500ml',
      quantity: 1,
      unitCost: 47
    }
  ]
}

const ADD_TO_CART = 'ADD_TO_CART';

const cartReducer = function(state=initialState, action) {
  switch (action.type) {
    case ADD_TO_CART: {
      return {
        ...state,
        cart: [...state.cart, action.payload]
      }
    }

    default:
      return state;
  }
}

function addToCart(product, quantity, unitCost) {
  return {
    type: ADD_TO_CART,
    payload: {
      product,
      quantity,
      unitCost
    }
  }
}

const allReducers = {
  products: productsReducer,
  shoppingCart: cartReducer
}

const rootReducer = combineReducers(allReducers);

let store = createStore(rootReducer);

console.log("initial state: ", store.getState());

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

store.dispatch(addToCart('Coffee 500gm', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));

unsubscribe();

保存代码后,Chrome会自动刷新。可以在控制台中确认新的商品已经添加了。

图片描述

组织Redux代码

index.js中的代码逐渐变得冗杂。我把所有的代码都写在index.js中是为了起步时的简单易懂。接下来,我们来看一下如何组织Redux项目。首先,在src文件夹中创建一下文件和文件夹:

src/
├── actions
│ └── cart-actions.js
├── index.js
├── reducers
│ ├── cart-reducer.js
│ ├── index.js
│ └── products-reducer.js
└── store.js

然后,我们把index.js中的代码进行整理:

// src/actions/cart-actions.js

export const ADD_TO_CART = 'ADD_TO_CART';

export function addToCart(product, quantity, unitCost) {
  return {
    type: ADD_TO_CART,
    payload: { product, quantity, unitCost }
  }
}
// src/reducers/products-reducer.js

export default function(state=[], action) {
  return state;
}
// src/reducers/cart-reducer.js

import  { ADD_TO_CART }  from '../actions/cart-actions';

const initialState = {
  cart: [
    {
      product: 'bread 700g',
      quantity: 2,
      unitCost: 90
    },
    {
      product: 'milk 500ml',
      quantity: 1,
      unitCost: 47
    }
  ]
}

export default function(state=initialState, action) {
  switch (action.type) {
    case ADD_TO_CART: {
      return {
        ...state,
        cart: [...state.cart, action.payload]
      }
    }

    default:
      return state;
  }
}
// src/reducers/index.js

import { combineReducers } from 'redux';
import productsReducer from './products-reducer';
import cartReducer from './cart-reducer';

const allReducers = {
  products: productsReducer,
  shoppingCart: cartReducer
}

const rootReducer = combineReducers(allReducers);

export default rootReducer;
// src/store.js

import { createStore } from "redux";
import rootReducer from './reducers';

let store = createStore(rootReducer);

export default store;
// src/index.js

import store from './store.js';
import { addToCart }  from './actions/cart-actions';

console.log("initial state: ", store.getState());

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

store.dispatch(addToCart('Coffee 500gm', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));

unsubscribe();

整理完代码之后,程序依然会正常运行。现在我们来添加修改和删除购物车中商品的逻辑。修改cart-actions.jscart-reducer.js文件:

// src/reducers/cart-actions.js
…
export const UPDATE_CART = 'UPDATE_CART';
export const DELETE_FROM_CART = 'DELETE_FROM_CART';
…
export function updateCart(product, quantity, unitCost) {
  return {
    type: UPDATE_CART,
    payload: {
      product,
      quantity,
      unitCost
    }
  }
}

export function deleteFromCart(product) {
  return {
    type: DELETE_FROM_CART,
    payload: {
      product
    }
  }
}
// src/reducers/cart-reducer.js
…
export default function(state=initialState, action) {
  switch (action.type) {
    case ADD_TO_CART: {
      return {
        ...state,
        cart: [...state.cart, action.payload]
      }
    }

    case UPDATE_CART: {
      return {
        ...state,
        cart: state.cart.map(item => item.product === action.payload.product ? action.payload : item)
      }
    }

    case DELETE_FROM_CART: {
      return {
        ...state,
        cart: state.cart.filter(item => item.product !== action.payload.product)
      }
    }

    default:
      return state;
  }
}

最后,我们在index.js中分发这两个action

// src/index.js
…
// Update Cart
store.dispatch(updateCart('Flour 1kg', 5, 110));

// Delete from Cart
store.dispatch(deleteFromCart('Coffee 500gm'));
…

保存完代码之后,可以在浏览器的控制台中检查修改和删除的结果。

使用Redux工具调试

如果我们的代码出错了,应该如何调试呢?

Redux拥有很多第三方的调试工具,可用于分析代码和修复bug。最受欢迎的是time-travelling tool,即redux-devtools-extension。设置它只需要三个步骤。

  • 首先,在Chrome中安装Redux Devtools扩展。
  • 然后,在运行Redux应用程序的终端里使用Ctrl+C停止服务器。并用npm或yarn安装redux-devtools-extension包。
yarn add redux-devtools-extension
  • 一旦安装完成,我们对store.js稍作修改:
// src/store.js
import { createStore } from "redux";
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from './reducers';

const store = createStore(rootReducer, composeWithDevTools());

export default store;

我们还可以把src/index.js中日志相关的代码删除掉。返回Chrome,右键单击该工具的图标,打开Redux DevTools面板:

图片描述

图片描述

可以看到,Redux Devtools很强大。你可以在action, statediff(方法差异)之间切换。选择左侧面板上的不同action,观察状态树的变化。你还可以通过进度条来播放actions序列。甚至可以通过工具直接分发操作信息。具体的请查看文档

集成React

在本文开头,我提到Redux可以很方便的与React集成。只需要简单的几步。

  • 首先,停止服务器,并安装react-redux包:
yarn add react-redux
  • 接下来,在index.js中加入React代码。我们还将使用Provider类将React应用程序包装在Redux容器中:
// src/index.js
…
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

const App = <h1>Redux Shopping Cart</h1>;

ReactDOM.render(
  <Provider store={store}>
    { App }
  </Provider> ,
  document.getElementById('root')
);
…

目前,已经完成了集成的第一部分。可以启动服务器以查看效果。第二部分涉及到使用刚刚安装的react-redux包中的几个方法。通过这些方法将React组件与Redux的storeaction相关联。此外,还可以使用ExpressFeathers这样的框架来设置API。API将为我们的应用程序提供对数据库服务的访问。

感谢网友整理了本文的相关代码,如需要,请移步这里

在Redux中,我们还可以安装其他一些包,比如axios等。我们React组件的state将由Redux处理,确保所有组件与数据库API的同步。想要更进一步的学习,请看Build a CRUD App Using React, Redux and FeathersJS

总结

我希望本文能对你有所帮助。当然,还有很多相关的内容需要学习。例如,处理异步操作、身份验证、日志记录等。如果觉得Redux适合你,可以看看以下几篇文章:

这篇文章是看到比较简明的Redux教程。当然也是翻译过来哒,文中提到了很多延伸文章,我还在一个个学习当中,遇到不错的依然会翻译给大家的。

?喜欢的话记得收藏哦!

查看原文

五月的星空 赞了文章 · 2019-04-14

Redux入门教程(快速上手)

满满的干货,耐心看完,你会发现redux原来这么可爱。

典型的Web应用程序通常由共享数据的多个UI组件组成。通常,多个组件的任务是负责展示同一对象的不同属性。这个对象表示可随时更改的状态。在多个组件之间保持状态的一致性会是一场噩梦,特别是如果有多个通道用于更新同一个对象。

举个小栗子,一个带有购物车的网站。在顶部,我们用一个UI组件显示购物车中的商品数量。我们还可以用另一个UI组件,显示购物车中商
品的总价。如果用户点击添加到购物车按钮,则这两个组件应立即更新当前的数据。如果用户从购物车中删除商品、更改数目、使用优惠券或者更改送货地点,则相关的UI组件都应该更新出正确的信息。
可以看到,随着功能范围的扩大,一个简单的购物车将会很难保持数据同步。

在这篇文章中,我将介绍Redux框架,它可以帮助你以简单易用的方式构建复杂项目并进行维护。为了使学习更容易,我们将使用一个简化的购物车项目来学习Redux的工作原理。你需要至少熟悉React库,因为你以后需要将其与Redux集成。

学习前提

在我们开始以前,确保你熟悉以下知识:

同时,确保你的设备已经安装:

什么是Redux

Redux是一个流行的JavaScript框架,为应用程序提供一个可预测的状态容器。Redux基于简化版本的Flux框架,Flux是Facebook开发的一个框架。在标准的MVC框架中,数据可以在UI组件和存储之间双向流动,而Redux严格限制了数据只能在一个方向上流动。 见下图:

图片描述

在Redux中,所有的数据(比如state)被保存在一个被称为store的容器中 → 在一个应用程序中只能有一个。store本质上是一个状态树,保存了所有对象的状态。任何UI组件都可以直接从store访问特定对象的状态。要通过本地或远程组件更改状态,需要分发一个action分发在这里意味着将可执行信息发送到store。当一个store接收到一个action,它将把这个action代理给相关的reducerreducer是一个纯函数,它可以查看之前的状态,执行一个action并且返回一个新的状态。

理解不变性(Immutability)

在我们开始实践之前,需要先了解JavaScript中的不变性意味着什么。在编码中,我们编写的代码一直在改变变量的值。这是可变性。但是可变性常常会导致意外的错误。如果代码只处理原始数据类型(numbers, strings, booleans),那么你不用担心。但是,如果在处理Arrays和Objects时,则需要小心执行可变操作。
接下来演示不变性

  • 打开终端并启动node(输入node)。
  • 创建一个数组,并将其赋值给另一个变量。
> let a = [1, 2, 3]
> let b = a
> b.push(8)
> b
[1, 2, 3, 8]
> a
[1, 2, 3, 8]

可以看到,更新数组b也会同时改变数组a。这是因为对象和数组是引用数据类型 → 这意味着这样的数据类型实际上并不保存值,而是存储指向存储单元的指针。
将a赋值给b,其实我们只是创建了第二个指向同一存储单元的指针。要解决这个问题,我们需要将引用的值复制到一个新的存储单元。在Javascript中,有三种不同的实现方式:

  1. 使用Immutable.js创建不可变的数据结构。
  2. 使用JavaScript库(如UnderscoreLodash)来执行不可变的操作。
  3. 使用ES6方法执行不可变操作。

本文将使用ES6方法,因为它已经在NodeJS环境中可用了,在终端中,执行以下操作:

> a = [1,2,3]
[ 1, 2, 3 ]
> b = Object.assign([],a)
[ 1, 2, 3 ]
> b.push(8)
> b
[ 1, 2, 3, 8 ] // b output
> a
[ 1, 2, 3 ] // a output

在上面的代码中,修改数组b将不会影响数组a。我们使用Object.assign()创建了一个新的副本,由数组b指向。我们也可以使用操作符(...)执行不可变操作:

> a = [1,2,3]
[ 1, 2, 3 ]
> b = [...a, 4, 5, 6]
[ 1, 2, 3, 4, 5, 6 ]
> a
[ 1, 2, 3 ]

我不会深入这个主题,但是这里还有一些额外的ES6功能,我们可以用它们执行不可变操作:

配置Redux

配置Redux开发环境的最快方法是使用create-react-app工具。在开始之前,确保已经安装并更新了nodejsnpmyarn。我们生成一个redux-shopping-cart项目并安装Redux

create-react-app redux-shopping-cart

cd redux-shopping-cart
yarn add redux # 或者npm install redux

首先,删除src文件夹中除index.js以外的所有文件。打开index.js,删除所有代码,键入以下内容:

import { createStore } from "redux";

const reducer = function(state, action) {
  return state;
}

const store = createStore(reducer);

让我解释一下上面的代码:

  1. 首先,我们从redux包中引入createStore()方法。
  2. 我们创建了一个名为reducer的方法。第一个参数state是当前保存在store中的数据,第二个参数action是一个容器,用于:

    • type - 一个简单的字符串常量,例如ADD, UPDATE, DELETE等。
    • payload - 用于更新状态的数据。
  3. 我们创建一个Redux存储区,它只能使用reducer作为参数来构造。存储在Redux存储区中的数据可以被直接访问,但只能通过提供的reducer进行更新。

注意到,我在第二点中所提到state。目前,state为undefined或null。要解决这个问题,需要分配一个默认的值给state,使其成为一个空数组:

const reducer = function(state=[], action) {
  return state;
}

让我们更进一步。目前我们创建的reducer是通用的。它的名字没有描述它的用途。那么我们如何使用多个reducer呢?我们将用到Redux包中提供的combineReducers函数。修改代码如下:

// src/index.js

import { createStore } from "redux";
import { combineReducers } from 'redux';

const productsReducer = function(state=[], action) {
  return state;
}

const cartReducer = function(state=[], action) {
  return state;
}

const allReducers = {
  products: productsReducer,
  shoppingCart: cartReducer
}

const rootReducer = combineReducers(allReducers);

let store = createStore(rootReducer);

在上面的代码中,我们将通用的reducer修改为productReducercartReducer。创建这两个空的reducer是为了展示如何在一个store中使用combineReducers函数组合多个reducer。

接下来,我们将为reducer定义一些测试数据。修改代码如下:

// src/index.js

…

const initialState = {
  cart: [
    {
      product: 'bread 700g',
      quantity: 2,
      unitCost: 90
    },
    {
      product: 'milk 500ml',
      quantity: 1,
      unitCost: 47
    }
  ]
}

const cartReducer = function(state=initialState, action) {
  return state;
}

…

let store = createStore(rootReducer);

console.log("initial state: ", store.getState());

我们使用store.getState()在控制台中打印出当前的状态。你可以在终端中执行npm start或者yarn start来运行dev服务器。并在控制台中查看state

图片描述

现在,我们的cartReducer什么也没做,但它应该在Redux的存储区中管理购物车商品的状态。我们需要定义添加、更新和删除商品的操作(action)。我们首先定义ADD_TO_CART的逻辑:

// src/index.js

…

const ADD_TO_CART = 'ADD_TO_CART';

const cartReducer = function(state=initialState, action) {
  switch (action.type) {
    case ADD_TO_CART: {
      return {
        ...state,
        cart: [...state.cart, action.payload]
      }
    }

    default:
      return state;
  }
}

…

我们继续来分析一下代码。一个reducer需要处理不同的action类型,因此我们需要一个SWITCH语句。当一个ADD_TO_CART类型的action在应用程序中分发时,switch中的代码将处理它。
正如你所看到的,我们将action.payload中的数据与现有的state合并以创建一个新的state。

接下来,我们将定义一个action,作为store.dispatch()的一个参数。action是一个Javascript对象,有一个必须的type和可选的payload。我们在cartReducer函数后定义一个:

…
function addToCart(product, quantity, unitCost) {
  return {
    type: ADD_TO_CART,
    payload: { product, quantity, unitCost }
  }
}
…

在这里,我们定义了一个函数,返回一个JavaScript对象。在我们分发消息之前,我们添加一些代码,让我们能够监听store事件的更改。

…
let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

unsubscribe();

接下来,我们通过分发消息到store来向购物车中添加商品。将下面的代码添加在unsubscribe()之前:

…
store.dispatch(addToCart('Coffee 500gm', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));

下面是整个index.js文件:

// src/index.js

import { createStore } from "redux";
import { combineReducers } from 'redux';

const productsReducer = function(state=[], action) {
  return state;
}

const initialState = {
  cart: [
    {
      product: 'bread 700g',
      quantity: 2,
      unitCost: 90
    },
    {
      product: 'milk 500ml',
      quantity: 1,
      unitCost: 47
    }
  ]
}

const ADD_TO_CART = 'ADD_TO_CART';

const cartReducer = function(state=initialState, action) {
  switch (action.type) {
    case ADD_TO_CART: {
      return {
        ...state,
        cart: [...state.cart, action.payload]
      }
    }

    default:
      return state;
  }
}

function addToCart(product, quantity, unitCost) {
  return {
    type: ADD_TO_CART,
    payload: {
      product,
      quantity,
      unitCost
    }
  }
}

const allReducers = {
  products: productsReducer,
  shoppingCart: cartReducer
}

const rootReducer = combineReducers(allReducers);

let store = createStore(rootReducer);

console.log("initial state: ", store.getState());

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

store.dispatch(addToCart('Coffee 500gm', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));

unsubscribe();

保存代码后,Chrome会自动刷新。可以在控制台中确认新的商品已经添加了。

图片描述

组织Redux代码

index.js中的代码逐渐变得冗杂。我把所有的代码都写在index.js中是为了起步时的简单易懂。接下来,我们来看一下如何组织Redux项目。首先,在src文件夹中创建一下文件和文件夹:

src/
├── actions
│ └── cart-actions.js
├── index.js
├── reducers
│ ├── cart-reducer.js
│ ├── index.js
│ └── products-reducer.js
└── store.js

然后,我们把index.js中的代码进行整理:

// src/actions/cart-actions.js

export const ADD_TO_CART = 'ADD_TO_CART';

export function addToCart(product, quantity, unitCost) {
  return {
    type: ADD_TO_CART,
    payload: { product, quantity, unitCost }
  }
}
// src/reducers/products-reducer.js

export default function(state=[], action) {
  return state;
}
// src/reducers/cart-reducer.js

import  { ADD_TO_CART }  from '../actions/cart-actions';

const initialState = {
  cart: [
    {
      product: 'bread 700g',
      quantity: 2,
      unitCost: 90
    },
    {
      product: 'milk 500ml',
      quantity: 1,
      unitCost: 47
    }
  ]
}

export default function(state=initialState, action) {
  switch (action.type) {
    case ADD_TO_CART: {
      return {
        ...state,
        cart: [...state.cart, action.payload]
      }
    }

    default:
      return state;
  }
}
// src/reducers/index.js

import { combineReducers } from 'redux';
import productsReducer from './products-reducer';
import cartReducer from './cart-reducer';

const allReducers = {
  products: productsReducer,
  shoppingCart: cartReducer
}

const rootReducer = combineReducers(allReducers);

export default rootReducer;
// src/store.js

import { createStore } from "redux";
import rootReducer from './reducers';

let store = createStore(rootReducer);

export default store;
// src/index.js

import store from './store.js';
import { addToCart }  from './actions/cart-actions';

console.log("initial state: ", store.getState());

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

store.dispatch(addToCart('Coffee 500gm', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));

unsubscribe();

整理完代码之后,程序依然会正常运行。现在我们来添加修改和删除购物车中商品的逻辑。修改cart-actions.jscart-reducer.js文件:

// src/reducers/cart-actions.js
…
export const UPDATE_CART = 'UPDATE_CART';
export const DELETE_FROM_CART = 'DELETE_FROM_CART';
…
export function updateCart(product, quantity, unitCost) {
  return {
    type: UPDATE_CART,
    payload: {
      product,
      quantity,
      unitCost
    }
  }
}

export function deleteFromCart(product) {
  return {
    type: DELETE_FROM_CART,
    payload: {
      product
    }
  }
}
// src/reducers/cart-reducer.js
…
export default function(state=initialState, action) {
  switch (action.type) {
    case ADD_TO_CART: {
      return {
        ...state,
        cart: [...state.cart, action.payload]
      }
    }

    case UPDATE_CART: {
      return {
        ...state,
        cart: state.cart.map(item => item.product === action.payload.product ? action.payload : item)
      }
    }

    case DELETE_FROM_CART: {
      return {
        ...state,
        cart: state.cart.filter(item => item.product !== action.payload.product)
      }
    }

    default:
      return state;
  }
}

最后,我们在index.js中分发这两个action

// src/index.js
…
// Update Cart
store.dispatch(updateCart('Flour 1kg', 5, 110));

// Delete from Cart
store.dispatch(deleteFromCart('Coffee 500gm'));
…

保存完代码之后,可以在浏览器的控制台中检查修改和删除的结果。

使用Redux工具调试

如果我们的代码出错了,应该如何调试呢?

Redux拥有很多第三方的调试工具,可用于分析代码和修复bug。最受欢迎的是time-travelling tool,即redux-devtools-extension。设置它只需要三个步骤。

  • 首先,在Chrome中安装Redux Devtools扩展。
  • 然后,在运行Redux应用程序的终端里使用Ctrl+C停止服务器。并用npm或yarn安装redux-devtools-extension包。
yarn add redux-devtools-extension
  • 一旦安装完成,我们对store.js稍作修改:
// src/store.js
import { createStore } from "redux";
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from './reducers';

const store = createStore(rootReducer, composeWithDevTools());

export default store;

我们还可以把src/index.js中日志相关的代码删除掉。返回Chrome,右键单击该工具的图标,打开Redux DevTools面板:

图片描述

图片描述

可以看到,Redux Devtools很强大。你可以在action, statediff(方法差异)之间切换。选择左侧面板上的不同action,观察状态树的变化。你还可以通过进度条来播放actions序列。甚至可以通过工具直接分发操作信息。具体的请查看文档

集成React

在本文开头,我提到Redux可以很方便的与React集成。只需要简单的几步。

  • 首先,停止服务器,并安装react-redux包:
yarn add react-redux
  • 接下来,在index.js中加入React代码。我们还将使用Provider类将React应用程序包装在Redux容器中:
// src/index.js
…
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

const App = <h1>Redux Shopping Cart</h1>;

ReactDOM.render(
  <Provider store={store}>
    { App }
  </Provider> ,
  document.getElementById('root')
);
…

目前,已经完成了集成的第一部分。可以启动服务器以查看效果。第二部分涉及到使用刚刚安装的react-redux包中的几个方法。通过这些方法将React组件与Redux的storeaction相关联。此外,还可以使用ExpressFeathers这样的框架来设置API。API将为我们的应用程序提供对数据库服务的访问。

感谢网友整理了本文的相关代码,如需要,请移步这里

在Redux中,我们还可以安装其他一些包,比如axios等。我们React组件的state将由Redux处理,确保所有组件与数据库API的同步。想要更进一步的学习,请看Build a CRUD App Using React, Redux and FeathersJS

总结

我希望本文能对你有所帮助。当然,还有很多相关的内容需要学习。例如,处理异步操作、身份验证、日志记录等。如果觉得Redux适合你,可以看看以下几篇文章:

这篇文章是看到比较简明的Redux教程。当然也是翻译过来哒,文中提到了很多延伸文章,我还在一个个学习当中,遇到不错的依然会翻译给大家的。

?喜欢的话记得收藏哦!

查看原文

赞 297 收藏 255 评论 29

五月的星空 赞了文章 · 2019-04-02

Vue.nextTick 的原理和用途

对于 Vue.nextTick 方法,自己有些疑惑。在查询了各种资料后,总结了一下其原理和用途,如有错误,请不吝赐教。

概览

官方文档说明:
  • 用法:

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

疑问:
  1. DOM 更新循环是指什么?
  2. 下次更新循环是什么时候?
  3. 修改数据之后使用,是加快了数据更新进度吗?
  4. 在什么情况下要用到?

原理

异步说明

Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。

在 Vue 的文档中,说明 Vue 是异步执行 DOM 更新的。关于异步的解析,可以查看阮一峰老师的这篇文章。截取关键部分如下:

具体来说,异步执行的运行机制如下。

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

下图就是主线程和任务队列的示意图。

clipboard.png

事件循环说明

简单来说,Vue 在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。

知乎上的例子:

//改变数据
vm.message = 'changed'

//想要立即使用更新后的DOM。这样不行,因为设置message后DOM还没有更新
console.log(vm.$el.textContent) // 并不会得到'changed'

//这样可以,nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function(){
    console.log(vm.$el.textContent) //可以得到'changed'
})

图解:

clipboard.png

事件循环:

第一个 tick(图例中第一个步骤,即'本次更新循环'):

  1. 首先修改数据,这是同步任务。同一事件循环的所有的同步任务都在主线程上执行,形成一个执行栈,此时还未涉及 DOM 。
  2. Vue 开启一个异步队列,并缓冲在此事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。

第二个 tick(图例中第二个步骤,即'下次更新循环'):

同步任务执行完毕,开始执行异步 watcher 队列的任务,更新 DOM 。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel 方法,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。

第三个 tick(图例中第三个步骤):

此时就是文档所说的

下次 DOM 更新循环结束之后

此时通过 Vue.nextTick 获取到改变后的 DOM 。通过 setTimeout(fn, 0) 也可以同样获取到。


简单总结事件循环:

同步代码执行 -> 查找异步队列,推入执行栈,执行Vue.nextTick[事件循环1] ->查找异步队列,推入执行栈,执行Vue.nextTick[事件循环2]...

总之,异步是单独的一个tick,不会和同步在一个 tick 里发生,也是 DOM 不会马上改变的原因。

对于事件循环,可以在这里查看更详细的内容: https://segmentfault.com/a/11...

用途

应用场景:需要在视图更新之后,基于新的视图进行操作。

created、mounted

需要注意的是,在 created 和 mounted 阶段,如果需要操作渲染后的试图,也要使用 nextTick 方法。

官方文档说明:

注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted
mounted: function () {
  this.$nextTick(function () {
    // Code that will run only after the
    // entire view has been rendered
  })
}

其他应用场景

其他应用场景如下三例:

例子1:

点击按钮显示原本以 v-show = false 隐藏起来的输入框,并获取焦点。

showsou(){
  this.showit = true //修改 v-show
  document.getElementById("keywords").focus()  //在第一个 tick 里,获取不到输入框,自然也获取不到焦点
}

修改为:

showsou(){
  this.showit = true
  this.$nextTick(function () {
    // DOM 更新了
    document.getElementById("keywords").focus()
  })
}

例子2:

点击获取元素宽度。

<div id="app">
    <p ref="myWidth" v-if="showMe">{{ message }}</p>
    <button @click="getMyWidth">获取p元素宽度</button>
</div>

getMyWidth() {
    this.showMe = true;
    //this.message = this.$refs.myWidth.offsetWidth;
    //报错 TypeError: this.$refs.myWidth is undefined
    this.$nextTick(()=>{
        //dom元素更新后执行,此时能拿到p元素的属性
        this.message = this.$refs.myWidth.offsetWidth;
  })
}

例子3:

使用 swiper 插件通过 ajax 请求图片后的滑动问题。

实例理解 nextTick 应用

下面的例子来自 https://www.cnblogs.com/hity-..., 稍有改动。各位可以复制运行一遍,加深理解。

<template>
    <div>
        <ul>
            <li class="example" v-for="item in list1">{{item}}</li>
        </ul>
        <ul>
            <li class="example" v-for="item in list2">{{item}}</li>
        </ul>
        <ol>
            <li class="example" v-for="item in list3">{{item}}</li>
        </ol>
        <ol>
            <li class="example" v-for="item in list4">{{item}}</li>
        </ol>
        <ol>
            <li class="example" v-for="item in list5">{{item}}</li>
        </ol>
    </div>
</template>
<script type="text/javascript">
export default {
    data() {
        return {
            list1: [],
            list2: [],
            list3: [],
            list4: [],
            list5: []
        }
    },
    created() {
        this.composeList12()
        this.composeList34()
        this.composeList5()
        this.$nextTick(function() {
            // DOM 更新了
            console.log('finished test ' + new Date().toString(),document.querySelectorAll('.example').length)
        })
    },
    methods: {
        composeList12() {
            let me = this
            let count = 10000

            for (let i = 0; i < count; i++) {
                this.$set(me.list1, i, 'I am a 测试信息~~啦啦啦' + i)
            }
            console.log('finished list1 ' + new Date().toString(),document.querySelectorAll('.example').length)

            for (let i = 0; i < count; i++) {
                this.$set(me.list2, i, 'I am a 测试信息~~啦啦啦' + i)
            }
            console.log('finished list2 ' + new Date().toString(),document.querySelectorAll('.example').length)

            this.$nextTick(function() {
                // DOM 更新了
                console.log('finished tick1&2 ' + new Date().toString(),document.querySelectorAll('.example').length)
            })
        },
        composeList34() {
            let me = this
            let count = 10000

            for (let i = 0; i < count; i++) {
                this.$set(me.list3, i, 'I am a 测试信息~~啦啦啦' + i)
            }
            console.log('finished list3 ' + new Date().toString(),document.querySelectorAll('.example').length)

            this.$nextTick(function() {
                // DOM 更新了
                console.log('finished tick3 ' + new Date().toString(),document.querySelectorAll('.example').length)
            })

            setTimeout(me.setTimeout1, 0)
        },
        setTimeout1() {
            let me = this
            let count = 10000

            for (let i = 0; i < count; i++) {
                this.$set(me.list4, i, 'I am a 测试信息~~啦啦啦' + i)
            }
            console.log('finished list4 ' + new Date().toString(),document.querySelectorAll('.example').length)

            me.$nextTick(function() {
                // DOM 更新了
                console.log('finished tick4 ' + new Date().toString(),document.querySelectorAll('.example').length)
            })
        },
        composeList5() {
            let me = this
            let count = 10000

            this.$nextTick(function() {
                // DOM 更新了
                console.log('finished tick5-1 ' + new Date().toString(),document.querySelectorAll('.example').length)
            })

            setTimeout(me.setTimeout2, 0)
        },
        setTimeout2() {
            let me = this
            let count = 10000

            for (let i = 0; i < count; i++) {
                this.$set(me.list5, i, 'I am a 测试信息~~啦啦啦' + i)
            }
            console.log('finished list5 ' + new Date().toString(),document.querySelectorAll('.example').length)

            me.$nextTick(function() {
                // DOM 更新了
                console.log('finished tick5 ' + new Date().toString(),document.querySelectorAll('.example').length)
            })
        }
    }
}
</script>

结果:

图片描述

参考文章

vue nextTick深入理解-vue性能优化、DOM更新时机、事件循环机制;
JavaScript 运行机制详解:再谈Event Loop
知乎:vue.js$nextTick的一个问题
JS 事件循环机制 - 任务队列、web API、JS主线程的相互协同

查看原文

赞 383 收藏 286 评论 30

认证与成就

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

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-08-28
个人主页被 495 人浏览