最近在项目中遇到这样一个问题
当页面加载完毕后由于选项卡的另外两张属于display:none;状态 所以另外两张选项卡内echarts的宽高都会变成默认100*100
查阅了很多网上的案例,得出一下一些解决方案:
1:
原因很简单,在tab页中,图表的父容器div是隐藏的(display:none),图表在执行js初始化的时候找不到这个元素,所以自动将“100%”转成了“100”,最后计算出来的图表就成了100px
解决办法:
找一个在tab页的切换操作中不会隐藏的父容器,把它的宽度的具体值取出后在初始化图表之前直接赋给图表
1 $("#chartMain").css('width',$("#TabContent").width());//获取父容器的宽度具体数值直接赋值给图表以达到宽度100%的效果 2 var Chart = echarts.init(document.getElementById('chartMain')); 3 4 // 指定图表的配置项和数据 5 option = { ...配置项和数据 }; 6 7 // 使用刚指定的配置项和数据显示图表。 8 Chart.setOption(option);
2:mychart.resize() 重新渲染高度
3: 后来我想到了问题所在,既然高度是因为display:none;导致的 那大可不必设置这个属性,但是在页面渲染完毕后加上即可
所以取消了选项卡的display:none; 但在页面加载完毕后
window.οnlοad=function(){
根基id在添加css display:none;
}
即可解决,
分割线
---------------------------------------------------------------------
接下来解决一下ifram内外通讯 互相通讯赋值ifram src 和高度问题
统一资源定位符,缩写为URL,是对网络资源(网页、图像、文件)的引用。URL指定资源位置和检索资源的机制(http、ftp、mailto)。
举个例子,这里是这篇文章的 URL 地址:
https://dmitripavlutin.com/parse-url-javascript
很多时候你需要获取到一段 URL 的某个组成部分。它们可能是 hostname(例如 dmitripavlutin.com),或者 pathname(例如 /parse-url-javascript)。
一个方便的用于获取 URL 组成部分的办法是通过 URL() 构造函数。
在这篇文章中,我将给大家展示一段 URL 的结构,以及它的主要组成部分。
接着,我会告诉你如何使用 URL() 构造函数来轻松获取 URL 的组成部分,比如 hostname,pathname,query 或者 hash。
1. URL 结构
一图胜千言。不需要过多的文字描述,通过下面的图片你就可以理解一段 URL 的各个组成部分:
image
2. URL() 构造函数
URL() 构造函数允许我们用它来解析一段 URL:
const url = new URL(relativeOrAbsolute [, absoluteBase]);
参数 relativeOrAbsolute 既可以是绝对路径,也可以是相对路径。如果第一个参数是相对路径的话,那么第二个参数 absoluteBase 则必传,且必须为第一个参数的绝对路径。
举个例子,让我们用一个绝对路径的 URL 来初始化 URL() 函数:
const url = new URL('http://example.com/path/index.html');
url.href; // => 'http://example.com/path/index.html'
或者我们可以使用相对路径和绝对路径:
const url = new URL('/path/index.html', 'http://example.com');
url.href; // => 'http://example.com/path/index.html'
URL() 实例中的 href 属性返回了完整的 URL 字符串。
在新建了 URL() 的实例以后,你可以用它来访问前文图片中的任意 URL 组成部分。作为参考,下面是 URL() 实例的接口列表:
interface URL {
href: USVString;
protocol: USVString;
username: USVString;
password: USVString;
host: USVString;
hostname: USVString;
port: USVString;
pathname: USVString;
search: USVString;
hash: USVString;
readonly origin: USVString;
readonly searchParams: URLSearchParams;
toJSON(): USVString;
}
上述的 USVString 参数在 JavaScript 中会映射成字符串。
3. Query 字符串
url.search 可以获取到 URL 当中 ? 后面的 query 字符串:
const url = new URL(
'http://example.com/path/index.html?message=hello&who=world'
);
url.search; // => '?message=hello&who=world'
如果 query 参数不存在,url.search 默认会返回一个空字符串 '':
const url1 = new URL('http://example.com/path/index.html');
const url2 = new URL('http://example.com/path/index.html?');
url1.search; // => ''
url2.search; // => ''
3.1 解析 query 字符串
相比于获得原生的 query 字符串,更实用的场景是获取到具体的 query 参数。
获取具体 query 参数的一个简单的方法是利用 url.searchParams 属性。这个属性是 URLSearchParams 的实例。
URLSearchParams 对象提供了许多用于获取 query 参数的方法,如get(param),has(param)等。
下面来看个例子:
const url = new URL(
'http://example.com/path/index.html?message=hello&who=world'
);
url.searchParams.get('message'); // => 'hello'
url.searchParams.get('missing'); // => null
url.searchParams.get('message') 返回了 message 这个 query 参数的值——hello。
如果使用 url.searchParams.get('missing') 来获取一个不存在的参数,则得到一个 null。
4. hostname
url.hostname 属性返回一段 URL 的 hostname 部分:
const url = new URL('http://example.com/path/index.html');
url.hostname; // => 'example.com'
5. pathname
url. pathname 属性返回一段 URL 的 pathname 部分:
const url = new URL('http://example.com/path/index.html?param=value');
url.pathname; // => '/path/index.html'
如果这段 URL 不含 path,则该属性返回一个斜杠 /:
const url = new URL('http://example.com/');
url.pathname; // => '/'
6. hash
最后,我们可以通过 url.hash 属性来获取 URL 中的 hash 值:
const url = new URL('http://example.com/path/index.html#bottom');
url.hash; // => '#bottom'
当 URL 中的 hash 不存在时,url.hash 属性会返回一个空字符串 '':
const url = new URL('http://example.com/path/index.html');
url.hash; // => ''
7. URL 校验
当使用 new URL() 构造函数来新建实例的时候,作为一种副作用,它同时也会对 URL 进行校验。如果 URL 不合法,则会抛出一个 TypeError。
举个例子,http ://example.com 是一段非法 URL,因为它在 http 后面多写了一个空格。
让我们用这个非法 URL 来初始化 URL() 构造函数:
try {
const url = new URL('http ://example.com');
} catch (error) {
error; // => TypeError, "Failed to construct URL: Invalid URL"
}
因为 http ://example.com 是一段非法 URL,跟我们想的一样,new URL() 抛出了一个 TypeError。
8. 修改 URL
除了获取 URL 的组成部分以外,像 search,hostname,pathname 和 hash 这些属性都是可写的——这也意味着你可以修改 URL。
举个例子,让我们把一段 URL 从 red.com 修改成 blue.io:
const url = new URL('http://red.com/path/index.html');
url.href; // => 'http://red.com/path/index.html'
url.hostname = 'blue.io';
url.href; // => 'http://blue.io/path/index.html'
注意,在 URL() 实例中只有 origin 和 searchParams 属性是只读的,其他所有的属性都是可写的,并且会修改原来的 URL。
9. 总结
URL() 构造函数是 JavaScript 中的一个能够很方便地用于解析(或者校验)URL 的工具。
new URL(relativeOrAbsolute [, absoluteBase]) 中的第一个参数接收 URL 的绝对路径或者相对路径。当第一个参数是相对路径时,第二个参数必传且必须为第一个参数的基路径。
在新建 URL() 的实例以后,你就能很轻易地获得 URL 当中的大部分组成部分了,比如:
url.search 获取原生的 query 字符串
url.searchParams 通过 URLSearchParams 的实例去获取具体的 query 参数
url.hostname获取 hostname
url.pathname 获取 pathname
url.hash 获取 hash 值
那么你最爱用的解析 URL 的 JavaScript 工具又是什么呢?
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
为什么需要额外的类型检查?
TypeScript 只在编译期执行静态类型检查!实际运行的是从 TypeScript 编译的 JavaScript,这些生成的 JavaScript 对类型一无所知。编译期静态类型检查在代码库内部能发挥很大作用,但对不合规范的输入(比如,从 API 处接收的输入)无能为力。
运行时检查的严格性
至少需要和编译期检查一样严格,否则就失去了编译期检查提供的保证。
如有必要,可以比编译期检查更严格,例如,年龄需要大于等于 0。
运行时类型检查策略
定制代码手动检查
灵活
可能比较枯燥,容易出错
容易和实际代码脱节
使用校验库手动检查
比如使用 joi:
import Joi from "@hapi/joi"const schema = Joi.object({ firstName: Joi.string().required(), lastName: Joi.string().required(), age: Joi.number().integer().min(0).required()});
灵活
容易编写
容易和实际代码脱节
手动创建 JSON Schema
例如:
{ "$schema": "http://json-schema.org/draft-07/schema#", "required": [ "firstName", "lastName", "age" ], "properties": { "firstName": { "type": "string" }, "lastName": { "type": "string" }, "age": { "type": "integer", "minimum": 0 } }}
使用标准格式,有大量库可以校验。
JSON 很容易存储和复用。
可能会很冗长,手写 JSON Schema 可能会很枯燥。
需要确保 Schema 和代码同步更新。
自动创建 JSON Schema
基于 TypeScript 代码生成 JSON Schema
-- 比如 typescript-json-schema 这个工具就可以做到这一点(同时支持作为命令行工具使用和通过代码调用)。
-- 需要确保 Schema 和代码同步更新。
基于 JSON 输入示例生成
-- 没有使用已经在 TypeScript 代码中定义的类型信息。
-- 如果提供的 JSON 输入示例和实际输入不一致,可能导致错误。
-- 仍然需要确保 Schema 和代码同步更新。
转译
例如使用 ts-runtime。
这种方式会将代码转译成功能上等价但内置运行时类型检查的代码。
比如,下面的代码:
interface Person { firstName: string; lastName: string; age: number;}const test: Person = { firstName: "Foo", lastName: "Bar", age: 55}
会被转译为:
import t from "ts-runtime/lib";const Person = t.type( "Person", t.object( t.property("firstName", t.string()), t.property("lastName", t.string()), t.property("age", t.number()) ));const test = t.ref(Person).assert({ firstName: "Foo", lastName: "Bar", age: 55});
这一方式的缺陷是无法控制在何处进行运行时检查(我们只需在输入输出的边界处进行运行时类型检查)。
顺便提一下,这是一个实验性的库,不建议在生产环境使用。
运行时类型派生静态类型
比如使用 io-ts 这个库。
这一方式下,我们定义运行时类型,TypeScript 会根据我们定义的运行时类型推断出静态类型。
运行时类型示例:
import t from "io-ts";const PersonType = t.type({ firstName: t.string, lastName: t.string, age: t.refinement(t.number, n => n >= 0, 'Positive')})
从中提取相应的静态类型:
interface Person extends t.TypeOf<typeof PersonType> {}
以上类型等价于:
interface Person { firstName: string; lastName: string; age: number;}
类型总是同步的。
io-ts 很强大,比如支持递归类型。
需要将类型定义为 io-ts 运行时类型,这在定义类时不适用:
-- 有一种变通的办法是使用 io-ts 定义一个接口,然后让类实现这个接口。然而,这意味着每次给类增加属性的时候都要更新 io-ts 类型。
不容易复用接口(比如前后端之间使用同一接口),因为这些接口是 io-ts 类型而不是普通的 TypeScript 类型。
基于装饰器的类校验
比如使用 class-validator 这个库。
基于类属性的装饰器。
和 Java 的 JSR-380 Bean Validation 2.0 (比如 Hibernate Validator 就实现了这一标准)很像。
-- 此类 Java EE 风格的库还有 typeorm (ORM 库,类似 Java 的 JPA)和 routing-controllers (用于定义 API,类似 Java 的 JAX-RS)。
代码示例:
import { plainToClass } from "class-transformer";import { validate, IsString, IsInt, Min } from "class-validator";class Person { @IsString() firstName: string; @IsString() lastName: string; @IsInt() @Min(0) age: number;}const input: any = { firstName: "Foo", age: -1};const inputAsClassInstance = plainToClass( Person, input as Person);validate(inputAsClassInstance).then(errors => { // 错误处理代码});
类型总是同步的。
需要对类进行检查时很有用。
可以用来检查接口(定义一个实现接口的类)。
注意:class-validator 用于具体的类实例。在上面的代码中,我们使用它的姊妹库 class-transformer 将普通输入转换为 Person 实例。转换过程本身不进行任何类型检查。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
如今,Page Speed(页面速度)的意义非凡。
自从Google改变Googlebot's的算法以高度支持快速,适合移动设备的网站以来,拥有快速网站变得越来越重要。如果这还不够好,用户通常会花更少的时间,转化率也会更低,你的网站体验越慢,用户的转化率就越低。
什么是Page Speed
Page Speed是将内容完全加载到网页上所花费的时间。
对于任何给定的用户来说,页面缓慢的原因可能有很多,你的用户可能正在火车上,通过信号弱的隧道,或者他们的互联网速度很慢。
通过遵循最佳实践,我们至少可以通过确保我们已经做了最好的工作来缓解问题。
现在你知道它是什么了,下面我就来教你如何提高页面速度。
注意:这些是按难度顺序列出的。在某个时候,你将需要开发人员来帮助优化你的网站。
1.使用CDN
CDN是内容传输网络的缩写。使用CDN可以让你有效地访问全球数百台小服务器,这些服务器为你提供网站的副本,大大减少了你的网站获取时间。如果你没有使用CDN,你的网站的每一个请求(包括图片、CSS和JavaScript)都会被缓慢地传送到你的服务器上。
根据HTTPArchive中的4.68亿个请求,48%的请求不是来自CDN。那是超过2.24亿的请求,如果他们花几分钟的时间给自己的网站添加一个CDN,速度可能会超过50%。
一定要检查你的CDN配置是否正确——在你的CDN中缓存丢失意味着CDN必须向你的源服务器请求资源,这就违背了使用CDN的初衷!所以,你的CDN必须要有一个正确的配置。
2.启用GZIP压缩
在一些CDN上,GZIP压缩只是一个标有 "启用压缩 "的复选框。这大概会减少一半的文件大小,你的用户需要下载文件才能使用你的网站,你的用户会因此而喜欢你。
3.使用较小的图像
这意味着既要降低分辨率(例如,摄像头的输出从4000x3000像素减少到网络的1000x750),又要通过压缩文件来减小尺寸。
如果你的网站使用WordPress,则有一些插件会在你上传图片时自动为你执行此操作。
在撰写博客文章时,我个人使用TinyJPG压缩图像。
https://tinyjpg.com/
4.减少页面发出的请求数
目标是减少加载页面顶部部分所需的请求数量。
这里有两种思维方式,你可以:
通过删除花哨的动画或不能改善网站体验的图像,减少整个页面上的请求数量。
或者,你可以通过使用延迟加载来推迟优先级不高的加载内容。
5.尽可能避免重定向
重定向会大大降低网站速度。使用响应式CSS并从一个域为你的网站提供服务,而不是为移动用户提供特殊的子域。
有些重定向是不可避免的,比如 www-> 根域 或 根域 ->www,但你的大部分流量不应该经历重定向来查看你的网站。
6.减少到第一个字节的时间
到第一个字节的时间是指你的浏览器在发出资源请求后,从服务器接收到第一个字节的数据所花费的时间。
有两个部分:
在服务器上花费的时间
发送数据所花费的时间
你可以通过优化你的服务器端渲染、数据库查询、API调用、负载平衡、你的应用程序的实际代码以及服务器的负载本身(特别是如果你使用的是廉价的虚拟主机——这将影响你的网站的性能),来改善你在服务器上花费的时间。
你可以使用CDN大大减少发送数据所花费的时间。
7.减少并删除阻止渲染的JavaScript
外部脚本(特别是那些用于营销的外部脚本)往往会写得很差,会阻止你的页面加载,直到它运行完毕。
你可以通过将外部脚本标记为异步来减少这种影响:
<script async src="https://example.com/external.js"></script>
你还可以延迟加载市场营销脚本,直到用户开始滚动为止:
window.addEventListener(
'scroll',
() =>
setTimeout(() => {
// 在此插入营销片段
}, 1000),
{ once: true }
);
8.缩小CSS和JS
Minifying是指使用工具来删除空格、换行符和缩短变量名。通常情况下,这将作为构建过程的一部分自动完成。
要缩小JavaScript,请查看UglifyJS。
http://lisperator.net/uglifyjs/
要缩小CSS,请查看cssnano。
9.删除未使用的CSS
自Chrome 59(2017年4月发布)以来,在Chrome DevTools中可以看到未使用的JS和CSS。
要看这个,打开DevTools,显示控制台抽屉(就是点击Esc时出现的那个烦人的东西),点击左下角的三个小点,打开 "Coverage",就可以看到。
点击带有重新加载图标的按钮将刷新页面,并审核CSS和JS的使用情况。
在Google Chrome浏览器中审核初始页面时,外观如下所示:
10.定期跟踪网站速度
在你的网站速度变慢的瞬间,修复网站速度问题就会容易得多。除此之外,如果你把检查网站速度作为一种习惯,那么修复网站速度慢的问题就会变成一件小得多的事情。
有免费的工具可以监视你网站的速度,其中的两个是WebPageTest和Google Lighthouse。这些工具的缺点是你需要记住在进行更改之前和之后都必须运行它们。
PerfBeacon是一项服务(由本文的作者创建),该服务定期运行Google Lighthouse,并让你随时跟踪网站的速度。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
了解如何使用JavaScript中的Cache API缓存资源。
Cache API允许服务工作者对要缓存的资源(HTML页面、CSS、JavaScript文件、图片、JSON等)进行控制。通过Cache API,服务工作者可以缓存资源以供脱机使用,并在以后检索它们。
检测Cache支持
检查 caches 对象在 window 中是否可用。
let isCacheSupported = 'caches' in window;
caches 是 CacheStorage 的一个实例。
创建/初始化Cache
我们可以使用 open 方法创建一个具有 name 的缓存,这将返回 promise。如果缓存已经存在,则不会创建新的缓存。
caches.open('cacheName').then( cache => {
});
你不能访问为其他源(域)设置的缓存。
你正在创建的缓存将为你的域创建。
你可以为同一个域添加多个缓存,可以通过 caches.keys() 访问。
将项目添加到缓存
可以使用三种方法 add,addAll,set 来缓存资源。 add() 和 addAll() 方法自动获取资源并对其进行缓存,而在 set 方法中,我们将获取数据并设置缓存。
add
let cacheName = 'userSettings';
let url = '/api/get/usersettings';
caches.open(cacheName).then( cache => {
cache.add(url).then( () => {
console.log("Data cached ")
});
});
在上面的代码中,内部对 /api/get/usersettings url的请求已发送到服务器,一旦接收到数据,响应将被缓存。
addAll
addAll 接受URL数组,并在缓存所有资源时返回Promise。
let urls = ['/get/userSettings?userId=1', '/get/userDetails'];
caches.open(cacheName).then( cache => {
cache.addAll(urls).then( () => {
console.log("Data cached ")
});
});
Cache.add/Cache.addAll 不缓存 Response.status 值不在200范围内的响应,Cache.put 可以让你存储任何请求/响应对。
put
put 为当前的 Cache 对象添加一个key/value对,在 put 中,我们需要手动获取请求并设置值。
注意:put() 将覆盖先前存储在高速缓存中与请求匹配的任何键/值对。
let cacheName = 'userSettings';
let url = '/api/get/userSettings';
fetch(url).then(res => {
return caches.open(cacheName).then(cache => {
return cache.put(url, res);
})
})
从缓存中检索
使用 cache.match() 可以得到存储到URL的 Response。
const cacheName = 'userSettings'
const url = '/api/get/userSettings'
caches.open(cacheName).then(cache => {
cache.match(url).then(settings => {
console.log(settings);
}
});
settings 是一个响应对象,它看起来像
Response {
body: (...),
bodyUsed: false,
headers: Headers,
ok: true,
status: 200,
statusText: "OK",
type: "basic",
url: "https://test.com/api/get/userSettings"
}
检索缓存中的所有项目
cache 对象包含 keys 方法,这些方法将拥有当前缓存对象的所有url。
caches.open(cacheName).then( (cache) => {
cache.keys().then((arrayOfRequest) => {
console.log(arrayOfRequest); // [Request, Request]
});
});
arrayOfRequest是一个Request对象数组,其中包含有关请求的所有详细信息。
检索所有缓存
caches.keys().then(keys => {
// keys是一个数组,其中包含键的列表
})
从缓存中删除项目
可以对 cache 对象使用 delete 方法来删除特定的缓存请求。
let cacheName = userSettings;
let urlToDelete = '/api/get/userSettings';
caches.open(cacheName).then(cache => {
cache.delete(urlToDelete)
})
完全删除缓存
caches.delete(cacheName).then(() => {
console.log('Cache successfully deleted!');
})
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
作为一个曾经的Java coder, 当我第一次看到js里面的装饰器(Decorator)的时候,就马上想到了Java中的注解,当然在实际原理和功能上面,Java的注解和js的装饰器还是有很大差别的。本文题目是Vue中使用装饰器,我是认真的,但本文将从装饰器的概念开发聊起,一起来看看吧。
通过本文内容,你将学到以下内容:
了解什么是装饰器
在方法使用装饰器
在class中使用装饰器
在Vue中使用装饰器
本文首发于公众号【前端有的玩】,不想当咸鱼,想要换工作,关注公众号,带你每日一起刷大厂面试题,关注 === 大厂offer。
什么是装饰器
装饰器是ES2016提出来的一个提案,当前处于Stage 2阶段,关于装饰器的体验,可以点击 https://github.com/tc39/proposal-decorators查看详情。装饰器是一种与类相关的语法糖,用来包装或者修改类或者类的方法的行为,其实装饰器就是设计模式中装饰者模式的一种实现方式。不过前面说的这些概念太干了,我们用人话来翻译一下,举一个例子。
在日常开发写bug过程中,我们经常会用到防抖和节流,比如像下面这样
class MyClass {
follow = debounce(function() {
console.log('我是子君,关注我哦')
}, 100)
}
const myClass = new MyClass()
// 多次调用只会输出一次
myClass.follow()
myClass.follow()
上面是一个防抖的例子,我们通过debounce函数将另一个函数包起来,实现了防抖的功能,这时候再有另一个需求,比如希望在调用follow函数前后各打印一段日志,这时候我们还可以再开发一个log函数,然后继续将follow包装起来
/**
* 最外层是防抖,否则log会被调用多次
*/
class MyClass {
follow = debounce(
log(function() {
console.log('我是子君,关注我哦')
}),
100
)
}
上面代码中的debounce和log两个函数,本质上是两个包装函数,通过这两个函数对原函数的包装,使原函数的行为发生了变化,而js中的装饰器的原理就是这样的,我们使用装饰器对上面的代码进行改造
class MyClass {
@debounce(100)
@log
follow() {
console.log('我是子君,关注我哦')
}
}
装饰器的形式就是 @ + 函数名,如果有参数的话,后面的括号里面可以传参
在方法上使用装饰器
装饰器可以应用到class上或者class里面的属性上面,但一般情况下,应用到class属性上面的场景会比较多一些,比如像上面我们说的log,debounce等等,都一般会应用到类属性上面,接下来我们一起来具体看一下如何实现一个装饰器,并应用到类上面。在实现装饰器之前,我们需要先了解一下属性描述符
了解一下属性描述符
在我们定义一个对象里面的属性的时候,其实这个属性上面是有许多属性描述符的,这些描述符标明了这个属性能不能修改,能不能枚举,能不能删除等等,同时ECMAScript将这些属性描述符分为两类,分别是数据属性和访问器属性,并且数据属性与访问器属性是不能共存的。
数据属性
数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性包含了四个描述符,分别是
configurable
表示能不能通过delete删除属性,能否修改属性的其他描述符特性,或者能否将数据属性修改为访问器属性。当我们通过let obj = {name: ''}声明一个对象的时候,这个对象里面所有的属性的configurable描述符的值都是true
enumerable
表示能不能通过for in或者Object.keys等方式获取到属性,我们一般声明的对象里面这个描述符的值是true,但是对于class类里面的属性来说,这个值是false
writable
表示能否修改属性的数据值,通过将这个修改为false,可以实现属性只读的效果。
value
表示当前属性的数据值,读取属性值的时候,从这里读取;写入属性值的时候,会写到这个位置。
访问器属性
访问器属性不包含数据值,他们包含了getter与setter两个函数,同时configurable与enumerable是数据属性与访问器属性共有的两个描述符。
getter
在读取属性的时候调用这个函数,默认这个函数为undefined
setter
在写入属性值的时候调用这个函数,默认这个函数为undefined
了解了这六个描述符之后,你可能会有几个疑问: 我如何去定义修改这些属性描述符?这些属性描述符与今天的文章主题有什么关系?接下来是揭晓答案的时候了。
使用Object.defineProperty
了解过vue2.0双向绑定原理的同学一定知道,Vue的双向绑定就是通过使用Object.defineProperty去定义数据属性的getter与setter方法来实现的,比如下面有一个对象
let obj = {
name: '子君',
officialAccounts: '前端有的玩'
}
我希望这个对象里面的用户名是不能被修改的,用Object.defineProperty该如何定义呢?
Object.defineProperty(obj,'name', {
// 设置writable 是 false, 这个属性将不能被修改
writable: false
})
// 修改obj.name
obj.name = "君子"
// 打印依然是子君
console.log(obj.name)
通过Object.defineProperty可以去定义或者修改对象属性的属性描述符,但是因为数据属性与访问器属性是互斥的,所以一次只能修改其中的一类,这一点需要注意。
定义一个防抖装饰器
装饰器本质上依然是一个函数,不过这个函数的参数是固定的,如下是防抖装饰器的代码
/**
*@param wait 延迟时长
*/
function debounce(wait) {
return function(target, name, descriptor) {
descriptor.value = debounce(descriptor.value, wait)
}
}
// 使用方式
class MyClass {
@debounce(100)
follow() {
console.log('我是子君,我的公众号是 【前端有的玩】,关注有惊喜哦')
}
}
我们逐行去分析一下代码
首先我们定义了一个 debounce函数,同时有一个参数wait,这个函数对应的就是在下面调用装饰器时使用的@debounce(100)
debounce函数返回了一个新的函数,这个函数即装饰器的核心,这个函数有三个参数,下面逐一分析
target: 这个类属性函数是在谁上面挂载的,如上例对应的是MyClass类
name: 这个类属性函数的名称,对应上面的follow
descriptor: 这个就是我们前面说的属性描述符,通过直接descriptor上面的属性,即可实现属性只读,数据重写等功能
然后第三行 descriptor.value = debounce(descriptor.value, wait), 前面我们已经了解到,属性描述符上面的value对应的是这个属性的值,所以我们通过重写这个属性,将其用debounce函数包装起来,这样在函数调用follow时实际调用的是包装后的函数
通过上面的三步,我们就实现了类属性上面可使用的装饰器,同时将其应用到了类属性上面
在class上使用装饰器
装饰器不仅可以应用到类属性上面,还可以直接应用到类上面,比如我希望可以实现一个类似Vue混入那样的功能,给一个类混入一些方法属性,应该如何去做呢?
// 这个是要混入的对象
const methods = {
logger() {
console.log('记录日志')
}
}
// 这个是一个登陆登出类
class Login{
login() {}
logout() {}
}
如何将上面的methods混入到Login中,首先我们先实现一个类装饰器
function mixins(obj) {
return function (target) {
Object.assign(target.prototype, obj)
}
}
// 然后通过装饰器混入
@mixins(methods)
class Login{
login() {}
logout() {}
}
这样就实现了类装饰器。对于类装饰器,只有一个参数,即target,对应的就是这个类本身。
了解完装饰器,我们接下来看一下如何在Vue中使用装饰器。
在Vue中使用装饰器
使用ts开发Vue的同学一定对vue-property-decorator不会感到陌生,这个插件提供了许多装饰器,方便大家开发的时候使用,当然本文的中点不是这个插件。其实如果我们的项目没有使用ts,也是可以使用装饰器的,怎么用呢?
配置基础环境
除了一些老的项目,我们现在一般新建Vue项目的时候,都会选择使用脚手架vue-cli3/4来新建,这时候新建的项目已经默认支持了装饰器,不需要再配置太多额外的东西,如果你的项目使用了eslint,那么需要给eslint配置以下内容。
parserOptions: {
ecmaFeatures:{
// 支持装饰器
legacyDecorators: true
}
}
使用装饰器
虽然Vue的组件,我们一般书写的时候export出去的是一个对象,但是这个并不影响我们直接在组件中使用装饰器,比如就拿上例中的log举例。
function log() {
/**
* @param target 对应 methods 这个对象
* @param name 对应属性方法的名称
* @param descriptor 对应属性方法的修饰符
*/
return function(target, name, descriptor) {
console.log(target, name, descriptor)
const fn = descriptor.value
descriptor.value = function(...rest) {
console.log(`这是调用方法【${name}】前打印的日志`)
fn.call(this, ...rest)
console.log(`这是调用方法【${name}】后打印的日志`)
}
}
}
export default {
created() {
this.getData()
},
methods: {
@log()
getData() {
console.log('获取数据')
}
}
}
看了上面的代码,是不是发现在Vue中使用装饰器还是很简单的,和在class的属性上面使用的方式一模一样,但有一点需要注意,在methods里面的方法上面使用装饰器,这时候装饰器的target对应的是methods。
除了在methods上面可以使用装饰器之外,你也可以在生命周期钩子函数上面使用装饰器,这时候target对应的是整个组件对象。
一些常用的装饰器
下面小编罗列了几个小编在项目中常用的几个装饰器,方便大家使用
1. 函数节流与防抖
函数节流与防抖应用场景是比较广的,一般使用时候会通过throttle或debounce方法对要调用的函数进行包装,现在就可以使用上文说的内容将这两个函数封装成装饰器, 防抖节流使用的是lodash提供的方法,大家也可以自行实现节流防抖函数哦
import { throttle, debounce } from 'lodash'
/**
* 函数节流装饰器
* @param {number} wait 节流的毫秒
* @param {Object} options 节流选项对象
* [options.leading=true] (boolean): 指定调用在节流开始前。
* [options.trailing=true] (boolean): 指定调用在节流结束后。
*/
export const throttle = function(wait, options = {}) {
return function(target, name, descriptor) {
descriptor.value = throttle(descriptor.value, wait, options)
}
}
/**
* 函数防抖装饰器
* @param {number} wait 需要延迟的毫秒数。
* @param {Object} options 选项对象
* [options.leading=false] (boolean): 指定在延迟开始前调用。
* [options.maxWait] (number): 设置 func 允许被延迟的最大值。
* [options.trailing=true] (boolean): 指定在延迟结束后调用。
*/
export const debounce = function(wait, options = {}) {
return function(target, name, descriptor) {
descriptor.value = debounce(descriptor.value, wait, options)
}
}
封装完之后,在组件中使用
import {debounce} from '@/decorator'
export default {
methods:{
@debounce(100)
resize(){}
}
}
2. loading
在加载数据的时候,为了个用户一个友好的提示,同时防止用户继续操作,一般会在请求前显示一个loading,然后在请求结束之后关掉loading,一般写法如下
export default {
methods:{
async getData() {
const loading = Toast.loading()
try{
const data = await loadData()
// 其他操作
}catch(error){
// 异常处理
Toast.fail('加载失败');
}finally{
loading.clear()
}
}
}
}
我们可以把上面的loading的逻辑使用装饰器重新封装,如下代码
import { Toast } from 'vant'
/**
* loading 装饰器
* @param {*} message 提示信息
* @param {function} errorFn 异常处理逻辑
*/
export const loading = function(message = '加载中...', errorFn = function() {}) {
return function(target, name, descriptor) {
const fn = descriptor.value
descriptor.value = async function(...rest) {
const loading = Toast.loading({
message: message,
forbidClick: true
})
try {
return await fn.call(this, ...rest)
} catch (error) {
// 在调用失败,且用户自定义失败的回调函数时,则执行
errorFn && errorFn.call(this, error, ...rest)
console.error(error)
} finally {
loading.clear()
}
}
}
}
然后改造上面的组件代码
export default {
methods:{
@loading('加载中')
async getData() {
try{
const data = await loadData()
// 其他操作
}catch(error){
// 异常处理
Toast.fail('加载失败');
}
}
}
}
3. 确认框
当你点击删除按钮的时候,一般都需要弹出一个提示框让用户确认是否删除,这时候常规写法可能是这样的
import { Dialog } from 'vant'
export default {
methods: {
deleteData() {
Dialog.confirm({
title: '提示',
message: '确定要删除数据,此操作不可回退。'
}).then(() => {
console.log('在这里做删除操作')
})
}
}
}
我们可以把上面确认的过程提出来做成装饰器,如下代码
import { Dialog } from 'vant'
/**
* 确认提示框装饰器
* @param {*} message 提示信息
* @param {*} title 标题
* @param {*} cancelFn 取消回调函数
*/
export function confirm(
message = '确定要删除数据,此操作不可回退。',
title = '提示',
cancelFn = function() {}
) {
return function(target, name, descriptor) {
const originFn = descriptor.value
descriptor.value = async function(...rest) {
try {
await Dialog.confirm({
message,
title: title
})
originFn.apply(this, rest)
} catch (error) {
cancelFn && cancelFn(error)
}
}
}
}
然后再使用确认框的时候,就可以这样使用了
export default {
methods: {
// 可以不传参,使用默认参数
@confirm()
deleteData() {
console.log('在这里做删除操作')
}
}
}
是不是瞬间简单多了,当然还可以继续封装很多很多的装饰器,因为文章内容有限,暂时提供这三个。
装饰器组合使用
在上面我们将类属性上面使用装饰器的时候,说道装饰器可以组合使用,在Vue组件上面使用也是一样的,比如我们希望在确认删除之后,调用接口时候出现loading,就可以这样写(一定要注意顺序)
export default {
methods: {
@confirm()
@loading()
async deleteData() {
await delete()
}
}
}
本节定义的装饰器,均已应用到这个项目中 https://github.com/snowzijun/vue-vant-base, 这是一个基于Vant开发的开箱即用移动端框架,你只需要fork下来,无需做任何配置就可以直接进行业务开发,欢迎使用,喜欢麻烦给一个star。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
目录
1. Animista
2. Animate CSS
3. Vivify
4. Magic Animations CSS3
5. cssanimation.io
6. Angrytools
7. Hover.css
8. WickedCSS
9. Three Dots
10. CSShake
1.Animista
网站地址:http://animista.net/
网站描述:在线生成 css 动画
Animista是一个在线动画生成器,同时也是一个动画库,它为我们提供了以下功能
1. 选择不同的动画
我们可以选择想要的动画类型(例如entrance/exist),除了可以选择某个动画(例如,scale-in)外,甚至还可以为该动画选择不同的展示效果(例如: scale-in-right)。
2. 定制
Animista还提供了一个功能,允许我们定制动画的某些部分,比如
duration
delay
direction
更好的是,可以选择要设置动画的对象:
3. 生成CSS代码
选择适合自己需要的动画后,我们可以直接从网站上获取代码,甚者可以进行压缩:
4. 下载代码
另一个好用的功能是,可以把自己收藏自己喜欢的动画,然后一起下载下来, 或者,我们也可以选择将这些动画的代码复制到一起。
2. Animate CSS
网站地址:http://daneden.github.io/anim...
网站描述:齐全的CSS3动画库
想必这个不用介绍,大部分人都知道了。Animate CSS 可能是最著名的动画库之一。这里简要介绍一下它的用法:
1. 用法
首先,必须在总需要动画元素上添加类animated ,然后是动画的名字。
<div class="animated slideInLeft"></div>
如果我们想让动画一直持续,可以添加infinite类。
通过 JS 来添加动画:
document.querySelector('.my-element').classList.add('animated', 'slideInLeft')
通过 JQ 来添加动画:
$(".my-element").addClass("animated slideInLeft")
2. 其它功能
Animate CSS提供了一些基本的类来控制动画的延迟和速度。
delay
可以添加 delay 类来延迟动画的播放。
<div class="animated slideInLeft delay-{1-5}"><div>
speed
我们还可以通过添加如下列出的类之一来控制动画速度。
类名 速度时间
show 2s
slower 3s
fast 800ms
faster 500ms
<div class="animated slideInLeft slow|slower|fast|faster"><div>
3. Vivify
网站地址: http://vivify.mkcreative.cz/
网站描述: 一个更加丰富css动画库
Vivify 是一个动画库,可以看作是Animate CSS的增强版。它们的工作方式完全相同,有Animate CSS的大多数类且还扩展了一些。
<div class="vivify slideInLeft"></div>
使用 JS 方式:
document.querySelector('.my-element').classList.add('vivify', 'slideInLeft')
使用 JQ 方式:
$(".my-element").addClass("vivify slideInLeft")
与Animate CSS一样,Vivify 还提供了一些类来控制动画的持续时间和延迟。
延迟和持续时间类在以下间隔中可用:
<div class="delay|duration-{100|150|200|250...1000|1250|1500|1750...10750}"></div>
4. Magic Animations CSS3
网站地址: https://www.minimamente.com/p...
网站描述: Magic CSS3 Animations 是 CSS3 动画的包,伴有特殊的效果,用户可以自由的在 web 项目中使用。
这个动画库有一些非常漂亮和流畅的动画,特别是3D的。没什么好说的,自己去尝试。
<div class="magictime fadeIn"></div>
使用 JS 方式:
document.querySelector('.my-element').classList.add('magictime', 'fadeIn')
使用 JQ 方式:
$(".my-element").addClass("magictime fadeIn")
5. cssanimation.io
网站地址: http://cssanimation.io/index....
cssanimation.io是一大堆不同动画的集合,总共大概有200个,这很强大。如果你连在这里都没有找到你所需的动画,那么在其它也将很难找到。
它的工作原理与 Animista 类似。例如,可以选择一个动画并直接从站点获取代码,或者也可以下载整个库。
用法
将cssanimation {animation_name}添加到指定的元素上。
<div class="cssanimation fadeIn"></div>
使用 JS
document.querySelector('.my-element').classList.add('cssanimation','fadeIn')
使用 JQ
$(".my-element").addClass("cssanimation fadeIn")
还可以添加 infinite 类,这样动画就可以循环播放。
<div class="cssanimation fadeIn infinite"></div>
此外,cssanimation.io还为我们提供了动漫字母的功能。使用这个需要引入letteranimation.js文件,然后将le {animation_name}添加到我们的文本元素中。
<div class="cssanimation leSnake"></div>
要使字母按顺序产生动画,添加sequence类,要使它们随机产生动画,添加random类。
<div class="cssanimation leSnake {sequence|random}"></div>
Sequence
Random
6.Angrytools
网站地址: https://angrytools.com/css/an...
如果使用不同的生成器,Angrytools实际上是一个集合,其中还包括CSS动画生成器。
它可能不像Animista那么复杂,但我觉得这个也很不错。这个站点还提供了一些自定义动画的特性,比如动画的持续时间或延迟。
但是我喜欢的是,我们可以在其展示时间轴上添加自定义的keyframes,然后可以直接在其中编写代码。 另外,也可以编辑现有的。
当我们完成的时候,可以得到完整的动画代码,也可以下载它。
7.Hover.css
网站地址: http://ianlunn.github.io/Hover/
网站描述: 纯CSS3鼠标滑过效果动画库
Hover.css是许多CSS动画的集合,与上面的动画不同,每次将元素悬停时都会触发。
一组CSS3支持的悬停效果,可应用于链接、按钮、徽标、SVG和特色图像等。
** 用法
它非常简单:只需将类的名称添加到元素中,比如
<button class="hvr-fade">Hover me!</button>
8.WickedCSS
网站地址: http://kristofferandreasen.gi...
WickedCSS是一个小的CSS动画库,它没有太多的动画变体,但至少有很大的变化。 其中大多数是我们已经熟悉的基础知识,但它们确实很干净。
它的用法很简单,只需将动画的名称添加到元素中即可。
<div class="bounceIn"></div>
** 使用 JS
document.querySelector('.my-element').classList.add('bounceIn')
** 使用 JQ
$(".my-element").addClass("bounceIn")
9.Three Dots
网站地址: https://nzbin.github.io/three...
Three Dots是一组CSS加载动画,它由三个点组成,而这些点仅由单个元素组成。
** 用法
只需创建一个div元素,并添加动画的名称
<div class="dot-elastic"></div>
10.CSShake
网站地址: https://elrumordelaluz.github...
顾名思义,CSShake是一个CSS动画库,其中包含不同类型的震动动画。
** 用法
将shake {animation name}添加到元素中。
<div class="shake shake-hard"></div>
使用 JS
document.querySelector('.my-element').classList.add('shake','shake-hard')
使用 JQ
$(".my-element").addClass("shake shake-hard")
人才们的 【三连】 就是小智不断分享的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言,最后,谢谢大家的观看。
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
原文:https://dev.to/weeb/10-of-the...
交流
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
毫无疑问,JavaScript是Web开发中最流行的编程语言之一。无论您使用的是React,Vue还是Angular,都只是JavaScript。围绕JS展开了广泛而重要的生态系统,提供了无数的框架和库,可帮助你更快地开发应用程序。
但是有时候最好退一步,尝试了解如何在没有库的情况下做事。看看下面的代码片段,以优雅的方式解决简单的问题,并在日常项目情况下使用这些知识或为编码面试做准备。
1.反转字符串
在此示例中,我们使用扩展运算符(…),Array的reverse方法和String的join方法来反转给定的字符串。
const reverseString = string => [...string].reverse().join('');
// 例子
reverseString('javascript'); // 'tpircsavaj'
reverseString('good'); // 'doog'
2.计算数字的阶乘
要计算给定数字的阶乘,我们使用箭头函数和嵌套三元运算符。
const factoriaOfNumber = number => number < 0 ? (() => {
throw new TypeError('No negative numbers please');
})()
: number <=1
? 1
: number * factoriaOfNumber(number -1);
// 例子
factoriaOfNumber(4); // 24
factoriaOfNumber(8); // 40320
3.将数字转换为数字数组
在此示例中,我们使用扩展运算符(…),Array的map方法和 parseInt 函数将给定的数字转换为一个单数的数组。
const convertToArray = number => [...`${number}`].map(el => parseInt(el));
// 例子
convertToArray(5678); // [5, 6, 7, 8]
convertToArray(123456789); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
4.检查数字是否为2的幂
这很简单。我们检查该数字不是伪造的,并使用按位AND运算符(&)来确定数字是否为2的幂。
const isNumberPowerOfTwo = number => !!number && (number & (number - 1)) == 0;
// 例子
isNumberPowerOfTwo(100); // false
isNumberPowerOfTwo(128); // true
5.从对象创建键值对数组
在此示例中,我们使用Object中的keys方法和Array中的map方法来映射Object的键并创建键/值对数组。
const keyValuePairsToArray = object => Object.keys(object).map(el => [el, object[el]]);
// 例子
keyValuePairsToArray({ Better: 4, Programming: 2 });
// [ ['Better', 4], ['Programming', 2] ]
keyValuePairsToArray({ x: 1, y: 2, z: 3 });
// [ ['x', 1], ['y', 2], ['z', 3] ]
6.返回数组中的[Number]个最大元素
为了从数组中返回最大元素,我们使用了一个箭头函数,该函数获取数组和我们希望函数返回的元素数。我们使用扩展运算符(…)以及Array中的sort和slice方法。请注意,如果我们不提供第二个参数,则 number 的默认值为 1,因此仅返回一个最大元素。
const maxElementsFromArray = (array, number = 1) => [...array].sort((x, y) => y - x).slice(0, number);
// 例子
maxElementsFromArray([1,2,3,4,5]); // [5]
maxElementsFromArray([7,8,9,10,10],2); // [10, 10]
7.检查数组中的所有元素是否相等
在这个简短的示例中,我们使用Array中的every方法检查数组中的所有元素是否相等。我们基本上检查每个元素是否等于数组中的第一个元素。
const elementsAreEqual = array => array.every(el => el === array[0]);
// 例子
elementsAreEqual([9,8,7,6,5]); // false
elementsAreEqual([4,4,4,4,4]); // true
8.返回两个数的平均值
在此示例中,我们使用了扩展运算符(…)和Array中的reduce方法来返回两个给定数字或一个数组的平均值。
const averageOfTwoNumbers = (...numbers) => numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0) / numbers.length;
// 例子
averageOfTwoNumbers(...[6,7,8]); // 7
averageOfTwoNumbers(6,7,8,9); // 7.5
9.返回两个或多个数字的总和
要返回两个或多个给定数字或一个数组的总和,我们再次使用扩展运算符(…)和Array中的reduce方法。
const sumOfNumbers = (...array) => [...array].reduce((accumulator, currentValue) => accumulator + currentValue, 0);
// 例子
sumOfNumbers(5,6,7,8,9.10); // 45
sumOfNumbers(...[1,2,3,4,5,6,7,8,9,10]); // 50
10.返回数字数组的幂集
在最后一个示例中,我们要返回数字数组的幂集。因此,我们使用Array中的reduce,map和concat方法。
const powersetOfArray = array => array.reduce((accumulator, currentValue) => accumulator.concat(accumulator.map(el => [currentValue].concat(el))), [[]]);
// 例子
powersetOfArray([4, 2]); // [[], [4], [2], [2, 4]]
powersetOfArray([1, 2, 3]); /
// [[], [1], [2], [2, 1], [3], [3, 1], [3, 2], [3, 2, 1]]
如你所见,使用JavaScript和一些ES6魔术来解决这些任务并不总是困难的。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
阅读之前
基本上,在JavaScript中遍历对象取决于对象是否可迭代。默认情况下,数组是可迭代的。map 和 forEach 包含在Array.prototype 中,因此我们无需考虑可迭代性。如果你想进一步学习,我推荐你看看什么是JavaScript中的可迭代对象!
什么是map()和forEach()?
map 和 forEach 是数组中的帮助器方法,可以轻松地在数组上循环。我们曾经像下面这样循环遍历一个数组,没有任何辅助函数。
var array = ['1', '2', '3'];
for (var i = 0; i < array.length; i += 1) {
console.log(Number(array[i]));
}
// 1
// 2
// 3
自JavaScript时代开始以来,就一直存在 for 循环。它包含3个表达式:初始值,条件和最终表达式。
这是循环数组的经典方法。从ECMAScript 5开始,新功能似乎使我们更加快乐。
map
map 的作用与 for 循环完全相同,只是 map 会创建一个新数组,其结果是在调用数组中的每个元素上调用提供的函数。
它需要两个参数:一个是稍后在调用 map 或 forEach 时调用的回调函数,另一个是回调函数被调用时使用的名为 thisArg 的上下文变量。
const arr = ['1', '2', '3'];
// 回调函数接受3个参数
// 数组的当前值作为第一个参数
// 当前值在数组中的位置作为第二个参数
// 原始源数组作为第三个参数
const cb = (str, i, origin) => {
console.log(`${i}: ${Number(str)} / ${origin}`);
};
arr.map(cb);
// 0: 1 / 1,2,3
// 1: 2 / 1,2,3
// 2: 3 / 1,2,3
回调函数可以如下使用。
arr.map((str) => { console.log(Number(str)); })
map 的结果不等于原始数组。
const arr = [1];
const new_arr = arr.map(d => d);
arr === new_arr; // false
你还可以将对象作为 thisArg 传递到map。
const obj = { name: 'Jane' };
[1].map(function() {
// { name: 'Jane' }
console.dir(this);
}, obj);
[1].map(() => {
// window
console.dir(this);
}, obj);
对象 obj 成为 map 的 thisArg。但是箭头回调函数无法将 obj 作为其 thisArg。
这是因为箭头函数与正常函数不同。
forEach
forEach 是数组的另一个循环函数,但 map 和 forEach 在使用中有所不同。map 和 forEach 可以使用两个参数——回调函数和 thisArg,它们用作其 this。
const arr = ['1', '2', '3'];
// 回调函数接受3个参数
// 数组的当前值作为第一个参数
// 当前值在数组中的位置作为第二个参数
// 原始源数组作为第三个参数
const cb = (str, i, origin) => {
console.log(`${i}: ${Number(str)} / ${origin}`);
};
arr.forEach(cb);
// 0: 1 / 1,2,3
// 1: 2 / 1,2,3
// 2: 3 / 1,2,3
那有什么不同?
map 返回其原始数组的新数组,但是 forEach 却没有。但是它们都确保了原始对象的不变性。
[1,2,3].map(d => d + 1); // [2, 3, 4];
[1,2,3].forEach(d => d + 1); // undefined;
如果更改数组内的值,forEach 不能确保数组的不变性。这个方法只有在你不接触里面的任何值时,才能保证不变性。
[{a: 1, b: 2}, {a: 10, b: 20}].forEach((obj) => obj.a += 1);
// [{a: 2, b: 2}, {a: 11, b: 21}]
// 数组已更改!
何时使用map()和forEach()?
由于它们之间的主要区别在于是否有返回值,所以你会希望使用 map 来制作一个新的数组,而使用 forEach 只是为了映射到数组上。
这是一个简单的例子。
const people = [
{ name: 'Josh', whatCanDo: 'painting' },
{ name: 'Lay', whatCanDo: 'security' },
{ name: 'Ralph', whatCanDo: 'cleaning' }
];
function makeWorkers(people) {
return people.map((person) => {
const { name, whatCanDo } = person;
return <li key={name}>My name is {name}, I can do {whatCanDo}</li>
});
}
<ul>makeWorkers(people)</ul>
比如在React中,map 是非常常用的制作元素的方法,因为 map 在对原数组的数据进行操作后,会创建并返回一个新的数组。
const mySubjectId = ['154', '773', '245'];
function countSubjects(subjects) {
let cnt = 0;
subjects.forEach(subject => {
if (mySubjectId.includes(subject.id)) {
cnt += 1;
}
});
return cnt;
}
countSubjects([
{ id: '223', teacher: 'Mark' },
{ id: '154', teacher: 'Linda' }
]);
// 1
另一方面,当你想对数据进行某些操作而不创建新数组时,forEach 很有用。顺便说一句,可以使用 filter 重构示例。
subjects.filter(subject => mySubjectId.includes(subject.id)).length;
综上所述,我建议你在创建一个新的数组时使用map,当你不需要制作一个新的数组,而是要对数据做一些事情时,就使用forEach。
速度比较
有些帖子提到 map 比 forEach 快。所以,我很好奇这是不是真的。我找到了这个对比结果。
该代码看起来非常相似,但结果却相反。有些测试说 forEach 更快,有些说 map 更快。也许你在告诉自己 map/forEach 比其他的快,你可能是对的。老实说,我不确定。我认为在现代Web开发中,可读性比 map 和 forEach 之间的速度重要得多。
但可以肯定的是——两者都比JavaScript内置的 for 循环慢。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
序言
之前发表了一篇文章 redux、mobx、concent特性大比拼, 看后生如何对局前辈,吸引了不少感兴趣的小伙伴入群开始了解和使用 concent,并获得了很多正向的反馈,实实在在的帮助他们提高了开发体验,群里人数虽然还很少,但大家热情高涨,技术讨论氛围浓厚,对很多新鲜技术都有保持一定的敏感度,如上个月开始逐渐被提及得越来越多的出自facebook的状态管理方案 recoil,虽然还处于实验状态,但是相必大家已经私底下开始欲欲跃试了,毕竟出生名门,有fb背书,一定会大放异彩。
不过当我体验完recoil后,我对其中标榜的更新保持了怀疑态度,有一些误导的嫌疑,这一点下文会单独分析,是否属于误导读者在读完本文后自然可以得出结论,总之本文主要是分析Concent与Recoil的代码风格差异性,并探讨它们对我们将来的开发模式有何新的影响,以及思维上需要做什么样的转变。
数据流方案之3大流派
目前主流的数据流方案按形态都可以划分以下这三类
redux流派
redux、和基于redux衍生的其他作品,以及类似redux思路的作品,代表作有dva、rematch等等。
mobx流派
借助definePerperty和Proxy完成数据劫持,从而达到响应式编程目的的代表,类mobx的作品也有不少,如dob等。
Context流派
这里的Context指的是react自带的Context api,基于Context api打造的数据流方案通常主打轻量、易用、概览少,代表作品有unstated、constate等,大多数作品的核心代码可能不超过500行。
到此我们看看Recoil应该属于哪一类?很显然按其特征属于Context流派,那么我们上面说的主打轻量对
Recoil并不适用了,打开其源码库发现代码并不是几百行完事的,所以基于Context api做得好用且强大就未必轻量,由此看出facebook对Recoil是有野心并给予厚望的。
我们同时也看看Concent属于哪一类呢?Concent在v2版本之后,重构数据追踪机制,启用了defineProperty和Proxy特性,得以让react应用既保留了不可变的追求,又享受到了运行时依赖收集和ui更新的性能提升福利,既然启用了defineProperty和Proxy,那么看起来Concent应该属于mobx流派?
事实上Concent属于一种全新的流派,不依赖react的Context api,不破坏react组件本身的形态,保持追求不可变的哲学,仅在react自身的渲染调度机制之上建立一层逻辑层状态分发调度机制,defineProperty和Proxy只是用于辅助收集实例和衍生数据对模块数据的依赖,而修改数据入口还是setState(或基于setState封装的dispatch, invoke, sync),让Concent可以0入侵的接入react应用,真正的即插即用和无感知接入。
即插即用的核心原理是,Concent自建了一个平行于react运行时的全局上下文,精心维护这模块与实例之间的归属关系,同时接管了组件实例的更新入口setState,保留原始的setState为reactSetState,所有当用户调用setState时,concent除了调用reactSetState更新当前实例ui,同时智能判断提交的状态是否也还有别的实例关心其变化,然后一并拿出来依次执行这些实例的reactSetState,进而达到了状态全部同步的目的。
Recoil初体验
我们以常用的counter来举例,熟悉一下Recoil暴露的四个高频使用的api
atom,定义状态
selector, 定义派生数据
useRecoilState,消费状态
useRecoilValue,消费派生数据
定义状态
外部使用atom接口,定义一个key为num,初始值为0的状态
const numState = atom({
key: "num",
default: 0
});
定义派生数据
外部使用selector接口,定义一个key为numx10,初始值是依赖numState再次计算而得到
const numx10Val = selector({
key: "numx10",
get: ({ get }) => {
const num = get(numState);
return num * 10;
}
});
定义异步的派生数据
selector的get支持定义异步函数
需要注意的点是,如果有依赖,必需先书写好依赖在开始执行异步逻辑
const delay = () => new Promise(r => setTimeout(r, 1000));
const asyncNumx10Val = selector({
key: "asyncNumx10",
get: async ({ get }) => {
// !!!这句话不能放在delay之下, selector需要同步的确定依赖
const num = get(numState);
await delay();
return num * 10;
}
});
消费状态
组件里使用useRecoilState接口,传入想要获去的状态(由atom创建而得)
const NumView = () => {
const [num, setNum] = useRecoilState(numState);
const add = ()=>setNum(num+1);
return (
<div>
{num}<br/>
<button onClick={add}>add</button>
</div>
);
}
消费派生数据
组件里使用useRecoilValue接口,传入想要获去的派生数据(由selector创建而得),同步派生数据和异步派生数据,皆可通过此接口获得
const NumValView = () => {
const numx10 = useRecoilValue(numx10Val);
const asyncNumx10 = useRecoilValue(asyncNumx10Val);
return (
<div>
numx10 :{numx10}<br/>
</div>
);
};
渲染它们查看结果
暴露定义好的这两个组件, 查看在线示例
export default ()=>{
return (
<>
<NumView />
<NumValView />
</>
);
};
顶层节点包裹React.Suspense和RecoilRoot,前者用于配合异步计算函数需要,后者用于注入Recoil上下文
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<React.Suspense fallback={<div>Loading...</div>}>
<RecoilRoot>
<Demo />
</RecoilRoot>
</React.Suspense>
</React.StrictMode>,
rootElement
);
Concent初体验
如果读过concent文档(还在持续建设中...),可能部分人会认为api太多,难于记住,其实大部分都是可选的语法糖,我们以counter为例,只需要使用到以下两个api即可
run,定义模块状态(必需)、模块计算(可选)、模块观察(可选)
运行run接口后,会生成一份concent全局上下文
setState,修改状态
定义状态&修改状态
以下示例我们先脱离ui,直接完成定义状态&修改状态的目的
import { run, setState, getState } from "concent";
run({
counter: {// 声明一个counter模块
state: { num: 1 }, // 定义状态
}
});
console.log(getState('counter').num);// log: 1
setState('counter', {num:10});// 修改counter模块的num值为10
console.log(getState('counter').num);// log: 10
我们可以看到,此处和redux很类似,需要定义一个单一的状态树,同时第一层key就引导用户将数据模块化管理起来.
引入reducer
上述示例中我们直接掉一个呢setState修改数据,但是真实的情况是数据落地前有很多同步的或者异步的业务逻辑操作,所以我们对模块填在reducer定义,用来声明修改数据的方法集合。
import { run, dispatch, getState } from "concent";
const delay = () => new Promise(r => setTimeout(r, 1000));
const state = () => ({ num: 1 });// 状态声明
const reducer = {// reducer声明
inc(payload, moduleState) {
return { num: moduleState.num + 1 };
},
async asyncInc(payload, moduleState) {
await delay();
return { num: moduleState.num + 1 };
}
};
run({
counter: { state, reducer }
});
然后我们用dispatch来触发修改状态的方法
因dispatch会返回一个Promise,所以我们需要用一个async 包裹起来执行代码
import { dispatch } from "concent";
(async ()=>{
console.log(getState("counter").num);// log 1
await dispatch("counter/inc");// 同步修改
console.log(getState("counter").num);// log 2
await dispatch("counter/asyncInc");// 异步修改
console.log(getState("counter").num);// log 3
})()
注意dispatch调用时基于字符串匹配方式,之所以保留这样的调用方式是为了照顾需要动态调用的场景,其实更推荐的写法是
import { dispatch } from "concent";
(async ()=>{
console.log(getState("counter").num);// log 1
await dispatch(reducer.inc);// 同步修改
console.log(getState("counter").num);// log 2
await dispatch(reducer.asyncInc);// 异步修改
console.log(getState("counter").num);// log 3
})()
接入react
上述示例主要演示了如何定义状态和修改状态,那么接下来我们需要用到以下两个api来帮助react组件生成实例上下文(等同于与vue 3 setup里提到的渲染上下文),以及获得消费concent模块数据的能力
register, 注册类组件为concent组件
useConcent, 注册函数组件为concent组件
import { register, useConcent } from "concent";
@register("counter")
class ClsComp extends React.Component {
changeNum = () => this.setState({ num: 10 })
render() {
return (
<div>
<h1>class comp: {this.state.num}</h1>
<button onClick={this.changeNum}>changeNum</button>
</div>
);
}
}
function FnComp() {
const { state, setState } = useConcent("counter");
const changeNum = () => setState({ num: 20 });
return (
<div>
<h1>fn comp: {state.num}</h1>
<button onClick={changeNum}>changeNum</button>
</div>
);
}
注意到两种写法区别很小,除了组件的定义方式不一样,其实渲染逻辑和数据来源都一模一样。
渲染它们查看结果
在线示例
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<div>
<ClsComp />
<FnComp />
</div>
</React.StrictMode>,
rootElement
);
对比Recoil,我们发现没有顶层并没有Provider或者Root类似的组件包裹,react组件就已接入concent,做到真正的即插即用和无感知接入,同时api保留为与react一致的写法。
组件调用reducer
concent为每一个组件实例都生成了实例上下文,方便用户直接通过ctx.mr调用reducer方法
mr 为 moduleReducer的简写,直接书写为ctx.moduleReducer也是合法的
// --------- 对于类组件 -----------
changeNum = () => this.setState({ num: 10 })
// ===> 修改为
changeNum = () => this.ctx.mr.inc(10);// or this.ctx.mr.asynCtx()
// --------- 对于函数组件 -----------
const { state, mr } = useConcent("counter");// useConcent 返回的就是ctx
const changeNum = () => mr.inc(20);// or ctx.mr.asynCtx()
异步计算函数
run接口里支持扩展computed属性,即让用户定义一堆衍生数据的计算函数集合,它们可以是同步的也可以是异步的,同时支持一个函数用另一个函数的输出作为输入来做二次计算,计算的输入依赖是自动收集到的。
const computed = {// 定义计算函数集合
numx10({ num }) {
return num * 10;
},
// n:newState, o:oldState, f:fnCtx
// 结构出num,表示当前计算依赖是num,仅当num发生变化时触发此函数重计算
async numx10_2({ num }, o, f) {
// 必需调用setInitialVal给numx10_2一个初始值,
// 该函数仅在初次computed触发时执行一次
f.setInitialVal(num * 55);
await delay();
return num * 100;
},
async numx10_3({ num }, o, f) {
f.setInitialVal(num * 1);
await delay();
// 使用numx10_2再次计算
const ret = num * f.cuVal.numx10_2;
if (ret % 40000 === 0) throw new Error("-->mock error");
return ret;
}
}
// 配置到counter模块
run({
counter: { state, reducer, computed }
});
上述计算函数里,我们刻意让numx10_3在某个时候报错,对于此错误,我们可以在run接口的第二位options配置里定义errorHandler来捕捉。
run({/**storeConfig*/}, {
errorHandler: (err)=>{
alert(err.message);
}
})
当然更好的做法,利用concent-plugin-async-computed-status插件来完成对所有模块计算函数执行状态的统一管理。
import cuStatusPlugin from "concent-plugin-async-computed-status";
run(
{/**storeConfig*/},
{
errorHandler: err => {
console.error('errorHandler ', err);
// alert(err.message);
},
plugins: [cuStatusPlugin], // 配置异步计算函数执行状态管理插件
}
);
该插件会自动向concent配置一个cuStatus模块,方便组件连接到它,消费相关计算函数的执行状态数据
function Test() {
const { moduleComputed, connectedState, setState, state, ccUniqueKey } = useConcent({
module: "counter",// 属于counter模块,状态直接从state获得
connect: ["cuStatus"],// 连接到cuStatus模块,状态从connectedState.{$moduleName}获得
});
const changeNum = () => setState({ num: state.num + 1 });
// 获得counter模块的计算函数执行状态
const counterCuStatus = connectedState.cuStatus.counter;
// 当然,可以更细粒度的获得指定结算函数的执行状态
// const {['counter/numx10_2']:num1Status, ['counter/numx10_3']: num2Status} = connectedState.cuStatus;
return (
<div>
{state.num}
<br />
{counterCuStatus.done ? moduleComputed.numx10 : 'computing'}
{/** 此处拿到错误可以用于渲染,当然也抛出去 */}
{/** 让ErrorBoundary之类的组件捕捉并渲染降级页面 */}
{counterCuStatus.err ? counterCuStatus.err.message : ''}
<br />
{moduleComputed.numx10_2}
<br />
{moduleComputed.numx10_3}
<br />
<button onClick={changeNum}>changeNum</button>
</div>
);
}
![]https://raw.githubusercontent...
查看在线示例
更新
开篇我说对Recoli提到的更新保持了怀疑态度,有一些误导的嫌疑,此处我们将揭开疑团
大家知道hook使用规则是不能写在条件控制语句里的,这意味着下面语句是不允许的
const NumView = () => {
const [show, setShow] = useState(true);
if(show){// error
const [num, setNum] = useRecoilState(numState);
}
}
所以用户如果ui渲染里如果某个状态用不到此数据时,某处改变了num值依然会触发NumView重渲染,但是concent的实例上下文里取出来的state和moduleComputed是一个Proxy对象,是在实时的收集每一轮渲染所需要的依赖,这才是真正意义上的按需渲染和更新。
const NumView = () => {
const [show, setShow] = useState(true);
const {state} = useConcent('counter');
// show为true时,当前实例的渲染对state.num的渲染有依赖
return {show ? <h1>{state.num}</h1> : 'nothing'}
}
点我查看代码示例
当然如果用户对num值有ui渲染完毕后,有发生改变时需要做其他事的需求,类似useEffect的效果,concent也支持用户将其抽到setup里,定义effect来完成此场景诉求,相比useEffect,setup里的ctx.effect只需定义一次,同时只需传递key名称,concent会自动对比前一刻和当前刻的值来决定是否要触发副作用函数。
conset setup = (ctx)=>{
ctx.effect(()=>{
console.log('do something when num changed');
return ()=>console.log('clear up');
}, ['num'])
}
function Test1(){
useConcent({module:'cunter', setup});
return <h1>for setup<h1/>
}
更多关于effect与useEffect请查看此文
current mode
关于concent是否支持current mode这个疑问呢,这里先说答案,concent是100%完全支持的,或者进一步说,所有状态管理工具,最终触发的都是setState或forceUpdate,我们只要在渲染过程中不要写具有任何副作用的代码,让相同的状态输入得到的渲染结果幂,即是在current mode下运行安全的代码。
current mode只是对我们的代码提出了更苛刻的要求。
// bad
function Test(){
track.upload('renderTrigger');// 上报渲染触发事件
return <h1>bad case</h1>
}
// good
function Test(){
useEffect(()=>{
// 就算仅执行了一次setState, current mode下该组件可能会重复渲染,
// 但react内部会保证该副作用只触发一次
track.upload('renderTrigger');
})
return <h1>bad case</h1>
}
我们首先要理解current mode原理是因为fiber架构模拟出了和整个渲染堆栈(即fiber node上存储的信息),得以有机会让react自己以组件为单位调度组件的渲染过程,可以悬停并再次进入渲染,安排优先级高的先渲染,重度渲染的组件会切片为多个时间段反复渲染,而concent的上下文本身是独立于react存在的(接入concent不需要再顶层包裹任何Provider), 只负责处理业务生成新的数据,然后按需派发给对应的实例(实例的状态本身是一个个孤岛,concent只负责同步建立起了依赖的store的数据),之后就是react自己的调度流程,修改状态的函数并不会因为组件反复重入而多次执行(这点需要我们遵循不该在渲染过程中书写包含有副作用的代码原则),react仅仅是调度组件的渲染时机,而组件的中断和重入针对也是这个渲染过程。
所以同样的,对于concent
const setup = (ctx)=>{
ctx.effect(()=>{
// effect是对useEffect的封装,
// 同样在current mode下该副作用也只触发一次(由react保证)
track.upload('renderTrigger');
});
}
// good
function Test2(){
useConcent({setup})
return <h1>good case</h1>
}
同样的,依赖收集在current mode模式下,重复渲染仅仅是导致触发了多次收集,只要状态输入一样,渲染结果幂等,收集到的依赖结果也是幂等的。
// 假设这是一个渲染很耗时的组件,在current mode模式下可能会被中断渲染
function HeavyComp(){
const { state } = useConcent({module:'counter'});// 属于counter模块
// 这里读取了num 和 numBig两个值,收集到了依赖
// 即当仅当counter模块的num、numBig的发生变化时,才触发其重渲染(最终还是调用setState)
// 而counter模块的其他值发生变化时,不会触发该实例的setState
return (
<div>num: {state.num} numBig: {state.numBig}</div>
);
}
最后我们可以梳理一下,hook本身是支持把逻辑剥离到用的自定义hook(无ui返回的函数),而其他状态管理也只是多做了一层工作,引导用户把逻辑剥离到它们的规则之下,最终还是把业务处理数据交回给react组件调用其setState或forceUpdate触发重渲染,current mode的引入并不会对现有的状态管理或者新生的状态管理方案有任何影响,仅仅是对用户的ui代码提出了更高的要求,以免因为current mode引发难以排除的bug
为此react还特别提供了React.Strict组件来故意触发双调用机制, https://reactjs.org/docs/stri... 以引导用户书写更符合规范的react代码,以便适配将来提供的current mode。
react所有新特性其实都是被fiber激活了,有了fiber架构,衍生出了hook、time slicing、suspense以及将来的Concurrent Mode,class组件和function组件都可以在Concurrent Mode下安全工作,只要遵循规范即可。
摘取自: https://reactjs.org/docs/stri...
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
所以呢,React.Strict其实为了引导用户写能够在Concurrent Mode里运行的代码而提供的辅助api,先让用户慢慢习惯这些限制,循序渐进一步一步来,最后再推出Concurrent Mode。
结语
Recoil推崇状态和派生数据更细粒度控制,写法上demo看起来简单,实际上代码规模大之后依然很繁琐。
// 定义状态
const numState = atom({key:'num', default:0});
const numBigState = atom({key:'numBig', default:100});
// 定义衍生数据
const numx2Val = selector({
key: "numx2",
get: ({ get }) => get(numState) * 2,
});
const numBigx2Val = selector({
key: "numBigx2",
get: ({ get }) => get(numBigState) * 2,
});
const numSumBigVal = selector({
key: "numSumBig",
get: ({ get }) => get(numState) + get(numBigState),
});
// ---> ui处消费状态或衍生数据
const [num] = useRecoilState(numState);
const [numBig] = useRecoilState(numBigState);
const numx2 = useRecoilValue(numx2Val);
const numBigx2 = useRecoilValue(numBigx2Val);
const numSumBig = useRecoilValue(numSumBigVal);
Concent遵循redux单一状态树的本质,推崇模块化管理数据以及派生数据,同时依靠Proxy能力完成了运行时依赖收集和追求不可变的完美整合。
run({
counter: {// 声明一个counter模块
state: { num: 1, numBig: 100 }, // 定义状态
computed:{// 定义计算,参数列表里解构具体的状态时确定了依赖
numx2: ({num})=> num * 2,
numBigx2: ({numBig})=> numBig * 2,
numSumBig: ({num, numBig})=> num + numBig,
}
},
});
// ---> ui处消费状态或衍生数据,在ui处结构了才产生依赖
const { state, moduleComputed, setState } = useConcent('counter')
const { numx2, numBigx2, numSumBig} = moduleComputed;
const { num, numBig } = state;
所以你将获得:
运行时的依赖收集 ,同时也遵循react不可变的原则
一切皆函数(state, reducer, computed, watch, event...),能获得更友好的ts支持
支持中间件和插件机制,很容易兼容redux生态
同时支持集中与分形模块配置,同步与异步模块加载,对大型工程的弹性重构过程更加友好
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
蓝蓝设计的小编 http://www.lanlanwork.com