lishibo

lishibo 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

lishibo 赞了文章 · 10月13日

Redux异步解决方案之Redux-Thunk原理及源码解析

前段时间,我们写了一篇Redux源码分析的文章,也分析了跟React连接的库React-Redux的源码实现。但是在Redux的生态中还有一个很重要的部分没有涉及到,那就是Redux的异步解决方案。本文会讲解Redux官方实现的异步解决方案----Redux-Thunk,我们还是会从基本的用法入手,再到原理解析,然后自己手写一个Redux-Thunk来替换它,也就是源码解析。

Redux-Thunk和前面写过的ReduxReact-Redux其实都是Redux官方团队的作品,他们的侧重点各有不同:

Redux:是核心库,功能简单,只是一个单纯的状态机,但是蕴含的思想不简单,是传说中的“百行代码,千行文档”。

React-Redux:是跟React的连接库,当Redux状态更新的时候通知React更新组件。

Redux-Thunk:提供Redux的异步解决方案,弥补Redux功能的不足。

本文手写代码已经上传GitHub,大家可以拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/React/redux-thunk/src/myThunk.js

基本用法

还是以我们之前的那个计数器作为例子,为了让计数器+1,我们会发出一个action,像这样:

function increment() {
  return {
    type: 'INCREMENT'
  }
};

store.dispatch(increment());

原始的Redux里面,action creator必须返回plain object,而且必须是同步的。但是我们的应用里面经常会有定时器,网络请求等等异步操作,使用Redux-Thunk就可以发出异步的action

function increment() {
  return {
    type: 'INCREMENT'
  }
};

// 异步action creator
function incrementAsync() {
  return (dispatch) => {
    setTimeout(() => {
      dispatch(increment());
    }, 1000);
  }
}

// 使用了Redux-Thunk后dispatch不仅仅可以发出plain object,还可以发出这个异步的函数
store.dispatch(incrementAsync());

下面再来看个更实际点的例子,也是官方文档中的例子:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

// createStore的时候传入thunk中间件
const store = createStore(rootReducer, applyMiddleware(thunk));

// 发起网络请求的方法
function fetchSecretSauce() {
  return fetch('https://www.baidu.com/s?wd=Secret%20Sauce');
}

// 下面两个是普通的action
function makeASandwich(forPerson, secretSauce) {
  return {
    type: 'MAKE_SANDWICH',
    forPerson,
    secretSauce,
  };
}

function apologize(fromPerson, toPerson, error) {
  return {
    type: 'APOLOGIZE',
    fromPerson,
    toPerson,
    error,
  };
}

// 这是一个异步action,先请求网络,成功就makeASandwich,失败就apologize
function makeASandwichWithSecretSauce(forPerson) {
  return function (dispatch) {
    return fetchSecretSauce().then(
      (sauce) => dispatch(makeASandwich(forPerson, sauce)),
      (error) => dispatch(apologize('The Sandwich Shop', forPerson, error)),
    );
  };
}

// 最终dispatch的是异步action makeASandwichWithSecretSauce
store.dispatch(makeASandwichWithSecretSauce('Me'));

为什么要用Redux-Thunk

在继续深入源码前,我们先来思考一个问题,为什么我们要用Redux-Thunk,不用它行不行?再仔细看看Redux-Thunk的作用:

// 异步action creator
function incrementAsync() {
  return (dispatch) => {
    setTimeout(() => {
      dispatch(increment());
    }, 1000);
  }
}

store.dispatch(incrementAsync());

他仅仅是让dispath多支持了一种类型,就是函数类型,在使用Redux-Thunk前我们dispatchaction必须是一个纯对象(plain object),使用了Redux-Thunk后,dispatch可以支持函数,这个函数会传入dispatch本身作为参数。但是其实我们不使用Redux-Thunk也可以达到同样的效果,比如上面代码我完全可以不要外层的incrementAsync,直接这样写:

setTimeout(() => {
  store.dispatch(increment());
}, 1000);

这样写同样可以在1秒后发出增加的action,而且代码还更简单,那我们为什么还要用Redux-Thunk呢,他存在的意义是什么呢?stackoverflow对这个问题有一个很好的回答,而且是官方推荐的解释。我再写一遍也不会比他写得更好,所以我就直接翻译了:

----翻译从这里开始----

不要觉得一个库就应该规定了所有事情!如果你想用JS处理一个延时任务,直接用setTimeout就好了,即使你使用了Redux也没啥区别。Redux确实提供了另一种处理异步任务的机制,但是你应该用它来解决你很多重复代码的问题。如果你没有太多重复代码,使用语言原生方案其实是最简单的方案。

直接写异步代码

到目前为止这是最简单的方案,Redux也不需要特殊的配置:

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

(译注:这段代码的功能是显示一个通知,5秒后自动消失,也就是我们经常使用的toast效果,原作者一直以这个为例。)

相似的,如果你是在一个连接了Redux组件中使用:

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

唯一的区别就是连接组件一般不需要直接使用store,而是将dispatch或者action creator作为props注入,这两种方式对我们都没区别。

如果你不想写重复的action名字,你可以将这两个action抽取成action creator而不是直接dispatch一个对象:

// actions.js
export function showNotification(text) {
  return { type: 'SHOW_NOTIFICATION', text }
}
export function hideNotification() {
  return { type: 'HIDE_NOTIFICATION' }
}

// component.js
import { showNotification, hideNotification } from '../actions'

this.props.dispatch(showNotification('You just logged in.'))
setTimeout(() => {
  this.props.dispatch(hideNotification())
}, 5000)

或者你已经通过connect()注入了这两个action creator

this.props.showNotification('You just logged in.')
setTimeout(() => {
  this.props.hideNotification()
}, 5000)

到目前为止,我们没有使用任何中间件或者其他高级技巧,但是我们同样实现了异步任务的处理。

提取异步的Action Creator

使用上面的方式在简单场景下可以工作的很好,但是你可能已经发现了几个问题:

  1. 每次你想显示toast的时候,你都得把这一大段代码抄过来抄过去。
  2. 现在的toast没有id,这可能会导致一种竞争的情况:如果你连续快速的显示两次toast,当第一次的结束时,他会dispatchHIDE_NOTIFICATION,这会错误的导致第二个也被关掉。

为了解决这两个问题,你可能需要将toast的逻辑抽取出来作为一个方法,大概长这样:

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  // 给通知分配一个ID可以让reducer忽略非当前通知的HIDE_NOTIFICATION
  // 而且我们把计时器的ID记录下来以便于后面用clearTimeout()清除计时器
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

现在你的组件可以直接使用showNotificationWithTimeout,再也不用抄来抄去了,也不用担心竞争问题了:

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')  

但是为什么showNotificationWithTimeout()要接收dispatch作为第一个参数呢?因为他需要将action发给store。一般组件是可以拿到dispatch的,为了让外部方法也能dispatch,我们需要给他dispath作为参数。

如果你有一个单例的store,你也可以让showNotificationWithTimeout直接引入这个store然后dispatchaction

// store.js
export default createStore(reducer)

// actions.js
import store from './store'

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  const id = nextNotificationId++
  store.dispatch(showNotification(id, text))

  setTimeout(() => {
    store.dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout('You just logged in.')

// otherComponent.js
showNotificationWithTimeout('You just logged out.') 

这样做看起来不复杂,也能达到效果,但是我们不推荐这种做法!主要原因是你的store必须是单例的,这让Server Render实现起来很麻烦。在Server端,你会希望每个请求都有自己的store,比便于不同的用户可以拿到不同的预加载内容。

一个单例的store也让单元测试很难写。测试action creator的时候你很难mockstore,因为他引用了一个具体的真实的store。你甚至不能从外部重置store状态。

所以从技术上来说,你可以从一个module导出单例的store,但是我们不鼓励这样做。除非你确定加肯定你以后都不会升级Server Render。所以我们还是回到前面一种方案吧:

// actions.js

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')  

这个方案就可以解决重复代码和竞争问题。

Thunk中间件

对于简单项目,上面的方案应该已经可以满足需求了。

但是对于大型项目,你可能还是会觉得这样使用并不方便。

比如,似乎我们必须将dispatch作为参数传递,这让我们分隔容器组件和展示组件变得更困难,因为任何发出异步Redux action的组件都必须接收dispatch作为参数,这样他才能将它继续往下传。你也不能仅仅使用connect()来绑定action creator,因为showNotificationWithTimeout()并不是一个真正的action creator,他返回的也不是Redux action

还有个很尴尬的事情是,你必须记住哪个action cerator是同步的,比如showNotification,哪个是异步的辅助方法,比如showNotificationWithTimeout。这两个的用法是不一样的,你需要小心的不要传错了参数,也不要混淆了他们。

这就是我们为什么需要找到一个“合法”的方法给辅助方法提供dispatch参数,并且帮助Redux区分出哪些是异步的action creator,好特殊处理他们

如果你的项目中面临着类似的问题,欢迎使用Redux Thunk中间件。

简单来说,React Thunk告诉Redux怎么去区分这种特殊的action----他其实是个函数:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk)
)

// 这个是普通的纯对象action
store.dispatch({ type: 'INCREMENT' })

// 但是有了Thunk,他就可以识别函数了
store.dispatch(function (dispatch) {
  // 这个函数里面又可以dispatch很多action
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })

  setTimeout(() => {
    // 异步的dispatch也可以
    dispatch({ type: 'DECREMENT' })
  }, 1000)
})

如果你使用了这个中间件,而且你dispatch的是一个函数,React Thunk会自己将dispatch作为参数传进去。而且他会将这些函数action“吃了”,所以不用担心你的reducer会接收到奇怪的函数参数。你的reducer只会接收到纯对象action,无论是直接发出的还是前面那些异步函数发出的。

这个看起来好像也没啥大用,对不对?在当前这个例子确实是的!但是他让我们可以像定义一个普通的action creator那样去定义showNotificationWithTimeout

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

注意这里的showNotificationWithTimeout跟我们前面的那个看起来非常像,但是他并不需要接收dispatch作为第一个参数。而是返回一个函数来接收dispatch作为第一个参数。

那在我们的组件中怎么使用这个函数呢,我们当然可以这样写:

// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)

这样我们直接调用了异步的action creator来得到内层的函数,这个函数需要dispatch做为参数,所以我们给了他dispatch参数。

然而这样使用岂不是更尬,还不如我们之前那个版本的!我们为啥要这么干呢?

我之前就告诉过你:只要使用了Redux Thunk,如果你想dispatch一个函数,而不是一个纯对象,这个中间件会自己帮你调用这个函数,而且会将dispatch作为第一个参数传进去。

所以我们可以直接这样干:

// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))

最后,对于组件来说,dispatch一个异步的action(其实是一堆普通action)看起来和dispatch一个普通的同步action看起来并没有啥区别。这是个好现象,因为组件就不应该关心那些动作到底是同步的还是异步的,我们已经将它抽象出来了。

注意因为我们已经教了Redux怎么区分这些特殊的action creator(我们称之为thunk action creator),现在我们可以在任何普通的action creator的地方使用他们了。比如,我们可以直接在connect()中使用他们:

// actions.js

function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

// component.js

import { connect } from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)

在Thunk中读取State

通常来说,你的reducer会包含计算新的state的逻辑,但是reducer只有当你dispatchaction才会触发。如果你在thunk action creator中有一个副作用(比如一个API调用),某些情况下,你不想发出这个action该怎么办呢?

如果没有Thunk中间件,你需要在组件中添加这个逻辑:

// component.js
if (this.props.areNotificationsEnabled) {
  showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}

但是我们提取action creator的目的就是为了集中这些在各个组件中重复的逻辑。幸运的是,Redux Thunk提供了一个读取当前store state的方法。那就是除了传入dispatch参数外,他还会传入getState作为第二个参数,这样thunk就可以读取store的当前状态了。

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch, getState) {
    // 不像普通的action cerator,这里我们可以提前退出
    // Redux不关心这里的返回值,没返回值也没关系
    if (!getState().areNotificationsEnabled) {
      return
    }

    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

但是不要滥用这种方法!如果你需要通过检查缓存来判断是否发起API请求,这种方法就很好,但是将你整个APP的逻辑都构建在这个基础上并不是很好。如果你只是用getState来做条件判断是否要dispatch action,你可以考虑将这些逻辑放到reducer里面去。

下一步

现在你应该对thunk的工作原理有了一个基本的概念,如果你需要更多的例子,可以看这里:https://redux.js.org/introduction/examples#async

你可能会发现很多例子都返回了Promise,这个不是必须的,但是用起来却很方便。Redux并不关心你的thunk返回了什么值,但是他会将这个值通过外层的dispatch()返回给你。这就是为什么你可以在thunk中返回一个Promise并且等他完成:

dispatch(someThunkReturningPromise()).then(...)

另外你还可以将一个复杂的thunk action creator拆分成几个更小的thunk action creator。这是因为thunk提供的dispatch也可以接收thunk,所以你可以一直嵌套的dispatch thunk。而且结合Promise的话可以更好的控制异步流程。

在一些更复杂的应用中,你可能会发现你的异步控制流程通过thunk很难表达。比如,重试失败的请求,使用token进行重新授权认证,或者在一步一步的引导流程中,使用这种方式可能会很繁琐,而且容易出错。如果你有这些需求,你可以考虑下一些更高级的异步流程控制库,比如Redux Saga或者Redux Loop。可以看看他们,评估下,哪个更适合你的需求,选一个你最喜欢的。

最后,不要使用任何库(包括thunk)如果你没有真实的需求。记住,我们的实现都是要看需求的,也许你的需求这个简单的方案就能满足:

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

不要跟风尝试,除非你知道你为什么需要这个!

----翻译到此结束----

StackOverflow的大神Dan Abramov对这个问题的回答实在太细致,太到位了,以致于我看了之后都不敢再写这个原因了,以此翻译向大神致敬,再贴下这个回答的地址:https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559

PS: Dan Abramov是Redux生态的核心作者,这几篇文章讲的ReduxReact-ReduxRedux-Thunk都是他的作品。

源码解析

上面关于原因的翻译其实已经将Redux适用的场景和原理讲的很清楚了,下面我们来看看他的源码,自己仿写一个来替换他。照例我们先来分析下要点:

  1. Redux-Thunk是一个Redux中间件,所以他遵守Redux中间件的范式。
  2. thunk是一个可以dispatch的函数,所以我们需要改写dispatch让他接受函数参数。

Redux中间件范式

在我前面那篇讲Redux源码的文章讲过中间件的范式以及Redux中这块源码是怎么实现的,没看过或者忘了的朋友可以再去看看。我这里再简单提一下,一个Redux中间件结构大概是这样:

function logger(store) {
  return function(next) {
    return function(action) {
      console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result
    }
  }
}

这里注意几个要点:

  1. 一个中间件接收store作为参数,会返回一个函数
  2. 返回的这个函数接收老的dispatch函数作为参数(也就是代码中的next),会返回一个新的函数
  3. 返回的新函数就是新的dispatch函数,这个函数里面可以拿到外面两层传进来的store和老dispatch函数

仿照这个范式,我们来写一下thunk中间件的结构:

function thunk(store) {
  return function (next) {
    return function (action) {
      // 先直接返回原始结果
      let result = next(action);
      return result
    }
  }
}

处理thunk

根据我们前面讲的,thunk是一个函数,接收dispatch getState两个参数,所以我们应该将thunk拿出来运行,然后给他传入这两个参数,再将它的返回值直接返回就行。

function thunk(store) {
  return function (next) {
    return function (action) {
      // 从store中解构出dispatch, getState
      const { dispatch, getState } = store;

      // 如果action是函数,将它拿出来运行,参数就是dispatch和getState
      if (typeof action === 'function') {
        return action(dispatch, getState);
      }

      // 否则按照普通action处理
      let result = next(action);
      return result
    }
  }
}

接收额外参数withExtraArgument

Redux-Thunk还提供了一个API,就是你在使用applyMiddleware引入的时候,可以使用withExtraArgument注入几个自定义的参数,比如这样:

const api = "http://www.example.com/sandwiches/";
const whatever = 42;

const store = createStore(
  reducer,
  applyMiddleware(thunk.withExtraArgument({ api, whatever })),
);

function fetchUser(id) {
  return (dispatch, getState, { api, whatever }) => {
    // 现在你可以使用这个额外的参数api和whatever了
  };
}

这个功能要实现起来也很简单,在前面的thunk函数外面再包一层就行:

// 外面再包一层函数createThunkMiddleware接收额外的参数
function createThunkMiddleware(extraArgument) {
  return function thunk(store) {
    return function (next) {
      return function (action) {
        const { dispatch, getState } = store;

        if (typeof action === 'function') {
          // 这里执行函数时,传入extraArgument
          return action(dispatch, getState, extraArgument);  
        }

        let result = next(action);
        return result
      }
    }
  }
}

然后我们的thunk中间件其实相当于没传extraArgument

const thunk = createThunkMiddleware();

而暴露给外面的withExtraArgument函数就直接是createThunkMiddleware了:

thunk.withExtraArgument = createThunkMiddleware;

源码解析到此结束。啥,这就完了?是的,这就完了!Redux-Thunk就是这么简单,虽然背后的思想比较复杂,但是代码真的只有14行!我当时也震惊了,来看看官方源码吧:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

总结

  1. 如果说Redux是“百行代码,千行文档”,那Redux-Thunk就是“十行代码,百行思想”。
  2. Redux-Thunk最主要的作用是帮你给异步action传入dispatch,这样你就不用从调用的地方手动传入dispatch,从而实现了调用的地方和使用的地方的解耦。
  3. ReduxRedux-Thunk让我深深体会到什么叫“编程思想”,编程思想可以很复杂,但是实现可能并不复杂,但是却非常有用。
  4. 在我们评估是否要引入一个库时最好想清楚我们为什么要引入这个库,是否有更简单的方案。

本文手写代码已经上传GitHub,大家可以拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/React/redux-thunk/src/myThunk.js

参考资料

Redux-Thunk文档:https://github.com/reduxjs/redux-thunk

Redux-Thunk源码: https://github.com/reduxjs/redux-thunk/blob/master/src/index.js

Dan Abramov在StackOverflow上的回答: https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。

作者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges

我也搞了个公众号[进击的大前端],不打广告,不写水文,只发高质量原创,欢迎关注~
image.png

查看原文

赞 19 收藏 15 评论 1

lishibo 赞了文章 · 10月13日

适合Vue用户的React教程,你值得拥有

双节旅游人如山,不如家中代码闲。
学以致用加班少,王者荣耀家中玩。

小编日常工作中使用的是Vue,对于React只是做过简单的了解,并没有做过深入学习。趁着这个双节假期,小编决定好好学一学React,今天这篇文章就是小编在学习React之后,将ReactVue的用法做的一个对比,通过这个对比,方便使用Vue的小伙伴可以快速将Vue中的写法转换为React的写法。

本文首发于公众号【前端有的玩】,玩前端,面试找工作,就在【前端有的玩】,欢迎关注

插槽,在React中没找到??

在使用Vue的时候,插槽是一个特别常用的功能,通过定义插槽,可以在调用组件的时候将外部的内容传入到组件内部,显示到指定的位置。在Vue中,插槽分为默认插槽,具名插槽和作用域插槽。其实不仅仅Vue,在React中其实也有类似插槽的功能,只是名字不叫做插槽,下面我将通过举例来说明。

默认插槽

现在项目需要开发一个卡片组件,如下图所示,卡片可以指定标题,然后卡片内容可以用户自定义,这时候对于卡片内容来说,就可以使用插槽来实现,下面我们就分别使用VueReact来实现这个功能

Vue实现

  1. 首先实现一个card组件,如下代码所示

    <template>
      <div class="card">
        <div class="card__title">
          <span>{{ title }}</span>
        </div>
        <div class="card__body">
          <slot></slot>
        </div>
      </div>
    </template>
    <script>
    export default {
      props: {
        title: {
          type: String,
          default: ''
        }
      }
    }
    </script>
    

    可以看到上面我们使用了<slot></slot>,这个就是组件的默认插槽,在使用组件的时候,传入的内容将会被放到<slot></slot>所在位置

  2. 在外部使用定义的card组件

    <template>
      <div>
        <my-card>
          <div>我将被放在card组件的默认插槽里面</div>
        </my-card>
      </div>
    </template>
    <script>
    import MyCard from '../components/card'
    export default {
      components: {
        MyCard
      }
    }
    </script>
    

    如上代码,就可以使用组件的默认插槽将外部的内容应用到组件里面指定的位置了。

React实现

虽然在React里面没有插槽的概念,但是React里面也可以通过props.children拿到组件标签内部的子元素的,就像上面代码<my-card>标签内的子元素,通过这个我们也可以实现类似Vue默认插槽的功能,一起看看代码。

  1. 使用React定义Card组件

    import React from 'react'
    
    export interface CardProps {
      title: string,
      children: React.ReactNode
    }
    
    export default function(props: CardProps) {
    
      return (
        <div className="card">
          <div className="card__title">
            <span>{props.title}</span>
          </div>
          <div className="card__body">
            {/**每个组件都可以获取到 props.children。它包含组件的开始标签和结束标签之间的内容 */}
            {props.children}
          </div>
        </div>
      );
    }
    1. 在外部使用Card组件
    import React from 'react'
    import Card from './components/Card'
    
    export default function () {
    
      return (
        <div>
          <Card title="标题">
            <div>我将被放在card组件的body区域内容</div>
          </Card>
        </div>
      );
    }

具名插槽

继续以上面的Card组件为例,假如我们现在需求发生了变化,组件的title也可以使用插槽,这时候对于Vue就可以使用具名插槽了,而React也是有办法实现的哦。

Vue实现

Vue的具名插槽主要解决的是一个组件需要多个插槽的场景,其实现是为<slot>添加name属性来实现了。

  1. 我们就上面的需求对card组件进行修改
<template>
  <div class="card">
    <div class="card__title">
      <!--如果传入了title,则使用title属性,否则使用具名插槽-->
      <span v-if="title">{{ title }}</span>
      <slot v-else name="title"></slot>
    </div>
    <div class="card__body">
      <!--对于内容区域依然使用默认插槽-->
      <slot></slot>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    title: {
      type: String,
      default: ''
    }
  }
}
</script>
  1. card组件修改完之后,我们再去调整一下使用card组件的地方
<template>
  <div>
    <my-card>
      <!--通过v-slot:title 使用具名插槽-->
      <template v-slot:title>
        <span>这里是标题</span>
      </template>
      <div>我将被放在card组件的默认插槽里面</div>
    </my-card>
  </div>
</template>
<script>
import MyCard from '../components/card'
export default {
  components: {
    MyCard
  }
}
</script>
React实现

React连插槽都没有, 更别提具名插槽了,但是没有不代表不能模拟出来。对于Reactprops,我们不仅仅可以传入普通的属性,还可以传入一个函数,这时候我们就可以在传入的这个函数里面返回JSX,从而就实现了具名插槽的功能。

  1. 对原有的Card组件进行修改
import React from 'react'

export interface CardProps {
  title?: string,
  // 加入了一个renderTitle属性,属性类型是Function
  renderTitle?: Function,
  children: React.ReactNode
}

export default function(props: CardProps) {

  const {title, renderTitle} = props
  // 如果指定了renderTtile,则使用renderTitle,否则使用默认的title
  let titleEl = renderTitle ? renderTitle() : <span>{title}</span>

  return (
    <div className="card">
      <div className="card__title">{titleEl}</div>
      <div className="card__body">
        {/**每个组件都可以获取到 props.children。它包含组件的开始标签和结束标签之间的内容 */}
        {props.children}
      </div>
    </div>
  );
}
  1. 这时候就可以在外部自定义title
import React from 'react'
import Card from './components/Card'

export default function () {
  return (
    <div>
      <Card  renderTitle={
        () => {
          return <span>我是自定义的标题</span>
        }
      }>
        <div>我将被放在card组件的body区域内容</div>
      </Card>
    </div>
  );
}

作用域插槽

有时让插槽内容能够访问子组件中才有的数据是很有用的,这个就是Vue提供作用域插槽的原因。我们继续使用上面的Card组件为例,现在我基于上面的卡片组件开发了一个人员信息卡片组件,用户直接使用人员信息卡片组件就可以将人员信息显示到界面中,但是在某些业务模块需要自定义人员信息显示方式,这时候我们就需要使用到作用域插槽了。

Vue实现
  1. 实现用户信息卡片组件,里面使用了作用域插槽
<template>
  <custom-card title="人员信息卡片">
    <div class="content">
      <!--这里使用了作用域插槽,将userInfo传出去了-->
      <slot name="userInfo" :userInfo="userInfo">
        <!--如果没有使用插槽,则显示默认内容-->
        <span>姓名: {{ userInfo.name }}</span>
        <span>性别: {{ userInfo.sex }}</span>
        <span>年龄: {{ userInfo.age }}</span>
      </slot>
    </div>
  </custom-card>
</template>
<script>
import CustomCard from '../card'
export default {
  components: {
    CustomCard
  },
  data() {
    return {
      userInfo: {
        name: '张三',
        sex: '男',
        age: 25
      }
    }
  }
}
</script>
  1. 在外部使用人员信息组件
<template>
  <div>
    <user-card>
      <template v-slot:userInfo="{ userInfo }">
        <div class="custom-user">
          <ul>
            <li>姓名: {{ userInfo.name }}</li>
            <li>年龄: {{ userInfo.age }}</li>
          </ul>
        </div>
      </template>
    </user-card>
  </div>
</template>
<script>
import UserCard from '../components/user-card'
export default {
  components: {
    UserCard
  }
}
</script>
React实现

在具名插槽那一小节我们通过给组件传入了一个函数,然后在函数中返回JSX的方式来模拟了具名插槽,那么对于作用域插槽,我们依然可以使用函数的这种方式,而作用域插槽传递的参数我们可以使用给函数传参的方式来替代

  1. 实现人员信息卡片组件

    import React, { useState } from 'react'
    
    import Card from './Card'
    
    interface UserCardProps {
      renderUserInfo?: Function
    }
    
    export interface UserInfo {
      name: string;
      age: number;
      sex: string;
    }
    
    export default function(props: UserCardProps) {
      const [userInfo] = useState<UserInfo>({
        name: "张三",
        age: 25,
        sex: "男",
      });
    
      const content = props.renderUserInfo ? (
        props.renderUserInfo(userInfo)
      ) : (
        <div>
          <span>姓名: {userInfo.name}</span>
          <span>年龄: {userInfo.age}</span>
          <span>性别: {userInfo.sex}</span>
        </div>
      );
    
      return <Card title="人员信息">
        {content}
      </Card>
    }
  2. 在外部使用人员信息卡片组件

    import React from 'react'
    import UserCard, { UserInfo } from "./components/UserCard";
    
    export default function () {
    
      return (
        <div>
          <UserCard
            renderUserInfo={(userInfo: UserInfo) => {
              return (
                <ul>
                  <li>姓名: {userInfo.name}</li>
                </ul>
              );
            }}
          ></UserCard>
        </div>
      );
    }

Context, React中的provide/inject

通常我们在项目开发中,对于多组件之间的状态管理,在Vue中会使用到Vuex,在React中会使用到redux或者Mobx,但对于小项目来说,使用这些状态管理库就显得比较大材小用了,那么在不使用这些库的情况下,如何去完成数据管理呢?比如面试最常问的祖孙组件通信。在Vue中我们可以使用provide/inject,在React中我们可以使用Context

假设有这样一个场景,系统现在需要提供一个换肤功能,用户可以切换皮肤,现在我们分别使用VueReact来实现这个功能。

Vue中的provide/inject

Vue中我们可以使用provide/inject来实现跨多级组件进行传值,就以上面所说场景为例,我们使用provide/inject来实现以下

首先,修改App.vue内容为以下内容

<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script>
export default {
  data() {
    return {
      themeInfo: {
        theme: 'dark'
      }
    }
  },
  provide() {
    return {
      theme: this.themeInfo
    }
  }
}
</script>

然后在任意层级的子组件中像下面这样使用

<template>
  <div :class="`child-${theme.theme}`">
  </div>
</template>
<script>
export default {
  inject: ['theme']
}
</script>

这样就可以实现theme在所有子组件中进行共享了

React中的Context

Vue中我们使用provide/inject实现了组件跨层级传值功能,在React中也提供了类似的功能即Context,下面我们使用Context来实现相同的功能。

在项目src目录下新建context目录,添加MyContext.js文件,然后添加以下内容

import {createContext} from 'react'
// 定义 MyContext,指定默认的主题为`light`
export const MyContext = createContext({
  theme: 'light'
})

MyContext提供了一个Provider,通过Provider可以将theme共享到所有的子组件。现在我们在所有的组件的共同父组件比如App.js上面添加MyContext.Providertheme共享出去

import { MyContext } from '@/context/MyContext';

export default function() {
  
  const [theme, setTheme] = useState('dark')
  
  return (
    <MyContext.Provider
        value={{
          theme
        }}
      >
        <Children></Children>
     </MyContext.Provider>
    )
  }

然后这时候就可以直接在所有的子组件里面使用定义的主题theme

import React, { useContext } from 'react'
import { MyContext } from '@/context/MyContext';

export default function() {
   const {theme}  = useContext(MyContext)
   return <div className={`child-${theme}`}>
}

没有了v-model,但也不影响使用

我们知道ReactVue都是单向数据流的,即数据的流向都是由外层向内层组件进行传递和更新的,比如下面这段React代码就是标准的单向数据流.

import React, { useState } from "react";

export default function(){
  const [name] = useState('子君')
  return <input value={name}></input>
}

vue中使用v-model

如上代码,我们在通过通过value属性将外部的值传递给了input组件,这个就是一个简单的单向数据流。但是在使用Vue的时候,还有两个比较特殊的语法糖v-model.sync,这两个语法糖可以让Vue组件拥有双向数据绑定的能力,比如下面的代码

<template>
   <input v-model="name"/>
</template>
<script>
  export default {
    data() {
      return {
        name:'子君'
      }
    }
  }
</script>

通过v-model,当用户修改input的值的时候,外部的name的值也将同步被修改。但这是Vue的语法糖啊,React是不支持的,所以React应该怎么办呢?这时候再想想自定义v-modelv-model实际上是通过定义value属性同时监听input事件来实现的,比如这样:

<template>
  <div class="custom-input">
     <input :value="value" @input="$_handleChange"/>
  </div>
</template>
<script>
  export default {
    props:{
      value:{
        type: String,
        default: ''
      }
    },
    methods:{
      $_handleChange(e) {
        this.$emit('input', e.target.value)
      }
    }
  }
</script>

react寻找v-model替代方案

同理,React虽然没有v-model语法糖,但是也可以通过传入属性然后监听事件来实现数据的双向绑定。

import React, { useState } from 'react'

export default function() {
  const [name, setName] = useState('子君')

  const handleChange = (e) => {
    setName(e.target.value)
  }
  return <div>
    <input value={name} onChange={handleChange}></input>
  </div>
}

小编刚开始使用react,感觉没有v-model就显得比较麻烦,不过麻烦归麻烦,代码改写也要写。就像上文代码一样,每一个表单元素都需要监听onChange事件,越发显得麻烦了,这时候就可以考虑将多个onChange事件合并成一个,比如像下面代码这样

import React, { useState } from 'react'

export default function () {
  const [name, setName] = useState('子君')
  const [sex, setSex] = useState('男')

  const handleChange = (e:any, method: Function) => {
    method(e.target.value)
  }
  return <div>
    <input value={name} onChange={(e) => handleChange(e, setName)}></input>
    <input value={sex} onChange={(e) => handleChange(e, setSex)}></input>
  </div>
}

没有了指令,我感觉好迷茫

Vue中我们一般绘制页面都会使用到templatetemplate里面提供了大量的指令帮助我们完成业务开发,但是在React中使用的是JSX,并没有指令,那么我们应该怎么做呢?下面我们就将Vue中最常用的一些指令转换为JSX里面的语法(注意: 在Vue中也可以使用JSX)

v-showv-if

Vue中我们隐藏显示元素可以使用v-show或者v-if,当然这两者的使用场景是有所不同的,v-show是通过设置元素的display样式来显示隐藏元素的,而v-if隐藏元素是直接将元素从dom中移除掉。

  1. 看一下Vue中的v-showv-if的用法

    <template>
      <div>
        <span v-show="showName">姓名:{{ name }}</span>
        <span v-if="showDept">{{ dept }}</span>
      </div>
    </template>
    <script>
    export default {
      data() {
        return {
          name: '子君',
          dept: '银河帝国',
          showName: false,
          showDept: true
        }
      }
    }
    </script>
    
  2. v-showv-if转换为JSX中的语法

    Vue中指令是为了在template方便动态操作数据而存在的,但是到了React中我们写的是JSX,可以直接使用JS,所以指令是不需要存在的,那么上面的v-show,v-if如何在JSX中替代呢

    import React, { useState } from 'react'
    
    export default function() {
      const [showName] = useState(false)
    
      const [showDept] = useState(true)
    
      const [userInfo] = useState({
        name:'子君',
        dept: '银河帝国'
      })
    
      return (
        <div>
          {/**模拟 v-show */}
          <span style={{display: showName ? 'block' : 'none'}}>{userInfo.name}</span>
          {/**模拟 v-if */}
          {showDept ? <span>{userInfo.dept}</span>: undefined}
        </div>
      )
    }

v-for

v-forVue中是用来遍历数据的,同时我们在使用v-for的时候需要给元素指定keykey的值一般是数据的id或者其他唯一且固定的值。不仅在Vue中,在React中也是存在key的,两者的key存在的意义基本一致,都是为了优化虚拟DOMdiff算法而存在的。

  1. Vue中使用v-for

    <template>
      <div>
        <ul>
          <li v-for="item in list" :key="item.id">
            {{ item.name }}
          </li>
        </ul>
      </div>
    </template>
    <script>
    export default {
      data() {
        return {
          list: [
            {
              id: 1,
              name: '子君'
            },
            {
              id: '2',
              name: '张三'
            },
            {
              id: '3',
              name: '李四'
            }
          ]
        }
      }
    }
    </script>
    
  2. React中使用v-for的替代语法

    react中虽然没有v-for,但是JSX中可以直接使用JS,所以我们可以直接遍历数组

    import React from 'react'
    
    export default function() {
      const data = [
        {
          id: 1,
          name: "子君",
        },
        {
          id: "2",
          name: "张三",
        },
        {
          id: "3",
          name: "李四",
        },
      ];
    
      return (
        <div>
          <ul>
            {
            data.map(item => {
              return <li key={item.id}>{item.name}</li>
            })
          }
          </ul>
        </div>
      )
    }

v-bindv-on

v-bindVue中是动态绑定属性的,v-on是用于监听事件的,因为React也有属性和事件的概念,所以我们在React也能发现可替代的方式。

  1. Vue中使用v-bindv-on

    <template>
      <div>
        <!--:value是v-bind:value的简写, @input是v-on:input的简写-->
        <input :value="value" @input="handleInput" />
      </div>
    </template>
    <script>
    export default {
      data() {
        return {
          value: '子君'
        }
      },
      methods: {
        handleInput(e) {
          this.value = e.target.value
        }
      }
    }
    </script>
    
  2. React中寻找替代方案

    Vue中,作者将事件和属性进行了分离,但是在React中,其实事件也是属性,所以在本小节我们不仅看一下如何使用属性和事件,再了解一下如何在React中自定义事件

    • 开发一个CustomInput组件

      import React from 'react'
      
      export interface CustomInputProps {
        value: string;
        //可以看出 onChange是一个普通的函数,也被定义到了组件的props里面了
        onChange: ((value: string,event: React.ChangeEvent<HTMLInputElement>) => void) | undefined;
      }
      
      export default function(props: CustomInputProps) {
        
        function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
          // props.onChange是一个属性,也是自定义的一个事件
          props.onChange && props.onChange(e.target.value, e)
        }
      
        return (
          <input value={props.value} onChange={handleChange}></input>
        )
      }
    • 使用CustomInput组件

      import React, { useState } from 'react'
      
      import CustomInput from './components/CustomInput'
      
      export default function() {
       const [value, setValue] =  useState('')
      
       function handleChange(value: string) {
         setValue(value)
       }
      
        return (
          <div>
            <CustomInput value={value} onChange={handleChange}></CustomInput>
          </div>
        )
      }

总结

刚开始从Vue转到React的时候,其实是有点不适应的,但是当慢慢的习惯之后,就会发现VueReact是存在很多共性的,可以参考的去学习。当然无论Vue还是React,上手比较快,但是想深入学习还是需要下功夫的,后续小编将会对VueReact的用法在做更深入的介绍,敬请期待。

查看原文

赞 23 收藏 13 评论 0

lishibo 收藏了文章 · 9月21日

浅谈 React 中的 XSS 攻击

70 篇原创好文~
本文首发于政采云前端团队博客:浅谈 React 中的 XSS 攻击

前言

前端一般会面临 XSS 这样的安全风险,但随着 React 等现代前端框架的流行,使我们在平时开发时不用太关注安全问题。以 React 为例,React 从设计层面上就具备了很好的防御 XSS 的能力。本文将以源码角度,看看 React 做了哪些事情来实现这种安全性的。

XSS 攻击是什么

Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。XSS 攻击通常指的是利用网页的漏洞,攻击者通过巧妙的方法注入 XSS 代码到网页,因为浏览器无法分辨哪些脚本是可信的,导致 XSS 脚本被执行。XSS 脚本通常能够窃取用户数据并发送到攻击者的网站,或者冒充用户,调用目标网站接口并执行攻击者指定的操作。

XSS 攻击类型

反射型 XSS

  • XSS 脚本来自当前 HTTP 请求
  • 当服务器在 HTTP 请求中接收数据并将该数据拼接在 HTML 中返回时,例子:
   // 某网站具有搜索功能,该功能通过 URL 参数接收用户提供的搜索词:
   https://xxx.com/search?query=123
   // 服务器在对此 URL 的响应中回显提供的搜索词:
   <p>您搜索的是: 123</p>
   // 如果服务器不对数据进行转义等处理,则攻击者可以构造如下链接进行攻击:
   https://xxx.com/search?query=<img data-original="empty.png" onerror ="alert('xss')">
   // 该 URL 将导致以下响应,并运行 alert('xss'):
   <p>您搜索的是: <img data-original="empty.png" onerror ="alert('xss')"></p>
   // 如果有用户请求攻击者的 URL ,则攻击者提供的脚本将在用户的浏览器中执行。

存储型 XSS

  • XSS 脚本来自服务器数据库中
  • 攻击者将恶意代码提交到目标网站的数据库中,普通用户访问网站时服务器将恶意代码返回,浏览器默认执行,例子:
   // 某个评论页,能查看用户评论。
   // 攻击者将恶意代码当做评论提交,服务器没对数据进行转义等处理
   // 评论输入:
   <textarea>
      <img data-original="empty.png" onerror ="alert('xss')">
   </textarea>
   // 则攻击者提供的脚本将在所有访问该评论页的用户浏览器执行

DOM 型 XSS

该漏洞存在于客户端代码,与服务器无关

  • 类似反射型,区别在于 DOM 型 XSS 并不会和后台进行交互,前端直接将 URL 中的数据不做处理并动态插入到 HTML 中,是纯粹的前端安全问题,要做防御也只能在客户端上进行防御。

React 如何防止 XSS 攻击

无论使用哪种攻击方式,其本质就是将恶意代码注入到应用中,浏览器去默认执行。React 官方中提到了 React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串,因此恶意代码无法成功注入,从而有效地防止了 XSS 攻击。我们具体看下:

自动转义

React 在渲染 HTML 内容和渲染 DOM 属性时都会将 "'&<> 这几个字符进行转义,转义部分源码如下:

for (index = match.index; index < str.length; index++) {
    switch (str.charCodeAt(index)) {
      case 34: // "
        escape = '&quot;';
        break;
      case 38: // &
        escape = '&amp;';
        break;
      case 39: // '
        escape = '&#x27;';
        break;
      case 60: // <
        escape = '&lt;';
        break;
      case 62: // >
        escape = '&gt;';
        break;
      default:
        continue;
    }
  }

这段代码是 React 在渲染到浏览器前进行的转义,可以看到对浏览器有特殊含义的字符都被转义了,恶意代码在渲染到 HTML 前都被转成了字符串,如下:

// 一段恶意代码
<img data-original="empty.png" onerror ="alert('xss')"> 
// 转义后输出到 html 中
&lt;img data-original=&quot;empty.png&quot; onerror =&quot;alert(&#x27;xss&#x27;)&quot;&gt; 

这样就有效的防止了 XSS 攻击。

JSX 语法

JSX 实际上是一种语法糖,Babel 会把 JSX 编译成 React.createElement() 的函数调用,最终返回一个 ReactElement,以下为这几个步骤对应的代码:

// JSX
const element = (
  <h1 className="greeting">
      Hello, world!
  </h1>
);
// 通过 babel 编译后的代码
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);
// React.createElement() 方法返回的 ReactElement
const element = {
  $$typeof: Symbol('react.element'),
  type: 'h1',
  key: null,
  props: {
    children: 'Hello, world!',
        className: 'greeting'   
  }
  ...
}

我们可以看到,最终渲染的内容是在 Children 属性中,那了解了 JSX 的原理后,我们来试试能否通过构造特殊的 Children 进行 XSS 注入,来看下面一段代码:

const storedData = `{
    "ref":null,
    "type":"body",
    "props":{
        "dangerouslySetInnerHTML":{
            "__html":"<img data-original=\"empty.png\" onerror =\"alert('xss')\"/>"
        }
    }
}`;
// 转成 JSON
const parsedData = JSON.parse(storedData);
// 将数据渲染到页面
render () {
    return <span> {parsedData} </span>; 
}

这段代码中, 运行后会报以下错误,提示不是有效的 ReactChild。

Uncaught (in promise) Error: Objects are not valid as a React child (found: object with keys {ref, type, props}). If you meant to render a collection of children, use an array instead.

那究竟是哪里出问题了?我们看一下 ReactElement 的源码:

const symbolFor = Symbol.for;
REACT_ELEMENT_TYPE = symbolFor('react.element');
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // 这个 tag 唯一标识了此为 ReactElement
    $$typeof: REACT_ELEMENT_TYPE,
    // 元素的内置属性
    type: type,
    key: key,
    ref: ref,
    props: props,
    // 记录创建此元素的组件
    _owner: owner,
  };
  ...
  return element;
}

注意到其中有个属性是 $$typeof`,它是用来标记此对象是一个 `ReactElement`,React 在进行渲染前会通过此属性进行校验,校验不通过将会抛出上面的错误。React 利用这个属性来防止通过构造特殊的 Children 来进行的 XSS 攻击,原因是 `$$typeof 是个 Symbol 类型,进行 JSON 转换后会 Symbol 值会丢失,无法在前后端进行传输。如果用户提交了特殊的 Children,也无法进行渲染,利用此特性,可以防止存储型的 XSS 攻击。

在 React 中可引起漏洞的一些写法

使用 dangerouslySetInnerHTML

dangerouslySetInnerHTML 是 React 为浏览器 DOM 提供 innerHTML 的替换方案。通常来讲,使用代码直接设置 HTML 存在风险,因为很容易使用户暴露在 XSS 攻击下,因为当使用 dangerouslySetInnerHTML 时,React 将不会对输入进行任何处理并直接渲染到 HTML 中,如果攻击者在 dangerouslySetInnerHTML 传入了恶意代码,那么浏览器将会运行恶意代码。看下源码:

function getNonChildrenInnerMarkup(props) {
  const innerHTML = props.dangerouslySetInnerHTML; // 有dangerouslySetInnerHTML属性,会不经转义就渲染__html的内容
  if (innerHTML != null) {
    if (innerHTML.__html != null) {
      return innerHTML.__html;
    }
  } else {
    const content = props.children;
    if (typeof content === 'string' || typeof content === 'number') {
      return escapeTextForBrowser(content);
    }
  }
  return null;
}

所以平时开发时最好避免使用 dangerouslySetInnerHTML,如果不得不使用的话,前端或服务端必须对输入进行相关验证,例如对特殊输入进行过滤、转义等处理。前端这边处理的话,推荐使用白名单过滤,通过白名单控制允许的 HTML 标签及各标签的属性。

通过用户提供的对象来创建 React 组件

举个例子:

// 用户的输入
const userProvidePropsString = `{"dangerouslySetInnerHTML":{"__html":"<img onerror='alert(\"xss\");' data-original='empty.png' />"}}"`;
// 经过 JSON 转换
const userProvideProps = JSON.parse(userProvidePropsString);
// userProvideProps = {
//   dangerouslySetInnerHTML: {
//     "__html": `<img onerror='alert("xss");' data-original='empty.png' />`
//      }
// };
render() {
     // 出于某种原因解析用户提供的 JSON 并将对象作为 props 传递
    return <div {...userProvideProps} /> 
}

这段代码将用户提供的数据进行 JSON 转换后直接当做 div 的属性,当用户构造了类似例子中的特殊字符串时,页面就会被注入恶意代码,所以要注意平时在开发中不要直接使用用户的输入作为属性。

使用用户输入的值来渲染 a 标签的 href 属性,或类似 img 标签的 src 属性等

const userWebsite = "javascript:alert('xss');";
<a href={userWebsite}></a>

如果没有对该 URL 进行过滤以防止通过 javascript:data: 来执行 JavaScript,则攻击者可以构造 XSS 攻击,此处会有潜在的安全问题。
用户提供的 URL 需要在前端或者服务端在入库之前进行验证并过滤。

服务端如何防止 XSS 攻击

服务端作为最后一道防线,也需要做一些措施以防止 XSS 攻击,一般涉及以下几方面:

  • 在接收到用户输入时,需要对输入进行尽可能严格的过滤,过滤或移除特殊的 HTML 标签、JS 事件的关键字等。
  • 在输出时对数据进行转义,根据输出语境 (html/javascript/css/url),进行对应的转义
  • 对关键 Cookie 设置 http-only 属性,JS 脚本就不能访问到 http-only 的 Cookie 了
  • 利用 CSP 来抵御或者削弱 XSS 攻击,一个 CSP 兼容的浏览器将会仅执行从白名单域获取到的脚本文件,忽略所有的其他脚本 (包括内联脚本和 HTML 的事件处理属性)

总结

出现 XSS 漏洞本质上是输入输出验证不充分,React 在设计上已经很安全了,但是一些反模式的写法还是会引起安全漏洞。Vue 也是类似,Vue 做的安全措施主要也是转义,HTML 的内容和动态绑定的属性都会进行转义。无论使用 React 或 Vue 等前端框架,都不能百分百的防止 XSS 攻击,所以服务端必须对前端参数做一些验证,包括但不限于特殊字符转义、标签、属性白名单过滤等。一旦出现安全问题一般都是挺严重的,不管是敏感数据被窃取或者用户资金被盗,损失往往无法挽回。我们平时开发中需要保持安全意识,保持代码的可靠性和安全性。

小游戏

看完文章可以尝试下 XSS 的小游戏,自己动手实践模拟 XSS 攻击,可以对 XSS 有更进一步的认识。

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

lishibo 赞了文章 · 9月21日

浅谈 React 中的 XSS 攻击

70 篇原创好文~
本文首发于政采云前端团队博客:浅谈 React 中的 XSS 攻击

前言

前端一般会面临 XSS 这样的安全风险,但随着 React 等现代前端框架的流行,使我们在平时开发时不用太关注安全问题。以 React 为例,React 从设计层面上就具备了很好的防御 XSS 的能力。本文将以源码角度,看看 React 做了哪些事情来实现这种安全性的。

XSS 攻击是什么

Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。XSS 攻击通常指的是利用网页的漏洞,攻击者通过巧妙的方法注入 XSS 代码到网页,因为浏览器无法分辨哪些脚本是可信的,导致 XSS 脚本被执行。XSS 脚本通常能够窃取用户数据并发送到攻击者的网站,或者冒充用户,调用目标网站接口并执行攻击者指定的操作。

XSS 攻击类型

反射型 XSS

  • XSS 脚本来自当前 HTTP 请求
  • 当服务器在 HTTP 请求中接收数据并将该数据拼接在 HTML 中返回时,例子:
   // 某网站具有搜索功能,该功能通过 URL 参数接收用户提供的搜索词:
   https://xxx.com/search?query=123
   // 服务器在对此 URL 的响应中回显提供的搜索词:
   <p>您搜索的是: 123</p>
   // 如果服务器不对数据进行转义等处理,则攻击者可以构造如下链接进行攻击:
   https://xxx.com/search?query=<img data-original="empty.png" onerror ="alert('xss')">
   // 该 URL 将导致以下响应,并运行 alert('xss'):
   <p>您搜索的是: <img data-original="empty.png" onerror ="alert('xss')"></p>
   // 如果有用户请求攻击者的 URL ,则攻击者提供的脚本将在用户的浏览器中执行。

存储型 XSS

  • XSS 脚本来自服务器数据库中
  • 攻击者将恶意代码提交到目标网站的数据库中,普通用户访问网站时服务器将恶意代码返回,浏览器默认执行,例子:
   // 某个评论页,能查看用户评论。
   // 攻击者将恶意代码当做评论提交,服务器没对数据进行转义等处理
   // 评论输入:
   <textarea>
      <img data-original="empty.png" onerror ="alert('xss')">
   </textarea>
   // 则攻击者提供的脚本将在所有访问该评论页的用户浏览器执行

DOM 型 XSS

该漏洞存在于客户端代码,与服务器无关

  • 类似反射型,区别在于 DOM 型 XSS 并不会和后台进行交互,前端直接将 URL 中的数据不做处理并动态插入到 HTML 中,是纯粹的前端安全问题,要做防御也只能在客户端上进行防御。

React 如何防止 XSS 攻击

无论使用哪种攻击方式,其本质就是将恶意代码注入到应用中,浏览器去默认执行。React 官方中提到了 React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串,因此恶意代码无法成功注入,从而有效地防止了 XSS 攻击。我们具体看下:

自动转义

React 在渲染 HTML 内容和渲染 DOM 属性时都会将 "'&<> 这几个字符进行转义,转义部分源码如下:

for (index = match.index; index < str.length; index++) {
    switch (str.charCodeAt(index)) {
      case 34: // "
        escape = '&quot;';
        break;
      case 38: // &
        escape = '&amp;';
        break;
      case 39: // '
        escape = '&#x27;';
        break;
      case 60: // <
        escape = '&lt;';
        break;
      case 62: // >
        escape = '&gt;';
        break;
      default:
        continue;
    }
  }

这段代码是 React 在渲染到浏览器前进行的转义,可以看到对浏览器有特殊含义的字符都被转义了,恶意代码在渲染到 HTML 前都被转成了字符串,如下:

// 一段恶意代码
<img data-original="empty.png" onerror ="alert('xss')"> 
// 转义后输出到 html 中
&lt;img data-original=&quot;empty.png&quot; onerror =&quot;alert(&#x27;xss&#x27;)&quot;&gt; 

这样就有效的防止了 XSS 攻击。

JSX 语法

JSX 实际上是一种语法糖,Babel 会把 JSX 编译成 React.createElement() 的函数调用,最终返回一个 ReactElement,以下为这几个步骤对应的代码:

// JSX
const element = (
  <h1 className="greeting">
      Hello, world!
  </h1>
);
// 通过 babel 编译后的代码
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);
// React.createElement() 方法返回的 ReactElement
const element = {
  $$typeof: Symbol('react.element'),
  type: 'h1',
  key: null,
  props: {
    children: 'Hello, world!',
        className: 'greeting'   
  }
  ...
}

我们可以看到,最终渲染的内容是在 Children 属性中,那了解了 JSX 的原理后,我们来试试能否通过构造特殊的 Children 进行 XSS 注入,来看下面一段代码:

const storedData = `{
    "ref":null,
    "type":"body",
    "props":{
        "dangerouslySetInnerHTML":{
            "__html":"<img data-original=\"empty.png\" onerror =\"alert('xss')\"/>"
        }
    }
}`;
// 转成 JSON
const parsedData = JSON.parse(storedData);
// 将数据渲染到页面
render () {
    return <span> {parsedData} </span>; 
}

这段代码中, 运行后会报以下错误,提示不是有效的 ReactChild。

Uncaught (in promise) Error: Objects are not valid as a React child (found: object with keys {ref, type, props}). If you meant to render a collection of children, use an array instead.

那究竟是哪里出问题了?我们看一下 ReactElement 的源码:

const symbolFor = Symbol.for;
REACT_ELEMENT_TYPE = symbolFor('react.element');
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // 这个 tag 唯一标识了此为 ReactElement
    $$typeof: REACT_ELEMENT_TYPE,
    // 元素的内置属性
    type: type,
    key: key,
    ref: ref,
    props: props,
    // 记录创建此元素的组件
    _owner: owner,
  };
  ...
  return element;
}

注意到其中有个属性是 $$typeof`,它是用来标记此对象是一个 `ReactElement`,React 在进行渲染前会通过此属性进行校验,校验不通过将会抛出上面的错误。React 利用这个属性来防止通过构造特殊的 Children 来进行的 XSS 攻击,原因是 `$$typeof 是个 Symbol 类型,进行 JSON 转换后会 Symbol 值会丢失,无法在前后端进行传输。如果用户提交了特殊的 Children,也无法进行渲染,利用此特性,可以防止存储型的 XSS 攻击。

在 React 中可引起漏洞的一些写法

使用 dangerouslySetInnerHTML

dangerouslySetInnerHTML 是 React 为浏览器 DOM 提供 innerHTML 的替换方案。通常来讲,使用代码直接设置 HTML 存在风险,因为很容易使用户暴露在 XSS 攻击下,因为当使用 dangerouslySetInnerHTML 时,React 将不会对输入进行任何处理并直接渲染到 HTML 中,如果攻击者在 dangerouslySetInnerHTML 传入了恶意代码,那么浏览器将会运行恶意代码。看下源码:

function getNonChildrenInnerMarkup(props) {
  const innerHTML = props.dangerouslySetInnerHTML; // 有dangerouslySetInnerHTML属性,会不经转义就渲染__html的内容
  if (innerHTML != null) {
    if (innerHTML.__html != null) {
      return innerHTML.__html;
    }
  } else {
    const content = props.children;
    if (typeof content === 'string' || typeof content === 'number') {
      return escapeTextForBrowser(content);
    }
  }
  return null;
}

所以平时开发时最好避免使用 dangerouslySetInnerHTML,如果不得不使用的话,前端或服务端必须对输入进行相关验证,例如对特殊输入进行过滤、转义等处理。前端这边处理的话,推荐使用白名单过滤,通过白名单控制允许的 HTML 标签及各标签的属性。

通过用户提供的对象来创建 React 组件

举个例子:

// 用户的输入
const userProvidePropsString = `{"dangerouslySetInnerHTML":{"__html":"<img onerror='alert(\"xss\");' data-original='empty.png' />"}}"`;
// 经过 JSON 转换
const userProvideProps = JSON.parse(userProvidePropsString);
// userProvideProps = {
//   dangerouslySetInnerHTML: {
//     "__html": `<img onerror='alert("xss");' data-original='empty.png' />`
//      }
// };
render() {
     // 出于某种原因解析用户提供的 JSON 并将对象作为 props 传递
    return <div {...userProvideProps} /> 
}

这段代码将用户提供的数据进行 JSON 转换后直接当做 div 的属性,当用户构造了类似例子中的特殊字符串时,页面就会被注入恶意代码,所以要注意平时在开发中不要直接使用用户的输入作为属性。

使用用户输入的值来渲染 a 标签的 href 属性,或类似 img 标签的 src 属性等

const userWebsite = "javascript:alert('xss');";
<a href={userWebsite}></a>

如果没有对该 URL 进行过滤以防止通过 javascript:data: 来执行 JavaScript,则攻击者可以构造 XSS 攻击,此处会有潜在的安全问题。
用户提供的 URL 需要在前端或者服务端在入库之前进行验证并过滤。

服务端如何防止 XSS 攻击

服务端作为最后一道防线,也需要做一些措施以防止 XSS 攻击,一般涉及以下几方面:

  • 在接收到用户输入时,需要对输入进行尽可能严格的过滤,过滤或移除特殊的 HTML 标签、JS 事件的关键字等。
  • 在输出时对数据进行转义,根据输出语境 (html/javascript/css/url),进行对应的转义
  • 对关键 Cookie 设置 http-only 属性,JS 脚本就不能访问到 http-only 的 Cookie 了
  • 利用 CSP 来抵御或者削弱 XSS 攻击,一个 CSP 兼容的浏览器将会仅执行从白名单域获取到的脚本文件,忽略所有的其他脚本 (包括内联脚本和 HTML 的事件处理属性)

总结

出现 XSS 漏洞本质上是输入输出验证不充分,React 在设计上已经很安全了,但是一些反模式的写法还是会引起安全漏洞。Vue 也是类似,Vue 做的安全措施主要也是转义,HTML 的内容和动态绑定的属性都会进行转义。无论使用 React 或 Vue 等前端框架,都不能百分百的防止 XSS 攻击,所以服务端必须对前端参数做一些验证,包括但不限于特殊字符转义、标签、属性白名单过滤等。一旦出现安全问题一般都是挺严重的,不管是敏感数据被窃取或者用户资金被盗,损失往往无法挽回。我们平时开发中需要保持安全意识,保持代码的可靠性和安全性。

小游戏

看完文章可以尝试下 XSS 的小游戏,自己动手实践模拟 XSS 攻击,可以对 XSS 有更进一步的认识。

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

赞 4 收藏 4 评论 0

lishibo 赞了文章 · 9月14日

金九银十跳槽季——七种排序算法

关注公众号“执鸢者”,获取大量教学视频及私人总结面筋(公众号原创文章)并进入专业交流群

被前端面试中算法虐惨的小林准备大干一场,好好准备一下面试中的高频算法题,由于前端算法相比于后端手撕的算法较容易,所以小林准备从最基础的七种排序算法开始。前方高能,请抓住方向盘……

一、冒泡排序

冒泡排序的思路:遍历数组,然后将最大数沉到最底部;<br/>时间复杂度:O(N^2);<br/>空间复杂度:O(1)
function BubbleSort(arr) {
    if(arr == null || arr.length <= 0){
        return [];
    }
    var len = arr.length;
    for(var end = len - 1; end > 0; end--){
        for(var i = 0; i < end; i++) {
            if(arr[i] > arr[i + 1]){
                swap(arr, i, i + 1);
            }
        }
    }
    return arr;
}
function swap(arr, i, j){
    // var temp = arr[i];
    // arr[i] = arr[j];
    // arr[j] = temp;
    //交换也可以用异或运算符
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

二、选择排序

选择排序的实现思路:遍历数组,把最小数放在头部;<br/>时间复杂度:O(N^2);<br/>空间复杂度:O(1)
function SelectionSort(arr) {
    if(arr == null || arr.length < 0) {
        return [];
    }
    for(var i = 0; i < arr.length - 1; i++) {
        var minIndex = i;
        for(var j = i + 1; j < arr.length; j++) {
            minIndex = arr[j] < arr[minIndex] ? j : minIndex;
        }
        swap(arr, i, minIndex);
    }
    return arr;
}

function swap(arr, i, j) {
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

三、插入排序

插入排序实现思路:将一个新的数,和前面的比较,只要当前数小于前一个则和前一个交换位置,否则终止;<br/>时间复杂度:O(N^2);<br/>空间复杂度:O(1)
function insertSort(arr) {
    if(arr == null  || arr.length <= 0){
        return [];
    }
    var len = arr.length;
    for(var i = 1; i < len; i++) {
        for(var j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
            swap(arr, j, j + 1);
        }
    }
    return arr;
}

function swap(arr, i, j){
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

四、归并排序

归并排序的思路:<br/>1.先左侧部分排好序<br/>2.再右侧部分排好序<br/>3.再准备一个辅助数组,用外排的方式,小的开始填,直到有个动到末尾,将另一个数组剩余部分拷贝到末尾<br/>4.再将辅助数组拷贝回原数组<br/>时间复杂度:O(N * logN)<br/>空间复杂度:O(N)
// 递归实现

function mergeSort(arr){
    if(arr == null  || arr.length <= 0){
        return [];
    }
    sortProcess(arr, 0, arr.length - 1);
    return arr;
}

function sortProcess(arr, L, R){
    //递归的终止条件,就是左右边界索引一样
    if(L == R){
        return;
    }
    var middle = L + ((R - L) >> 1);//找出中间值
    sortProcess(arr, L, middle);//对左侧部分进行递归
    sortProcess(arr, middle + 1, R);//对右侧部分进行递归
    merge(arr, L, middle, R);//然后利用外排方式进行结合
}

function merge(arr, L, middle, R){
    var help = [];
    var l = L;
    var r = middle + 1;
    var index = 0;
    //利用外排方式进行
    while(l <= middle && r <= R){
        help[index++] = arr[l] < arr[r] ? arr[l++] : arr[r++];
    }
    while(l <= middle){
        help.push(arr[l++]);
    }
    while(r <= R){
        help.push(arr[r++]);
    }

    for(var i = 0; i < help.length; i++) {
        arr[L + i] = help[i];
    }
    //arr.splice(L, help.length, ...help);//这个利用了ES6的语法
}
// 循环实现

function mergeSort(arr){
    if(arr ==null || arr.length <= 0){
        return [];
    }
    var len = arr.length;
    //i每次乘2,是因为每次合并以后小组元素就变成两倍个了
    for(var i = 1; i < len; i *= 2){
        var index = 0;//第一组的起始索引
        while( 2 * i  + index <= len){
            index += 2 * i;
            merge(arr, index - 2 * i, index - i, index);
        }
        //说明剩余两个小组,但其中一个小组数据的数量已经不足2的幂次方个
        if(index + i < len){
            merge(arr, index, index + i, len);
        }
    }
    return arr;
}

//利用外排的方式进行结合
function merge(arr, start, mid, end){
    //新建一个辅助数组
    var help = [];
    var l = start, r = mid;
    var i = 0;
    while(l < mid && r < end){
        help[i++] = arr[l] < arr[r] ? arr[l++] : arr[r++];
    }
    while(l < mid){
        help[i++] = arr[l++];
    }
    while(r < end){
        help[i++] = arr[r++];
    }
    for(var j = 0; j < help.length; j++){
        arr[start + j] = help[j];
    }
}

五、快速排序

快速排序实现思路:随机取出一个值进行划分,大于该值放右边,小于该值放左边(该算法在经典快排的基础上经过荷兰国旗思想和随机思想进行了改造)<br/>时间复杂度:O(N*logN) <br/>空间复杂度:O(logN)
function quickSort(arr) {
    if(arr == null || arr.length <= 0){
        return [];
    }
    quick(arr, 0, arr.length - 1);
}

function quick(arr, L, R){
    //递归结束条件是L >= R
    if(L < R){
        //随机找一个值,然后和最后一个值进行交换,将经典排序变为快速排序
        swap(arr, L + Math.floor(Math.random() * (R - L + 1)), R);
        //利用荷兰国旗问题获得划分的边界,返回的值是小于区域的最大索引和大于区域的最小索引,在这利用荷兰国旗问题将等于区域部分就不用动了
        var tempArr = partition(arr, L, R, arr[R]);
        quick(arr, L, tempArr[0]);
        quick(arr, tempArr[1], R);
    }
}
//返回值是小于区域最后的索引和大于区域的第一个索引
function partition(arr, L, R, num){
    var less = L - 1;
    var more = R + 1;
    var cur = L;
    while(cur < more){
        if(arr[cur] < num){
            swap(arr, ++less, cur++);
        }else if(arr[cur] > num) {
            swap(arr, --more, cur);
        }else{
            cur++;
        }
    }
    return [less, more];
}
function swap(arr, i, j){
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

六、堆排序

堆排序思路:<br/>1.让数组变成大根堆<br/>2.把最后一个位置和堆顶做交换<br/>3.则最大值在最后,则剩下部分做heapify,则重新调整为大根堆,则堆顶位置和该部分最后位置做交换<br/>4.重复进行,直到减完,则这样最后就调整完毕,整个数组排完序(为一个升序)<br/>时间复杂度:O(N * logN)<br/>空间复杂度:O(1)
function heapSort(arr) {
    if(arr == null || arr.length <= 0) {
        return [];
    }

    //首先是建立大顶堆的过程
    for(var i = 0; i < arr.length; i++) {
        heapInsert(arr, i);
    }
    var size = arr.length;//这个值用来指定多少个数组成堆,当得到一个排序的值后这个值减一
    //将堆顶和最后一个位置交换
    /**
     * 当大顶堆建立完成后,然后不断将最后一个位置和堆顶交换;
     * 这样最大值就到了最后,则剩下部分做heapify,重新调整为大根堆,则堆顶位置和倒数第二个位置交换,重复进行,直到全部排序完毕*/
    //由于前面已经是大顶堆,所以直接交换
    swap(arr, 0, --size);
    while(size > 0) {
        //重新变成大顶堆
        heapify(arr, 0, size);
        //进行交换
        swap(arr, 0, --size);
    }
}

//加堆过程中
function heapInsert(arr, index) {
    //比较当前位置和其父位置,若大于其父位置,则进行交换,并将索引移动到其父位置进行循环,否则跳过
    //结束条件是比父位置小或者到达根节点处
    while(arr[index] > arr[parseInt((index - 1) / 2)]){
        //进行交换
        swap(arr, index, parseInt((index - 1) / 2));
        index = parseInt((index - 1) / 2);
    }
}
//减堆过程
/**
 * size指的是这个数组前多少个数构成一个堆
 * 如果你想把堆顶弹出,则把堆顶和最后一个数交换,把size减1,然后从0位置经历一次heapify,调整一下,剩余部分变成大顶堆*/
function heapify(arr, index, size) {
    var left = 2 * index + 1;
    while(left < size) {
        var largest = (left + 1 < size && arr[left] < arr[left + 1]) ? left + 1 : left;
        largest = arr[index] > arr[largest] ? index : largest;

        //如果最大值索引和传进来索引一样,则该值到达指定位置,直接结束循环
        if(index == largest) {
            break;
        }

        //进行交换,并改变索引和其左子节点
        swap(arr, index, largest);
        index = largest;
        left = 2 * index + 1;
    }
}

function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

七、桶排序

桶排序会经历三次遍历:准备一个数组、遍历一遍数组、重构一遍数组,是非基于比较的排序,下面以一个问题来阐述其思路。<br/>问题:<br/> 给定一个数组,求如果排序之后,相邻两个数的最大差值,要求时间复杂度O(N),且要求不能用基于比较的排序<br/>
思路:<br/>1.准备桶:数组中有N个数就准备N+1个桶<br/>2.遍历一遍数组,找到最大值max和最小值min
。若min = max,则差值=0;若min≠max,则最小值放在0号桶,最大值放在N号桶,剩下的数属于哪个范围就进哪个桶<br/>3.根据鸽笼原理,则肯定有一个桶为空桶,设计该桶的目的是为了否定最大值在一个桶中,则最大差值的两个数一定来自于两个桶,但空桶两侧并不一定是最大值<br/>4.所以只记录所有进入该桶的最小值min和最大值max和一个布尔值表示该桶有没有值<br/>5.然后遍历这个数组,如果桶是空的,则跳到下一个数,如果桶非空,则找前一个非空桶,则最大差值=当前桶min - 上一个非空桶max,用全局变量更新最大值<br/>时间复杂度:O(N)<br/>空间复杂度:O(N)
function maxGap(arr) {
    if(arr == null || arr.length <= 0) {
        return 0;
    }
    var len = arr.length;
    var max = -Infinity, min = Infinity;
    //遍历一遍数组,找到最大值max和最小值min
    for(var i = 0; i < len; i++) {
        max = max > arr[i] ? max : arr[i];
        min = min > arr[i] ? arr[i] : min;
    }

    //若min = max,则差值为0;
    if(min == max) {
        return 0;
    }

    var hasNum = new Array(len + 1);
    var mins = new Array(len + 1);
    var maxs = new Array(len + 1);

    var bid = 0;//指定桶的编号

    for(var i = 0; i < len; i++) {
        bid = bucket(arr[i], min, max, len);//获得该值是在哪个桶//由于有N+1个桶,所以间隔就是N个,所以此处除以的是len,然后通过这个函数得到应该放到哪个桶里
        maxs[bid] = hasNum[bid] ? Math.max(arr[i], maxs[bid]) : arr[i];
        mins[bid] = hasNum[bid] ? Math.min(arr[i], mins[bid]) : arr[i];
        hasNum[bid] = true;
    }

    var res = 0;
    var lastMax = maxs[0];

    for(var i = 0; i < len + 1; i++) {
        if(hasNum[i]) {
            res = Math.max(mins[i] - lastMax, res);
            lastMax = maxs[i];
        }
    }
    return res;
}

//获得桶号
//这个函数用于判断在哪个桶中,参数分别为值、最小值、最大值、桶间隔
function bucket(value, min, max, len) {
    return parseInt((value - min) / ((max - min) / len));
}
欢迎老铁们加群或者私聊
image
查看原文

赞 11 收藏 10 评论 0

lishibo 收藏了文章 · 9月14日

金九银十跳槽季——七种排序算法

关注公众号“执鸢者”,获取大量教学视频及私人总结面筋(公众号原创文章)并进入专业交流群

被前端面试中算法虐惨的小林准备大干一场,好好准备一下面试中的高频算法题,由于前端算法相比于后端手撕的算法较容易,所以小林准备从最基础的七种排序算法开始。前方高能,请抓住方向盘……

一、冒泡排序

冒泡排序的思路:遍历数组,然后将最大数沉到最底部;<br/>时间复杂度:O(N^2);<br/>空间复杂度:O(1)
function BubbleSort(arr) {
    if(arr == null || arr.length <= 0){
        return [];
    }
    var len = arr.length;
    for(var end = len - 1; end > 0; end--){
        for(var i = 0; i < end; i++) {
            if(arr[i] > arr[i + 1]){
                swap(arr, i, i + 1);
            }
        }
    }
    return arr;
}
function swap(arr, i, j){
    // var temp = arr[i];
    // arr[i] = arr[j];
    // arr[j] = temp;
    //交换也可以用异或运算符
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

二、选择排序

选择排序的实现思路:遍历数组,把最小数放在头部;<br/>时间复杂度:O(N^2);<br/>空间复杂度:O(1)
function SelectionSort(arr) {
    if(arr == null || arr.length < 0) {
        return [];
    }
    for(var i = 0; i < arr.length - 1; i++) {
        var minIndex = i;
        for(var j = i + 1; j < arr.length; j++) {
            minIndex = arr[j] < arr[minIndex] ? j : minIndex;
        }
        swap(arr, i, minIndex);
    }
    return arr;
}

function swap(arr, i, j) {
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

三、插入排序

插入排序实现思路:将一个新的数,和前面的比较,只要当前数小于前一个则和前一个交换位置,否则终止;<br/>时间复杂度:O(N^2);<br/>空间复杂度:O(1)
function insertSort(arr) {
    if(arr == null  || arr.length <= 0){
        return [];
    }
    var len = arr.length;
    for(var i = 1; i < len; i++) {
        for(var j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
            swap(arr, j, j + 1);
        }
    }
    return arr;
}

function swap(arr, i, j){
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

四、归并排序

归并排序的思路:<br/>1.先左侧部分排好序<br/>2.再右侧部分排好序<br/>3.再准备一个辅助数组,用外排的方式,小的开始填,直到有个动到末尾,将另一个数组剩余部分拷贝到末尾<br/>4.再将辅助数组拷贝回原数组<br/>时间复杂度:O(N * logN)<br/>空间复杂度:O(N)
// 递归实现

function mergeSort(arr){
    if(arr == null  || arr.length <= 0){
        return [];
    }
    sortProcess(arr, 0, arr.length - 1);
    return arr;
}

function sortProcess(arr, L, R){
    //递归的终止条件,就是左右边界索引一样
    if(L == R){
        return;
    }
    var middle = L + ((R - L) >> 1);//找出中间值
    sortProcess(arr, L, middle);//对左侧部分进行递归
    sortProcess(arr, middle + 1, R);//对右侧部分进行递归
    merge(arr, L, middle, R);//然后利用外排方式进行结合
}

function merge(arr, L, middle, R){
    var help = [];
    var l = L;
    var r = middle + 1;
    var index = 0;
    //利用外排方式进行
    while(l <= middle && r <= R){
        help[index++] = arr[l] < arr[r] ? arr[l++] : arr[r++];
    }
    while(l <= middle){
        help.push(arr[l++]);
    }
    while(r <= R){
        help.push(arr[r++]);
    }

    for(var i = 0; i < help.length; i++) {
        arr[L + i] = help[i];
    }
    //arr.splice(L, help.length, ...help);//这个利用了ES6的语法
}
// 循环实现

function mergeSort(arr){
    if(arr ==null || arr.length <= 0){
        return [];
    }
    var len = arr.length;
    //i每次乘2,是因为每次合并以后小组元素就变成两倍个了
    for(var i = 1; i < len; i *= 2){
        var index = 0;//第一组的起始索引
        while( 2 * i  + index <= len){
            index += 2 * i;
            merge(arr, index - 2 * i, index - i, index);
        }
        //说明剩余两个小组,但其中一个小组数据的数量已经不足2的幂次方个
        if(index + i < len){
            merge(arr, index, index + i, len);
        }
    }
    return arr;
}

//利用外排的方式进行结合
function merge(arr, start, mid, end){
    //新建一个辅助数组
    var help = [];
    var l = start, r = mid;
    var i = 0;
    while(l < mid && r < end){
        help[i++] = arr[l] < arr[r] ? arr[l++] : arr[r++];
    }
    while(l < mid){
        help[i++] = arr[l++];
    }
    while(r < end){
        help[i++] = arr[r++];
    }
    for(var j = 0; j < help.length; j++){
        arr[start + j] = help[j];
    }
}

五、快速排序

快速排序实现思路:随机取出一个值进行划分,大于该值放右边,小于该值放左边(该算法在经典快排的基础上经过荷兰国旗思想和随机思想进行了改造)<br/>时间复杂度:O(N*logN) <br/>空间复杂度:O(logN)
function quickSort(arr) {
    if(arr == null || arr.length <= 0){
        return [];
    }
    quick(arr, 0, arr.length - 1);
}

function quick(arr, L, R){
    //递归结束条件是L >= R
    if(L < R){
        //随机找一个值,然后和最后一个值进行交换,将经典排序变为快速排序
        swap(arr, L + Math.floor(Math.random() * (R - L + 1)), R);
        //利用荷兰国旗问题获得划分的边界,返回的值是小于区域的最大索引和大于区域的最小索引,在这利用荷兰国旗问题将等于区域部分就不用动了
        var tempArr = partition(arr, L, R, arr[R]);
        quick(arr, L, tempArr[0]);
        quick(arr, tempArr[1], R);
    }
}
//返回值是小于区域最后的索引和大于区域的第一个索引
function partition(arr, L, R, num){
    var less = L - 1;
    var more = R + 1;
    var cur = L;
    while(cur < more){
        if(arr[cur] < num){
            swap(arr, ++less, cur++);
        }else if(arr[cur] > num) {
            swap(arr, --more, cur);
        }else{
            cur++;
        }
    }
    return [less, more];
}
function swap(arr, i, j){
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

六、堆排序

堆排序思路:<br/>1.让数组变成大根堆<br/>2.把最后一个位置和堆顶做交换<br/>3.则最大值在最后,则剩下部分做heapify,则重新调整为大根堆,则堆顶位置和该部分最后位置做交换<br/>4.重复进行,直到减完,则这样最后就调整完毕,整个数组排完序(为一个升序)<br/>时间复杂度:O(N * logN)<br/>空间复杂度:O(1)
function heapSort(arr) {
    if(arr == null || arr.length <= 0) {
        return [];
    }

    //首先是建立大顶堆的过程
    for(var i = 0; i < arr.length; i++) {
        heapInsert(arr, i);
    }
    var size = arr.length;//这个值用来指定多少个数组成堆,当得到一个排序的值后这个值减一
    //将堆顶和最后一个位置交换
    /**
     * 当大顶堆建立完成后,然后不断将最后一个位置和堆顶交换;
     * 这样最大值就到了最后,则剩下部分做heapify,重新调整为大根堆,则堆顶位置和倒数第二个位置交换,重复进行,直到全部排序完毕*/
    //由于前面已经是大顶堆,所以直接交换
    swap(arr, 0, --size);
    while(size > 0) {
        //重新变成大顶堆
        heapify(arr, 0, size);
        //进行交换
        swap(arr, 0, --size);
    }
}

//加堆过程中
function heapInsert(arr, index) {
    //比较当前位置和其父位置,若大于其父位置,则进行交换,并将索引移动到其父位置进行循环,否则跳过
    //结束条件是比父位置小或者到达根节点处
    while(arr[index] > arr[parseInt((index - 1) / 2)]){
        //进行交换
        swap(arr, index, parseInt((index - 1) / 2));
        index = parseInt((index - 1) / 2);
    }
}
//减堆过程
/**
 * size指的是这个数组前多少个数构成一个堆
 * 如果你想把堆顶弹出,则把堆顶和最后一个数交换,把size减1,然后从0位置经历一次heapify,调整一下,剩余部分变成大顶堆*/
function heapify(arr, index, size) {
    var left = 2 * index + 1;
    while(left < size) {
        var largest = (left + 1 < size && arr[left] < arr[left + 1]) ? left + 1 : left;
        largest = arr[index] > arr[largest] ? index : largest;

        //如果最大值索引和传进来索引一样,则该值到达指定位置,直接结束循环
        if(index == largest) {
            break;
        }

        //进行交换,并改变索引和其左子节点
        swap(arr, index, largest);
        index = largest;
        left = 2 * index + 1;
    }
}

function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

七、桶排序

桶排序会经历三次遍历:准备一个数组、遍历一遍数组、重构一遍数组,是非基于比较的排序,下面以一个问题来阐述其思路。<br/>问题:<br/> 给定一个数组,求如果排序之后,相邻两个数的最大差值,要求时间复杂度O(N),且要求不能用基于比较的排序<br/>
思路:<br/>1.准备桶:数组中有N个数就准备N+1个桶<br/>2.遍历一遍数组,找到最大值max和最小值min
。若min = max,则差值=0;若min≠max,则最小值放在0号桶,最大值放在N号桶,剩下的数属于哪个范围就进哪个桶<br/>3.根据鸽笼原理,则肯定有一个桶为空桶,设计该桶的目的是为了否定最大值在一个桶中,则最大差值的两个数一定来自于两个桶,但空桶两侧并不一定是最大值<br/>4.所以只记录所有进入该桶的最小值min和最大值max和一个布尔值表示该桶有没有值<br/>5.然后遍历这个数组,如果桶是空的,则跳到下一个数,如果桶非空,则找前一个非空桶,则最大差值=当前桶min - 上一个非空桶max,用全局变量更新最大值<br/>时间复杂度:O(N)<br/>空间复杂度:O(N)
function maxGap(arr) {
    if(arr == null || arr.length <= 0) {
        return 0;
    }
    var len = arr.length;
    var max = -Infinity, min = Infinity;
    //遍历一遍数组,找到最大值max和最小值min
    for(var i = 0; i < len; i++) {
        max = max > arr[i] ? max : arr[i];
        min = min > arr[i] ? arr[i] : min;
    }

    //若min = max,则差值为0;
    if(min == max) {
        return 0;
    }

    var hasNum = new Array(len + 1);
    var mins = new Array(len + 1);
    var maxs = new Array(len + 1);

    var bid = 0;//指定桶的编号

    for(var i = 0; i < len; i++) {
        bid = bucket(arr[i], min, max, len);//获得该值是在哪个桶//由于有N+1个桶,所以间隔就是N个,所以此处除以的是len,然后通过这个函数得到应该放到哪个桶里
        maxs[bid] = hasNum[bid] ? Math.max(arr[i], maxs[bid]) : arr[i];
        mins[bid] = hasNum[bid] ? Math.min(arr[i], mins[bid]) : arr[i];
        hasNum[bid] = true;
    }

    var res = 0;
    var lastMax = maxs[0];

    for(var i = 0; i < len + 1; i++) {
        if(hasNum[i]) {
            res = Math.max(mins[i] - lastMax, res);
            lastMax = maxs[i];
        }
    }
    return res;
}

//获得桶号
//这个函数用于判断在哪个桶中,参数分别为值、最小值、最大值、桶间隔
function bucket(value, min, max, len) {
    return parseInt((value - min) / ((max - min) / len));
}
欢迎老铁们加群或者私聊
image
查看原文

lishibo 收藏了文章 · 9月11日

Vue双向数据绑定原理。

前言:

对于传统的dom操作,当数据变化时更新视图需要先获取到目标节点,然后将改变后的值放入节点中,视图发生变化时,需要绑定事件修改数据。双向数据恰好能解决这种复杂的操作,当数据发生变化时会自动更新视图,视图发生变化时也会自动更新数据,极大的提高了开发效率。那双向数据绑定到底是怎么实现的了,下面来讲述双向数据绑定的原理。

1、Vue双向数据绑定的原理。

Vue实现双向数据绑定是采用数据劫持和发布者-订阅者模式。数据劫持是利用ES5的Object.defineProperty(obj,key,val)方法来劫持每个属性的getter和setter,在数据变动是发布消息给订阅者,从而触发相应的回调来更新视图,下面来一步步实现。

<div id="app">
    用户名:<input type="text" v-model="name">
    密码:<input type="text" v-model="passWord">
    {{name}}   {{passWord}}
    <div><div>{{name}}</div></div>
</div>

<script>

    function Vue(option){
      this.data = option.data;
      this.id = option.el;
      var dom = nodeToFragment(document.getElementById(this.id), this);
      document.getElementById(this.id).appendChild(dom);
    }
 
    var vm = new Vue({
      el: "app",
      data: {
        name: "zhangsan",
        passWord: "123456"
      }
    })
<script>

如上一段html,想要实现双向数据绑定,我们需要先解析这一段html,找到带有v-model指令和{{}}的节点(此处节点包括元素节点和文本节点),然后我们定义了一个Vue的构造函数,在实例化创建对象vm时,传入id='app'和对应的数据data,我们现在需要实现的功能是,当实例化创建对象时,将对应的'name'和'passWord'属性渲染到页面上。

在解析上面一段模板时,需要先了解一下DocuemntFragment(碎片化文档)这个概念,你可以把他认为是一个dom节点收容器,当你创造了10个节点,当每个节点都插入到文档当中都会引发一次浏览器的回流,也就是说浏览器要回流10次,十分消耗资源。而使用碎片化文档,也就是说我把10个节点都先放入到一个容器当中,最后我再把容器直接插入到文档就可以了!浏览器只回流了1次。

解析html

// 此方法是将传入的dom节点转为文档碎片,参数node是需要解析的html片段,vm是Vue构造函数实例化的对象。
function nodeToFragment(node, vm){
      // 创建一个文档碎片
      var fragment = document.createDocumentFragment();
      var child;
      // 获取到node中的第一个子节点,当有子节点时,执行循环体
      while(child = node.firstChild){
        // appendChild会将参数中节点移除,因此此循环会将node中的节点一个个移除,移动到fragment文档碎片中,直到node中没有节点,循环结束。
        fragment.appendChild(child);
      }
      // 此处fragment已经获取到node中所有节点,loopNode函数用来循环每一层的节点。
      loopNode(fragment.childNodes, vm);
      return fragment;
    }
    
function loopNode(nodes, vm){
      //此处传入的nodes是一个类数组,使用Array.from()方法将其转化为数组。
      Array.from(nodes).forEach((node) => {
        // 此处得到的node是nodes中的直接子节点,compile函数是用来解析这些节点,如果是元素节点,解析是否有v-model指令,如果是文本节点,解析是否有{{}}。
        compile(node, vm);
        // 如果node还有子节点,则继续解析
        if(node.childNodes.length>0){
          loopNode(node.childNodes, vm);
        }
      })
  }

function compile(node, vm){
      // 如果是元素节点
      if(node.nodeType === 1){
        // 获得元素节点上所有的属性,以键值对的方式存储在attrs中,attrs属于类数组
        var attrs = node.attributes;
        Array.from(attrs).forEach(element => {
          if(element.nodeName == "v-model"){
            var name = element.nodeValue;
            // 初始化带有v-model指令的元素的值
            node.value = vm.data[name];
          }
        });
      }
      // 正则匹配到文本中有{{}}的文本
      var reg = /\{\{([^}]*)\}\}/g;
      var textContent = node.textContent;
      // 如果是文本节点且文本中带有{{}}的节点
      if(node.nodeType === 3 && reg.test(textContent)){
        // 将文本内容存放在当前节点的自定义属性上
        node.my = textContent;
        // 此处node.textContent 和 node.my的文本一样,如果上一步不将文本存储到自定义属性中,那么下次将无法匹配到{{}},replace方法用来替换文本中的{{name}}和{{passWord}}。
        node.textContent = node.my.replace(reg, function(){
          var attr = arguments[0].slice(2,arguments[0].length-2); 
          return vm.data[attr];
        })
      }
    }

上面我们已经实现了将data中的属性填充到页面中,接下来我们需要做的是,当data中属性值发生变化时,我们需要监听到数据的变化,Vue中对数据监听使用的是Object.defineProperty(data,key,val)方法(不清楚该方法的可查阅),Object.defineProperty(data,key,val)可以监听对象属性的变化,当获取data中某个属性的值时,会调用该属性的get()方法,当修改某个属性的值时会调用当前属性的set()方法。

    function observe(data){
      if(typeof data != 'object' || !data){
        return
      }
      Object.keys(data).forEach((key)=>{
        defineReactive(data, key, data[key]);
      })
    }

    function defineReactive(data, key, val){
      // data中子属性是对象时,继续监听
      observe(val);
      Object.defineProperty(data, key, {
        get: function(){
          return val;
        },
        set: function(newVal){
          if(newVal !== val){
            val = newVal;
          } else {
            return;
          }
        }
      })
    }

修改Vue构造函数如下,当实例化Vue时,实现了对数据data的监听,并解析模板,将data中对应属性填充到页面中。
image
但是,当data中属性值发生变化时,页面并不会更新,那接下来我们需要解决的就是,当data中属性发生变化时,自动更新视图,视图发生变化时,主动更新数据,连接视图和数据我们需要在定义一个构造函数Watcher。
首先我们来考虑下当data中name属性发生变化时,我们需要更新的视图有如下三个节点,一个元素节点和两个文本节点
image
当data中passWord属性变化时,需要更新的视图有两个节点
image
也就是说,当某个属性发生变化时,我们可能要更新多个视图,那我们如何去定位需要更新那些节点了?因此我们需要将绑定了data中属性的节点保存到一个数组中,当data中对应属性发生变化时,循环数组,拿到节点,执行更新方法。
回顾一下我们compile中的代码,下图中标记1、2处就是获取到data中的属性名。接下来我们定义一个Watcher构造函数,在解析模板时实例化Watcher,3、4处是新增代码,实例化Watcher。
image

Watcher构造函数中有两个方法,一个update方法和一个get方法,实例化Watcher时调用Watcher中的get方法,此方法会触发data中对应的属性的get方法。

    function Watcher(vm, node, name){
      this.vm = vm;
      this.node = node;
      this.name = name;
      this.value = this.get();
    }
    
    Watcher.prototype.get = function(){
      //触发data中属性get方法前将当前实例化对象存入target属性中
      Dep.target = this;
      //取data中的this.name属性,会触发该属性的get方法
      var value = this.vm.data[this.name];
      Dep.target = null;
      return value;
    }
    
    Watcher.prototype.update = function(){
  
    }

上文中提到,我们需要定义数组来存储对应属性的节点,也就是说,data中每个属性都必须有一个数组来存储节点,下面我们来定义一个Dep构造函数,用来收集节点。

    function Dep(){
      // 存放Watcher的实例对象
      this.subs = [];
    } 
    Dep.prototype.addSub = function(sub){
      this.subs.push(sub);
    }
    Dep.prototype.notify = function(){
      this.subs.forEach((sub)=>{
        sub.update();
      })
    }

每个属性都需要一个数组,因此我们在监听data属性时实例化Dep,Dep的实例在闭包的情况下创建,我们可以修改数据监听中的get方法,上文在实例化Watcher时,触发get方法,将Watcher的实例存入数组中,当修改data中属性值时,调用set方法,Dep实例对象调用notify方法,实现更新。

    //修改后的defineReactive方法
    function defineReactive(data, key, val){
      //为每个属性创建一个Dep实例
      var dep = new Dep();
      observe(val);
      Object.defineProperty(data, key, {
        get: function(){
          //实例化Watcher时,触发了get方法,此时Dep.target为Watcher实例化对象
          Dep.target && dep.addSub(Dep.target);
          return val;
        },
        set: function(newVal){
          if(newVal !== val){
            val = newVal;
            // 当调用set方法时,通知所有订阅者执行更新方法
            dep.notify();
          } else {
            return;
          }
        }
      })
    }

实现更新方法

    function Watcher(vm, node, name){
      ...
    }
    
    Watcher.prototype.get = function(){
      ...
    }
    
    Watcher.prototype.update = function(){
        if(this.node.nodeType === 1){
            this.node.nodeValue = this.get();
        } else {
            this.node.textContent = this.node.my.replace(/\{\{([^}]*)\}\}/g, function(){
              var attr = arguments[0].slice(2,arguments[0].length-2);
              return this.vm.data[attr];
            })
        }
    }

完成到这里,我们就已经实现了数据变化时自动更新视图,我们来梳理一下流程。就拿上面例子来说,当我们执行vm.data['name'] = 'lisi'时,便会触发set方法,set方法中调用Dep实例的notify方法,此方法会遍历this.subs数组,这个数组中存放的元素是Watcher的实例化对象,调用sub.update()方法便会更新视图。

当视图发生变化时,需要修改相应数据,只需要给相应节点绑定事件即可,修改compile方法如下,给相应节点增加input事件。

  if(node.nodeType === 1){
    // 获得元素节点上所有的属性,以键值对的方式存储在attr中,attr属于类数组
    var attr = node.attributes;
    Array.from(attr).forEach(element => {
      if(element.nodeName == "v-model"){
        var name = element.nodeValue;
        // 给带有v-model指令的元素绑定input事件
        node.addEventListener('input', function(e){
          vm.data[name] = e.target.value;
        })
        // 初始化带有v-model指令的元素的值
        node.value = vm.data[name];
        new Watcher(vm, node, name);
      }
    });
  }

到这里双向数据绑定就完成了,下面附上完整代码。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Vue双向数据绑定</title>
</head>

<body>
  <div id="app">
    用户名:<input type="text" v-model="name">
    密码:<input type="text" v-model="passWord">
    {{name}} {{passWord}}
    <div>
      <div>{{name}}</div>
    </div>
  </div>

  <script>

    function Vue(option) {
      this.data = option.data;
      this.id = option.el;
      observe(this.data);
      var dom = nodeToFragment(document.getElementById(this.id), this);
      document.getElementById(this.id).appendChild(dom);
    }

    function nodeToFragment(node, vm) {
      // 创建一个文档碎片
      var fragment = document.createDocumentFragment();
      var child;
      // 获取到node中的第一个节点
      while (child = node.firstChild) {
        // appendChild会将传入的节点移除,因此此循环会将node中的节点一个个移除,移动到fragment文档碎片中。
        fragment.appendChild(child);
      }
      // console.dir(fragment);
      loopNode(fragment.childNodes, vm);
      return fragment;
    }

    function loopNode(nodes, vm) {
      //此处传入的nodes是一个类数组,将其转化为数组
      Array.from(nodes).forEach((node) => {
        compile(node, vm);
        // 如果node还有子节点,则继续解析
        if (node.childNodes.length > 0) {
          loopNode(node.childNodes, vm);
        }
      })
    }

    function compile(node, vm) {
      // 如果是元素节点
      if (node.nodeType === 1) {
        // 获得元素节点上所有的属性,以键值对的方式存储在attr中,attr属于类数组
        var attr = node.attributes;
        Array.from(attr).forEach(element => {
          if (element.nodeName == "v-model") {
            var name = element.nodeValue;
            // 给带有v-model指令的元素绑定input时间
            node.addEventListener('input', function (e) {
              vm.data[name] = e.target.value;
            })
            // 初始化带有v-model指令的元素的值
            node.value = vm.data[name];
            new Watcher(vm, node, name);
          }
        });
      }
      // 正则匹配到文本中有{{}}的文本
      var reg = /\{\{([^}]*)\}\}/g;
      var textContent = node.textContent;
      // 如果是文本节点且文本中带有{{}}的节点
      if (node.nodeType === 3 && reg.test(textContent)) {
        // 将文本内容存放在当前节点的自定义属性上
        node.my = textContent;
        // 此处node.textContent 和 node.my的文本一样,如果上一步不将文本存储到自定义属性中,那么下次将无法匹配到{{}}。
        node.textContent = node.my.replace(reg, function () {
          var attr = arguments[0].slice(2, arguments[0].length - 2);
          new Watcher(vm, node, attr);
          return vm.data[attr];
        })
      }
    }

    function observe(data) {
      if (typeof data != 'object' || !data) {
        return
      }

      Object.keys(data).forEach((key) => {
        defineReactive(data, key, data[key]);
      })
    }

    function defineReactive(data, key, val) {
      var dep = new Dep();
      observe(val);
      Object.defineProperty(data, key, {
        get: function () {
          Dep.target && dep.addSub(Dep.target);
          return val;
        },
        set: function (newVal) {
          if (newVal !== val) {
            val = newVal;
            dep.notify();
          } else {
            return;
          }
        }
      })
    }

    function Dep() {
      this.subs = [];
    }

    Dep.prototype.addSub = function (sub) {
      this.subs.push(sub);
    }

    Dep.prototype.notify = function () {
      this.subs.forEach((sub) => {
        sub.update();
      })
    }

    function Watcher(vm, node, name) {
      this.vm = vm;
      this.node = node;
      this.name = name;
      this.value = this.get();
    }

    Watcher.prototype.get = function () {
      Dep.target = this;
      var value = this.vm.data[this.name];
      Dep.target = null;
      return value;
    }

    Watcher.prototype.update = function () {
      if (this.node.nodeType === 1) {
        this.node.nodeValue = this.get();
      } else {
        this.node.textContent = this.node.my.replace(/\{\{([^}]*)\}\}/g, function () {
          var attr = arguments[0].slice(2, arguments[0].length - 2);
          return this.vm.data[attr];
        })
      }
    }

    var vm = new Vue({
      el: "app",
      data: {
        name: "lishibo",
        passWord: "123456",
        obj: {
          obj1: 'obj1'
        },
        arr: ['arr1', 'arr2']
      }
    })
  </script>
</body>
</html>
查看原文

lishibo 赞了文章 · 9月10日

try catch引发的性能优化深度思考

image

关键代码拆解成如下图所示(无关部分已省略):

demo

起初我认为可能是这个 getRowDataItemNumberFormat 函数里面某些方法执行太慢,从 formatData.replaceunescape(已废弃,官方建议使用 decodeURI 或者 decodeURIComponent 替代) 方法都怀疑了一遍,发现这些方法都不是该函数运行慢的原因。为了深究原因,我给 style.formatData 传入了不同的值,发现这个函数的运行效率出现不同的表现。开始有点疑惑为什么 style.formatData 的值导致这个函数的运行效率差别如此之大。

进一步最终定位发现如果 style.formatData 为 undefined 的时候,效率骤降,如果 style.formatData 为合法的字符串的时候,效率是正常值。我开始意识到这个问题的原因在那里了,把目光转向了 try catch 代码块,这是一个很可疑的地方,在很早之前曾经听说过不合理的 try catch 是会影响性能的,但是之前从没遇到过,结合了一些资料,我发现比较少案例去探究这类代码片段的性能,我决定写代码去验证下:

window.a = 'a';
window.c = undefined;
function getRowDataItemNumberFormatTryCatch() {
    console.time('getRowDataItemNumberFormatTryCatch');
    for (let i = 0; i < 3000; i++) {
        try {
            a.replace(/%022/g, '"');
        }
        catch (error) {
        }
    }
    console.timeEnd('getRowDataItemNumberFormatTryCatch');
}

我尝试把 try catch 放入一个 for 循环中,让它运行 3000 次,看看它的耗时为多少,我的电脑执行该代码的时间大概是 0.2 ms 左右,这是一个比较快的值,但是这里 a.replace 是正常运行的,也就是 a 是一个字符串能正常运行 replace 方法,所以这里的耗时是正常的。我对他稍微做了一下改变,如下:

function getRowDataItemNumberFormatTryCatch2() {
    console.time('getRowDataItemNumberFormatTryCatch');
    for (let i = 0; i < 3000; i++) {
        try {
            c.replace(/%022/g, '"');
        }
        catch (error) {
        }
    }
    console.timeEnd('getRowDataItemNumberFormatTryCatch');
}

这段代码跟上面代码唯一的区别是,c.replace 此时应该是会报错的,因为 cundefined,这个错误会被 try catch 捕捉到,而上面的代码耗时出现了巨大的变化,上升到 40 ms,相差了将近 200 倍!并且上述代码和首图的 getRowDataItemNumberFormat 函数代码均出现了 Minor GC,注意这个 Minor GC 也是会耗时的。

demo

这可以解释一部分原因了,我们上面运行的代码是一个性能比较关键的部分,不应该使用 try catch 结构,因为该结构是相当独特的。与其他构造不同,它运行时会在当前作用域中创建一个新变量。每次 catch 执行该子句都会发生这种情况,将捕获的异常对象分配给一个变量。

即使在同一作用域内,此变量也不存在于脚本的其他部分中。它在 catch 子句的开头创建,然后在子句末尾销毁。因为此变量是在运行时创建和销毁的(这些都需要额外的耗时!),并且这是 JavaScript 语言的一种特殊情况,所以某些浏览器不能非常有效地处理它,并且在捕获异常的情况下,将捕获处理程序放在性能关键的循环中可能会导致性能问题,这是我们为什么上面会出现 Minor GC 并且会有严重耗时的原因。

如果可能,应在代码中的较高级别上进行异常处理,在这种情况下,异常处理可能不会那么频繁发生,或者可以通过首先检查是否允许所需的操作来避免。上面的 getRowDataItemNumberFormatTryCatch2 函数示例显示的循环,如果里面所需的属性不存在,则该循环可能引发多个异常,为此性能更优的写法应该如下:

function getRowDataItemNumberFormatIf() {
    console.time('getRowDataItemNumberFormatIf');
    for (let i = 0; i < 3000; i++) {
        if (c) {
            c.replace(/%022/g, '"');
        }
    }
    console.timeEnd('getRowDataItemNumberFormatIf')
}

上面的这段代码语义上跟 try catch 其实是相似的,但运行效率迅速下降至 0.04ms,所以 try catch 应该通过检查属性或使用其他适当的单元测试来完全避免使用此构造,因为这些构造会极大地影响性能,因此应尽量减少使用它们。

如果一个函数被重复调用,或者一个循环被重复求值,那么最好避免其中包含这些构造。它们最适合仅执行一次或仅执行几次且不在性能关键代码内执行的代码。尽可能将它们与其他代码隔离,以免影响其性能。

例如,可以将它们放在顶级函数中,或者运行它们一次并存储结果,这样你以后就可以再次使用结果而不必重新运行代码。

demo

getRowDataItemNumberFormat 在经过上述思路改造后,运行效率得到了质的提升,在实测 300 多次循环中减少的时间如下图,足足优化了将近 2s 多的时间,如果是 3000 次的循环,那么它的优化比例会更高:

demo
demo

由于上面的代码是从项目中改造出来演示的,可能并不够直观,所以我重新写了另外一个相似的例子,代码如下,这里面的逻辑和上面的 getRowDataItemNumberFormat 函数讲道理是一致的,但是我让其发生错误的时候进入 catch 逻辑执行任务。

事实上 plus1plus2 函数的代码逻辑是一致的,只有代码语义是不相同,一个是返回 1,另一个是错误抛出1,一个求和方法在 try 片段完成,另一个求和方法再 catch 完成,我们可以粘贴这段代码在浏览器分别去掉不同的注释观察结果。

我们发现 try 片段中的代码运行大约使用了 0.1 ms,而 catch 完成同一个求和逻辑却执行了大约 6 ms,这符合我们上面代码观察的预期,如果把计算范围继续加大,那么这个差距将会更加明显,实测如果计算 300000 次,那么将会由原来的 60 倍差距扩大到 500 倍,那就是说我们执行的 catch 次数越少折损效率越少,而如果我们执行的 catch 次数越多那么折损的效率也会越多。

所以在不得已的情况下使用 try catch 代码块,也要尽量保证少进入到 catch 控制流分支中。

const plus1 = () => 1;
const plus2 = () => { throw 1 };
console.time('sum');
let sum = 0;
for (let i = 0; i < 3000; i++) {
    try {
        // sum += plus1(); // 正确时候 约 0.1ms
        sum += plus2(); // 错误时候 约 6ms
    } catch (error) {
        sum += error;
    }
}
console.timeEnd('sum');

上面的种种表现进一步引发了我对项目性能的一些思考,我搜了下我们这个项目至少存在 800 多个 try catch,糟糕的是我们无法保证所有的 try catch 是不损害代码性能并且有意义的,这里面肯定会隐藏着很多上述类的 try catch 代码块。

从性能的角度来看,目前 V8 引擎确实在积极的通过 try catch 来优化这类代码片段,在以前浏览器版本中上面整个循环即使发生在 try catch 代码块内,它的速度也会变慢,因为以前浏览器版本会默认禁用 try catch 内代码的优化来方便我们调试异常。

try catch 需要遍历某种结构来查找 catch 处理代码,并且通常以某种方式分配异常(例如:需要检查堆栈,查看堆信息,执行分支和回收堆栈)。尽管现在大部分浏览器已经优化了,我们也尽量要避免去写出上面相似的代码,比如以下代码:

try {
    container.innerHTML = "I'm alloyteam";
}
catch (error) {
    // todo
}

上面这类代码我个人更建议写成如下形式,如果你实际上抛出并捕获了一个异常,它可能会变慢,但是由于在大多数情况下上面的代码是没有异常的,因此整体结果会比异常更快。

这是因为代码控制流中没有分支会降低运行速度,换句话说就是这个代码执行没错误的时候,没有在 catch 中浪费你的代码执行时间,我们不应该编写过多的 try catch 这会在我们维护和检查代码的时候提升不必要的成本,有可能分散并浪费我们的注意力。

当我们预感代码片段有可能出错,更应该是集中注意力去处理 successerror 的场景,而非使用 try catch 来保护我们的代码,更多时候 try catch 反而会让我们忽略了代码存在的致命问题。

if (container) container.innerHTML = "I'm alloyteam";
else // todo

在简单代码中应当减少甚至不用 try catch ,我们可以优先考虑 if else 代替,在某些复杂不可测的代码中也应该减少 try catch(比如异步代码),我们看过很多 asyncawait 的示例代码都是结合 try catch 的,在很多性能场景下我认为它并不合理,个人觉得下面的写法应该是更干净,整洁和高效的。

因为 JavaScript 是事件驱动的,虽然一个错误不会停止整个脚本,但如果发生任何错误,它都会出错,捕获和处理该错误几乎没有任何好处,代码主要部分中的 try catch 代码块是无法捕获事件回调中发生的错误。

通常更合理的做法是在回调方法通过第一个参数传递错误信息,或者考虑使用 Promisereject() 来进行处理,也可以参考 node 中的常见写法如下:

;(async () => {
    const [err, data] = await readFile();
    if (err) {
        // todo
    };
})()

fs.readFile('<directory>', (err, data) => {
    if (err) {
        // todo
    }
});

结合了上面的一些分析,我自己做出一些浅显的总结:

    1. 如果我们通过完善一些测试,尽量确保不发生异常,则无需尝试使用 try catch 来捕获异常。
    1. 非异常路径不需要额外的 try catch,确保异常路径在需要考虑性能情况下优先考虑 if else,不考虑性能情况请君随意,而异步可以考虑回调函数返回 error 信息对其处理或者使用 Promse.reject()
    1. 应当适当减少 try catch 使用,也不要用它来保护我们的代码,其可读性和可维护性都不高,当你期望代码是异常时候,不满足上述1,2的情景时候可考虑使用。

最后,笔者希望这篇文章能给到你我一些方向和启发吧,如有疏漏不妥之处,还请不吝赐教!

附笔记链接,阅读往期更多优质文章可移步查看,希望对你有些许的帮助,你的点赞是对我最大的鼓励:

查看原文

赞 18 收藏 10 评论 4

lishibo 赞了文章 · 9月10日

初学者应该看的JavaScript Promise 完整指南

作者:Adrian Mejia
译者:前端小智
来源:adrianmjia
点赞再看,养成习惯

本文 GitHubhttps://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

这篇文章算是 JavaScript Promises 比较全面的教程,该文介绍了必要的方法,例如 thencatchfinally。 此外,还包括处理更复杂的情况,例如与Promise.all并行执行Promise,通过Promise.race 来处理请求超时的情况,Promise 链以及一些最佳实践和常见的陷阱。

1.JavaScript Promises

Promise 是一个允许我们处理异步操作的对象,它是 es5 早期回调的替代方法。

与回调相比,Promise 具有许多优点,例如:

  • 让异步代码更易于阅读。
  • 提供组合错误处理。

* 更好的流程控制,可以让异步并行或串行执行。

回调更容易形成深度嵌套的结构(也称为回调地狱)。 如下所示:

a(() => {
  b(() => {
    c(() => {
      d(() => {
        // and so on ...
      });
    });
  });
});

如果将这些函数转换为 Promise,则可以将它们链接起来以生成更可维护的代码。 像这样:

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d)
  .catch(console.error);

在上面的示例中,Promise 对象公开了.then.catch方法,我们稍后将探讨这些方法。

1.1 如何将现有的回调 API 转换为 Promise?

我们可以使用 Promise 构造函数将回调转换为 Promise。

Promise 构造函数接受一个回调,带有两个参数resolvereject

  • Resolve:是在异步操作完成时应调用的回调。
  • Reject:是发生错误时要调用的回调函数。

构造函数立即返回一个对象,即 Promise 实例。 当在 promise 实例中使用.then方法时,可以在Promise “完成” 时得到通知。 让我们来看一个例子。

Promise 仅仅只是回调?

并不是。承诺不仅仅是回调,但它们确实对.then.catch方法使用了异步回调。 Promise 是回调之上的抽象,我们可以链接多个异步操作并更优雅地处理错误。来看看它的实际效果。

Promise 反面模式(Promises 地狱)

a(() => {
  b(() => {
    c(() => {
      d(() => {
        // and so on ...
      });
    });
  });
});

不要将上面的回调转成下面的 Promise 形式:

a().then(() => {
  return b().then(() => {
    return c().then(() => {
      return d().then(() =>{
        // ⚠️ Please never ever do to this! ⚠️
      });
    });
  });
});

上面的转成,也形成了 Promise 地狱,千万不要这么转。相反,下面这样做会好点:

a()
  .then(b)
  .then(c)
  .then(d)

超时

你认为以下程序的输出的是什么?

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('time is up ⏰');
  }, 1e3);

  setTimeout(() => {
    reject('Oops 🔥');
  }, 2e3);
});

promise
  .then(console.log)
  .catch(console.error);

是输出:

time is up ⏰
Oops! 🔥

还是输出:

time is up ⏰

是后者,因为当一个Promise resolved 后,它就不能再被rejected

一旦你调用一种方法(resolvereject),另一种方法就会失效,因为 promise 处于稳定状态。 让我们探索一个 promise 的所有不同状态。

1.2 Promise 状态

Promise 可以分为四个状态:

  • ⏳ Pending:初始状态,异步操作仍在进行中。
  • ✅ Fulfilled:操作成功,它调用.then回调,例如.then(onSuccess)
  • ⛔️ Rejected: 操作失败,它调用.catch.then的第二个参数(如果有)。 例如.catch(onError).then(..., onError)
  • 😵 Settled:这是 promise 的最终状态。promise 已经死亡了,没有别的办法可以解决或拒绝了。 .finally方法被调用。

clipboard.png

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

1.3 Promise 实例方法

Promise API 公开了三个主要方法:thencatchfinally。 我们逐一配合事例探讨一下。

Promise then

then方法可以让异步操作成功或失败时得到通知。 它包含两个参数,一个用于成功执行,另一个则在发生错误时使用。

promise.then(onSuccess, onError);

你还可以使用catch来处理错误:

promise.then(onSuccess).catch(onError);

Promise 链

then 返回一个新的 Promise ,这样就可以将多个Promise 链接在一起。就像下面的例子一样:

Promise.resolve()
  .then(() => console.log('then#1'))
  .then(() => console.log('then#2'))
  .then(() => console.log('then#3'));

Promise.resolve立即将Promise 视为成功。 因此,以下所有内容都将被调用。 输出将是

then#1
then#2
then#3

Promise catch

Promise .catch方法将函数作为参数处理错误。 如果没有出错,则永远不会调用catch方法。

假设我们有以下承诺:1秒后解析或拒绝并打印出它们的字母。

const a = () => new Promise((resolve) => setTimeout(() => { console.log('a'), resolve() }, 1e3));
const b = () => new Promise((resolve) => setTimeout(() => { console.log('b'), resolve() }, 1e3));
const c = () => new Promise((resolve, reject) => setTimeout(() => { console.log('c'), reject('Oops!') }, 1e3));
const d = () => new Promise((resolve) => setTimeout(() => { console.log('d'), resolve() }, 1e3));

请注意,c使用reject('Oops!')模拟了拒绝。

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d)
  .catch(console.error)

输出如下:

图片描述

在这种情况下,可以看到abc上的错误消息。

我们可以使用then函数的第二个参数来处理错误。 但是,请注意,catch将不再执行。

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d, () => console.log('c errored out but no big deal'))
  .catch(console.error)

图片描述

由于我们正在处理 .then(..., onError)部分的错误,因此未调用catchd不会被调用。 如果要忽略错误并继续执行Promise链,可以在c上添加一个catch。 像这样:

Promise.resolve()
  .then(a)
  .then(b)
  .then(() => c().catch(() => console.log('error ignored')))
  .then(d)
  .catch(console.error)

图片描述

当然,这种过早的捕获错误是不太好的,因为容易在调试过程中忽略一些潜在的问题。

Promise finally

finally方法只在 Promise 状态是 settled 时才会调用。

如果你希望一段代码即使出现错误始终都需要执行,那么可以在.catch之后使用.then

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d)
  .catch(console.error)
  .then(() => console.log('always called'));

或者可以使用.finally关键字:

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d)
  .catch(console.error)
  .finally(() => console.log('always called'));

1.4 Promise 类方法

我们可以直接使用 Promise 对象中四种静态方法。

  • Promise.all
  • Promise.reject
  • Promise.resolve
  • Promise.race

Promise.resolve 和 Promise.reject

这两个是帮助函数,可以让 Promise 立即解决或拒绝。可以传递一个参数,作为下次 .then 的接收:

Promise.resolve('Yay!!!')
  .then(console.log)
  .catch(console.error)

上面会输出 Yay!!!

Promise.reject('Oops 🔥')
  .then(console.log)
  .catch(console.error)

使用 Promise.all 并行执行多个 Promise

通常,Promise 是一个接一个地依次执行的,但是你也可以并行使用它们。

假设是从两个不同的api中轮询数据。如果它们不相关,我们可以使用Promise.all()同时触发这两个请求。

在此示例中,主要功能是将美元转换为欧元,我们有两个独立的 API 调用。 一种用于BTC/USD,另一种用于获得EUR/USD。 如你所料,两个 API 调用都可以并行调用。 但是,我们需要一种方法来知道何时同时完成最终价格的计算。 我们可以使用Promise.all,它通常在启动多个异步任务并发运行并为其结果创建承诺之后使用,以便人们可以等待所有任务完成。

const axios = require('axios');

const bitcoinPromise = axios.get('https://api.coinpaprika.com/v1/coins/btc-bitcoin/markets');
const dollarPromise = axios.get('https://api.exchangeratesapi.io/latest?base=USD');
const currency = 'EUR';

// Get the price of bitcoins on
Promise.all([bitcoinPromise, dollarPromise])
  .then(([bitcoinMarkets, dollarExchanges]) => {
    const byCoinbaseBtc = d => d.exchange_id === 'coinbase-pro' && d.pair === 'BTC/USD';
    const coinbaseBtc = bitcoinMarkets.data.find(byCoinbaseBtc)
    const coinbaseBtcInUsd = coinbaseBtc.quotes.USD.price;
    const rate = dollarExchanges.data.rates[currency];
    return rate * coinbaseBtcInUsd;
  })
  .then(price => console.log(`The Bitcoin in ${currency} is ${price.toLocaleString()}`))
  .catch(console.log);

如你所见,Promise.all接受了一系列的 Promises。 当两个请求的请求都完成后,我们就可以计算价格了。

我们再举一个例子:

const a = () => new Promise((resolve) => setTimeout(() => resolve('a'), 2000));
const b = () => new Promise((resolve) => setTimeout(() => resolve('b'), 1000));
const c = () => new Promise((resolve) => setTimeout(() => resolve('c'), 1000));
const d = () => new Promise((resolve) => setTimeout(() => resolve('d'), 1000));

console.time('promise.all');
Promise.all([a(), b(), c(), d()])
  .then(results => console.log(`Done! ${results}`))
  .catch(console.error)
  .finally(() => console.timeEnd('promise.all'));

解决这些 Promise 要花多长时间? 5秒? 1秒? 还是2秒?

这个留给你们自己验证咯。

Promise race

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

const a = () => new Promise((resolve) => setTimeout(() => resolve('a'), 2000));
const b = () => new Promise((resolve) => setTimeout(() => resolve('b'), 1000));
const c = () => new Promise((resolve) => setTimeout(() => resolve('c'), 1000));
const d = () => new Promise((resolve) => setTimeout(() => resolve('d'), 1000));

console.time('promise.race');
Promise.race([a(), b(), c(), d()])
  .then(results => console.log(`Done! ${results}`))
  .catch(console.error)
  .finally(() => console.timeEnd('promise.race'));

输出是什么?

输出 b。使用 Promise.race,最先执行完成就会结果最后的返回结果。

你可能会问:Promise.race的用途是什么?

我没胡经常使用它。但是,在某些情况下,它可以派上用场,比如计时请求或批量处理请求数组。

Promise.race([
  fetch('http://slowwly.robertomurray.co.uk/delay/3000/url/https://api.jsonbin.io/b/5d1fb4dd138da811182c69af'),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('request timeout')), 1000))
])
.then(console.log)
.catch(console.error);

图片描述

如果请求足够快,那么就会得到请求的结果。

图片描述

大家都说简历没项目写,我就帮大家找了一个项目,还附赠【搭建教程】

1.5 Promise 常见问题

串行执行 promise 并传递参数

这次,我们将对Node的fs使用promises API,并将两个文件连接起来:

const fs = require('fs').promises; // requires node v8+

fs.readFile('file.txt', 'utf8')
  .then(content1 => fs.writeFile('output.txt', content1))
  .then(() => fs.readFile('file2.txt', 'utf8'))
  .then(content2 => fs.writeFile('output.txt', content2, { flag: 'a+' }))
  .catch(error => console.log(error));

在此示例中,我们读取文件1并将其写入output 文件。 稍后,我们读取文件2并将其再次附加到output文件。 如你所见,writeFile promise返回文件的内容,你可以在下一个then子句中使用它。

如何链接多个条件承诺?

你可能想要跳过 Promise 链上的特定步骤。有两种方法可以做到这一点。

const a = () => new Promise((resolve) => setTimeout(() => { console.log('a'), resolve() }, 1e3));
const b = () => new Promise((resolve) => setTimeout(() => { console.log('b'), resolve() }, 2e3));
const c = () => new Promise((resolve) => setTimeout(() => { console.log('c'), resolve() }, 3e3));
const d = () => new Promise((resolve) => setTimeout(() => { console.log('d'), resolve() }, 4e3));

const shouldExecA = true;
const shouldExecB = false;
const shouldExecC = false;
const shouldExecD = true;

Promise.resolve()
  .then(() => shouldExecA && a())
  .then(() => shouldExecB && b())
  .then(() => shouldExecC && c())
  .then(() => shouldExecD && d())
  .then(() => console.log('done'))

如果你运行该代码示例,你会注意到只有ad被按预期执行。

另一种方法是创建一个链,然后仅在以下情况下添加它们:

const chain = Promise.resolve();

if (shouldExecA) chain = chain.then(a);
if (shouldExecB) chain = chain.then(b);
if (shouldExecC) chain = chain.then(c);
if (shouldExecD) chain = chain.then(d);

chain
  .then(() => console.log('done'));

如何限制并行 Promise?

要做到这一点,我们需要以某种方式限制Promise.all

假设你有许多并发请求要执行。 如果使用 Promise.all 是不好的(特别是在API受到速率限制时)。 因此,我们需要一个方法来限制 Promise 个数, 我们称其为promiseAllThrottled

// simulate 10 async tasks that takes 5 seconds to complete.
const requests = Array(10)
  .fill()
  .map((_, i) => () => new Promise((resolve => setTimeout(() => { console.log(`exec'ing task #${i}`), resolve(`task #${i}`); }, 5000))));

promiseAllThrottled(requests, { concurrency: 3 })
  .then(console.log)
  .catch(error => console.error('Oops something went wrong', error));

输出应该是这样的:

图片描述

以上代码将并发限制为并行执行的3个任务。

实现promiseAllThrottled 一种方法是使用Promise.race来限制给定时间的活动任务数量。

/**
 * Similar to Promise.all but a concurrency limit
 *
 * @param {Array} iterable Array of functions that returns a promise
 * @param {Object} concurrency max number of parallel promises running
 */
function promiseAllThrottled(iterable, { concurrency = 3 } = {}) {
  const promises = [];

  function enqueue(current = 0, queue = []) {
    // return if done
    if (current === iterable.length) { return Promise.resolve(); }
    // take one promise from collection
    const promise = iterable[current];
    const activatedPromise = promise();
    // add promise to the final result array
    promises.push(activatedPromise);
    // add current activated promise to queue and remove it when done
    const autoRemovePromise = activatedPromise.then(() => {
      // remove promise from the queue when done
      return queue.splice(queue.indexOf(autoRemovePromise), 1);
    });
    // add promise to the queue
    queue.push(autoRemovePromise);

    // if queue length >= concurrency, wait for one promise to finish before adding more.
    const readyForMore = queue.length < concurrency ? Promise.resolve() : Promise.race(queue);
    return readyForMore.then(() => enqueue(current + 1, queue));
  }

  return enqueue()
    .then(() => Promise.all(promises));
}

promiseAllThrottled一对一地处理 Promises 。 它执行Promises并将其添加到队列中。 如果队列小于并发限制,它将继续添加到队列中。 达到限制后,我们使用Promise.race等待一个承诺完成,因此可以将其替换为新的承诺。 这里的技巧是,promise 自动完成后会自动从队列中删除。 另外,我们使用 race 来检测promise 何时完成,并添加新的 promise 。

人才们的 【三连】 就是小智不断分享的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言,最后,谢谢大家的观看。


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://adrianmejia.com/promi...

交流

文章每周持续更新,可以微信搜索 【大迁世界 】 第一时间阅读,回复 【福利】 有多份前端视频等着你,本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,欢迎Star。

查看原文

赞 17 收藏 10 评论 0

lishibo 赞了文章 · 9月9日

秒懂科普!HTTP 3 如此简单

来源:https://www.jesuisundev.com/e...

什么是 HTTP3?本文以一种深入浅出的方式,让你快速了解 HTTP3。

你连 HTTP2 都还没搞明白,就有人开始谈 HTTP3 了,真让人火大。但 HTTP3 会受到关注也是有理由的:它速度很快。

1很久以前

谈未来之前,咱们先讲讲现实。

你了解 HTTP 吗?这个定义于 1991 年的协议是用来管理 Web 的。它的全名是超文本传输协议,让你可以从网页中获取资源,网页数据从 Web 服务器传输到你的浏览器上。它基于较低级别的协议——TCP,这里是重点——而且它是无状态的。这意味着每个请求都是完全独立的。页面上显示的每个 GIF 图片都在互联网上独立存在,这对这些 GIF 图片本身来说是好事。但对我们来说,这样的一个系统是有些支离破碎的。

问题在于每个请求一次只会查找一个文件。每次都要创建一个昂贵的 TCP 连接。想象一下,如果你的页面上有 10,000 个小技巧,这会是多么沉重的负担啊。

我知道有很多人喜欢我上一篇文章中制作的图片,所以为了更好地向大家解释互联网的协议机制,这里我会再做一张图。

尽管浏览器可以同时发出六个不同的请求,但是 HTTP 仍然很慢,并且需要很多 TCP 连接。另外,我们开发人员通常不会在意这一点。我们喜欢在页面上塞满各种垃圾。比如说巨大的 jQuery 库,包含 300 个无用的 CSS 样式表,结尾是一个透明的 8 兆大 PNG 图。

当谷歌发现我们在互联网上到处倾倒垃圾后,他们就开始搞一个称为 SPDY 的东西了。目的是什么呢?当然是加快互联网的速度。

SPDY 是一个规范,建议继续使用 HTTP,但要更改一些规则。通过压缩标头、对请求进行优先级排序和多路复用,它将把所有 TCP 请求和连接变成单独的一个!

具体来说,当你读取 HTML 时,浏览器会查看你在页面中要询问的所有内容。然后,它可以一次获取所有内容,这样就可以避免一个文件一个文件地获取了。

HTTP2 的第一份草案基于 SPDY。HTTP2 很快被广泛采用,随后互联网上的一切变得快多了。今天,互联网上 42.7%的内容使用 HTTP2

2关于 HTTP3

HTTP2 是以 HTTP 为基础并改动一些规则的产物。HTTP3 也是如此。换句话说,解释清楚现状后,我就可以很容易地讲明白未来是什么样子的。

谷歌是一个极客组织,他们永远不会停止脚步。SPDY 演变成为 HTTP2 后,他们认为它仍然不够快。因此,他们开始讨论 QUIC 这个项目。这是谷歌开发的第二项将成为 HTTP 协议的正式升级的技术。那么,这个协议有什么特别之处?

HTTP3 的主要改进在传输层上。传输层不会再有我前面提到的那些繁重的 TCP 连接了。现在,一切都会走 UDP。

顺便说一下,QUIC 的意思是“快速 UDP Internet 连接”。协议的这种更改将显著加快连接建立和数据传输的速度。然而,虽说 UDP 肯定更快、更简单,但它不具备 TCP 的可靠性和错误处理能力。

TCP 必须进行多次往返,才能以方形且稳定的方式建立连接。UDP 不会顾虑那么多,而且它确实可以快速运行,代价是稳定性下降和丢包的风险。但是,UDP 能大大减少请求中的延迟。到同一服务器的重复连接的延迟几乎为零,因为不需要往返来建立连接。

HTTP3 是 HTTP2 的复用和压缩,协议从 TCP 更改为 UDP。然后,谷歌的那些人在协议中添加了他们做的层,以确保稳定性、数据包接收顺序及安全性。

因此,HTTP3 在保持 QUIC 稳定性的同时使用 UDP 来实现高速度,同时又不会牺牲 TLS 的安全性。是的,在 QUIC 中就有 TLS1.3,你可以用它发起优雅的 SSL。这些层的底层机制是下面这样:

2018 年,QUIC 演变成为 HTTP3。互联网工程任务组(Internet Engineerring Task Force)的那帮制定互联网协议的哥们同意了这个提案。这是个好消息,因为对于我们这些急躁的人们来说,互联网的速度永远都不够快。

3结语

HTTP3 代表着充满魅力的未来,它的 HTTP 基础潜能已经被谷歌的那些极客发挥到极致。在撰写本文时,只有 4.6%的互联网内容在使用 HTTP3,但这个数字在未来几年中可能会增长许多。本文只是简单谈了谈这方面的知识,但如果你想了解更多,网上有很多的文章可供你参考。

image

查看原文

赞 7 收藏 2 评论 1

认证与成就

  • 获得 21 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 5月28日
个人主页被 99 人浏览