閱讀之前
基本上,在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.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的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.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的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.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的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.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的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.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的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.syprn.cn