302017.11

模拟代码帮助理解reselect的createSelector函数

reselect提供的createSelector API具有记忆功能,但是它的用法奇怪,让入门者比较难理解,我也废了不少力才理解到这块。现在通过模拟脚本,来协助理解。先来看下我们怎么创建一个selector函数:

let selector = createSelector([fun1, fun2], fun3)

在使用selector的时候,把它当作一个函数,传入state和props作为参数。接下来我们通过selector的反推来理解:

let someState = selector(state, props)
 <- 在mapStateToProps中这样用推导出 =>
let someState = (function(state, props) {
  let state1 = fun1(state, props)
  let state2 = fun2(state, props)
  return fun3(state1, state2)
})(state, props)

这样就可以非常容易的理解,createSelector传入的参数中的函数,各自在什么时候执行,执行的结果拿来干什么。

createSelector的第一个参数是个数组,数组的个数和第二个参数函数(fun3)的参数个数相同,这个怎么做到呢?其实很简单,用扩展运算符...或者用.apply都可以做到。

最后的问题是,如何做到记忆功能呢?

reselect的记忆功能的规则是,fun3的实参如果不变,那么说明它的运算结果也不变,可以直接返回缓存起来的结果。

所以,要使记忆功能生效,你必须保证fun3的实参不变,说白了,就是fun1, fun2的计算结果不变,因此fun1, fun2必须是返回固定值的函数。这种函数比pure function还要硬性,即使参数不同,也要永远返回一个值。当然,我们是不可能做到这样的,如果fun1依赖的state发生来变化,那么它的结果自然就会变,这个时候,fun3就不再返回缓存,而是重新计算结果,同时缓存新的结果,下次就可以用这个缓存了。这样,就做到selector的响应式。

最后的问题是,如果fun1, fun2的结果会随着props的不同而返回不同的结果呢?这种情况普遍存在,一个react组件可能在一个页面里面被多次使用,每次使用的时候props可能不同。这就会导致reselect的记忆功能失效。

解决的办法还是要从记忆功能的原理中去寻找。

每一个计算结果的缓存,与传入fun3的参数是一一对应的,fun3可以说是一个pure function,参数相同的情况下,得到的结果永远相同。有两种解决的想法:

  1. 为每一个组件设置单独的映射,这个可以通过react-redux的connect来实现,当mapStateToProps返回的是一个函数时,那么这个函数的运算结果仅对组件的当前实例生效,也就是说,在我们写mapStateToProps函数时,不能直接返回映射关系,而是返回一个函数,这个函数里面去做一些处理后再返回映射关系。下面有例子。
  2. 既然fun3的计算结果是根据参数来缓存的,那么我们可以尝试对参数做hash,固定的参数对应固定的fun3函数体,不同的参数对应不同的fun3函数体,当在不同的参数之间切换时,如果发现这个hash有存在的fun3函数体,那么就立即用它的缓存。下面也有例子。

想法1的例子:

const makeMapStateToProps = () => {
 const getSelector = makeSelector() // 下一段代码看makeSelector是怎么写的
 const mapStateToProps = (state, props) => {
  return {
   todos: getSelector(state, props)
  }
 }
 return mapStateToProps
}
export default connect(makeMapStateToProps)(MyComponent)
import { createSelector } from 'reselect'
function getSelector(state, props) {
  return state[props.id]
}

export function makeSelector() {
  return createSelector(
    [ getSelector ],
    (user) => {
      user.total_books = user.books.length
      return user
    },
  )
}

通过结合文章开头的推导代码,你会发现,每个组件的实例的props.id是一定的,因此对应的user也是一定的,那么每次都可以使用缓存起来的user。当然,如果props.id改变来,那么缓存就失效了。

想法2,对makeSelector做深度改造:

let selectors = {}
function makeSelector(uid) {
  if (selectors[uid]) return selectors[uid]
  let selector = createSelector(...)
}
function deleteSelector(uid) {
  delete selectors[uid]
}

在connect的时候,直接在makeSelector的时候传入props.id作为标记,mapStateToProps不再返回函数作为结果。当不再对uid对应的用户进行操作之后,要即时删除这个selector。

14:06:50 已有0条回复
282017.11

为什么需要react-redux提供的connect函数呢?redux的store作为一个可以全局可引用的对象,完全可以不依赖react-redux啊,只需要把store独立出来,在需要的地方直接import进来即可。

在经过一段时间的思考之后,终于明白一个道理,react组件是一个纯粹自我封闭的组件,数据流是单向的,这一点非常重要,如何决定数据流单向呢?完全依靠props的传递,也就是说如果在组件内引用store,通过getState来获取数据,就会造成数据来源不清晰的状况。因此,为了避免这种数据流混乱的局面,有必要在组件实例化时,将store中的数据绑定到组件的某个props上。connect函数就是做这样一件事,它把store中的state转化为组件的props,传递给组件,对于组件开发者而言,如果不考虑自己已经理解这套机理,那么只要遵循一个原则:但凡组件本身无法直接获取的数据,都通过props获取。另外一位开发者则可以在拿到这个组件之后,用connect函数对它进行包装,保证这些props是有值的。

由此可见,在react中,把握组件开发的核心思想非常重要。

10:03:00 已有0条回复
162017.11

在重写slickgrid.js的时候,我一直有一种疑惑,究竟应该沿用jquery还是采用react作为视图层面的引擎?说到这里,很多人可能完全还没体会到,jquery和react是同一层面的东西,它们都是用来操作dom,和view层打交道的工具。很多人会立即跳起来,react高端多了,它有virtual dom,不直接和dom打交道。可是说了这么多,真的在开发某个第三方库的时候考虑过这个问题吗?对于撰写一个第三方的组件,它对于使用的开发者而言,无所谓技术框架,它有自己完整的api,因此对于使用者而言,不用深入学习它背后依赖的是jquery还是react。

在选择视图层库时,还有其它的选择,比如一些模板引擎,甚至是框架,但是对我而言,仅考虑jquery和react。它们对比到底有哪些区别呢?

Jquery React
界面构建 $().append() Jsx+data
操作界面方式 $().doSomething() setState
编程方式 随机调用 生命周期
获取子节点 $().find() -
事件响应 $().on() 生命周期+原生事件响应
维护方式 单文件维护 父子组件查找

在构建界面这一点上,react胜出一大截,利用jsx+data的方式构建界面快且理解容易,描述直接。但是在操作界面细节的时候,react则反过来,通过操作state来操作界面,虽然抽象来很多,却在理解直接性上差了很多。

综合之后,我觉得,jquery依然是更好的选择,除了在构建界面上比react差很多以外,其它方面都是比较令人满意的。特别是当项目大到一定复杂程度的时候,react的结构,会让项目维护难度加大。而jquery不存在这种情况,无论你项目多大,jquery都能一针扎在你要操作的那个DOM节点上。

有没有一种方案,可以优化jquery的界面构建过程?例如先通过数据抽象出virtual dom,然后通过virtual dom构建界面,构建过程还是通过jquery,后续的所有维护都通过jquery?

12:48:38 已有0条回复
152017.11

别再拿奇技淫巧搬砖了

132017.11

how to testing translated text when using react-i18next

When you using jest to test react application, you may have doubts about how to test components which are wrapped by react-i18next's translate function, and how could you verify the translation is applied to your application. Now let's talk about this topic.

1. testing a translated component

This is easy, according to react-i18next document here, you have several choices, but the best one I think is to do as want you did in your app.js:

const enzymeWrapper = mount(
 <Provider store={store}>
  <I18nextProvider i18n={i18n}>
   <ContactTable />
  </I18nextProvider>
 </Provider>
);

You even do not need a mock config (like react-redux), just do as what you have done in your app.js.

But here you will facing another problem if you use backend mode in your i18n initialize file.

2. verify translated text

You want to know whether i18next has translated your text in testing, so you do what I have told you in previous code. But if you use backend mode in your initialize file, you will find that your texts are not translated, the key in t function will be return.

The reason is when you use backend mode, jest will not request translations files from server side (there is no server side in jest testing), so the resolution is to convert backend mode to sync mode. Create a new initialize file for testing:

import i18n from "i18next";
import enCommonTranslations from "./locales/en/common.json";
import zhCommonTranslations from "./locales/en/common.json";
i18n
    .init({
        lng: "en",
        ns: ["common"],
        defaultNS: "common",
        resources: {
            en: {
                common: enCommonTranslations
            },
            zh: {
                common: zhCommonTranslations
            }
        },
        debug: false,
        interpolation: {
            escapeValue: false
        },
        react: {
            wait: false,
            nsMode: "fallback"
        }
    });
export default i18n;

Use this new file as initialize file in your testing, and you will be excited. All languages are perloaded before unit test, and now you can get translated text from your component.

16:01:02 已有0条回复
082017.11

How to bind scroll/mousewheel between different elements by jquery?

In a case, I want to sync scrollbar action between different element by jquery. For example, when scroll $a's scrollbar, or mouse wheel scroll down on $a, I want to scroll $b's scrollbar, or even scrollTop/scrollLeft when $b is overflow:hidden. Now let's do it:

let syncScrollTopA2B = ($a, $b) => {
  $a.on('scroll', function() {
    $b.scrollTop($a.scrollTop())
  })
}
let syncMousewheelTopA2B = ($a, $b) => {
  $a.on('mousewheel DOMMouseScroll', function(e) {
    let { deltaY } = e.originalEvent
    $a.scrollTop($a.scrollTop() + deltaY)
    $b.scrollTop($a.scrollTop())
  })
}
let syncTop = ($a, $b) => {
  syncScrollTopA2B($a, $b)
  syncMousewheelTopA2B($b, $a)
}
let syncScrollLeftA2B = ($a, $b) => {
  $a.on('scroll', function() {
    $b.scrollLeft($a.scrollLeft())
  })
}
let syncMousewheelLeftA2B = ($a, $b) => {
  $a.on('mousewheel DOMMouseScroll', function(e) {
    if (e.shiftKey) { // press shift key on your keyboard
      let { deltaX } = e.originalEvent
      $a.scrollLeft($a.scrollLeft() + deltaX)
      $b.scrollLeft($a.scrollLeft())
    }
  })
}
let syncLeft = ($a, $b) => {
  syncScrollLeftA2B($a, $b)
  syncMousewheelLeftA2B($b, $a)
}

When pull scrollbar on $a, scrollTop will sync to $b. The same with mouse wheel event. However, horizontal mouse wheel event is different, you should press shift on your keyboard at the same time.

11:35:46 已有0条回复
012017.11

Difference between 'data' and 'state' in Frontend

After using React for months, I have to say it is very different between 'data' and 'state'. However at the first glance, they are the same thing. Now let's talk about this topic.

DATA is a stable structured source type. 'stable' means it will now change unless you request a new data to cover the variable. 'structured' means it is not compatible in different system.

STATE is a fluxible/changeable temporary source type. 'changeable' means it can be changed by current code scope. 'temporary' means when current scope is destroyed it should be destroyed too.

However, we always make a reference from a state to a data. This make the relationship complex.

I think there are several rules for developers to keep in mind:

  1. you should not use state to change its refer data, a data which is referred by a state should only be used for response (render, calculation..), no change
  2. data should be managed in a redux store like manager tool, if you want to request new data, you should dispatch a action to the store and let the store notify the application, and the application subscribe a listener in which can get the new data
  3. change a state means change the view, visual view is based on state, one state, one view, never make it unexpected when using state.

This is what I thought about data and state.

16:59:40 已有0条回复