阅读之前
基本上,在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界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
近年来暗黑模式的设计趋势开始一点点明显,Ant Design 在这次 4.0 的升级中也对这类暗黑场景化的设计开始进行初步的探索,接下来就让我们一起来看下 Ant Design 这一针对企业级的设计体系是如何设计暗黑模式的。
暗黑模式是指把所有 UI 换成黑色或者深色的一个模式。
需要说明的是,暗黑模式不只夜间模式:
暗黑模式更多的目的是希望使用者更加专注自己的操作任务,所以对于信息内容的表达会更注重视觉性;
而夜间模式则更多是出于在夜间或暗光环境使用下的健康角度考虑,避免在黑暗环境中长时间注视高亮光源带来的视觉刺激,所以在保证可读性的前提下,会采用更弱的对比来减少屏幕光对眼睛的刺激。
同时,从使用场景上来说,暗黑模式既可以在黑暗环境,也可以在亮光环境下使用,可以理解为是对浅色主题的一种场景化补充,而夜间模式只建议在黑暗环境下使用,在亮光环境使用时很可能并不保证信息可读性。
1. 更加专注内容
试想一下,我们在电影院看电影时,为什么要全场关灯?甚至有些 APP,在影片的下方也会又一个模拟关灯效果的操作,来让整个手机屏幕变黑,只剩下视屏画面的部分,这都帮助我们可以更专注、更沉浸在当前的内容下。
色彩具有层级关系,深色会在视觉感官上自动后退,浅色部分则会向前延展,这样对比强烈的层次关系可以让用户更注重被凸显出来的内容和交互操作;尤其在信息负责界面内层级关系的合理拉开对操作效率都有明显的促进作用。
2. 在暗光环境下更加适用
如今社会我们身处黑夜使用手机、电脑、iPad等设备的次数越来越多,环境光与屏幕亮度的明暗差距在夜间会被放大 ,亮度对比带来视觉刺激也更加明显,使用暗色模式可以缩小屏幕显示内容与环境光强度的差距,同时也可以为设备的续航带来积极影响,可以保证使用者在暗光环境下使用 OLED 设备的舒适度。
3. 大众喜爱
黑色一直以来就可以给人以高级、神秘的语义象征,相比于浅色模式,暗色模式藏着更多可能性。
在这次暗黑模式的设计中主要遵循以下两大设计原则:
1. 内容的舒适性
不论是颜色还是文字或是组件本身,在暗色环境下的使用感受应当是舒适的,而不是十分费力的,如果一个颜色在浅色下使用正常,在暗色下却亮的刺眼或根本看不见,那必然不够舒适也不可读;所以在颜色的处理上不建议直接使用,这样会让界面变得到处都是「亮点」,让眼睛不适的同时,也会带来许多误操作。
2. 信息的一致性
暗黑模式下的信息内容需要和浅色模式保持一致性,不应该打破原有的层级关系。举个例子,在浅色模式下越深的颜色,与界面背景色对比度越大,也就越容易被人注意,视觉层级越高,比如 tooltip;在暗黑模式下我们同样需要遵循这一规律,所以对应所使用的颜色也就越浅,反之则会越深。
在大量的企业级产品界面中,我们通常用只用一个白色背景结合分割线就可以搞定所有界面的板块层级,因为在浅色模式下有投影可以借助,然而暗黑模式中投影将不足以起到如此功效,我们需要通过颜色来区分层级关系。
在经过对蚂蚁企业级页面的典型布局结构评估后,我们在中性色中增加了三个梯度,将中性色扩展至 13 个
并定义出通用情况下页面中的框架层次,主要分为三大块:
在目前的暗黑体系下,我们分别为这三大块从浅到深定义了#1F1F1F、#141414、#000000 三个颜色,在实际应用中,你也可以根据自身业务的需求,从中性色板中直接选用或是依据透明度的思路自定义出合适的中性色彩。当定义出较为明确的框架层次和颜色后,也对后续系统中组件的颜色配置有着重要的指导意义。我们需要考虑组件出现在不同颜色背景下的可能性及其表现,尽量保持一致性。
众所周知,暗黑模式与浅色模式最大的不同就在色彩的处理上,在暗黑模式中,我们并不想打破浅色模式下基础色板的配置规律及色值,当一个应用或站点深浅模式并存时,希望在色彩上有一定延续和关联,而不是毫不相关的两套色板,这样一是避免开发及后续的维护成本,二是实际切换和使用时,可以保证一致性,这意味着需要借助一定规则。
这里分享一下我们的处理思路:
基于 Ant Design 自然的设计价值观,我们先从自然界中寻找灵感,如果说浅色模式如同初升时的朝阳,那暗黑模式就是落日下的晚霞,各有各的韵味,同一片天,唯一不同的是,受光线亮度的影响,晚霞整体会暗一些。
所以我们大体的设计思路也是基于浅色的基础色板,通过结合透明度的转换,去得到一套暗黑模式下的色彩。这样的好处是,深浅模式下的色彩根基是同一个,在这样的基础上经过透明度的变换得到的结果也会相对和谐,同时也符合我们一致性的原则。
这里我们借助下面这两个概念对透明度进行转换:
对比度极性
对比度极性分为正极性和负极性。
这里可以给大家分享对比度查阅的一个工具:WebAIM,只要输入色值就可以看到具体的值,十分方便。
正负极性差值
顾名思义便是正负两者的差值,这里取的是绝对值。
根据一致性原则,我们尝试通过对比一套颜色的正负极性变化趋势来找到转换规律。
首先可以看下,如果一个颜色在不做任何修改的前提下直接使用,它的正负极性趋势以及差值趋势的走势和关系是怎么样的,我们尝试描绘出这样的曲线,他们的变化规律也将作为我们规则转换的参考标准。
经过对比,可以发现一些变化规律:
首先来说下「差值趋势」,横向对比可以发现,不同颜色的正负极性走势是很不一样的,可以看到在黄绿色段差值曲线达到一个变化峰值,这是由于黄绿色本身由于明度、饱和度值相比其他颜色偏高,所以总是有种刺眼的感觉,生活中也会用它来作为警示、提醒的作用,所以在深浅背景下的对比度有一个比较大的差异,可以说这个变化是正常的。
这点也可以从「正负对比度极性趋势」两者间的相对关系反应出来,从红色到洋红,绿线一开始从逐渐在蓝线的上方一点,开始逐渐上移,最后在峰值处开始慢慢下移,在「极客蓝」这个色相中接近重叠,在洋红处又慢慢下移,说明浅色下越深的颜色,在深色中越亮,反之则越暗。
纵向比对单个色板,可以发现,其斜率变化主要受颜色的明度、饱和度影响,可以反应一个颜色的不同梯度在黑白背景下的变化规律。
有了这个大的变化规律,我们便可做到心中有数。首先以 Ant Design 的品牌色「破晓蓝」为例,经过在多个业务、场景中不断尝试与调整,我们得到一个透明度转换规则:
并将这个规则应用在其他 11 套色板中,验证其可用性。这个过程确实没有什么快捷通道,唯一的办法就是不断尝试。
最后,我们将经过规则转换的实色与常规颜色的变化趋势做对比:
可以看到在大趋势走向上左右两侧图基本一致,这代表两个色板在变化规律接近一致,基本可以证明规则的合理性。区别在于,对比度负极性和差值相对于右侧未经处理的值明显有所下降。这是由于透明度的处理让暗色下的颜色在明度、饱和度上有所下降导致。
再仔细观察可以发现,在左侧的常规颜色中,从破晓蓝-洋红这段偏冷色系几个颜色中,差值趋势变化最平缓的是「极客蓝」这颜色,这说明该颜色在深浅背景下的表现较为稳定,起伏不大,当基于一个统一的透明度规则前提下,会让它在暗色下的对比度减弱,所以反而会导致差值趋势变大,所以我们会发现差值趋势变化较小的颜色转移到了「破晓蓝」、「洋红」中,也是一个正常现象。
最后可以看到颜色在调整过后实际应用的效果
在官网中点击「切换」即可预览:
如果上述 12 个色板不满足你的业务需求,你也可以在官网上自己选择颜色,我们会根据规则帮你生成一个暗色色板。
另外,如果在实际应用过程中,你选了色相在 225~325 间的深冷色系作为主色或强调色使用,建议适当提高透明度的值,避免在暗色界面上引起阅读障碍。
暗黑模式中,文字的使用与浅色模式基本一致,依旧从 85%-65%-45%,唯一不同的地方在 disable 的状态,其透明度值提升为 30%,避免颜色过淡真的「不可见」。另外,对于 #FFFFFF 的纯白色文字,尽量避免大面积使用,尤其对于表格、列表这类偏阅读浏览的场景,如有需要,做小范围强调即可。
暗黑模式中的阴影规则与浅色模式基本保持一致,但由于本身的界面背景较深,在阴影色值上也有所加深,帮助层次更好的体现,整体将色值扩大到原先的 4 倍,但在阴影的位移、扩展上均保持不变。
分割线在暗黑模式中建议根据界面中常用的背景色,通过透明度换算成实色使用,Ant Design 中分割线主要有 #434343 和 #303030 两种颜色,分别对应浅色模式下的 #D9D9D 和 #F0F0F0,这样做的目的主要是为了避免带来额外的交错叠加,尤其对于表格类以及很多带有 border 属性的组件,实色会更为稳妥便于记忆。
适应性方面,Ant Design 的暗黑模式可以与海兔及可视化资产进行无缝衔接,使用时可以任意组合拖拽,你可以下载 Kitchen 插件,获取海量资产。
暗黑模式最近越来越受到人们的关注,与某一特定产品的暗黑设计不同,Ant Design 的暗黑模式更多以设计体系的角度考虑企业级这个大场景下的内容,在易用性、扩展度、复用度等方面还有许多需要完善和继续研究探索的地方,我们会在未来的迭代中逐步进行,先完成再完善。希望上述内容能对大家在暗黑模式的设计上有所帮助。
文章来源:优设 作者:AlibabaDesign
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
JavaScript不具有 sleep() 函数,该函数会导致代码在恢复执行之前等待指定的时间段。如果需要JavaScript等待,该怎么做呢?
假设您想将三则消息记录到Javascript控制台,每条消息之间要延迟一秒钟。JavaScript中没有 sleep() 方法,所以你可以尝试使用下一个最好的方法 setTimeout()。
不幸的是,setTimeout() 不能像你期望的那样正常工作,这取决于你如何使用它。你可能已经在JavaScript循环中的某个点上试过了,看到 setTimeout() 似乎根本不起作用。
问题的产生是由于将 setTimeout() 误解为 sleep() 函数,而实际上它是按照自己的一套规则工作的。
在本文中,我将解释如何使用 setTimeout(),包括如何使用它来制作一个睡眠函数,使JavaScript暂停执行并在连续的代码行之间等待。
浏览一下 setTimeout() 的文档,它似乎需要一个 "延迟 "参数,以毫秒为单位。
回到原始问题,您尝试调用 setTimeout(1000) 在两次调用 console.log() 函数之间等待1秒。
不幸的是 setTimeout() 不能这样工作:
setTimeout(1000)
console.log(1)
setTimeout(1000)
console.log(2)
setTimeout(1000)
console.log(3)
for (let i = 0; i <= 3; i++) {
setTimeout(1000)
console.log(`#${i}`)
}
这段代码的结果完全没有延迟,就像 setTimeout() 不存在一样。
回顾文档,你会发现问题在于实际上第一个参数应该是函数调用,而不是延迟。毕竟,setTimeout() 实际上不是 sleep() 方法。
你重写代码以将回调函数作为第一个参数并将必需的延迟作为第二个参数:
setTimeout(() => console.log(1), 1000)
setTimeout(() => console.log(2), 1000)
setTimeout(() => console.log(3), 1000)
for (let i = 0; i <= 3; i++) {
setTimeout(() => console.log(`#${i}`), 1000)
}
这样一来,三个console.log的日志信息在经过1000ms(1秒)的单次延时后,会一起显示,而不是每次重复调用之间延时1秒的理想效果。
在讨论如何解决此问题之前,让我们更详细地研究一下 setTimeout() 函数。
检查setTimeout ()
你可能已经注意到上面第二个代码片段中使用了箭头函数。这些是必需的,因为你需要将匿名回调函数传递给 setTimeout(),该函数将在超时后运行要执行的代码。
在匿名函数中,你可以指定在超时时间后执行的任意代码:
// 使用箭头语法的匿名回调函数。
setTimeout(() => console.log("你好!"), 1000)
// 这等同于使用function关键字
setTimeout(function() { console.log("你好!") }, 1000)
理论上,你可以只传递函数作为第一个参数,回调函数的参数作为剩余的参数,但对我来说,这似乎从来没有正确的工作:
// 应该能用,但不能用
setTimeout(console.log, 1000, "你好")
人们使用字符串解决此问题,但是不建议这样做。从字符串执行JavaScript具有安全隐患,因为任何不当行为者都可以运行作为字符串注入的任意代码。
// 应该没用,但确实有用
setTimeout(`console.log("你好")`, 1000)
那么,为什么在我们的第一组代码示例中 setTimeout() 失败?好像我们在正确使用它,每次都重复了1000ms的延迟。
原因是 setTimeout() 作为同步代码执行,并且对 setTimeout() 的多次调用均同时运行。每次调用 setTimeout() 都会创建异步代码,该代码将在给定延迟后稍后执行。由于代码段中的每个延迟都是相同的(1000毫秒),因此所有排队的代码将在1秒钟的单个延迟后同时运行。
如前所述,setTimeout() 实际上不是 sleep() 函数,取而代之的是,它只是将异步代码排入队列以供以后执行。幸运的是,可以使用 setTimeout() 在JavaScript中创建自己的 sleep() 函数。
如何编写sleep函数
通过Promises,async 和 await 的功能,您可以编写一个 sleep() 函数,该函数将按预期运行。
但是,你只能从 async 函数中调用此自定义 sleep() 函数,并且需要将其与 await 关键字一起使用。
这段代码演示了如何编写一个 sleep() 函数:
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))
const repeatedGreetings = async () => {
await sleep(1000)
console.log(1)
await sleep(1000)
console.log(2)
await sleep(1000)
console.log(3)
}
repeatedGreetings()
此JavaScript sleep() 函数的功能与您预期的完全一样,因为 await 导致代码的同步执行暂停,直到Promise被解决为止。
一个简单的选择
另外,你可以在第一次调用 setTimeout() 时指定增加的超时时间。
以下代码等效于上一个示例:
setTimeout(() => console.log(1), 1000)
setTimeout(() => console.log(2), 2000)
setTimeout(() => console.log(3), 3000)
使用增加超时是可行的,因为代码是同时执行的,所以指定的回调函数将在同步代码执行的1、2和3秒后执行。
它会循环运行吗?
如你所料,以上两种暂停JavaScript执行的选项都可以在循环中正常工作。让我们看两个简单的例子。
这是使用自定义 sleep() 函数的代码段:
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))
async function repeatGreetingsLoop() {
for (let i = 0; i <= 5; i++) {
await sleep(1000)
console.log(`Hello #${i}`)
}
}
repeatGreetingsLoop()
这是一个简单的使用增加超时的代码片段:
for (let i = 0; i <= 5; i++) {
setTimeout(() => console.log(`Hello #${i}`), 1000 * i)
}
我更喜欢后一种语法,特别是在循环中使用。
总结
JavaScript可能没有 sleep() 或 wait() 函数,但是使用内置的 setTimeout() 函数很容易创建一个JavaScript,只要你谨慎使用它即可。
就其本身而言,setTimeout() 不能用作 sleep() 函数,但是你可以使用 async 和 await 创建自定义JavaScript sleep() 函数。
采用不同的方法,可以将交错的(增加的)超时传递给 setTimeout() 来模拟 sleep() 函数。之所以可行,是因为所有对setTimeout() 的调用都是同步执行的,就像JavaScript通常一样。
希望这可以帮助你在代码中引入一些延迟——仅使用原始JavaScript,而无需外部库或框架。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
1. 随机排列
在开发者,有时候我们需要对数组的顺序进行重新的洗牌。 在 JS 中并没有提供数组随机排序的方法,这里提供一个随机排序的方法:
function shuffle(arr) {
var i, j, temp;
for (i = arr.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}
2. 唯一值
在开发者,我们经常需要过滤重复的值,这里提供几种方式来过滤数组的重复值。
使用 Set 对象
使用 Set() 函数,此函数可与单个值数组一起使用。对于数组中嵌套的对象值而言,不是一个好的选择。
const numArray = [1,2,3,4,2,3,4,5,1,1,2,3,3,4,5,6,7,8,2,4,6];
// 使用 Array.from 方法
Array.from(new Set(numArray));
// 使用展开方式
[...new Set(numArray)]
使用 Array.filter
使用 filter 方法,我们可以对元素是对象的进行过滤。
const data = [
{id: 1, name: 'Lemon'},
{id: 2, name: 'Mint'},
{id: 3, name: 'Mango'},
{id: 4, name: 'Apple'},
{id: 5, name: 'Lemon'},
{id: 6, name: 'Mint'},
{id: 7, name: 'Mango'},
{id: 8, name: 'Apple'},
]
function findUnique(data) {
return data.filter((value, index, array) => {
if (array.findIndex(item => item.name === value.name) === index) {
return value;
}
})
}
3. 使用 loadsh 的 lodash 方法
import {uniqBy} from 'lodash'
const data = [
{id: 1, name: 'Lemon'},
{id: 2, name: 'Mint'},
{id: 3, name: 'Mango'},
{id: 4, name: 'Apple'},
{id: 5, name: 'Lemon'},
{id: 6, name: 'Mint'},
{id: 7, name: 'Mango'},
{id: 8, name: 'Apple'},
]
function findUnique(data) {
return uniqBy(data, e => {
return e.name
})
}
3. 按属性对 对象数组 进行排序
我们知道 JS 数组中的 sort 方法是按字典顺序进行排序的,所以对于字符串类, 该方法是可以很好的正常工作,但对于数据元素是对象类型,就不太好使了,这里我们需要自定义一个排序方法。
在比较函数中,我们将根据以下条件返回值:
小于0:A 在 B 之前
大于0 :B 在 A 之前
等于0 :A 和 B 彼此保持不变
const data = [
{id: 1, name: 'Lemon', type: 'fruit'},
{id: 2, name: 'Mint', type: 'vegetable'},
{id: 3, name: 'Mango', type: 'grain'},
{id: 4, name: 'Apple', type: 'fruit'},
{id: 5, name: 'Lemon', type: 'vegetable'},
{id: 6, name: 'Mint', type: 'fruit'},
{id: 7, name: 'Mango', type: 'fruit'},
{id: 8, name: 'Apple', type: 'grain'},
]
function compare(a, b) {
// Use toLowerCase() to ignore character casing
const typeA = a.type.toLowerCase();
const typeB = b.type.toLowerCase();
let comparison = 0;
if (typeA > typeB) {
comparison = 1;
} else if (typeA < typeB) {
comparison = -1;
}
return comparison;
}
data.sort(compare)
4. 把数组转成以指定符号分隔的字符串
JS 中有个方法可以做到这一点,就是使用数组中的 .join() 方法,我们可以传入指定的符号来做数组进行分隔。
const data = ['Mango', 'Apple', 'Banana', 'Peach']
data.join(',');
// return "Mango,Apple,Banana,Peach"
5. 从数组中选择一个元素
对于此任务,我们有多种方式,一种是使用 forEach 组合 if-else 的方式 ,另一种可以使用filter 方法,但是使用forEach 和filter的缺点是:
在forEach中,我们要额外的遍历其它不需要元素,并且还要使用 if 语句来提取所需的值。
在filter 方法中,我们有一个简单的比较操作,但是它将返回的是一个数组,而是我们想要是根据给定条件从数组中获得单个对象。
为了解决这个问题,我们可以使用 find函数从数组中找到确切的元素并返回该对象,这里我们不需要使用if-else语句来检查元素是否满足条件。
const data = [
{id: 1, name: 'Lemon'},
{id: 2, name: 'Mint'},
{id: 3, name: 'Mango'},
{id: 4, name: 'Apple'}
]
const value = data.find(item => item.name === 'Apple')
// value = {id: 4, name: 'Apple'}
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
新拟物化——Neumorphism ,这么说可能不容易理解,但如果说「新拟物风格」,想必 UI 界的设计师们就知道这股「风头」,在2020年刮的多么凶猛了。
乌克兰设计师亚历山大·普卢托 (Alexander Plyuto) 在 Dribble 平台发布了一张 UI 作品《Skeuomorph Mobile Banking》,由于该作品使用了拟物化的设计风格,令人耳目一新,导致了作品的热度持续飙升,并登上了平台 Popular 榜首。Dribble 的评论区直接炸开了锅,大家纷纷讨论。
△ 普卢托的《Skeuomorph Mobile Banking》,获得了3000多次喜欢
随后一位评论者杰森·凯利(Jason Kelley)在评论中将 New Skeuomorphism 「新拟物化」组合得到的 Neuomorphism 称为「新拟物」 ,并决定去掉「 o 」,于是新设计词汇「 Neumorphism 」便产生了。之后大家便用此做 Tag ,为自己的新拟物化设计作品打标签上传。
此风格的出现也给一直流行的扁平化设计添加了新的展现形式。今年2月初,三星召开 Galaxy Unpacked 活动,为宣传新设备而发出的邀请函,便应用了新拟物化。
△ 凸出的部分,用来比喻新机型的卖点
想要了解新拟物的由来,就必须知道拟物的概念。拟物又被称为拟物化,或是现实主义(Realism),概括的说其主要目标是使用户界面更有代入感,降低人们使用的学习成本,产生熟悉亲和的情感联系。
A skeuomorph, or skeuomorphism is a design element of a product that imitates design elements functionally necessary in the original product design, but which have become ornamental in the new design. Skeuomorphs may be deliberately employed to make the new look comfortablyold and familiar.
via:en.wikipedia.org/wiki/Skeuomorph维基百科上关于拟物化的释义
Apple 苹果公司最早提出了拟物化的设计概念,通过模拟现实物体的纹理、材质来进行界面设计,当时的 UI 设计师们都为拟物化设计「痴狂」。苹果创始人乔布斯也非常推崇拟物化,他认为:「通过拟物化,用这种更加自然的认知体验方式,可以减少用户对电脑操作产生的恐惧感」。不妨来回忆下曾经拟物化的 IOS 界面:
△ IOS 5系统中的相机展开状态(拟物化的镜头)
△ 拟物化的精美 ICON
△ IOS 6系统中,被精细刻画的录音机(底部指针也很惟妙惟肖)
而新拟物则是拟物的变体,在拟物的基础上改变了图形的样式,让设计元素看起来更有真实感,不再是精细的模拟,更像是从界面中「生长」出来。设计师 Michal Malewicz 以卡片的形式,将新拟物和质感设计(Material Design)对比,阐述了二者在实现时的差别。
新拟物卡片给人呈现的是一种无缝隙的「闭合」感,由界面中凸起;而质感设计卡片,则是漂浮状,阴影向四周发散,没有边界限制;二者的光影效果也非常明显,新拟物偏柔和,质感设计则相反,非常凸显物体本体。
Michal Malewicz 还标注了新拟物卡片的背景、阴影和高光的色值,整体色调比较接近。
拟物化风格的结构由背景色+高光色+阴影组成,掌握了基本规律,就可以通过改变按钮、卡片的参数进行调整变换。
△ 形状、阴影参数的不同,实际效果也有区别
新拟物也经常被拿来与扁平化比较,因为拟物和扁平化是两个相对的概念。其实在苹果创造的设计系统的早期界面其实是非常拟物风的,但系统从 IOS 7开始,才转向扁平的设计风格。
随着 AR、VR 技术的进步,其实对于真实物理环境,或者说对显示效率的提升之后,我们对接近物理环境的设计更热衷了。比较有代表性的就是 Google 推出的 Material Design System,它基于人们去模拟真实的物理世界的样子,进而在数字世界里展现我们对于真实世界的一个反馈后,这样的设计流程和逻辑,也让我们的设计更真实,更具有感染力。当然也不止 Google 一家发布了这样偏拟物化的设计风格。
从美学角度来看,其实新拟物化抛弃了之前很多拟物化里不必要的冗余,比如一些阴影、细节的繁琐设计,更偏近现在先进科技发展的设计风格。比如 Windows 推出的 Fluent Design System ,正迎合了未来的 AR、VR 技术广泛普及后的设计环境,希望打造一个先趋的设计系统。
在 Fluent Design System 提到的特点有:Lignt、Depth、Motion、Material、Scale。
1. Lignt
光照,它指的是点击或 Hover 的动作上面加入光照的效果,或是像柔和的光源洒落在空间中,可以去反应物体本身的反光质感,它和 Material Design 强调的光影的扩散的光影效果是不同的概念。
2. Depth
深度,其实它的概念从 Material Design 开始就已经被强调了,但是 Fluent Design System 希望是用更多的方式去呈现,比如井深的模糊效果,视差滚动的动态效果,物件彼此的大小与位置等等。
3. Motion
动效,其实它想强调的动态效果更接近真实的世界,更强调细腻的变化,比如李安的电影「比利·林恩的中场战事」,这个电影拍摄的帧数与以往传统电影不一样,看起来的感觉会更加的流畅自然,你体验过之后会很难回去之后那种电影呈现效果了。而 Windows 强调的 Motion 也是一样的,比起这种单调的动作,它也会去强调每个设计对象彼此之间的动态效果的时间差,看起来会更加的流畅自然。而且与真实空间中前景后景的物理概念一样,不同的时间差会更容易凸显出想要凸显的主题效果,也会更加的聚焦。
4. Material
材质,其实在 Windows 提出的 Fluent Design System 里面,它会出现大量的模糊,透明的背景。也就是模拟毛玻璃的材质感。通常也会代入一些微光源的效果。除了能够吸睛,吸引你的视觉之外呢,其实在 AR、VR 的界面上面感知空间中的物件是很重要的,所以模糊的背景的利用可以在不影响观看内容情况下,还能起到背景暗示的作用。其实毛玻璃效果在 Windows 系统中已经被运用到了,但是由于当时的效能以及干扰视线的问题仅仅运用在了一些小区域,而这次 Fluent Design System 的就成为了最强烈的视觉焦点,其实同样的 iOS 和 Mac iOS 系统里面在最近的更新也大量使用了模糊效果。
6. Scale
缩放,在视觉上眼前的物体大,后面的物体小,所以缩放也是来营造空间感、纵深感,尺度感的这样一个设计特性。
常应用于图标、卡片或按钮元素设计上,背景板多为干净的纯色;界面平滑,没有明显的颗粒感;
△ HYPE4《 Neumorphic Bank Redesign in Dark and Light mode 》
△ Filip Legierski 《 Banking App 》
按钮的外边框均设置了阴影、渐变效果,突出立体感;
△ Samson Vowles《 Neumorphic dark ui kit components 》
在视觉处理上,凸出的按钮为可点击的状态,凹进去则代表已选中。
△ Emy Lascan《 Freebie Neumorphic UX UI Elements 》
层次结构弱
Whale Lab 观察发现,新拟物弱化颜色区分而强调近远景阴影布局,所以整体色彩都相近,除了在个别的位置加入其它颜色点缀,用户识别起来也会迷茫;而卡片、按钮都使用了阴影,高光效果,层次结构不明确,也很难带来流畅的体验;
△ 新拟物风格,Filip Legierski《 Neumorphism UI Elements 》
对比度和视觉限制
明显的对比是界面设计的重要原则。由于新拟物风格具有各种阴影色调和角度,可单击的内容与不可单击的内容区域在哪里不是很好区分。根据产品的功能和要求,每个应用神经同构的产品都可以具有自己的UI阶段规则;但是由于阴影,角度和浮动水平的不同,由于缺乏一致性,迷失方向的可操作项,「神经变形」会给用户带来混乱,最终为残疾用户造成使用障碍。
如同下面这个例子,按钮状态已点击和未点击的一个效果,由于受压状态的反差太小,则看起来的效果也没有什么不同。
增加开发难度
更为严重的是,不少设计者在使用 Neumorphism 进行界面开发过程中,也遭遇到了不少局限。要实现这个风格,主要有两个方式:
卡片、按钮切图,每个状态(Normal、Hover、Pressed)都要裁切,导致资源库图片量过载;
代码实现,这个风格的实现效果是对元素增加两个不同方向的投影,但需要开发对每个元素添加投影,样式代码增多,工作量浩大。
网站neumorphism.io,可以快速生成 Neumorphism UI 。设置按钮的参数值,就能看到多样的新拟物化效果,非常神奇!
新技术、事物、趋势的出现,起初都会给人们带来焦虑甚至是恐慌。不管是拟物还是扁平,Whale Lab 觉得若是绝对化的去推崇某一种,都是错误的,尽管苹果放弃了拟物进入扁平化,也不一定代表扁平就是最好,毕竟二者始终相辅相成。不敢否认,新拟物风格在今后是否变得「真香」,但对于设计师来说,从用户体验、产品出发的优秀设计,都值得被认可与尊敬。
文章来源:优设 作者:UX辞典
蓝蓝设计的小编 http://www.lanlanwork.com