首页

JavaScript中的map()和forEach()有什么区别?

seo达人

阅读之前

基本上,在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界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务




细聊Concent & Recoil , 探索react数据流的新开发模式

seo达人

序言

之前发表了一篇文章 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界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务



如何使JavaScript休眠或等待

seo达人

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界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务

5 个 JS 数组技巧可提高你的开发技能

seo达人

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界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服务


Vue 数据更新了但页面没有更新的 7 种情况汇总及延伸总结

seo达人

1. Vue 无法检测实例被创建时不存在于 data 中的 property

原因:由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

场景:


var vm = new Vue({

 data:{},

 // 页面不会变化

 template: '<div>{{message}}</div>'

})

vm.message = 'Hello!' // `vm.message` 不是响应式的

解决办法:


var vm = new Vue({

 data: {

   // 声明 a、b 为一个空值字符串

   message: '',

 },

 template: '<div>{{ message }}</div>'

})

vm.message = 'Hello!'

2. Vue 无法检测对象 property 的添加或移除

原因:官方 - 由于 JavaScript(ES5) 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。

场景:


var vm = new Vue({

 data:{

   obj: {

     id: 001

   }

 },

 // 页面不会变化

 template: '<div>{{ obj.message }}</div>'

})


vm.obj.message = 'hello' // 不是响应式的

delete vm.obj.id       // 不是响应式的

解决办法:


// 动态添加 - Vue.set

Vue.set(vm.obj, propertyName, newValue)


// 动态添加 - vm.$set

vm.$set(vm.obj, propertyName, newValue)


// 动态添加多个

// 代替 Object.assign(this.obj, { a: 1, b: 2 })

this.obj = Object.assign({}, this.obj, { a: 1, b: 2 })


// 动态移除 - Vue.delete

Vue.delete(vm.obj, propertyName)


// 动态移除 - vm.$delete

vm.$delete(vm.obj, propertyName)

3. Vue 不能检测通过数组索引直接修改一个数组项

原因:官方 - 由于 JavaScript 的限制,Vue 不能检测数组和对象的变化;尤雨溪 - 性能代价和获得用户体验不成正比。

场景:


var vm = new Vue({

 data: {

   items: ['a', 'b', 'c']

 }

})

vm.items[1] = 'x' // 不是响应性的

解决办法:


// Vue.set

Vue.set(vm.items, indexOfItem, newValue)


// vm.$set

vm.$set(vm.items, indexOfItem, newValue)


// Array.prototype.splice

vm.items.splice(indexOfItem, 1, newValue)

拓展:Object.defineProperty() 可以监测数组的变化

Object.defineProperty() 可以监测数组的变化。但对数组新增一个属性(index)不会监测到数据变化,因为无法监测到新增数组的下标(index),删除一个属性(index)也是。

场景:


var arr = [1, 2, 3, 4]

arr.forEach(function(item, index) {

   Object.defineProperty(arr, index, {

   set: function(value) {

     console.log('触发 setter')

     item = value

   },

   get: function() {

     console.log('触发 getter')

     return item

   }

 })

})

arr[1] = '123'  // 触发 setter

arr[1]          // 触发 getter 返回值为 "123"

arr[5] = 5      // 不会触发 setter 和 getter

4. Vue 不能监测直接修改数组长度的变化

原因:官方 - 由于 JavaScript 的限制,Vue 不能检测数组和对象的变化;尤雨溪 - 性能代价和获得用户体验不成正比。

场景:


var vm = new Vue({

 data: {

   items: ['a', 'b', 'c']

 }

})

vm.items.length = 2 // 不是响应性的

解决办法:


vm.items.splice(newLength)

5. 在异步更新执行之前操作 DOM 数据不会变化

原因:Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

场景:


<div id="example">{{message}}</div>

var vm = new Vue({

 el: '#example',

 data: {

   message: '123'

 }

})

vm.message = 'new message' // 更改数据

vm.$el.textContent === 'new message' // false

vm.$el.style.color = 'red' // 页面没有变化

解决办法:


var vm = new Vue({

 el: '#example',

 data: {

   message: '123'

 }

})

vm.message = 'new message' // 更改数据

//使用 Vue.nextTick(callback) callback 将在 DOM 更新完成后被调用

Vue.nextTick(function () {

 vm.$el.textContent === 'new message' // true

 vm.$el.style.color = 'red' // 文字颜色变成红色

})

拓展:异步更新带来的数据响应的误解

<!-- 页面显示:我更新啦! -->

<div id="example">{{message.text}}</div>

var vm = new Vue({

 el: '#example',

 data: {

   message: {},

 }

})

vm.$nextTick(function () {

 this.message = {}

 this.message.text = '我更新啦!'

})

上段代码中,我们在 data 对象中声明了一个 message 空对象,然后在下次 DOM 更新循环结束之后触发的异步回调中,执行了如下两段代码:


this.message = {};

this.message.text = '我更新啦!'

到这里,模版更新了,页面最后会显示 我更新啦!。


模板更新了,应该具有响应式特性,如果这么想那么你就已经走入了误区。


一开始我们在 data 对象中只是声明了一个 message 空对象,并不具有 text 属性,所以该 text 属性是不具有响应式特性的。


但模板切切实实已经更新了,这又是怎么回事呢?


那是因为 Vue.js 的 DOM 更新是异步的,即当 setter 操作发生后,指令并不会立马更新,指令的更新操作会有一个延迟,当指令更新真正执行的时候,此时 text 属性已经赋值,所以指令更新模板时得到的是新值。


模板中每个指令/数据绑定都有一个对应的 watcher 对象,在计算过程中它把属性记录为依赖。之后当依赖的 setter 被调用时,会触发 watcher 重新计算 ,也就会导致它的关联指令更新 DOM。



具体流程如下所示:


执行 this.message = {}; 时, setter 被调用。

Vue.js 追踪到 message 依赖的 setter 被调用后,会触发 watcher 重新计算。

this.message.text = '我更新啦!'; 对 text 属性进行赋值。

异步回调逻辑执行结束之后,就会导致它的关联指令更新 DOM,指令更新开始执行。

所以真正的触发模版更新的操作是 this.message = {};这一句引起的,因为触发了 setter,所以单看上述例子,具有响应式特性的数据只有 message 这一层,它的动态添加的属性是不具备的。


对应上述第二点 - Vue 无法检测对象 property 的添加或移除

6. 循环嵌套层级太深,视图不更新?

看到网上有些人说数据更新的层级太深,导致数据不更新或者更新缓慢从而导致试图不更新?


由于我没有遇到过这种情况,在我试图重现这种场景的情况下,发现并没有上述情况的发生,所以对于这一点不进行过多描述(如果有人在真实场景下遇到这种情况留个言吧)。


针对上述情况有人给出的解决方案是使用强制更新:


如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。

vm.$forceUpdate()

7. 拓展:路由参数变化时,页面不更新(数据不更新)

拓展一个因为路由参数变化,而导致页面不更新的问题,页面不更新本质上就是数据没有更新。


原因:路由视图组件引用了相同组件时,当路由参会变化时,会导致该组件无法更新,也就是我们常说中的页面无法更新的问题。

场景:


<div id="app">

 <ul>

   <li><router-link to="/home/foo">To Foo</router-link></li>    

   <li><router-link to="/home/baz">To Baz</router-link></li>    

   <li><router-link to="/home/bar">To Bar</router-link></li>    

 </ul>    

 <router-view></router-view>

</div>

const Home = {

 template: `<div>{{message}}</div>`,

 data() {

   return {

     message: this.$route.params.name

   }

 }

}


const router = new VueRouter({

 mode:'history',

   routes: [

   {path: '/home', component: Home },

   {path: '/home/:name', component: Home }

 ]

})


new Vue({

 el: '#app',

 router

})

上段代码中,我们在路由构建选项 routes 中配置了一个动态路由 '/home/:name',它们共用一个路由组件 Home,这代表他们复用 RouterView 。


当进行路由切换时,页面只会渲染第一次路由匹配到的参数,之后再进行路由切换时,message 是没有变化的。


解决办法:


解决的办法有很多种,这里只列举我常用到几种方法。

通过 watch 监听 $route 的变化。


const Home = {

 template: `<div>{{message}}</div>`,

 data() {

   return {

     message: this.$route.params.name

   }

 },

 watch: {

      '$route': function() {

      this.message = this.$route.params.name

   }

   }

}

...

new Vue({

 el: '#app',

 router

})

给 <router-view> 绑定 key 属性,这样 Vue 就会认为这是不同的 <router-view>。


弊端:如果从 /home 跳转到 /user 等其他路由下,我们是不用担心组件更新问题的,所以这个时候 key 属性是多余的。

<div id="app">

 ...

 <router-view :key="key"></router-view>

</div>

理解 redux-thunk 源码

seo达人

前言

前面几篇我们就 Redux 展开了几篇文章,这次我们来实现 react-thunk,就不是叫实现 redux-thunk 了,直接上源码,因为源码就11行。如果对 Redux 中间件还不理解的,可以看我写的 Redux 文章。


实现一个迷你Redux(基础版)

实现一个Redux(完善版)

浅谈React的Context API

带你实现 react-redux

为什么要用 redux-thunk

在使用 Redux 过程,通过 dispatch 方法派发一个 action 对象。当我们使用 redux-thunk 后,可以 dispatch 一个 function。redux-thunk会自动调用这个 function,并且传递 dispatch, getState 方法作为参数。这样一来,我们就能在这个 function 里面处理异步逻辑,处理复杂逻辑,这是原来 Redux 做不到的,因为原来就只能 dispatch 一个简单对象。


用法

redux-thunk 作为 redux 的中间件,主要用来处理异步请求,比如:


export function fetchData() {

 return (dispatch, getState) => {

   // to do ...

   axios.get('https://jsonplaceholder.typicode.com/todos/1').then(res => {

     console.log(res)

   })

 }

}

redux-thunk 源码

redux-thunk 的源码比较简洁,实际就11行。前几篇我们说到 redux 的中间件形式,

本质上是对 store.dispatch 方法进行了增强改造,基本是类似这种形式:


const middleware = (store) => next => action => {}

在这里就不详细解释了,可以看 实现一个Redux(完善版)


先给个缩水版的实现:


const thunk = ({ getState, dispatch }) => next => action => {

   if (typeof action === 'function') {

       return action(dispatch, getState)

   }

   return next(action)

}

export default thunk

原理:即当 action 为 function 的时候,就调用这个 function (传入 dispatch, getState)并返回;如果不是,就直接传给下一个中间件。

完整源码如下:


function createThunkMiddleware(extraArgument) {

 return ({ dispatch, getState }) => next => action => {

   // 如果action是一个function,就返回action(dispatch, getState, extraArgument),否则返回next(action)。

   if (typeof action === 'function') {

     return action(dispatch, getState, extraArgument)

   }

   // next为之前传入的store.dispatch,即改写前的dispatch

   return next(action)

 }

}


const thunk = createThunkMiddleware()

// 给thunk设置一个变量withExtraArgument,并且将createThunkMiddleware整个函数赋给它

thunk.withExtraArgument = createThunkMiddleware


export default thunk

我们发现其实还多了 extraArgument 传入,这个是自定义参数,如下用法:


const api = "https://jsonplaceholder.typicode.com/todos/1";

const whatever = 10;


const store = createStore(

 reducer,

 applyMiddleware(thunk.withExtraArgument({ api, whatever })),

);


// later

function fetchData() {

 return (dispatch, getState, { api, whatever }) => {

   // you can use api and something else here

 };

}

总结

同 redux-thunk 非常流行的库 redux-saga 一样,都是在 redux 中做异步请求等副作用。Redux 相关的系列文章就暂时写到这部分为止,下次会写其他系列。

Typescript 内置的模块导入兼容方式

seo达人

一、前言

前端的模块化规范包括 commonJS、AMD、CMD 和 ES6。其中 AMD 和 CMD 可以说是过渡期的产物,目前较为常见的是commonJS 和 ES6。在 TS 中这两种模块化方案的混用,往往会出现一些意想不到的问题。


二、import * as

考虑到兼容性,我们一般会将代码编译为 es5 标准,于是 tsconfig.json 会有以下配置:


{

 "compilerOptions": {

   "module": "commonjs",

   "target": "es5",

 }

}

代码编译后最终会以 commonJS 的形式输出。

使用 React 的时候,这种写法 import React from "react" 会收到一个莫名其妙的报错:


Module "react" has no default export

这时候你只能把代码改成这样:import * as React from "react"。

究其原因,React 是以 commonJS 的规范导出的,而 import React from "react" 这种写法会去找 React 模块中的 exports.default,而 React 并没有导出这个属性,于是就报了如上错误。而 import * as React 的写法会取 module.exports 中的值,这样使用起来就不会有任何问题。我们来看看 React 模块导出的代码到底是怎样的(精简过):


...

var React = {

 Children: {

   map: mapChildren,

   forEach: forEachChildren,

   count: countChildren,

   toArray: toArray,

   only: onlyChild

 },


 createRef: createRef,

 Component: Component,

 PureComponent: PureComponent,

 ...

}


module.exports = React;

可以看到,React 导出的是一个对象,自然也不会有 default 属性。


二、esModuleInterop

为了兼容这种这种情况,TS 提供了配置项 esModuleInterop 和 allowSyntheticDefaultImports,加上后就不会有报错了:


{

 "compilerOptions": {

   "module": "commonjs",

   "target": "es5",

   "allowSyntheticDefaultImports": true,

   "esModuleInterop": true

 }

}

其中 allowSyntheticDefaultImports 这个字段的作用只是在静态类型检查时,把 import 没有 exports.default 的报错忽略掉。

而 esModuleInterop 会真正的在编译的过程中生成兼容代码,使模块能正确的导入。还是开始的代码:


import React from "react";

现在 TS 编译后是这样的:


var __importDefault = (this && this.__importDefault) || function (mod) {

   return (mod && mod.__esModule) ? mod : { "default": mod };

};


Object.defineProperty(exports, "__esModule", { value: true });


var react_1 = __importDefault(require("react"));

编译器帮我们生成了一个新的对象,将模块赋值给它的 default 属性,运行时就不会报错了。


三、Tree Shaking

如果把 TS 按照 ES6 规范编译,就不需要加上 esModuleInterop,只需要 allowSyntheticDefaultImports,防止静态类型检查时报错。


{

 "compilerOptions": {

   "module": "es6",

   "target": "es6",

   "allowSyntheticDefaultImports": true

 }

}

什么情况下我们会考虑导出成 ES6 规范呢?多数情况是为了使用 webpack 的 tree shaking 特性,因为它只对 ES6 的代码生效。


顺便再发散一下,讲讲 babel-plugin-component。


import { Button, Select } from 'element-ui'

上面的代码经过编译后,是下面这样的:


var a = require('element-ui');

var Button = a.Button;

var Select = a.Select;

var a = require('element-ui') 会引入整个组件库,即使只用了其中的 2 个组件。

babel-plugin-component 的作用是将代码做如下转换:


// 转换前

import { Button, Select } from 'element-ui'

// 转换后

import Button from 'element-ui/lib/button'

import Select from 'element-ui/lib/select'

最终编译出来是这个样子,只会加载用到的组件:


var Button = require('element-ui/lib/button');

var Select = require('element-ui/lib/select');

四、总结

本文讲解了 TypeScript 是如何导入不同模块标准打包的代码的。无论你导入的是 commonJS 还是 ES6 的代码,万无一失的方式是把 esModuleInterop 和 allowSyntheticDefaultImports 都配置上。

使用 VSCode 开发 Gatsby 项目配置

seo达人

初始化

使用 https://github.com/XYShaoKang... 作为基础模板

gatsby new gatsby-project-config https://github.com/XYShaoKang/gatsby-hello-world

Prettier 配置

安装 VSCode 扩展

按 Ctrl + P (MAC 下: Cmd + P) 输入以下命令,按回车安装


ext install esbenp.prettier-vscode

安装依赖

yarn add -D prettier

Prettier 配置文件.prettierrc.js

// .prettierrc.js

module.exports = {

 trailingComma: 'es5',

 tabWidth: 2,

 semi: false,

 singleQuote: true,

 endOfLine: 'lf',

 printWidth: 50,

 arrowParens: 'avoid',

}

ESLint 配置

安装 VSCode 扩展

按 Ctrl + P (MAC 下: Cmd + P) 输入以下命令,按回车安装


ext install dbaeumer.vscode-eslint

安装 ESLint 依赖

yarn add -D eslint babel-eslint eslint-config-google eslint-plugin-react eslint-plugin-filenames

ESLint 配置文件.eslintrc.js

使用官方仓库的配置,之后在根据需要修改


// https://github.com/gatsbyjs/gatsby/blob/master/.eslintrc.js

// .eslintrc.js

module.exports = {

 parser: 'babel-eslint',

 extends: [

   'google',

   'eslint:recommended',

   'plugin:react/recommended',

 ],

 plugins: ['react', 'filenames'],

 parserOptions: {

   ecmaVersion: 2016,

   sourceType: 'module',

   ecmaFeatures: {

     jsx: true,

   },

 },

 env: {

   browser: true,

   es6: true,

   node: true,

   jest: true,

 },

 globals: {

   before: true,

   after: true,

   spyOn: true,

   __PATH_PREFIX__: true,

   __BASE_PATH__: true,

   __ASSET_PREFIX__: true,

 },

 rules: {

   'arrow-body-style': [

     'error',

     'as-needed',

     { requireReturnForObjectLiteral: true },

   ],

   'no-unused-expressions': [

     'error',

     {

       allowTaggedTemplates: true,

     },

   ],

   'consistent-return': ['error'],

   'filenames/match-regex': [

     'error',

     '^[a-z-\\d\\.]+$',

     true,

   ],

   'no-console': 'off',

   'no-inner-declarations': 'off',

   quotes: ['error', 'backtick'],

   'react/display-name': 'off',

   'react/jsx-key': 'warn',

   'react/no-unescaped-entities': 'off',

   'react/prop-types': 'off',

   'require-jsdoc': 'off',

   'valid-jsdoc': 'off',

 },

 settings: {

   react: {

     version: '16.4.2',

   },

 },

}

解决 Prettier ESLint 规则冲突

推荐配置


安装依赖


yarn add -D eslint-config-prettier eslint-plugin-prettier

在.eslintrc.js中的extends添加'plugin:prettier/recommended'


module.exports = {

 extends: ['plugin:prettier/recommended'],

}

VSCode 中 Prettier 和 ESLint 协作

方式一:使用 ESLint 扩展来格式化代码

配置.vscode/settings.json


// .vscode/settings.json

{

 "eslint.format.enable": true,

 "[javascript]": {

   "editor.defaultFormatter": "dbaeumer.vscode-eslint"

 },

 "[javascriptreact]": {

   "editor.defaultFormatter": "dbaeumer.vscode-eslint"

 }

}

ESLint 扩展会默认忽略.开头的文件,比如.eslintrc.js

如果需要格式化.开头的文件,可以在.eslintignore中添加一个否定忽略来启用对应文件的格式化功能.


!.eslintrc.js

或者直接使用!.*,这样可以开启所有点文件的格式化功能


方式二:使用 Prettier 扩展来格式化代码

在版prettier-vscode@v5.0.0中已经删除了直接对linter的集成,所以版没法像之前那样,通过prettier-eslint来集成ESLint的修复了(一定要这样用的话,可以通过降级到prettier-vscode@4来使用了).如果要使用Prettier来格式化的话,就只能按照官方指南中的说的集成方法,让Prettier来处理格式,通过配置在保存时使用ESlint自动修复代码.只是这样必须要保存文件时,才能触发ESLint的修复了.


配置 VSCode 使用 Prettier 来格式化 js 和 jsx 文件

在项目中新建文件.vscode/settings.json


// .vscode/settings.json

{

 "[javascript]": {

   "editor.defaultFormatter": "esbenp.prettier-vscode"

 },

 "[javascriptreact]": {

   "editor.defaultFormatter": "esbenp.prettier-vscode"

 },

 "editor.codeActionsOnSave": {

   "source.fixAll.eslint": true

 }

}

说实话这个体验很糟糕,之前直接一键格式化代码并且修复 ESLint 错误,可以对比格式化之前和格式化之后的代码,如果感觉不对可以直接撤销更改就好了.现在必须要通过保存,才能触发修复 ESlint 错误.而在开发过程中,通过监听文件改变来触发热加载或者重新编译是很常见的操作.这样之后每次想要去修复 ESLint 错误,还是只是想看看修复错误之后的样子,都必须要去触发热加载或重新编译,每次操作的成本就太高了.

我更推荐第一种方式使用 ESLint 扩展来对代码进行格式化.


调试 Gatsby 配置

调试构建过程

添加配置文件.vscode/launch.json


// .vscode/launch.json

{

 // 使用 IntelliSense 了解相关属性。

 // 悬停以查看现有属性的描述。

 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387

 "version": "0.2.0",

 "configurations": [

   {

     "name": "Gatsby develop",

     "type": "node",

     "request": "launch",

     "protocol": "inspector",

     "program": "${workspaceRoot}/node_modules/gatsby/dist/bin/gatsby",

     "args": ["develop"],

     "stopOnEntry": false,

     "runtimeArgs": ["--nolazy"],

     "sourceMaps": false,

     "outputCapture": "std"

   }

 ]

}

的gatsby@2.22.*版本中调试不能进到断点,解决办法是降级到2.21.*,yarn add gatsby@2.21.40,等待官方修复再使用版本的

调试客户端

需要安装 Debugger for Chrome 扩展


ext install msjsdiag.debugger-for-chrome

添加配置文件.vscode/launch.json


// .vscode/launch.json

{

 // 使用 IntelliSense 了解相关属性。

 // 悬停以查看现有属性的描述。

 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387

 "version": "0.2.0",

 "configurations": [

   {

     "type": "chrome",

     "request": "launch",

     "name": "Gatsby Client Debug",

     "url": "http://localhost:8000",

     "webRoot": "${workspaceFolder}"

   }

 ]

}

先启动 Gatsby,yarn develop,然后按 F5 开始调试.

JavaScript必须掌握的基础 --- 闭包

seo达人

闭包(Closure)的定义

闭包是一个让初级JavaScript使用者既熟悉又陌生的一个概念。因为闭包在我们书写JavaScript代码时,随处可见,但是我们又不知道哪里用了闭包。

关于闭包的定义,网上(书上)的解释总是千奇百怪,我们也只能“取其精华去其糟粕”去总结一下。

  1. 即使函数在当前作用域外调用,但是还能访问当前作用域中的变量和函数
  2. 有权访问另一个函数作用域中的变量(函数)的函数。
  3. 闭包是指那些能够访问自由变量的函数

ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数都是闭包。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量也就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:一下才算是闭包:

    • 即使创建它的上下文已经销毁,它仍然存在。
    • 在代码中引用了自由变量。

闭包跟词法作用域,作用域链,执行上下文这几个JavaScript中重要的概念都有关系,因此要想真的理解闭包,至少要对那几个概念不陌生。

闭包的优点:

  1. 可以是用函数内部的变量(函数),也可以说是可以访问函数作用域。
  2. 拥有私有变量,避免污染全局变量

闭包的缺点:

  1. 私有变量一直存在,占用内存。

我们来一步一步引出闭包。

自执行函数 ( IIFE )

自执行函数也叫立即调用函数(IIFE),是一个在定义时就执行的函数。

var a=1;
(function() { console.log(a)
})()

上述代码是一个最简单的自执行函数。

在ES6之前,是没有块级作用域的,只有全局作用域和函数作用域,因此自执行函数还能在ES6之前实现块级作用域。

// ES6 块级作用域 var a = 1; if(true) { let a=111; console.log(a); // 111 } console.log(a); // 1 

这里 if{} 中用let声明了一个 a。这个 a 就具有块级作用域,在这个 {} 中访问 a ,永远访问的都是 let 声明的a,跟全局作用域中的a没有关系。如果我们把 let 换成 var ,就会污染全局变量 a 。

如果用自执行函数来实现:

var a = 1;
(function() { if(true) { var a=111; console.log(a); // 111 }
})() console.log(a); // 1

为什么要在这里要引入自执行函数的概念呢?因为通常我们会用自执行函数来创建闭包,实现一定的效果。

来看一个基本上面试提问题:

for(var i=0;i<5;i++) {
    setTimeout(function() { console.log(i);
    },1000)
}

在理想状态下我们期望输出的是 0 ,1 ,2 ,3 ,4。但是实际上输出的是5 ,5 ,5 ,5 ,5。为什么是这样呢?其实这里不仅仅涉及到作用域,作用域链还涉及到Event Loop、微任务、宏任务。但是在这里不讲这些。

下面我们先解释它为什么会输出 5个5,然后再用自执行函数来修改它,以达到我们预期的结果。

提示:for 循环中,每一次的都声明一个同名变量,下一个变量的值为上一次循环执行完同名变量的值。

首先用var声明变量 for 是不会产生块级作用域的,所以在 () 中声明的 i 为全局变量。相当于:

// 伪代码 var i; for(i=0;i<5;i++) {
    setTimeout(function() { console.log(i);
    },1000)
}

setTimeout中的第一个参数为一个全局的匿名函数。相当于:

// 伪代码 var i; var f = function() { console.log(i);
} for(i=0;i<5;i++) {
    setTimeout(f,1000)
}

由于setTimeout是在1秒之后执行的,这个时候for循环已经执行完毕,此时的全局变量 i 已经变成了 5 。1秒后5个setTimeout中的匿名函数会同时执行,也就是5个 f 函数执行。这个时候 f 函数使用的变量 i 根据作用域链的查找规则找到了全局作用域中的 i 。因此会输出 5 个5。

那我们怎样来修改它呢?

  • 思路1:让setTimeout匿名函数中访问的变量 i 不再访问全局作用域中的 i 。因此把它包裹在一个函数作用域中。这时 匿名函数访问变量 i 时,会先去包裹它的函数作用域中查找。
for(var i=0;i<5;i++) {
    (function (){ setTimeout(function() { console.log(i);
        },1000)
    })();
}

上述例子会输出我们期望的值吗?答案是否。为什么呢?我们虽然把 setTimeout 包裹在一个匿名函数中了,但是当setTimeout中匿名函数执行时,首先去匿名函数中查找 i 的值,找不到还是会找到全局作用域中,最终 i 的值仍然是全局变量中的 i ,仍然为 5个5.

那我们把外层的匿名函数中声明一个变量 j 让setTimeout中的匿名函数访问这个 j 不就找不到全局变量中的变量了吗。

for(var i=0;i<5;i++) {
    (function (){ var j = i;
        setTimeout(function() { console.log(j);
        },1000)
    })();
}

这个时候才达到了我们预期的结果:0 1 2 3 4。

我们来优化一下:

for(var i=0;i<5;i++) {
    (function (i){ setTimeout(function() { console.log(i);
        },1000)
    })(i);
}

*思路2:用 let 声明变量,产生块级作用域。

for(let i=0;i<5;i++) {
    setTimeout(function() { console.log(i);
    },1000)
}

这时for循环5次,产生 5 个块级作用域,也会声明 5 个具有块级作用域的变量 i ,因此setTimeout中的匿名函数每次执行时,访问的 i 都是当前块级作用域中的变量 i 。

理论中的闭包

什么是理论中的闭包?就是看似像闭包,其实并不是闭包。它只是类似于闭包。

 function foo() { var a=2; function bar() { console.log(a); // 2 }
    bar();
}
foo();

上述代码根据最上面我们对闭包的定义,它并不完全是闭包,虽然是一个函数可以访问另一个函数中的变量,但是被嵌套的函数是在当前词法作用域中被调用的。

实践中的闭包

我们怎样把上述代码foo 函数中的bar函数,在它所在的词法作用域外执行呢?

下面的代码就清晰的展示了闭包:

function foo() { var a=2; function bar() { console.log(a);
    } return bar;
} var baz=foo();
baz(); // 2 —— 朋友,这就是闭包的效果。

上述代码中 bar 被当做 foo函数返回值。foo函数执行后把返回值也就是 bar函数 赋值给了全局变量 baz。当 baz 执行时,实际上也就是 bar 函数的执行。我们知道 foo 函数在执行后,foo 的内部作用域会被销毁,因为引擎有垃圾回收期来释放不再使用的内存空间。所以在bar函数执行时,实际上foo函数内部的作用域已经不存在了,理应来说 bar函数 内部再访问 a 变量时是找不到的。但是闭包的神奇之处就在这里。由于 bar 是在 foo 作用域中被声明的,所以 bar函数 会一直保存着对 foo 作用域的引用。这时就形成了闭包。

我们先看个例子:

var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope;
    } return f;
} var foo = checkscope();
foo();

我们用伪代码来解释JavaScript引擎在执行上述代码时的步骤:

  1. JavaScript引擎遇到可执行代码时,就会进入一个执行上下文(环境)
  2. 首先遇到的是全局代码,因此进入全局执行上下文,把全局执行上下文压入执行上下文栈。
  3. 全局上下文创建时会先在内部创建VO/AO,作用域链,this。然后执行代码。
  4. 当遇到 checkscope 函数执行时,进入checkscope的执行上下文,然后压入执行上下文栈。
  5. checkscope 执行上下文创建时会先在内部创建VO/AO,作用域链,this。然后执行代码。
  6. 当checkscope 函数执行完毕时,会从执行上下文栈中弹出,此时它的AO也会被浏览器回收。(这是理想状态下)
  7. 执行foo函数,向上查找foo的值,发现foo的值为checkscope函数内部函数f。因此这一步为执行 checkscope 内部函数f。
  8. 执行f函数同执行 checkscope 的步骤一致。
  9. f 函数执行完毕,从执行上下文栈中弹出。

但是我们想一个问题,checkscope函数执行完毕,它的执行上下文从栈中弹出,也就是销毁了不存在了,f 函数还能访问包裹函数的作用域中的变量(scope)吗?答案是可以。

理由是在第6步,我们说过当checkscope 执行函数执行完毕时,它的执行上下文会从栈中弹出,此时活动对象也会被回收,按理说当 f 在访问checkscope的活动对象时是访问不到的。

其实这里还有个概念,叫做作用域链:当 checkscope 函数被创建时,会创建对应的作用域链,里面值存放着包裹它的作用域对应执行上下文的变量对象,在这里只是全局执行上下文的变量对象,当checkscope执行时,此时的作用域链变化了 ,里面存放的是变量对象(活动对象)的集合,最顶端是当前函数的执行上下文的活动对象。端是全局执行上下文的变量对象。类似于:

checkscope.scopeChain = [
    checkscope.AO
    global.VO
] 

当checkscope执行碰到了 f 函数的创建,因此 f 函数也会创建对应的作用域链,默认以包裹它的函数执行时对应的作用域链为基础。因此此时 f 函数创建时的作用域链如下:

checkscope.scopeChain = [
    checkscope.AO
    global.VO
]

当 f 函数执行时,此时的作用域链变化如下:

checkscope.scopeChain = [
    f.AO
    checkscope.AO
    global.VO
]

当checkscope函数执行完毕,内部作用域会被回收,但是 f函数 的作用域链还是存在的,里面存放着 checkscope函数的活动对象,因此在f函数执行时会从作用域链中查找内部使用的 scope 标识符,从而在作用域链的第二位找到了,也就是在 checkscope.AO 找到了变量scope的值。

正是因为JavaScript做到了这一点,因此才会有闭包的概念。还有人说闭包并不是为了拥有它采取设计它的,而是设计作用域链时的副作用产物。

闭包是JavaScript中最难的点,也是平常面试中常问的问题,我们必须要真正的去理解它,如果只靠死记硬背是经不起考验的。

日历

链接

blogger

蓝蓝 http://www.lanlanwork.com

存档