Friday, July 8, 2022

SegmentFault 最新的文章

SegmentFault 最新的文章


熬夜爆肝整理的一份elasticsearch中文文档手册

Posted: 01 Jul 2022 02:50 PM PDT

由于本文篇幅较长,想要获取PDF,请关注'公众号-菜鸟成长学习笔记'回复"es手册"即可领取文件。

es概括

Elaticsearch,简称为 ES, ES 是一个开源的高扩展的分布式全文搜索引擎,Elasticsearch 是面向文档型数据库,一条数据在这里就是一个文档。

基本要素

ES是一个文档型数据库,在与传统的关系型数据库上,存在着一定的差异。下面将ES里面涉及到的元素与关系型数据库进行一一对应。

ElasticSearch索引(index)类型(type)文档(document)字段(field)
MySQL数据库(database)数据表(table)数据行(row)数据列(column)

索引操作

创建索引

向 ES 服务器发 PUT 请求 : http://127.0.0.1:9200/shopping。创建索引只能使用PUT请求,PUT是幂等性的,也就是说不存在的时候就会创建,存在的时候就不会重新创建而是返回索引已经存在的信息。

{     "acknowledged": true,//响应结果     "shards_acknowledged": true,//分片结果     "index": "shopping"//索引名称 }

查询索引

向 ES 服务器发 GET 请求 : http://127.0.0.1:9200/shopping

{     "shopping": {//索引名         "aliases": {},//别名         "mappings": {},//映射         "settings": {//设置             "index": {//设置 - 索引                 "creation_date": "1617861426847",//设置 - 索引 - 创建时间                 "number_of_shards": "1",//设置 - 索引 - 主分片数量                 "number_of_replicas": "1",//设置 - 索引 - 主分片数量                 "uuid": "J0WlEhh4R7aDrfIc3AkwWQ",//设置 - 索引 - 主分片数量                 "version": {//设置 - 索引 - 主分片数量                     "created": "7080099"                 },                 "provided_name": "shopping"//设置 - 索引 - 主分片数量             }         }     } }

查看所有索引

向 ES 服务器发 GET 请求 : http://127.0.0.1:9200/_cat/indices?v

这里请求路径中的_cat 表示查看的意思, indices 表示索引,所以整体含义就是查看当前 ES服务器中的所有索引,就好像 MySQL 中的 show tables 的感觉,服务器响应结果如下 :

health status index    uuid                   pri rep docs.count docs.deleted store.size pri.store.size yellow open   shopping J0WlEhh4R7aDrfIc3AkwWQ   1   1          0            0       208b           208b

删除索引

向 ES 服务器发 DELETE 请求 : http://127.0.0.1:9200/shopping

返回结果如下:

{     "acknowledged": true }

文档操作

文档创建

假设索引已经创建好了,接下来我们来创建文档,并添加数据。这里的文档可以类比为关系型数据库中的表数据,添加的数据格式为 JSON 格式

在 Postman 中,向 ES 服务器发 POST 请求 : http://127.0.0.1:9200/shopping/_doc,请求体JSON内容为:

{     "title":"小米手机",     "category":"小米",     "images":"http://www.gulixueyuan.com/xm.jpg",     "price":3999.00 }

返回结果

{     "_index": "shopping",//索引     "_type": "_doc",//类型-文档     "_id": "ANQqsHgBaKNfVnMbhZYU",//唯一标识,可以类比为 MySQL 中的主键,随机生成     "_version": 1,//版本     "result": "created",//结果,这里的 create 表示创建成功     "_shards": {//         "total": 2,//分片 - 总数         "successful": 1,//分片 - 总数         "failed": 0//分片 - 总数     },     "_seq_no": 0,     "_primary_term": 1 }
注意,此处发送文档创建请求的方式必须为 POST,不能是 PUT,否则会发生错误 。

上面的数据创建后,由于没有指定数据唯一性标识(ID),默认情况下, ES 服务器会随机生成一个。

如果想要自定义唯一性标识,需要在创建时指定: http://127.0.0.1:9200/shopping/_doc/1,请求体JSON内容为:

{     "title":"小米手机",     "category":"小米",     "images":"http://www.gulixueyuan.com/xm.jpg",     "price":3999.00 }

返回结果如下:

{     "_index": "shopping",     "_type": "_doc",     "_id": "1",//<-----自定义唯一性标识     "_version": 1,     "result": "created",     "_shards": {         "total": 2,         "successful": 1,         "failed": 0     },     "_seq_no": 1,     "_primary_term": 1 }

文档查询

查看文档时,需要指明文档的唯一性标识,类似于 MySQL 中数据的主键查询
在 Postman 中,向 ES 服务器发 GET 请求 : http://127.0.0.1:9200/shopping/_doc/1

返回结果如下:

{     "_index": "shopping",     "_type": "_doc",     "_id": "1",     "_version": 1,     "_seq_no": 1,     "_primary_term": 1,     "found": true,     "_source": {         "title": "小米手机",         "category": "小米",         "images": "http://www.gulixueyuan.com/xm.jpg",         "price": 3999     } }

查找不存在的内容,向 ES 服务器发 GET 请求 : http://127.0.0.1:9200/shoppin...。返回结果如下:

{     "_index": "shopping",     "_type": "_doc",     "_id": "1001",     "found": false }

查看索引下所有数据,向 ES 服务器发 GET 请求 : http://127.0.0.1:9200/shopping/_search

返回结果如下:

{     "took": 133,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 2,             "relation": "eq"         },         "max_score": 1,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "ANQqsHgBaKNfVnMbhZYU",                 "_score": 1,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 3999                 }             },             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "1",                 "_score": 1,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 3999                 }             }         ]     } }

文档删除

删除一个文档不会立即从磁盘上移除,它只是被标记成已删除(逻辑删除)。

在 Postman 中,向 ES 服务器发 DELETE 请求 : http://127.0.0.1:9200/shopping/_doc/1
返回结果:

{     "_index": "shopping",     "_type": "_doc",     "_id": "1",     "_version": 4,     "result": "deleted",//<---删除成功     "_shards": {         "total": 2,         "successful": 1,         "failed": 0     },     "_seq_no": 4,     "_primary_term": 1 }

文档修改

全量修改

和新增文档一样,输入相同的 URL 地址请求,如果请求体变化,会将原有的数据内容覆盖

在 Postman 中,向 ES 服务器发 POST 请求 : http://127.0.0.1:9200/shopping/_doc/1
请求体JSON内容为:

{     "title":"华为手机",     "category":"华为",     "images":"http://www.gulixueyuan.com/hw.jpg",     "price":1999.00 }

修改成功后,服务器响应结果:

{     "_index": "shopping",     "_type": "_doc",     "_id": "1",     "_version": 2,     "result": "updated",//<---updated 表示数据被更新     "_shards": {         "total": 2,         "successful": 1,         "failed": 0     },     "_seq_no": 2,     "_primary_term": 1 }

局部更新

修改数据时,也可以只修改某一给条数据的局部信息

在 Postman 中,向 ES 服务器发 POST 请求 : http://127.0.0.1:9200/shopping/_update/1

请求体JSON内容为:

{     "doc": {         "title":"小米手机",         "category":"小米"     } }

返回结果如下:

{     "_index": "shopping",     "_type": "_doc",     "_id": "1",     "_version": 3,     "result": "updated",//<----updated 表示数据被更新     "_shards": {         "total": 2,         "successful": 1,         "failed": 0     },     "_seq_no": 3,     "_primary_term": 1 }

URL待条件查询

查找category为小米的文档,在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search?q=category:小米,返回结果如下:

{     "took": 94,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 3,             "relation": "eq"         },         "max_score": 1.3862942,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "ANQqsHgBaKNfVnMbhZYU",                 "_score": 1.3862942,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 3999                 }             },             ......         ]     } }

上述为URL带参数形式查询,这很容易让不善者心怀恶意,或者参数值出现中文会出现乱码情况。为了避免这些情况,我们可用使用带JSON请求体请求进行查询。

请求体带参查询

接下带JSON请求体,还是查找category为小米的文档,在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:

{     "query":{         "match":{             "category":"小米"         }     } }

返回结果如下:

{     "took": 3,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 3,             "relation": "eq"         },         "max_score": 1.3862942,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "ANQqsHgBaKNfVnMbhZYU",                 "_score": 1.3862942,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 3999                 }             },             ......         ]     } }

带请求体方式的查找所有内容

查找所有文档内容,也可以这样,在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下

{     "query":{         "match_all":{}     } }

则返回所有文档内容:

{     "took": 2,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 6,             "relation": "eq"         },         "max_score": 1,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "ANQqsHgBaKNfVnMbhZYU",                 "_score": 1,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 3999                 }             },             ......         ]     } }

查询指定字段

如果你想查询指定字段,在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:

{     "query":{         "match_all":{}     },     "_source":["title"] }

返回结果如下:

{     "took": 5,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 6,             "relation": "eq"         },         "max_score": 1,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "ANQqsHgBaKNfVnMbhZYU",                 "_score": 1,                 "_source": {                     "title": "小米手机"                 }             },             ......         ]     } }

分页查询

在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:

{     "query":{         "match_all":{}     },     "from":0,     "size":2 }

返回结果如下:

{     "took": 1,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 6,             "relation": "eq"         },         "max_score": 1,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "ANQqsHgBaKNfVnMbhZYU",                 "_score": 1,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 3999                 }             },         ]     } }

查询排序

如果你想通过排序查出价格最高的手机,在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:

{     "query":{         "match_all":{}     },     "sort":{         "price":{             "order":"desc"         }     } }

返回结果如下:

{     "took": 96,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 6,             "relation": "eq"         },         "max_score": null,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "ANQqsHgBaKNfVnMbhZYU",                 "_score": null,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 3999                 },                 "sort": [                     3999                 ]             },             ......         ]     } }

多条件查询

假设想找出小米牌子,价格为3999元的。(must相当于数据库的&&),在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:

{     "query":{         "bool":{             "must":[{                 "match":{                     "category":"小米"                 }             },{                 "match":{                     "price":3999.00                 }             }]         }     } }

返回结果如下:

{     "took": 134,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 1,             "relation": "eq"         },         "max_score": 2.3862944,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "ANQqsHgBaKNfVnMbhZYU",                 "_score": 2.3862944,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 3999                 }             }         ]     } }

假设想找出小米和华为的牌子。(should相当于数据库的||)在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:

{     "query":{         "bool":{             "should":[{                 "match":{                     "category":"小米"                 }             },{                 "match":{                     "category":"华为"                 }             }]         },         "filter":{             "range":{                 "price":{                     "gt":2000                 }             }         }     } }

返回结果如下:

{     "took": 8,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 6,             "relation": "eq"         },         "max_score": 1.3862942,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "ANQqsHgBaKNfVnMbhZYU",                 "_score": 1.3862942,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 3999                 }             },             .....         ]     } }

范围查询

假设想找出小米和华为的牌子,价格大于2000元的手机。在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:

{     "query":{         "bool":{             "should":[{                 "match":{                     "category":"小米"                 }             },{                 "match":{                     "category":"华为"                 }             }],             "filter":{                 "range":{                     "price":{                         "gt":2000                     }                 }             }         }     } }

返回结果如下:

{     "took": 72,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 1,             "relation": "eq"         },         "max_score": 1.3862942,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "ANQqsHgBaKNfVnMbhZYU",                 "_score": 1.3862942,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 3999                 }             }         ]     } }

全文检索

这功能像搜索引擎那样,如品牌输入"小华",返回结果带回品牌有"小米"和华为的。在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:

{     "query":{         "match":{             "category" : "小华"         }     } }

返回结果如下:

{     "took": 7,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 6,             "relation": "eq"         },         "max_score": 0.6931471,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "ANQqsHgBaKNfVnMbhZYU",                 "_score": 0.6931471,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 3999                 }             },             ......             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "BtR6sHgBaKNfVnMbX5Y5",                 "_score": 0.6931471,                 "_source": {                     "title": "华为手机",                     "category": "华为",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 1999                 }             },             ......         ]     } }

完全匹配

在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:

{     "query":{         "match_phrase":{             "category" : "为"         }     } }

返回结果如下:

{     "took": 2,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 3,             "relation": "eq"         },         "max_score": 0.6931471,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "BtR6sHgBaKNfVnMbX5Y5",                 "_score": 0.6931471,                 "_source": {                     "title": "华为手机",                     "category": "华为",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 1999                 }             },             ......         ]     } }

高亮查询

在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:

{     "query":{         "match_phrase":{             "category" : "为"         }     },     "highlight":{         "fields":{             "category":{}//<----高亮这字段         }     } }

返回结果如下:

{     "took": 100,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 3,             "relation": "eq"         },         "max_score": 0.6931471,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "BtR6sHgBaKNfVnMbX5Y5",                 "_score": 0.6931471,                 "_source": {                     "title": "华为手机",                     "category": "华为",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 1999                 },                 "highlight": {                     "category": [                         "华<em>为</em>"//<------高亮一个为字。                     ]                 }             },             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "B9R6sHgBaKNfVnMbZpZ6",                 "_score": 0.6931471,                 "_source": {                     "title": "华为手机",                     "category": "华为",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 1999                 },                 "highlight": {                     "category": [                         "华<em>为</em>"                     ]                 }             },             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "CdR7sHgBaKNfVnMbsJb9",                 "_score": 0.6931471,                 "_source": {                     "title": "华为手机",                     "category": "华为",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 1999                 },                 "highlight": {                     "category": [                         "华<em>为</em>"                     ]                 }             }         ]     } }

分组查询

在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:

{     "aggs":{//聚合操作         "price_group":{//名称,随意起名             "terms":{//分组                 "field":"price"//分组字段             }         }     } }

返回结果如下:

{     "took": 63,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 6,             "relation": "eq"         },         "max_score": 1,         "hits": [             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "ANQqsHgBaKNfVnMbhZYU",                 "_score": 1,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 3999                 }             },             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "A9R5sHgBaKNfVnMb25Ya",                 "_score": 1,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 1999                 }             },             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "BNR5sHgBaKNfVnMb7pal",                 "_score": 1,                 "_source": {                     "title": "小米手机",                     "category": "小米",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 1999                 }             },             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "BtR6sHgBaKNfVnMbX5Y5",                 "_score": 1,                 "_source": {                     "title": "华为手机",                     "category": "华为",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 1999                 }             },             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "B9R6sHgBaKNfVnMbZpZ6",                 "_score": 1,                 "_source": {                     "title": "华为手机",                     "category": "华为",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 1999                 }             },             {                 "_index": "shopping",                 "_type": "_doc",                 "_id": "CdR7sHgBaKNfVnMbsJb9",                 "_score": 1,                 "_source": {                     "title": "华为手机",                     "category": "华为",                     "images": "http://www.gulixueyuan.com/xm.jpg",                     "price": 1999                 }             }         ]     },     "aggregations": {         "price_group": {             "doc_count_error_upper_bound": 0,             "sum_other_doc_count": 0,             "buckets": [                 {                     "key": 1999,                     "doc_count": 5                 },                 {                     "key": 3999,                     "doc_count": 1                 }             ]         }     } }

上面返回结果会附带原始数据的。若不想要不附带原始数据的结果,在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:

{     "aggs":{         "price_group":{             "terms":{                 "field":"price"             }         }     },     "size":0 }

返回结果如下:

{     "took": 60,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 6,             "relation": "eq"         },         "max_score": null,         "hits": []     },     "aggregations": {         "price_group": {             "doc_count_error_upper_bound": 0,             "sum_other_doc_count": 0,             "buckets": [                 {                     "key": 1999,                     "doc_count": 5                 },                 {                     "key": 3999,                     "doc_count": 1                 }             ]         }     } }

查询平均值

在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:

{     "aggs":{         "price_avg":{//名称,随意起名             "avg":{//求平均                 "field":"price"             }         }     },     "size":0 }

返回结果如下:

{     "took": 14,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 6,             "relation": "eq"         },         "max_score": null,         "hits": []     },     "aggregations": {         "price_avg": {             "value": 2332.3333333333335         }     } }

映射关系

有了索引库,等于有了数据库中的 database。接下来就需要建索引库(index)中的映射了,类似于数据库(database)中的表结构(table)。创建数据库表需要设置字段名称,类型,长度,约束等;索引库也一样,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做映射(mapping)。
先创建一个索引:

# PUT http://127.0.0.1:9200/user

返回结果:

{     "acknowledged": true,     "shards_acknowledged": true,     "index": "user" }

创建映射

# PUT http://127.0.0.1:9200/user/_mapping  {     "properties": {         "name":{             "type": "text",             "index": true         },         "sex":{             "type": "keyword",             "index": true         },         "tel":{             "type": "keyword",             "index": false         }     } }

返回结果如下:

{     "acknowledged": true }

查询映射

#GET http://127.0.0.1:9200/user/_mapping

返回结果如下:

{     "user": {         "mappings": {             "properties": {                 "name": {                     "type": "text"                 },                 "sex": {                     "type": "keyword"                 },                 "tel": {                     "type": "keyword",                     "index": false                 }             }         }     } }

增加数据

#PUT http://127.0.0.1:9200/user/_create/1001 {     "name":"小米",     "sex":"男的",     "tel":"1111" }

返回结果如下:

{     "_index": "user",     "_type": "_doc",     "_id": "1001",     "_version": 1,     "result": "created",     "_shards": {         "total": 2,         "successful": 1,         "failed": 0     },     "_seq_no": 0,     "_primary_term": 1 }

查找name含有"小"数据:

#GET http://127.0.0.1:9200/user/_search {     "query":{         "match":{             "name":"小"         }     } }

返回结果如下:

{     "took": 495,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 1,             "relation": "eq"         },         "max_score": 0.2876821,         "hits": [             {                 "_index": "user",                 "_type": "_doc",                 "_id": "1001",                 "_score": 0.2876821,                 "_source": {                     "name": "小米",                     "sex": "男的",                     "tel": "1111"                 }             }         ]     } }

查找sex含有"男"数据:

#GET http://127.0.0.1:9200/user/_search {     "query":{         "match":{             "sex":"男"         }     } }

返回结果如下:

{     "took": 1,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 0,             "relation": "eq"         },         "max_score": null,         "hits": []     } }

找不想要的结果,只因创建映射时"sex"的类型为"keyword"。"sex"只能完全为"男的",才能得出原数据。

#GET http://127.0.0.1:9200/user/_search {     "query":{         "match":{             "sex":"男的"         }     } }

返回结果如下:

{     "took": 2,     "timed_out": false,     "_shards": {         "total": 1,         "successful": 1,         "skipped": 0,         "failed": 0     },     "hits": {         "total": {             "value": 1,             "relation": "eq"         },         "max_score": 0.2876821,         "hits": [             {                 "_index": "user",                 "_type": "_doc",                 "_id": "1001",                 "_score": 0.2876821,                 "_source": {                     "name": "小米",                     "sex": "男的",                     "tel": "1111"                 }             }         ]     } }

查询电话

# GET http://127.0.0.1:9200/user/_search {     "query":{         "match":{             "tel":"11"         }     } }

返回结果如下:

{     "error": {         "root_cause": [             {                 "type": "query_shard_exception",                 "reason": "failed to create query: Cannot search on field [tel] since it is not indexed.",                 "index_uuid": "ivLnMfQKROS7Skb2MTFOew",                 "index": "user"             }         ],         "type": "search_phase_execution_exception",         "reason": "all shards failed",         "phase": "query",         "grouped": true,         "failed_shards": [             {                 "shard": 0,                 "index": "user",                 "node": "4P7dIRfXSbezE5JTiuylew",                 "reason": {                     "type": "query_shard_exception",                     "reason": "failed to create query: Cannot search on field [tel] since it is not indexed.",                     "index_uuid": "ivLnMfQKROS7Skb2MTFOew",                     "index": "user",                     "caused_by": {                         "type": "illegal_argument_exception",                         "reason": "Cannot search on field [tel] since it is not indexed."                     }                 }             }         ]     },     "status": 400 }

报错只因创建映射时"tel"的"index"为false。

原生JS以后也支持类型注解啦?

Posted: 06 Jul 2022 08:12 PM PDT

大家好,我卡颂。

布达佩斯2022 JSConf会议上,tc39(ES标准委员会)成员Gil Tayar介绍了一份当前仍处于stage 1阶段的提案 —— Type Annotations,意在让原生JS支持类型注解。

Gil Tayar

换句话说,如果提案通过,很多.ts文件将后缀改为.js后就能直接在浏览器中运行。

一份tc39提案通常会经历5个阶段:

  • stage 0:被提出
  • stage 1:接受审议
  • stage 2:规范基本完成
  • stage 3:等待被实现
  • stage 4:纳入语言标准中

所以Type Annotations当前仍处于接受审议的状态。

但是提案发起者Gil Tayar对这份提案的通过很有信心,本文我们来聊聊这份提案的相关内容。

欢迎加入人类高质量前端框架群,带飞

为什么需要原生类型注解?

根据20年、21年state of JS的统计,静态类型高票当选JS中当前最欠缺的功能

同时,在Github报告中,TS被列为第四大最常用的语言

所以,对前端工程师来说,类型注解需求很大。

那么,既然已经有了TS,为什么还需要原生JS支持类型注解呢?

通常来说,从开发者编写的源代码线上生产环境代码间需要经过代码编译

代码编译主要包括两个步骤:

  1. 降级编译(包括高级语法转换为低级语法,高级方法的polyfill
  2. 代码转译(比如压缩、混淆、tree-shaking、类型擦除)

所谓类型擦除,是指擦除代码中的类型注解,让其变成符合原生JS规范的代码,比如:

// 擦除前 function add(a: number, b: number): number {   return a + b; } // 擦除后 function add(a, b) {   return a + b; }

随着时间的推移,各主流浏览器兼容性越来越好,步骤1在可预见的未来重要性会逐渐降低。

对于TS开发者,从源代码线上生产环境代码间可能只需要类型擦除

如果原生JS支持类型注解,就能省去类型擦除对应的编译流程,让代码更容易在宿主环境执行。

和TS的关系

这份提案的目的,并不是另起炉灶,独立实现一套原生JS的类型注解。而是与TS团队合作,提出一套合适的规范。

新的规范与TS规范的关系类似下图:

一方面,Type Annotations提案从TS中借鉴了很多特性,这就是图中相交的部分。

你可以到grammar-conventions看到规范当前定义的类型

另一方面,TS迭代速度很快,新的特性产出很快。而Type Annotations作为JS语言的一部分,迭代会更加保守,所以TS中一些特性在Type Annotations中并不支持。

此外,TS中一些结构(比如EnumsNamespaces)存在运行时的语义,Type Annotations也不会支持。

这些就是TS中存在,而Type Annotations中不存在的部分。

最后,Type Annotations设计的初衷并不是与TS强绑定,而仅仅是提供一套类型规范,开发者编写代码时的类型检查还是由各种类型检查器(比如TSFlow)实现。

所以,Type Annotations还有一部分特性是TS当前未定义的,这也是为了规范更广泛的适用性考虑的,也就是图中Type Annotations存在,而TS不存在的部分。

这部分特性需要TS后续实现,这也是为什么Type Annotations要与TS团队合作的一大原因。

对开发者意味着什么

如果Type Annotations最终出现在ES20xx版中,届时开发者编写代码的步骤是:

  1. 选择合适的类型检查器(比如TS),这个类型检查器需要完全遵循Type Annotations规范(而不是自己的规范,比如TS规范)
  2. 编写带类型声明的原生JS代码
  3. 类型检查器会检查类型错误,并给予报错或提示

对于如下原生JS代码,如果开发者传入了错误的类型,JS会报错么?

function add(a: number, b: number): number {   return a + b; }  // 错误的类型传参 add('KaSong', 123);

答案是:不会。

Type Annotations仅仅是一套规范,该规范由各种类型检查器执行。

JS的宿主环境(比如浏览器)在执行带类型声明的JS代码时,会忽略类型声明。

总结

有同学可能会问:就为了减少编译时类型擦除这一步,就提出原生类型规范,有必要么?

甚至当Type Annotations落地后,开发者上线前在进行代码压缩时,类型擦除也会作为代码压缩的职责之一。

从这个角度看,甚至没有减少编译时的工作量。

所以提出原生的类型规范,有必要么?

前端的发展实际是一个努力去编译时流程的过程。

比如,编译时代码需要降级,需要polyfill?随着IE11停止服务,主流浏览器纷纷跟进标准落地,降级与polyfill的需求逐渐变少。

再比如,代码需要打包?随着ESM规范落地,在当前,至少在开发环境中代码已经不需要打包(使用Vite)。

Type Annotations的出现,就是遵循努力去编译时流程这一趋势的产物。

从这个角度看,还是很有必要的。

使用纯 CSS 实现超酷炫的粘性气泡效果

Posted: 04 Jul 2022 07:23 PM PDT

最近,在 CodePen 上看到这样一个非常有意思的效果:

这个效果的核心难点在于气泡的一种特殊融合效果。

其源代码在:CodePen Demo -- Goey footer,作者主要使用的是 SVG 滤镜完成的该效果,感兴趣的可以戳源码看看。

其中,要想灵活运用 SVG 中的 feGaussianBlur 滤镜还是需要有非常强大的 SVG 知识储备的。那么,仅仅使用 CSS 能否实现该效果呢?

嘿嘿,强大的 CSS 当然是可以的。本文,就将带领大家一步步使用纯 CSS,完成上述效果。

借助 SASS 完成大致效果

首先,如果上述效果没有气泡的融合效果,可能就仅仅是这样:

要制作这样一个效果还是比较简单的,只是代码会比较多,我们借助 SASS 预处理器即可。

假设我们有如下 HTML 结构:

<div class="g-wrap">   <div class="g-footer">     <div class="g-bubble"></div>     <div class="g-bubble"></div>     // ... 200 个 g-bubble   </div> </div>

核心要做的,仅仅是让 200 个 .g-bubble 从底部无规律的进行向上升起的动画。

这里,就需要运用我们在 深入浅出 CSS 动画 这篇文章中所介绍的一种技巧 -- 利用 animation-duration 和 animation-delay 构建随机效果

利用 animation-duration 和 animation-delay 构建随机效果

同一个动画,我们利用一定范围内随机的 animation-duration 和一定范围内随机的 animation-delay,可以有效的构建更为随机的动画效果,让动画更加的自然。

我们来模拟一下,如果是使用 10 个 animation-durationanimation-delay 都一致的圆的话,核心伪代码:

<ul>     <li></li>     <!--共 10 个...-->      <li></li> </ul>
ul {     display: flex;     flex-wrap: nowrap;     gap: 5px; } li {     background: #000;     animation: move 3s infinite 1s linear; } @keyframes move {     0% {         transform: translate(0, 0);     }     100% {         transform: translate(0, -100px);     } }

这样,小球的运动会是这样的整齐划一:

要让小球的运动显得非常的随机,只需要让 animation-durationanimation-delay 都在一定范围内浮动即可,改造下 CSS:

@for $i from 1 to 11 {     li:nth-child(#{$i}) {         animation-duration: #{random(2000)/1000 + 2}s;         animation-delay: #{random(1000)/1000 + 1}s;     } }

我们利用 SASS 的循环和 random() 函数,让 animation-duration 在 2-4 秒范围内随机,让 animation-delay 在 1-2 秒范围内随机,这样,我们就可以得到非常自然且不同的上升动画效果,基本不会出现重复的画面,很好的模拟了随机效果:

CodePen Demo -- 利用范围随机 animation-duration 和 animation-delay 实现随机动画效果

好,我们把上述介绍的技巧,套用到我们本文要实现的效果中去,HTML 结构再看一眼:

<div class="g-wrap">   <div class="g-footer">     <div class="g-bubble"></div>     <div class="g-bubble"></div>     // ... 200 个 g-bubble   </div> </div>

核心的 CSS 代码:

.g-footer {     position: absolute;     bottom: 0;     left: 0;     height: 86px;     width: 100%;     background: #26b4f5; }  @for $i from 0 through 200 {      .g-bubble:nth-child(#{$i}) {         position: absolute;         background: #26b4f5;         $width: random(100) + px;         left: #{(random(100)) + '%'};         top: #{(random(100))}px;         width: $width;         height: $width;         animation: moveToTop #{(random(2500) + 1500) / 1000}s ease-in-out -#{random(5000)/1000}s infinite;     } } @keyframes moveToTop {     90% {         opacity: 1;     }     100% {         opacity: .08;         transform: translate(-50%, -180px) scale(.3);     } }

这里:

  1. 我们利用了 SASS 随机函数 $width: random(100) + px;,随机生成不同大小的 div 圆形
  2. 利用 SASS 随机函数 left: #{(random(100)) + '%'}top: #{(random(100))}px 基于父元素随机定位
  3. 最为核心的是 animation: moveToTop #{(random(2500) + 1500) / 1000}s ease-in-out -#{random(5000)/1000}s infinite,让所有 div 圆的运动都是随机的

上述(1)、(2)综合结果,会生成这样一种布局,均匀分散排布的圆形:

注:这里为了方便理解,我隐藏了最外层 g-footer 的颜色,并且给 g-bubble 添加了黑色边框

接着,如果我们替换一下 animation 语句,使用统一的动画时长,去掉负的延迟,变成 animation: moveToTop 4s ease-in-out infinite,动画就会是这样:

整体是整齐划一,没有杂乱无章的感觉的。

运用上随机效果,animation: moveToTop #{(random(2500) + 1500) / 1000}s ease-in-out -#{random(5000)/1000}s infinite,就能得到上述的,不同气泡随机上升的感觉:

添加融合效果

接下来,也是最重要的一步,如何让气泡与气泡之间,以及气泡和底部 .g-footer 之间产生融合效果呢?

这个技巧在此前非常多篇文章中,也频繁提及过,就是利用 filter: contrast() 滤镜与 filter: blur() 滤镜。

如果你还不了解这个技巧,可以戳我的这篇文章看看:你所不知道的 CSS 滤镜技巧与细节

简述下该技巧:

单独将两个滤镜拿出来,它们的作用分别是:

  1. filter: blur(): 给图像设置高斯模糊效果。
  2. filter: contrast(): 调整图像的对比度。

但是,当他们"合体"的时候,产生了奇妙的融合现象。

仔细看两圆相交的过程,在边与边接触的时候,会产生一种边界融合的效果,通过对比度滤镜把高斯模糊的模糊边缘给干掉,利用高斯模糊实现融合效果。

基于此,我们再简单改造下我们的 CSS 代码,所需要加的代码量非常少:

.g-wrap {     background: #fff;     filter: contrast(8); } .g-footer {     // ... 其他保持一致     filter: blur(5px); }

就这么简单,父容器添加白色底色以及对比度滤镜 filter: contrast(8),子容器添加 filter: blur(5px) 即可,这样,我们就能得气泡的融合效果,基本得到我们想要的效果:

利用 backdrop-filter 替代 filter 消除边缘

但是!利用 filter: blur() 会有一个小问题。

运用了 filter: blur() 的元素,元素边缘的模糊度不够,会导致效果在边缘失真,我们仔细看看动画的边缘:

如何解决呢?也好办,在这里,我们尝试利用 backdrop-filter 去替换 filter

两者之间的差异在于,filter 是作用于元素本身,而 backdrop-filter 是作用于元素背后的区域所覆盖的所有元素,如果你想了解更多关于 backdrop-filter 的信息,可以戳我的这篇文章:深入探讨 filter 与 backdrop-filter 的异同

简单改造下代码,原代码:

.g-footer {     // ...      filter: blur(5px); }

改造后的代码:

 .g-footer {     // ... 去掉 filter: blur(5px)     &:before {         content: "";         position: absolute;         top: -300px;         left: 0;         right: 0;         bottom: 0;         z-index: 1;         backdrop-filter: blur(5px);     } }

我们通过去到原来添加在 .g-footer 上的 filter: blur(5px),通过他的伪元素,叠加一层新的元素在它本身之上,并且添加了替代的 backdrop-filter: blur(5px)

当然,因为这里的 blur(5px) 还需要为气泡与气泡之间的融合服务,所以为了覆盖动画全区域,我们还设置了 top: -300px,扩大了它的作用范围。

最终,我们就能完美的复刻文章一开头,使用 SVG 滤镜实现的效果:

在文章中,我省去了大部分基础的 CSS 代码,完整的代码,你可以戳这里:CodePen Demo -- Bubble Rises

最后

本文与之前的 巧用 CSS 实现酷炫的充电动画 内使用的技巧非常类似,但本文也有一些新的知识点,大家可以结合着一起看看。

好了,本文到此结束,希望对你有帮助 :)

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

switch 有四样写法你知道么

Posted: 30 Jun 2022 04:30 PM PDT

JavaScript 的 switch 有四样写法,你知道么?不管你知道不知道,反正我是不知道。

我所知道的 JavaScript 的 switch 语句只有一种写法。但要说到对分支的处理,写法可就多了去了。if 分支写法可以算一种,switch 分支写法可以算第二种,第三种是使用策略模式,如果要把条件运算符也算上的话,嗯,刚好四种。

不过本文的主角是 switch。大家都了解 switch 的写法一般来说是 switch 变量或表达式,case 常量。嗯,比如说,一个百分制成绩,90 及 90 分以上算优秀,80 及以上 90 以下算良好,60 及以上 80 以下算合格,60 以下为不合格,用 switch 大概会这么写:

function calcGrade(score) {     const line = score / 10 | 0;     switch (line) {         case 10: case 9:             return "优秀";         case 8:             return "良好";         case 7: case 6:             return "合格";         default:             return "不合格";     } }
代码中 score / 10 | 0Math.floor(score / 10) 是一样的效果,就是除以 10 取商的整数部分。

这段 switch 用得中规中矩,用取整的办法来避免使用一长串 if ... else 分支也算是取了巧。

但是现在规则改了,将合格和良好的分隔点从 80 分降到 75 分,该怎么办?

按上面取整的办法依然可以,不过这次除数不再是 10,而是 5。相应地,case 也多了很多:

  • 18、19、20 是优秀
  • 15、16、17 是良好
  • 12、13、14 是合格
  • 剩下的是不合格

写 9 个 case,真不如用 if ... else 算了。

是吗?其实用 switch 也有简单一些的写法:

function calcGrade(score) {     switch (true) {         case score >= 90:             return "优秀";         case score >= 75:             return "良好";         case score >= 60:             return "合格";         default:             return "不合格";     } }

是不是感觉有些奇怪?这完全不是习惯了的 switch 表达式 case 常量,而是正好相反,switch 常量 case 表达式!如果你拿这段程序去跑一下,会发现一点问题都没有。因为——switch 和 case 是按 === 来匹配的,它并不在乎是表达式还是常量,或者说,switch 和 case 后面都可以接表达式!

是的,表达式!

所以上面那个示例中,把 switch(true) 改成 switch( 2 > 1) 也是一样的效果。

好啦,脑洞已开。switch 到底有多少种写法已经不重要了。接下来要研究的是 switch 的变种 ——

看到 C# 有 switch 表达式,眼馋,能实现吗?

不用眼馋,JavaScript 里一切都可以是表达式 …… 如果不是,用 IIFE 封装一个就是了

function calcGrade(score) {     return (value => {         switch (true) {             case value >= 90:                 return "优秀";             case value >= 75:                 return "良好";             case value >= 60:                 return "合格";             default:                 return "不合格";         }     })(score); }

注意这里把 score 作为 IIFE 的参数,是因为在实际使用中,可能需要传入的是一个表达式。这种情况下应该提前求值,而且只求一次(避免替在的副作用)。

不过这样的封装显然没什么意义,如果真要这样封装,不如封成策略:

function calcGrade(score) {     return ((value, rules) => rules.find(({ t }) => t(value)).v)(         score,         [             { t: n => n >= 90, v: "优秀" },             { t: n => n >= 75, v: "良好" },             { t: n => n >= 60, v: "合格" },             { t: () => true, v: "不合格" },         ]     ); }

每项策略都是一个含有 tester (t) 和值 (v) 的对象。tester 是一个判断函数,传入需要判断的值,也就是 switch (表达式) 这里表达式,而这个表达式也是提前求值之后作为 IIFE 的参数传入的。应用策略的过程简单粗暴,就是找到第一个符合条件的策略,把它的值取出来。

当然这样用策略有点大材小用。真正需要用策略的情况,策略中通常不是一个值,而是一个行为,也就函数。

我们知道在 switch 语句中,各个 case 之间是在同一个作用域内,所以不能在两个 case 语句中声明同一个局部变量。虽然用 { } 包裹可以解决这些问题,但代码看起来不怎么好看,特别是还要注意不要忘了 break。如果用策略的话,看起来可能会顺眼一眼,也不用担心 break 的问题:

这里为了演示,在策略行为中将先输出成绩,再返回等级。
function calcGrade(score) {     return ((value, rules) => rules.find(({ t }) => t(value)).fn(value))(         score,         [             {                 t: n => n >= 90,                 fn: score => {                     const grade = "优秀";                     console.log(grade, score);                     return grade;                 }             },             {                 t: n => n >= 75,                 fn: score => {                     const grade = "良好";                     console.log(grade, score);                     return grade;                 }             },             {                 t: n => n >= 60,                 fn: score => {                     const grade = "合格";                     console.log(grade, score);                     return grade;                 }             },             {                 t: () => true,                 fn: score => {                     const grade = "不合格";                     console.log(grade, score);                     return grade;                 }             },         ]     ); }

代码确实有点长,因为有策略行为逻辑在里面。如果真的是要当作 switch 表达式来用的话,策略部分应该是一个表达式,不会太长的。上面的代码中,策略行为相似,可以封装成一个函数,这样就能写成表达式的形式了:

function calcGrade(score) {     const printGrade = (grade, score) => {         console.log(grade, score);         return grade;     };      return ((value, rules) => rules.find(({ t }) => t(value)).fn(value))(         score,         [             { t: n => n >= 90, fn: score => printGrade("优秀", score) },             { t: n => n >= 75, fn: score => printGrade("良好", score) },             { t: n => n >= 60, fn: score => printGrade("合格", score) },             { t: () => true, fn: score => printGrade("不合格", score) },         ]     ); }

现在看起来是不是像样了?

上面的代码形式各异,干的事情都差不多,也没有谁优谁劣的比较。看得顺眼怎么都优雅,看不顺眼怎么都不受宠。在不同的情况下,选用合适的做法就好。上面的代码使用的 find() 来查找策略,如果改用 filter(),那又会是另一番景象了 ~(~ ̄▽ ̄)~

Vue3响应式源码分析 - ref + ReactiveEffect篇

Posted: 30 Jun 2022 01:52 AM PDT

在Vue3中,因为reactive创建的响应式对象是通过Proxy来实现的,所以传入数据不能为基础类型,所以 ref 对象是对reactive不支持的数据的一个补充。

refreactive 中还有一个重要的工作就是收集、触发依赖,那么依赖是什么呢?怎么收集触发?一起来看一下吧:

我们先来看一下 ref 的源码实现:

export function ref(value?: unknown) {   return createRef(value, false) }  export function shallowRef(value?: unknown) {   return createRef(value, true) }  const toReactive = (value) => isObject(value) ? reactive(value) : value;  function createRef(rawValue: unknown, shallow: boolean) {   // 如果是ref则直接返回   if (isRef(rawValue)) {     return rawValue   }   return new RefImpl(rawValue, shallow) }  class RefImpl<T> {   private _value: T   // 存放 raw 原始值   private _rawValue: T    // 存放依赖   public dep?: Dep = undefined   public readonly __v_isRef = true    constructor(value: T, public readonly __v_isShallow: boolean) {     // toRaw 拿到value的原始值     this._rawValue = __v_isShallow ? value : toRaw(value)     // 如果不是shallowRef,使用 reactive 转成响应式对象     this._value = __v_isShallow ? value : toReactive(value)   }    // getter拦截器   get value() {     // 收集依赖     trackRefValue(this)     return this._value   }    // setter拦截器   set value(newVal) {     // 如果是需要深度响应的则获取 入参的raw     newVal = this.__v_isShallow ? newVal : toRaw(newVal)     // 新值与旧值是否改变     if (hasChanged(newVal, this._rawValue)) {       this._rawValue = newVal       // 更新value 如果是深入创建并且是对象的话 还需要转化为reactive代理       this._value = this.__v_isShallow ? newVal : toReactive(newVal)       // 触发依赖       triggerRefValue(this, newVal)     }   } }

RefImpl 采用ES6类的写法,包含 getset,其实大家可以用 webpack 等打包工具打包成 ES5 的代码,发现其实就是 Object.defineProperty

可以看到,shallowRefref 都调用了 createRef,只是传入的参数不同。当使用 shallowRef 时,不会调用 toReactive 去将对象转换为响应式,由此可见,shallowRef对象只支持对value值的响应式,ref对象支持对value深度响应式,ref.value.a.b.c中的修改都能被拦截,举个🌰:

<template>     <p>{{ refData.a }}</p>     <p>{{ shallowRefData.a }}</p>     <button @click="handleChange">change</button> </template>   let refData = ref({   a: 'ref' }) let shallowRefData = shallowRef({   a: 'shallowRef' })  const handleChange = () => {   refData.value.a = "ref1"   shallowRefData.value.a = "shallowRef1" }

当我们点击按钮修改数据后,界面上的 refData.a 的值会变为 ref1,而 shallowRefData.a 应该会不发生变化,但其实在这个例子里,shallowRefData.a 在视图上也会发生变化的🐶,因为修改 refData.a 时候,触发了setter函数,内会去调用 triggerRefValue(this, newVal) 从而触发了 视图更新,所以shallow的最新数据也会被更新到了视图上 (把 refData.value.a = "ref1" 去掉它就不会变了)。

ref 里最关键的还是trackRefValuetriggerRefValue,负责收集触发依赖。

如何收集依赖:

function trackRefValue(ref) {     // 判断是否需要收集依赖     // shouldTrack 全局变量,代表当前是否需要 track 收集依赖     // activeEffect 全局变量,代表当前的副作用对象 ReactiveEffect     if (shouldTrack && activeEffect) {         ref = toRaw(ref);         {             // 如果没有 dep 属性,则初始化 dep,dep 是一个 Set<ReactiveEffect>,存储副作用函数             // trackEffects 收集依赖             trackEffects(ref.dep || (ref.dep = createDep()), {                 target: ref,                 type: "get",                 key: 'value'             });         }     } }

为什么要判断 shouldTrackactiveEffect,因为在Vue3中有些时候不需要收集依赖:

  • 当没有 effect 包裹时,比如定义了一个ref变量,但没有任何地方使用到,这时候就没有依赖,activeEffect 为 undefined,就不需要收集依赖了
  • 比如在数组的一些会改变自身长度的方法里,也不应该收集依赖,容易造成死循环,此时 shouldTrack 为 false

*依赖是什么?

ref.dep 用于储存 依赖 (副作用对象),ref 被修改时就会触发,那么依赖是什么呢?依赖就是 ReactiveEffect

为什么要收集依赖(副作用对象),因为在Vue3中,一个响应式变量的变化,往往会触发一些副作用,比如视图更新、计算属性变化等等,需要在响应式变量变化时去触发其它一些副作用函数。

在我看来 ReactiveEffect 其实就和 Vue2 中的 Watcher 的作用差不多,我之前写的《Vue源码学习-响应式原理》里做过说明:

class ReactiveEffect {     constructor(fn, scheduler = null, scope) {         // 传入一个副作用函数         this.fn = fn;         this.scheduler = scheduler;         this.active = true;         // 存储 Dep 对象,如上面的 ref.dep         // 用于在触发依赖后, ref.dep.delete(effect),双向删除依赖)         this.deps = [];         this.parent = undefined;         recordEffectScope(this, scope);     }     run() {         // 如果当前effect已经被stop         if (!this.active) {             return this.fn();         }         let parent = activeEffect;         let lastShouldTrack = shouldTrack;         while (parent) {             if (parent === this) {                 return;             }             parent = parent.parent;         }         try {             // 保存上一个 activeEffect             this.parent = activeEffect;             activeEffect = this;             shouldTrack = true;             // trackOpBit: 根据深度生成 trackOpBit             trackOpBit = 1 << ++effectTrackDepth;             // 如果不超过最大嵌套深度,使用优化方案             if (effectTrackDepth <= maxMarkerBits) {                 // 标记所有的 dep 为 was                 initDepMarkers(this);             }             // 否则使用降级方案             else {                 cleanupEffect(this);             }             // 执行过程中重新收集依赖标记新的 dep 为 new             return this.fn();         }         finally {             if (effectTrackDepth <= maxMarkerBits) {                 // 优化方案:删除失效的依赖                 finalizeDepMarkers(this);             }             // 嵌套深度自 + 重置操作的位数             trackOpBit = 1 << --effectTrackDepth;             // 恢复上一个 activeEffect             activeEffect = this.parent;             shouldTrack = lastShouldTrack;             this.parent = undefined;             if (this.deferStop) {                 this.stop();             }         }     } }

ReactiveEffect 是副作用对象,它就是被收集依赖的实际对象,一个响应式变量可以有多个依赖,其中最主要的就是 run 方法,里面有两套方案,当 effect 嵌套次数不超过最大嵌套次数的时候,使用优化方案,否则使用降级方案。

降级方案:

function cleanupEffect(effect) {     const { deps } = effect;     if (deps.length) {         for (let i = 0; i < deps.length; i++) {             // 从 ref.dep 中删除 ReactiveEffect             deps[i].delete(effect);         }         deps.length = 0;     } }

这个很简单,删除全部依赖,然后重新收集。在各个 dep 中,删除该 ReactiveEffect 对象,然后执行 this.fn()(副作用函数) 时,当获取响应式变量触发 getter 时,又会重新收集依赖。之所以要先删除然后重新收集,是因为随着响应式变量的变化,收集到的依赖前后可能不一样。

const toggle = ref(false) const visible = ref('show') effect(() = {   if (toggle.value) {     console.log(visible.value)   } else {     console.log('xxxxxxxxxxx')   } }) toggle.value = true
  • 当 toggle 为 true 时,toggle、visible 都能收集到依赖
  • 当 toggle 为 false 时,只有visible 可以收集到依赖

优化方案:

全部删除,再重新收集,明显太消耗性能了,很多依赖其实是不需要被删除的,所以优化方案的做法是:

// 响应式变量上都有一个 dep 用来保存依赖 const createDep = (effects) => {     const dep = new Set(effects);     dep.w = 0;     dep.n = 0;     return dep; };
  1. 执行副作用函数前,给 ReactiveEffect 依赖的响应式变量,加上 w(was的意思) 标记。
  2. 执行 this.fn(),track 重新收集依赖时,给 ReactiveEffect 的每个依赖,加上 n(new的意思) 标记。
  3. 最后,对有 w 但是没有 n 的依赖进行删除。

其实就是一个筛选的过程,我们现在来第一步,如何加上 was 标记:

// 在 ReactiveEffect 的 run 方法里 if (effectTrackDepth <= maxMarkerBits) {     initDepMarkers(this); }  const initDepMarkers = ({ deps }) => {     if (deps.length) {         for (let i = 0; i < deps.length; i++) {             deps[i].w |= trackOpBit;         }     } };

这里使用了位运算,快捷高效。trackOpBit是什么呢?代表当前嵌套深度(effect可以嵌套),在Vue3中有一个全局变量 effectTrackDepth

// 全局变量 嵌套深度  let effectTrackDepth = 0;  // 在 ReactiveEffect 的 run 方法里 // 每次执行 effect 副作用函数前,全局变量嵌套深度会自增1 trackOpBit = 1 << ++effectTrackDepth  // 执行完副作用函数后会自减 trackOpBit = 1 << --effectTrackDepth;

当深度为 1 时,trackOpBit为 2(二进制:00000010),这样执行 deps[i].w |= trackOpBit 时,操作的是第二位,所以第一位是用不到的。

为什么Vue3中嵌套深度最大是 30 ?

1 << 30 // 0100 0000 0000 0000 0000 0000 0000 0000 // 1073741824  1 << 31 // 1000 0000 0000 0000 0000 0000 0000 0000 // -2147483648 溢出

因为js中位运算是以32位带符号的整数进行运算的,最左边一位是符号位,所以可用的正数最多只能到30位。

可以看到,在执行副作用函数之前,使用 deps[i].w |= trackOpBit,对依赖在不同深度是否被依赖( w )进行标记,然后执行 this.fn(),重新收集依赖,上面说到收集依赖调用 trackRefValue 方法,该方法内会调用 trackEffects

function trackEffects(dep, debuggerEventExtraInfo) {     let shouldTrack = false;     if (effectTrackDepth <= maxMarkerBits) {         // 查看是否记录过当前依赖         if (!newTracked(dep)) {             dep.n |= trackOpBit;             // 如果 w 在当前深度有值,说明effect之前已经收集过             // 不是新增依赖,不需要再次收集             shouldTrack = !wasTracked(dep);         }     }     else {         shouldTrack = !dep.has(activeEffect);     }     if (shouldTrack) {         // dep添加当前正在使用的effect         dep.add(activeEffect);          // effect的deps也记录当前dep 双向引用         activeEffect.deps.push(dep);     } }

可以看到再重新收集依赖的时候,使用 dep.n |= trackOpBit 对依赖在不同深度是否被依赖( n )进行标记,这里还用到两个工具函数:

const wasTracked = (dep) => (dep.w & trackOpBit) > 0; const newTracked = (dep) => (dep.n & trackOpBit) > 0;

使用 wasTracked 和 newTracked,判断 dep 是否在当前深度被标记。比如判断依赖在深度 1 时 (trackOpBit第二位是1) 是否被标记,采用按位与:

trackOpBit.png

最后,如果已经超过最大深度,因为采用降级方案,是全部删除然后重新收集的,所以肯定是最新的,所以只需要把 trackOpBit 恢复,恢复上一个 activeEffect:

finally {     if (effectTrackDepth <= maxMarkerBits) {         // 优化方案:删除失效的依赖         finalizeDepMarkers(this);     }     trackOpBit = 1 << --effectTrackDepth;     // 恢复上一个 activeEffect     activeEffect = this.parent;     shouldTrack = lastShouldTrack;     this.parent = undefined;     if (this.deferStop) {         this.stop();     } }

如果没超过最大深度,就像之前说的把失效的依赖删除掉,然后更新一下deps的顺序:

const finalizeDepMarkers = (effect) => {     const { deps } = effect;     if (deps.length) {         let ptr = 0;         for (let i = 0; i < deps.length; i++) {             const dep = deps[i];             // 把有 w 没有 n 的删除             if (wasTracked(dep) && !newTracked(dep)) {                 dep.delete(effect);             }             else {                 // 更新deps,因为有的可能会被删掉                 // 所以要把前面空的补上,用 ptr 单独控制下标                  deps[ptr++] = dep;             }             // 与非,恢复到进入时的状态             dep.w &= ~trackOpBit;             dep.n &= ~trackOpBit;         }         deps.length = ptr;     } };

举个简单的🌰,理解起来可能简单点,有两个组件,一个父组件,一个子组件,子组件接收父组件传递的 toggle 参数显示在界面上,toggle 还控制着 visible 的显示,点击按钮切换 toggle 的值:

// Parent <script setup lang="ts"> const toggle = ref(true) const visible = ref('show')  const handleChange = () => {   toggle.value = false } </script>  <template>   <div>     <p v-if="toggle">{{ visible }}</p>     <p v-else>xxxxxxxxxxx</p>     <button @click="handleChange">change</button>     <Child :toggle="toggle" />   </div> </template> 
// Child <script setup lang="ts"> const props = defineProps({   toggle: {     type: Boolean,   }, }); </script>  <template>   <p>{{ toggle }}</p> </template>

第一次渲染,因为toggle 默认为 true,我们可以收集到 togglevisible 的依赖,

  1. Parent 组件, 执行 run 方法中的 initDepMarkers 方法,首次进入,还未收集依赖,ReactiveEffectdeps 长度为0,跳过。
  2. 执行 run 方法中的 this.fn,重新收集依赖,触发 trackEffects:

    • toggle 的 dep = {n: 2, w: 0}shouldTrack 为 true,收集依赖。
    • visible 的 dep = {n: 2, w: 0}shouldTrack 为 true,收集依赖。
  3. 进入 Child 组件,执行 run 方法中的 initDepMarkers 方法,首次进入,还为收集依赖,deps长度为0,跳过。
  4. 执行 run 方法中的 this.fn,重新收集依赖,触发 trackEffects:

    • toggle 的 dep = {n: 4, w: 0}shouldTrack 为 true,收集依赖。

这样首次进入页面的收集依赖就结束了,然后我们点击按钮,把 toggle 改为 false:

  1. Parent 组件: 执行 run 方法中的 initDepMarkers 方法,之前在 Parent 组件里收集到了两个变量的依赖,所以将他们 w 标记:

    • toggle 的 dep = {n: 0, w: 2}
    • visible 的 dep = {n: 0, w: 2}
  2. 执行 run 方法中的 this.fn,重新收集依赖,触发 trackEffects:

    • toggle 的 dep = {n: 2, w: 2}shouldTrack 为 false,不用 收集依赖。
    • visible 不显示了,所以没有重新收集到,还是 {n: 0, w: 2}
  3. 进入 Child 组件,执行 run 方法中的 initDepMarkers 方法,之前 收集过 toggle 依赖了,将 toggle 的 w 做标记,toggle 的 dep = {n: 0, w: 4}
  4. 执行 run 方法中的 this.fn,重新收集依赖,触发 trackEffects:

    • toggle 的 dep = {n: 4, w: 4}shouldTrack 为 false,不用收集依赖。

最后发现 visiblew 没有 n,在 finalizeDepMarkers 中删除掉失效依赖。

如何触发依赖:

在一开始讲到的 ref 源码里,可以看到在 setter 时会调用 triggerRefValue 触发依赖:

function triggerRefValue(ref, newVal) {     ref = toRaw(ref);     if (ref.dep) {         {             triggerEffects(ref.dep, {                 target: ref,                 type: "set",                 key: 'value',                 newValue: newVal             });         }     } }  function triggerEffects(   dep: Dep | ReactiveEffect[] ) {   // 循环去取每个依赖的副作用对象 ReactiveEffect   for (const effect of isArray(dep) ? dep : [...dep]) {     // effect !== activeEffect 防止递归,造成死循环     if (effect !== activeEffect || effect.allowRecurse) {       // effect.scheduler可以先不管,ref 和 reactive 都没有       if (effect.scheduler) {         effect.scheduler()       } else {         // 执行 effect 的副作用函数         effect.run()       }     }   } }

触发依赖最终的目的其实就是去执行依赖每个的副作用对象副作用函数,这里的副作用函数可能是执行更新视图、watch数据监听、计算属性等。


🤨🧐我个人再看源码的时候还遇到了一个问题,不知道大家遇到没有(我看的代码版本算是比较新v3.2.37),一开始我也是上网看一些源码的解析文章,看到好多讲解 effect 这个函数的,先来看看这个方法的源码:

function effect(fn, options) {     if (fn.effect) {         fn = fn.effect.fn;     }     const _effect = new ReactiveEffect(fn);     if (options) {         extend(_effect, options);         if (options.scope)             recordEffectScope(_effect, options.scope);     }     if (!options || !options.lazy) {         _effect.run();     }     const runner = _effect.run.bind(_effect);     runner.effect = _effect;     // 返回一个包装后的函数,执行收集依赖     return runner; }

这个函数看上去挺简单的,创建一个 ReactiveEffect 副作用对象,将用户传入的参数附加到对象上,然后调用 run 方法收集依赖,如果有 lazy 配置不会自动去收集依赖,用户主动执行 effect 包装后的函数,也能够正确的收集依赖。

🤨🧐但我找了一圈,发现源码里一个地方都没调用,于是我就在想是不是以前用到过,现在去掉了,去commit记录里找了一圈,还真找到了:

这次更新把 ReactiveEffect 改为用类来实现,避免不必要时也创建 effect runner,节省了17%的内存等。

原来的 effect 方法包括了现在的 ReactiveEffect,在视图更新渲染、watch等地方都直接引用了这个方法,但更新后都是直接 new ReactiveEffect,然后去触发 run 方法,不走 effect 了,可以说现在的 ReactiveEffect 类就是之前的 effect 方法 。

export function effect<T = any>(   fn: () => T,   options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> {   const effect = createReactiveEffect(fn, options)   return effect }  let uid = 0  function createReactiveEffect<T = any>(   fn: () => T,   options: ReactiveEffectOptions ): ReactiveEffect<T> {   const effect = function reactiveEffect(): unknown {     if (!effect.active) {       return fn()     }     if (!effectStack.includes(effect)) {       cleanup(effect)       try {         enableTracking()         effectStack.push(effect)         activeEffect = effect         return fn()       } finally {         effectStack.pop()         resetTracking()         const n = effectStack.length         activeEffect = n > 0 ? effectStack[n - 1] : undefined       }     }   } as ReactiveEffect   effect.id = uid++   effect.allowRecurse = !!options.allowRecurse   effect._isEffect = true   effect.active = true   effect.raw = fn   effect.deps = []   effect.options = options   return effect }

结尾

我是周小羊,一个前端萌新,写文章是为了记录自己日常工作遇到的问题和学习的内容,提升自己,如果您觉得本文对你有用的话,麻烦点个赞鼓励一下哟~

No comments:

Post a Comment