harden

harden 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

前端菜鸟

个人动态

harden 收藏了文章 · 2020-06-08

解读redux工作原理

欢迎访问个人博客的其他文章

1. 前言

随着WEB应用变得越来越复杂,再加上node前后端分离越来越流行,那么对数据流动的控制就显得越发重要。redux是在flux的基础上产生的,基本思想是保证数据的单向流动,同时便于控制、使用、测试。

redux不依赖于任意框架(库),只要subscribe相应框架(库)的内部方法,就可以使用该应用框架保证数据流动的一致性。

那么如何使用redux呢?下面一步步进行解析,并带有源码说明,不仅做到知其然,还要做到知其所以然

2. 主干逻辑介绍(createStore)

2.1 简单demo入门

先来一个直观的认识:

// 首先定义一个改变数据的plain函数,成为reducer
function count (state, action) {
    var defaultState = {
        year: 2015,
      };
    state = state || defaultState;
    switch (action.type) {
        case 'add':
            return {
                year: state.year + 1
            };
        case 'sub':
            return {
                year: state.year - 1
            }
        default :
            return state;
    }
}

// store的创建
var createStore = require('redux').createStore;
var store = createStore(count);

// store里面的数据发生改变时,触发的回调函数
store.subscribe(function () {
      console.log('the year is: ', store.getState().year);
});

// action: 触发state改变的唯一方法(按照redux的设计思路)
var action1 = { type: 'add' };
var action2 = { type: 'add' };
var action3 = { type: 'sub' };

// 改变store里面的方法
store.dispatch(action1); // 'the year is: 2016
store.dispatch(action2); // 'the year is: 2017
store.dispatch(action3); // 'the year is: 2016

2.2 挖掘createStore实现

为了说明主要问题,仅列出其中的关键代码,全部代码,可以点击这里阅读。

a 首先看createStore到底都返回的内容:

export default function createStore(reducer, initialState) {
    ...
    return {
        dispatch,
        subscribe,
        getState,
        replaceReducer
    }
}

每个属性的含义是:

  • dispatch: 用于action的分发,改变store里面的state

  • subscribe: 注册listener,store里面state发生改变后,执行该listener

  • getState: 读取store里面的state

  • replaceReducer: 替换reducer,改变state修改的逻辑

b 关键代码解析

export default function createStore(reducer, initialState) {
    // 这些都是闭包变量
    var currentReducer = reducer
    var currentState = initialState
    var listeners = []
    var isDispatching = false;

    // 返回当前的state
    function getState() {
        return currentState
    }

    // 注册listener,同时返回一个取消事件注册的方法
    function subscribe(listener) {
        listeners.push(listener)
        var isSubscribed = true

        return function unsubscribe() {
            if (!isSubscribed) {
                return
            }

            isSubscribed = false
            var index = listeners.indexOf(listener)
            listeners.splice(index, 1)
        }
    }

    // 通过action该改变state,然后执行subscribe注册的方法
    function dispatch(action) {
        try {
          isDispatching = true
              currentState = currentReducer(currentState, action)
        } finally {
              isDispatching = false
        }
        listeners.slice().forEach(listener => listener())
        return action
    }

    // 替换reducer,修改state变化的逻辑
    function replaceReducer(nextReducer) {
           currentReducer = nextReducer
           dispatch({ type: ActionTypes.INIT })
       }

       // 初始化时,执行内部一个dispatch,得到初始state
       dispatch({ type: ActionTypes.INIT })
}

如果还按照2.1的方式进行开发,那跟flux没有什么大的区别,需要手动解决很多问题,那redux如何将整个流程模板化(Boilerplate)呢?

3. 保证store的唯一性

随着应用越来越大,一方面,不能把所有的数据都放到一个reducer里面,另一方面,为每个reducer创建一个store,后续store的维护就显得比较麻烦。如何将二者统一起来呢?

3.1 demo入手

通过combineReducers将多个reducer合并成一个rootReducer:

// 创建两个reducer: count year
function count (state, action) {
  state = state || {count: 1}
  switch (action.type) {
    default:
      return state;
  }
}
function year (state, action) {
  state = state || {year: 2015}
  switch (action.type) {
    default:
      return state;
  }
}

// 将多个reducer合并成一个
var combineReducers = require('./').combineReducers;
var rootReducer = combineReducers({
  count: count,
  year: year,
});

// 创建store,跟2.1没有任何区别
var createStore = require('./').createStore;
var store = createStore(rootReducer);

var util = require('util');
console.log(util.inspect(store));
//输出的结果,跟2.1的store在结构上不存在区别
// { dispatch: [Function: dispatch],
//   subscribe: [Function: subscribe],
//   getState: [Function: getState],
//   replaceReducer: [Function: replaceReducer]
// }

3.2 源码解析combineReducers

// 高阶函数,最后返回一个reducer
export default function combineReducers(reducers) {
    // 提出不合法的reducers, finalReducers就是一个闭包变量
    var finalReducers = pick(reducers, (val) => typeof val === 'function')
    // 将各个reducer的初始state均设置为undefined
    var defaultState = mapValues(finalReducers, () => undefined)

    // 一个总reducer,内部包含子reducer
    return function combination(state = defaultState, action) {
        var finalState = mapValues(finalReducers, (reducer, key) => {
            var previousStateForKey = state[key]
            var nextStateForKey = reducer(previousStateForKey, action)
            hasChanged = hasChanged || nextStateForKey !== previousStateForKey
            return nextStateForKey
        }
    }

    return hasChanged ? finalState : state

}

4. 自动实现dispatch

4.1 demo介绍

在2.1中,要执行state的改变,需要手动dispatch:

var action = { type: '***', payload: '***'};
dispatch(action);

手动dispatch就显得啰嗦了,那么如何自动完成呢?

var bindActionCreators = require('redux').bindActionCreators;
// 可以在具体的应用框架隐式进行该过程(例如react-redux的connect组件中)
bindActionCreators(action)

4.2 源码解析

// 隐式实现dispatch
function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

export default function bindActionCreators(actionCreators, dispatch) {
    if (typeof actionCreators === 'function') {
        return bindActionCreator(actionCreators, dispatch)
    }
    return mapValues(actionCreators, actionCreator =>
        bindAQctionCreator(actionCreator, dispatch)
    )
}

5. 支持插件 - 对dispatch的改造

5.1 插件使用demo

一个action可以是同步的,也可能是异步的,这是两种不同的情况, dispatch执行的时机是不一样的:

// 同步的action creator, store可以默认实现dispatch
function add() {
    return { tyle: 'add' }
}
dispatch(add());

// 异步的action creator,因为异步完成的时间不确定,只能手工dispatch
function fetchDataAsync() {
    return function (dispatch) {
        requst(url).end(function (err, res) {
            if (err) return dispatch({ type: 'SET_ERR', payload: err});
            if (res.status === 'success') {
                dispatch({ type: 'FETCH_SUCCESS', payload: res.data });
            }
        })
    }
}

下面的问题就变成了,如何根据实际情况实现不同的dispatch方法,也即是根据需要实现不同的moddleware:

// 普通的dispatch创建方法
var store = createStore(reducer, initialState);
console.log(store.dispatch);

// 定制化的dispatch
var applyMiddleware = require('redux').applyMiddleware;
// 实现action异步的middleware
var thunk = requre('redux-thunk');
var store = applyMiddleware([thunk])(createStore);
// 经过处理的dispatch方法
console.log(store.dispatch);

5.2 源码解析

// next: 其实就是createStore
export default function applyMiddleware(...middlewares) {
  return (next) => (reducer, initialState) => {
    var store = next(reducer, initialState)
    var dispatch = store.dispatch
    var chain = []

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch // 实现新的dispatch方法
    }
  }
}
// 再看看redux-thunk的实现, next就是store里面的上一个dispatch
function thunkMiddleware({ dispatch, getState }) {
    return function(next) {
        return function(action) {
            typeof action === 'function' ?
            action(dispatch, getState) :
            next(action);
        }
    }
  return next => action =>
    typeof action === 'function' ?
      action(dispatch, getState) :
      next(action);
}

6. 与react框架的结合

6.1 基本使用

目前已经有现成的工具react-redux来实现二者的结合:

var rootReducers = combineReducers(reducers);
var store = createStore(rootReducers);
var Provider = require('react-redux').Provider;
// App 为上层的Component
class App extend React.Component{
    render() {
        return (
            <Provier store={store}>
                <Container />
            </Provider>
        );
    }
}

// Container作用: 1. 获取store中的数据; 2.将dispatch与actionCreator结合起来
var connect = require('react-redux').connect;
var actionCreators = require('...');
// MyComponent是与redux无关的组件
var MyComponent = require('...');

function select(state) {
    return {
        count: state.count
    }
}
export default connect(select, actionCreators)(MyComponent)

6.2 Provider -- 提供store

React通过Context属性,可以将属性(props)直接给子孙component,无须通过props层层传递, Provider仅仅起到获得store,然后将其传递给子孙元素而已:

export default class Provider extends Component {
  getChildContext() { // getChildContext: 将store传递给子孙component
    return { store: this.store }
  }

  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }

  componentWillReceiveProps(nextProps) {
    const { store } = this
    const { store: nextStore } = nextProps

    if (store !== nextStore) {
      warnAboutReceivingStore()
    }
  }

  render() {
    let { children } = this.props
    return Children.only(children)
  }
}

6.3 connect -- 获得store及dispatch(actionCreator)

connect是一个高阶函数,首先传入mapStateToProps、mapDispatchToProps,然后返回一个生产Component的函数(wrapWithConnect),然后再将真正的Component作为参数传入wrapWithConnect(MyComponent),这样就生产出一个经过包裹的Connect组件,该组件具有如下特点:

  • 通过this.context获取祖先Component的store

  • props包括stateProps、dispatchProps、parentProps,合并在一起得到nextState,作为props传给真正的Component

  • componentDidMount时,添加事件this.store.subscribe(this.handleChange),实现页面交互

  • shouldComponentUpdate时判断是否有避免进行渲染,提升页面性能,并得到nextState

  • componentWillUnmount时移除注册的事件this.handleChange

  • 在非生产环境下,带有热重载功能

    // 主要的代码逻辑
    export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {

       return function wrapWithConnect(WrappedComponent) {
         class Connect extends Component {
               constructor(props, context) {
                   // 从祖先Component处获得store
                   this.store = props.store || context.store
                   this.stateProps = computeStateProps(this.store, props)
                   this.dispatchProps = computeDispatchProps(this.store, props)
                   this.state = { storeState: null }
                   // 对stateProps、dispatchProps、parentProps进行合并      
                   this.updateState()
               }
               shouldComponentUpdate(nextProps, nextState) {
                   // 进行判断,当数据发生改变时,Component重新渲染
                   if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
                     this.updateState(nextProps)
                     return true
                   }
               }
               componentDidMount() {
                   // 改变Component的state
                 this.store.subscribe(() = {
                     this.setState({
                       storeState: this.store.getState()
                     })
                 })
               }
               render() {
                   // 生成包裹组件Connect
                 return (
                   <WrappedComponent {...this.nextState} />
                 )
               }
           }
           Connect.contextTypes = {
             store: storeShape
           }
           return Connect;
       }

    }

7. redux与react-redux关系图

图片描述

查看原文

harden 关注了专栏 · 2020-03-31

冴羽的JavaScript博客

冴羽写JS文章的地方,预计写四个系列:JavaScript深入系列、JavaScript专题系列 、ES6系列、React系列。

关注 1461

harden 赞了文章 · 2020-03-31

2020年前端开发应该如何准备面试(面试官角度分享)

近期疫情的影响,大家都有发现了,很多公司为了减少现金开支都在裁员,裁员的同时也有很多公司在疯狂招人。这让大家都很疑惑了,今年到底是就业容易还是就业难呢?招聘旺季金三银四还有没有?今年的就业应该如何去准备?

阿靖最近也在忙着招聘,今天就来跟大家分享今年的就业市场情况和面试关注点,帮助大家提前准备好面试。
1905236.jpg

今年的金三银四没了?

先说一下大家比较关心的金三银四,往年的3月份这个时候,是招聘量最高的时候,同比今年确实减少了一点,主要是受疫情影响,很多公司为了避免接触,加上没有全面复工的情况,招聘量比以往少了很多。

但是也有很多在疫情期间发展比较好的公司,都在非常积极的招聘,不方便接触就直接线上面试。今年的招聘情况,国家和招聘平台都在联合很多公司在做招聘宣传,预计等疫情过去,5、6月会迎来一个高峰期,到时候应届毕业生会大批涌出来,竞争也比较大。

今年的面试重点

今年的前端开发工程师面试,很多人都比较迷茫,1.担心受疫情影响,难度增加。2.Vue等框架新知识增加,会不会在面试中考到。

其实和疫情没有太大影响,今年的面试考察重在基础(技术知识和编程原理),丰富的项目(项目经验和细节),了解工作流程,面试表述到位。

其实这些面试的考察都是和往年差不多的,不过很多同学不知道怎么准备,下面我结合一下面试招聘的基本流程来讲讲如何准备,大家可以根据这几方面提前准备好。
102203.jpg

壹 第一印象—简历

简历是到公司面试的敲门砖,如果无法通过的第一步简历筛选,就没有后面的事了。

所以简历一定要写好,千万不要出现错别字或者语句不通的地方,特别是别把专业名词写错了。

如果有条件,可以找老师或者同学帮你看看简历,力求第一印象良好。

关于简历的内容怎么写,网上很多人教你如何写简历,百度一搜一大堆都不一样的,有些教你把技术栈罗列出来,有的又不推荐,很多人都看懵了。

我从面试官的角度来说,这些情况要根据个人实际情况出发来写。面试官想看到你最想表现的东西。比如技术栈的罗列,如果你简历中写的项目经验能体现你罗列的技术栈,你可以不写出来。但如果不能完全体现你的技术栈,那能写出来很有必要。

因为简历是你的能力体现,是展示你能力的地方,一定要突出自己的特长。

最后提醒:
如果是自荐简历,特别是校招,发送到邮箱的时候,一定要署名!!一般格式(谁+应聘什么岗位+技术等级),这样可以让面试官面方便找到你的简历,你也可以在众多没名字的简历中脱颖而出!

贰 技术基础知识

基础知识一直都是重点考察的内容,包含有HTML(5)、CSS(3)、JavaScript到node、webpack、Vue所有可能考察的知识。

基础知识不仅仅要知道是什么,更要明白怎么用,为什么这么用。死记硬背能应付一时,如果你遇到了想深入聊聊的面试官,死记硬背就没用了。所以每个知识点都要理解透彻,讲的清楚。

今天来罗列一些常考的面试知识点,大家可以提前准备

HTML&CSS:

flex布局、垂直居中、清除浮动、BFC、三栏布局、两栏布局、动画、盒模型、H5新特性

JavaScript:

继承、原型链、this指向、设计模式、call, apply, bind, new实现、防抖节流、let, var, const 区别、event、loop、promise使用及实现、promise并行执行和顺序执行、闭包、垃圾回收和内存泄漏、数组方法、数组乱序, 数组扁平化、事件委托、事件监听、事件模型、typescript

Vue:

vue数据双向绑定原理、vue computed原理、vue编译器结构图、生命周期、vue组件通信、mmvm模式、mvc模式理解、vue dom diff、vuex、vue-router

react:

dom-diff、列表key属性、jsx原理(createElement)、react-router原理、redux原理、生命周期、react setState、react组件通信、性能优化

网络:

HTTP1, HTTP2, HTTPS、浏览从输入网址到回车发生了什么、前端跨域、浏览器缓存、cookie, session, token, localstorage, sessionstorage、状态码、TCP连接(三次握手, 四次挥手)

叁 项目经验会考什么

面试中,项目经验非常重要,是作为能力评估的重要参考标准。对于基础知识的面试,项目经验面试更要考察你的实际开发能力,需要能讲出自己的实战理解和给出具体的实现方案。

一般都会问到一些技术上的实现技巧,可能会让你用代码来实现,一般说清楚思路就可以了,也可以写伪代码来体现。一般能表述清楚,评价不会太低。

项目经验会考察的问题包括但不限于:

项目开发流程、技术架构、说说做的比较好的项目、你在项目中担任的角色、项目中的收获、项目两点、项目中遇到的难点、简历中罗列的技术栈你是怎么理解的,如果让你运用实现你怎么做、组件设计和实现、兼容性问题、底层原理、性能优化、工程化、前端学习规划、算法

肆 和HR的面试

很多做技术的人,都很小看HR的面试环节,认为我们是做技术的,HR并不会问到技术相关的问题,感觉HR不会对我们的面试起关键作用。其实恰恰相反,HR的面试意见是占重要地位的。

在和HR聊天的时候,一定要注意技巧,有的可以说,有的不能随便说。不要以为HR和你聊天很轻松,你就什么都乱说。比如说,HR问你,是否还有面试其他公司,是否还有其他公司的offer,这个时候你不能直接回答说有多少或者完全没有。你可以说,已经面试了很多家公司,但感觉都不是很合适,想找一家能完全施展我能力的公司。要表现出在等这家公司的答复,表现出你想进这家公司。

还有一些问题,一定要快速回答,比如说绩效、上家公司的离职原因。这种问题如果你想了半天,回答得含含糊糊的,别人会认为你不诚实。这些问题最好都能提前准备好。

伍 谈薪资

谈薪资是大家最感兴趣的话题,怎么谈到一个好的薪资,才是我们去应聘的目的。这个环节也是面试中考察的重要一个环节,要看能否对自己又正确的判断。

一般我们要怎么提薪资呢?

要根据自己的实际情况,包括面试的表现和工作年限。一般跳槽的面试,在原有的情况下涨幅10%-30%是正常的,但是也不是绝对,还有50%、60%的情况,主要是要学会自我评估。如果你是初次就业,要根据自己的能力评级,结合自己的面试表现,参考市场行情来提薪资。

举个例子,如果你的的面试感觉非常好,并且你已经3年没跳槽了,可以尝试按你这个工作经验的市场价要高一点(50%、60%);如果面试表现差一点,就提30%。这个再差就再低一点。

切记薪资不能乱要!不要看别人要多少你就要多少!
每个公司都有自己的招聘预留目标,要按照自己的能力来要薪资,要太高对方公司不会和你谈,直接拒绝你的。因为投简历的不止你一个人,对方公司还有很多选择,别人性价比比你高,除非你的优势非常明显。

如果你在面试之前已经能拿到了别的公司的offer,那用来对比要高薪资是可以的。

190516.jpeg

最后总结

总结来说,面试成功=基础知识+项目经验+技术理解+表达技巧+运气。我们无法控制运气,但是我们可以在别的地方花更多时间,每个环节都提前做好准备。

面试一方面是为了找到工作,升职加薪,另一方面也是对于自我能力的考察。在我看来,能够面试成功不仅仅是来自面试前的临时抱佛脚,更重要的是在平时学习和工作中不断积累和坚持,把每个知识点、每一次项目开发、每次遇到的难点知识,做好积累,实践和总结。

前几天我有个学员问了我个问题:

现在的年轻人想成功一定要有很好的机遇,那我们是应该努力锻炼能力还是应该主动去寻找机遇呢?

在我看来,在遇到机遇之前,你要有很好的能力,抓住每一次机会积累你的能力,等到机遇来临,你不至于只能怪自己能力不足,抓不住机遇。共勉!

祝大家在疫情后都能拿到期望的offer!
更多前端开发技术分享,欢迎关注公众号【前端研究所】
前端研究所片尾2.png

查看原文

赞 22 收藏 18 评论 1

harden 收藏了文章 · 2020-03-22

三句话总结 async await 用法

公司有个项目,类似用户自定义试卷试题的功能,很多表单需要验证,但是又要根据配置自动生成,所以,每个输入框都是一个组件,验证是异步,如果全部都用Promise看起来也很头大,各方查阅,总结如下。

三句话看懂 async/await

1 async 函数执行结果都是Promise

clipboard.png

async function HiAsync() {
 return "hi";
}
async function HelloAsync() {
 return Promise.resolve('hello')
}

console.log(HiAsync())
console.log(HelloAsync())

HiAsync().then(r => {
    console.log(r) // 'hi'
})
HelloAsync().then(r => {
    console.log(r)  // 'hello'
})

2 await 总能等到结果

(即便是嵌套多层的异步)

clipboard.png

function getSomething() {
    return "a";
}

async function testAsync() {
    return new Promise((re, rj) => {
        setTimeout(() => { re('b') }, 500)
    })
}
async function deepAsync() {
    let p2 = new Promise((re, rj) => {
        setTimeout(() => { re('c') }, 10)
    })
    return new Promise((re, rj) => {
        setTimeout(() => { re(p2) }, 500)
    })
}

async function test() {
    const v1 = await getSomething();
    console.log('v1',v1)
    const v2 = await testAsync();
    console.log('v2',v2)
    const v3 = await deepAsync();
    console.log('v3',v3);
}
test();

3 await 的使用时 必须在async 函数中

easy? 表述可还清楚?有遗漏请指正。

查看原文

harden 收藏了文章 · 2020-03-22

如何正确合理使用 JavaScript async/await !

阿里云最近在做活动,低至2折,有兴趣可以看看:
https://promotion.aliyun.com/...

为了保证的可读性,本文采用意译而非直译。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

ES8 引入的 async/await 在 JavaScript 的异步编程中是一个极好的改进。它提供了使用同步样式代码异步访问 resoruces 的方式,而不会阻塞主线程。然而,它们也存在一些坑及问题。在本文中,将从不同的角度探讨 async/await,并演示如何正确有效地使用这对兄弟。

前置知识

async 作用是什么

MDN 可以看出:

async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

如果 async 函数没有返回值, 它会返回 Promise.resolve(undefined)

await 作用是什么

MDN 了解到:

await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,await 可以等任意表达式的结果)。

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。

async/await 的优点

async/await 带给我们的最重要的好处是同步编程风格。让我们看一个例子:

图片描述

很明显,async/await 版本比 promise 版本更容易理解。如果忽略 await 关键字,代码看起来就像任何其他同步语言,比如 Python

最佳的地方不仅在于可读性。async/await 到今天为止,所有主流浏览器都完全支持异步功能。

图片描述

本地浏览器的支持意味着你不必转换代码。更重要的是,它便于调试。当在函数入口点设置断点并跨过 await 行时,将看到调试器在 bookModel.fetchAll() 执行其任务时暂停片刻,然后它将移动到下一个.filter 行,这比 promise 代码要简单得多,在 promise 中,必须在 .filter 行上设置另一个断点。

图片描述

另一个不太明显的优点是 async 关键字。 async声明 getBooksByAuthorWithAwait()函数返回值确保是一个 promise,因此调用者可以安全地使用 getBooksByAuthorWithAwait().then(...) 或await getBooksByAuthorWithAwait()。 想想下面的例子(不好的做法!):

图片描述

在上述代码中,getBooksByAuthorWithPromise 可能返回 promise(正常情况下)或 null 值(异常情况下),在异常情况下,调用者不能调用 .then()。有了async 声明,这种情况就不会出现了。

async/await 可能会产生误导

一些文章将 async/waitPromise 进行了比较,并声称它是 JavaScript 下一代异步编程风格,对此作者深表异议。async/await 是一种改进,但它只不过是一种语法糖,不会完全改变我们的编程风格。

从本质上说,async 函数仍然是 promise。在正确使用 async 函数之前,你必须先了解 promise,更糟糕的是,大多数时候你需要在使用 promises 的同时使用 async 函数。

考虑上面示例中的 getBooksByAuthorWithAwait()getbooksbyauthorwithpromise() 函数。请注意,它们不仅功能相同,而且具有完全相同的接口!

这意味着 getbooksbyauthorwithwait() 将返回一个 promise,所以也可以使用 .then(...)方式来调用它。

嗯,这未必是件坏事。只有 await 的名字给人一种感觉,“哦,太好了,可以把异步函数转换成同步函数了”,这实际上是错误的。

async/await

那么在使用 async/await 时可能会犯什么错误呢?下面是一些常见的例子。

太过串行化

尽管 await 可以使代码看起来像是同步的,但实际它们仍然是异步的,必须小心避免太过串行化。

图片描述

上述代码在逻辑上看似正确的,然而,这是错误的。

  1. await bookModel.fetchAll() 会等待 fetchAll() 直到 fetchAll() 返回结果。
  2. 然后 await authorModel.fetch(authorId) 被调用。

注意,authorModel.fetch(authorId) 并不依赖于 bookModel.fetchAll() 的结果,实际上它们可以并行调用!然而,用了 await,两个调用变成串行的,总的执行时间将比并行版本要长得多得多。

下面是正确的方式:

图片描述

更糟糕的是,如果你想要一个接一个地获取项目列表,你必须依赖使用 promises:

图片描述

简而言之,你仍然需要将流程视为异步的,然后使用 await 写出同步的代码。在复杂的流程中,直接使用 promise 可能更方便。

错误处理

promise中,异步函数有两个可能的返回值: resolvedrejected。我们可以用 .then() 处理正常情况,用 .catch() 处理异常情况。然而,使用 async/await方式的,错误处理可能比较棘手。

try…catch

最标准的(也是作者推荐的)方法是使用 try...catch 语法。在 await 调用时,在调用 await 函数时,如果出现非正常状况就会抛出异常,await 命令后面的 promise 对象,运行结果可能是 rejected,所以最好把await 命令放在 try...catch 代码块中。如下例子:

图片描述

在捕捉到异常之后,有几种方法来处理它:

  • 处理异常,并返回一个正常值。(不在 catch 块中使用任何 return 语句相当于使用 return undefined,undefined 也是一个正常值。)
  • 如果你想让调用者处理它,你可以直接抛出普通的错误对象,如 throw errorr,它允许你在 promise 链中使用 async getBooksByAuthorWithAwait() 函数(也就是说,可以像getBooksByAuthorWithAwait().then(...).catch(error => ...) 处理错误); 或者可以用 Error 对象将错误封装起来,如 throw new Error(error),当这个错误在控制台中显示时,它将给出完整的堆栈跟踪信息。
  • 拒绝它,就像 return Promise.reject(error) ,这相当于 throw error,所以不建议这样做。

使用 try...catch 的好处:

  • 简单,传统。只要有Java或c++等其他语言的经验,理解这一点就不会有任何困难。
  • 如果不需要每步执行错误处理,你仍然可以在一个 try ... catch 块中包装多个 await 调用来处理一个地方的错误。

这种方法也有一个缺陷。由于 try...catch 会捕获代码块中的每个异常,所以通常不会被 promise 捕获的异常也会被捕获到。比如:

图片描述

运行此代码,你将得到一个错误 ReferenceError: cb is not defined。这个错误是由console.log()打印出来的的,而不是 JavaScript 本身。有时这可能是致命的:如果 BookModel 被包含在一系列函数调用中,其中一个调用者吞噬了错误,那么就很难找到这样一个未定义的错误。

让函数返回两个值

另一种错误处理方法是受到Go语言的启发。它允许异步函数返回错误和结果。详情请看这篇博客文章:

How to write async await without try-catch blocks in Javascript

简而言之,你可以像这样使用异步函数:

[err, user] = await to(UserModel.findById(1));

作者个人不喜欢这种方法,因为它将 Go 语言的风格带入到了 JavaScript 中,感觉不自然。但在某些情况下,这可能相当有用。

使用 .catch

这里介绍的最后一种方法就是继续使用 .catch()

回想一下 await 的功能:它将等待 promise 完成它的工作。值得注意的一点是 promise.catch() 也会返回一个 promise ,所以我们可以这样处理错误:

图片描述

这种方法有两个小问题:

  • 它是 promises 和 async 函数的混合体。你仍然需要理解 是promises 如何工作的。
  • 错误处理先于正常路径,这是不直观的。

结论

ES7引入的 async/await 关键字无疑是对J avaScrip t异步编程的改进。它可以使代码更容易阅读和调试。然而,为了正确地使用它们,必须完全理解 promise,因为 async/await 只不过是 promise 的语法糖,本质上仍然是 promise

原文:

https://hackernoon.com/javasc...

你的点赞是我持续分享好东西的动力,欢迎点赞!

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

clipboard.png

查看原文

harden 收藏了文章 · 2020-03-21

前端性能优化小结(面试干货)

性能优化.jpeg

前言

移动互联网时代,用户对于网页的打开速度要求越来越高。首屏作为直面用户的第一屏,其重要性不言而喻。优化用户体验更是我们前端开发非常需要 focus 的东西之一。

从用户的角度而言,当打开一个网页,往往关心的是从输入完网页地址后到最后展现完整页面这个过程需要的时间,这个时间越短,用户体验越好。所以作为网页的开发者,就从输入url到页面渲染呈现这个过程中去提升网页的性能。

所以输入URL后发生了什么呢?在浏览器中输入url会经历域名解析、建立TCP连接、发送http请求、资源解析等步骤。

http缓存优化是网页性能优化的重要一环,这一部分我会在后续笔记中做一个详细总结,本文暂不多做详细整理。本文主要从网页渲染过程、网页交互以及Vue应用优化三个角度对性能优化做一个小结。

一、页面加载及渲染过程优化

1. 浏览器渲染流程

首先谈谈拿到服务端资源后浏览器渲染的流程:
webkit 渲染引擎工作流程图.png

1. 解析 HTML 文件,构建 DOM 树,同时浏览器主进程负责下载 CSS 文件
2. CSS 文件下载完成,解析 CSS 文件成树形的数据结构,然后结合 DOM 树合并成 RenderObject 树
3. 布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的尺寸,位置等计算
4. 绘制 RenderObject 树 (paint),绘制页面的像素信息
5. 浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面

2. CRP(关键渲染路径Critical Rendering Path)优化

关键渲染路径是浏览器将 HTML、CSS、JavaScript 转换为在屏幕上呈现的像素内容所经历的一系列步骤。也就是我们刚刚提到的的的浏览器渲染流程。

为尽快完成首次渲染,我们需要最大限度减小以下三种可变因素:

* 关键资源的数量: 可能阻止网页首次渲染的资源。
* 关键路径长度: 获取所有关键资源所需的往返次数或总时间。
* 关键字节: 实现网页首次渲染所需的总字节数,等同于所有关键资源传送文件大小的总和。
优化 DOM
* 删除不必要的代码和注释包括空格,尽量做到最小化文件。
* 可以利用 GZIP 压缩文件。
* 结合 HTTP 缓存文件。
优化 CSSOM

首先,DOM 和 CSSOM 通常是并行构建的,所以 CSS 加载不会阻塞 DOM 的解析

然而,由于 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,
所以他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成(或者 CSS 资源加载失败)后,才能开始渲染。因此,CSS 加载会阻塞 Dom 的渲染

由此可见,对于 CSSOM 缩小、压缩以及缓存同样重要,我们可以从这方面考虑去优化。

* 减少关键 CSS 元素数量
* 当我们声明样式表时,请密切关注媒体查询的类型,它们极大地影响了 CRP 的性能 。
优化 JavaScript

当浏览器遇到 script 标记时,会阻止解析器继续操作,直到 CSSOM 构建完毕,JavaScript 才会运行并继续完成 DOM 构建过程。

* async: 当我们在 script 标记添加 async 属性以后,浏览器遇到这个 script 标记时会继续解析 DOM,同时脚本也不会被 CSSOM 阻止,即不会阻止 CRP。
* defer: 与 async 的区别在于,脚本需要等到文档解析后( DOMContentLoaded 事件前)执行,而 async 允许脚本在文档解析时位于后台运行(两者下载的过程不会阻塞 DOM,但执行会)。
* 当我们的脚本不会修改 DOM 或 CSSOM 时,推荐使用 async 。
* 预加载 —— preload & prefetch 。
* DNS 预解析 —— dns-prefetch 。
小结
* 分析并用 **关键资源数 关键字节数 关键路径长度** 来描述我们的 CRP 。
* 最小化关键资源数: 消除它们(内联)、推迟它们的下载(defer)或者使它们异步解析(async)等等 。
* 优化关键字节数(缩小、压缩)来减少下载时间 。
* 优化加载剩余关键资源的顺序: 让关键资源(CSS)尽早下载以减少 CRP 长度 。

补充阅读: 前端性能优化之关键路径渲染优化

3. 浏览器重绘(Repaint)和回流(Reflow)

回流必将引起重绘,重绘不一定会引起回流。

重绘(Repaint)

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

回流(Reflow)

当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:

* 页面首次渲染
* 浏览器窗口大小发生改变
* 元素尺寸或位置发生改变元素内容变化(文字数量或图片大小等等)
* 元素字体大小变化
* 添加或者删除可见的 DOM 元素
* 激活 CSS 伪类(例如:hover)
* 查询某些属性或调用某些方法
* 一些常用且会导致回流的属性和方法
clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftscrollIntoView()、scrollIntoViewIfNeeded()、getComputedStyle()、
getBoundingClientRect()、scrollTo()

性能影响

回流比重绘的代价要更高。
有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。现代浏览器会对频繁的回流或重绘操作进行优化:浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。

当你访问以下属性或方法时,浏览器会立刻清空队列:

clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
width、height
getComputedStyle()
getBoundingClientRect()

因为队列中可能会有影响到这些属性或方法返回值的操作,即使你希望获取的信息与队列中操作引发的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。

如何避免

CSS

  • 避免使用 table 布局。
  • 尽可能在 DOM 树的最末端改变 class。
  • 避免设置多层内联样式。
  • 将动画效果应用到 position 属性为 absolute 或 fixed 的元素上。
  • 避免使用 CSS 表达式(例如:calc())。

Javascript

  • 避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。
// 优化前
const el = document.getElementById('test');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
// 优化后,一次性修改样式,这样可以将三次重排减少到一次重排
const el = document.getElementById('test');
el.style.cssText += '; border-left: 1px ;border-right: 2px; padding: 5px;'
  • 避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。
  • 也可以先为元素设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

4. 图片懒加载

图片懒加载在一些图片密集型的网站中运用比较多,通过图片懒加载可以让一些不可视的图片不去加载,避免一次性加载过多的图片导致请求阻塞(浏览器一般对同一域名下的并发请求的连接数有限制),这样就可以提高网站的加载速度,提高用户体验。

原理

将页面中的img标签src指向一张小图片或者src为空,然后定义data-src(这个属性可以自定义命名,我才用data-src)属性指向真实的图片。src指向一张默认的图片,否则当src为空时也会向服务器发送一次请求。可以指向loading的地址。注意,图片要指定宽高。

<img data-original="default.jpg" data-data-original="666.jpg" />

当载入页面时,先把可视区域内的img标签的data-src属性值负给src,然后监听滚动事件,把用户即将看到的图片加载。这样便实现了懒加载。

实例
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <style>
    img {
      display: block;
      margin-bottom: 50px;
      width: 400px;
      height: 400px;
    }
  </style>
</head>
<body>
  <img data-original="Go.png" data-data-original="./lifecycle.jpeg" alt="">
  <img data-original="Go.png" data-data-original="./lifecycle.jpeg" alt="">
  <img data-original="Go.png" data-data-original="./lifecycle.jpeg" alt="">
  <img data-original="Go.png" data-data-original="./lifecycle.jpeg" alt="">
  <img data-original="Go.png" data-data-original="./lifecycle.jpeg" alt="">
  <img data-original="Go.png" data-data-original="./lifecycle.jpeg" alt="">
  <img data-original="Go.png" data-data-original="./lifecycle.jpeg" alt="">
  <img data-original="Go.png" data-data-original="./lifecycle.jpeg" alt="">
  <img data-original="Go.png" data-data-original="./lifecycle.jpeg" alt="">
  <img data-original="Go.png" data-data-original="./lifecycle.jpeg" alt="">
  <img data-original="Go.png" data-data-original="./lifecycle.jpeg" alt="">
  <script>
    let num = document.getElementsByTagName('img').length;
    let img = document.getElementsByTagName("img");
    let n = 0; //存储图片加载到的位置,避免每次都从第一张图片开始遍历

    lazyload(); //页面载入完毕加载可是区域内的图片

    window.onscroll = lazyload;

    function lazyload() { //监听页面滚动事件
      let seeHeight = document.documentElement.clientHeight; //可见区域高度
      let scrollTop = document.documentElement.scrollTop || document.body.scrollTop; //滚动条距离顶部高度
      for (let i = n; i < num; i++) {
        if (img[i].offsetTop < seeHeight + scrollTop) {
          if (img[i].getAttribute("src") == "Go.png") {
            img[i].src = img[i].getAttribute("data-src");
          }
          n = i + 1;
        }
      }
    }
  </script>

</body>

</html>

5. 事件委托

事件委托其实就是利用JS事件冒泡机制把原本需要绑定在子元素的响应事件(click、keydown……)委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。
优点

1. 大量减少内存占用,减少事件注册。
2. 新增元素实现动态绑定事件

例如有一个列表需要绑定点击事件,每一个列表项的点击都需要返回不同的结果。

传统写法:

<ul id="color-list">
    <li>red</li>
    <li>yellow</li>
    <li>blue</li>
    <li>green</li>
    <li>black</li>
    <li>white</li>
  </ul>
  <script>
    (function () {
      var color_list = document.querySelectorAll('li')
      console.log("color_list", color_list)
      for (let item of color_list) {
        item.onclick = showColor;
      }
      function showColor(e) {
        alert(e.target.innerHTML)
        console.log("showColor -> e.target", e.target.innerHTML)
      }
    })();
  </script>

传统方法会利用for循环遍历列表为每一个列表元素绑定点击事件,当列表中元素数量非常庞大时,需要绑定大量的点击事件,这种方式就会产生性能问题。这种情况下利用事件委托就能很好的解决这个问题。

改用事件委托:

<ul id="color-list">
    <li>red</li>
    <li>yellow</li>
    <li>blue</li>
    <li>green</li>
    <li>black</li>
    <li>white</li>
  </ul>
  <script>
    (function () {
      var color_list = document.getElementByid('color-list');
      color_list.addEventListener('click', showColor, true);
      function showColor(e) {
        var x = e.target;
        if (x.nodeName.toLowerCase() === 'li') {
          alert(x.innerHTML);
        }
      }
    })();
  </script>

二、渲染完成后的页面交互优化:

1. 防抖(debounce)/节流(throttle)

防抖(debounce)

输入搜索时,可以用防抖debounce等优化方式,减少http请求;

这里以滚动条事件举例:防抖函数 onscroll 结束时触发一次,延迟执行

function debounce(func, wait) {
  let timeout;
  return function() {
    let context = this; // 指向全局
    let args = arguments;
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(() => {
      func.apply(context, args); // context.func(args)
    }, wait);
  };
}
// 使用
window.onscroll = debounce(function() {
  console.log('debounce');
}, 1000);
节流(throttle)

节流函数:只允许一个函数在N秒内执行一次。滚动条调用接口时,可以用节流throttle等优化方式,减少http请求;

下面还是一个简单的滚动条事件节流函数:节流函数 onscroll 时,每隔一段时间触发一次,像水滴一样

function throttle(fn, delay) {
  let prevTime = Date.now();
  return function() {
    let curTime = Date.now();
    if (curTime - prevTime > delay) {
      fn.apply(this, arguments);
      prevTime = curTime;
    }
  };
}
// 使用
var throtteScroll = throttle(function() {
  console.log('throtte');
}, 1000);
window.onscroll = throtteScroll;

三、Vue相关性能优化

1. 如何定位 Vue 应用性能问题

Vue 应用的性能问题可以分为两个部分,第一部分是运行时性能问题,第二部分是加载性能问题。

和其他 web 应用一样,定位 Vue 应用性能问题最好的工具是 Chrome Devtool,通过 Performance 工具可以用来录制一段时间的 CPU 占用、内存占用、FPS 等运行时性能问题,通过 Network 工具可以用来分析加载性能问题。
1.png
更多 Chrome Devtool 使用方式请参考 使用 Chrome Devtool 定位性能问题 的指南

2. Vue 应用运行时性能优化建议

运行时性能主要关注 Vue 应用初始化之后对 CPU、内存、本地存储等资源的占用,以及对用户交互的及时响应。

引入生产环境的 Vue 文件

开发环境下,Vue 会提供很多警告来帮你对付常见的错误与陷阱。而在生产环境下,这些警告语句没有用,反而会增加应用的体积。有些警告检查还有一些小的运行时开销

当使用 webpack 或 Browserify 类似的构建工具时,Vue 源码会根据 process.env.NODE_ENV 决定是否启用生产环境模式,默认情况为开发环境模式。在 webpack 与 Browserify 中都有方法来覆盖此变量,以启用 Vue 的生产环境模式,同时在构建过程中警告语句也会被压缩工具去除。
详细的做法请参阅 生产环境部署

使用单文件组件预编译模板

当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法

预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。

详细的做法请参阅 预编译模板

提取组件的 CSS 到单独到文件

当使用单文件组件时,组件内的 CSS 会以 <style> 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。

查阅这个构建工具各自的文档来了解更多:

利用Object.freeze()提升性能

Object.freeze() 可以冻结一个对象,冻结之后不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter,这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。

但 Vue 在遇到像 Object.freeze() 这样被设置为不可配置之后的对象属性时,不会为对象加上 setter getter 等数据劫持的方法。 参考 Vue 源码

Object.freeze()### 应用场景
由于 Object.freeze() 会把对象冻结,所以比较适合展示类的场景,如果你的数据属性需要改变,可以重新替换成一个新的 Object.freeze()的对象。

扁平化 Store 数据结构

很多时候,我们会发现接口返回的信息是如下的深层嵌套的树形结构:

{
  "id": "123",
  "author": {
    "id": "1",
    "name": "Paul"
  },
  "title": "My awesome blog post",
  "comments": [
    {
      "id": "324",
      "commenter": {
        "id": "2",
        "name": "Nicole"
      }
    }
  ]
}

假如直接把这样的结构存储在 store 中,如果想修改某个 commenter 的信息,我们需要一层层去遍历找到这个用户的信息,同时有可能这个用户的信息出现了多次,还需要把其他地方的用户信息也进行修改,每次遍历的过程会带来额外的性能开销。

假设我们把用户信息在 store 内统一存放成 users[id]这样的结构,修改和读取用户信息的成本就变得非常低。

你可以手动去把接口里的信息通过类似数据的表一样像这样存起来,也可以借助一些工具,这里就需要提到一个概念叫做 JSON数据规范化(normalize), Normalizr 是一个开源的工具,可以将上面的深层嵌套的 JSON 对象通过定义好的 schema 转变成使用 id 作为字典的实体表示的对象。

避免持久化 Store 数据带来的性能问题

当你有让 Vue App 离线可用,或者有接口出错时候进行灾备的需求的时候,你可能会选择把 Store 数据进行持久化,这个时候需要注意以下几个方面:

  • 持久化时写入数据的性能问题

Vue 社区中比较流行的 vuex-persistedstate,利用了 store 的 subscribe 机制,来订阅 Store 数据的 mutation,如果发生了变化,就会写入 storage 中,默认用的是 localstorage 作为持久化存储。

也就是说默认情况下每次 commit 都会向 localstorage 写入数据,localstorage 写入是同步的,而且存在不小的性能开销,如果你想打造 60fps 的应用,就必须避免频繁写入持久化数据。

我们应该尽量减少直接写入 Storage 的频率:

* 多次写入操作合并为一次,比如采用函数节流或者将数据先缓存在内存中,最后在一并写入
* 只有在必要的时候才写入,比如只有关心的模块的数据发生变化的时候才写入
  • 避免持久化存储的容量持续增长

由于持久化缓存的容量有限,比如 localstorage 的缓存在某些浏览器只有 5M,我们不能无限制的将所有数据都存起来,这样很容易达到容量限制,同时数据过大时,读取和写入操作会增加一些性能开销,同时内存也会上涨。

尤其是将 API 数据进行 normalize 数据扁平化后之后,会将一份数据散落在不同的实体上,下次请求到新的数据也会散落在其他不同的实体上,这样会带来持续的存储增长。

因此,当设计了一套持久化的数据缓存策略的时候,同时应该设计旧数据的缓存清除策略,例如请求到新数据的时候将旧的实体逐个进行清除。

优化无限列表性能

如果你的应用存在非常长或者无限滚动的列表,那么采用 窗口化 的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。

vue-virtual-scroll-listvue-virtual-scroller 都是解决这类问题的开源项目。你也可以参考 Google 工程师的文章 Complexities of an Infinite Scroller 来尝试自己实现一个虚拟的滚动列表来优化性能,主要使用到的技术是 DOM 回收、墓碑元素和滚动锚定。

Google 工程师绘制的无限列表设计
2.png

通过组件懒加载优化超长应用内容初始渲染性能

上面提到的无限列表的场景,比较适合列表内元素非常相似的情况,不过有时候,你的 Vue 应用的超长列表内的内容往往不尽相同,例如在一个复杂的应用的主界面中,整个主界面由非常多不同的模块组成,而用户看到的往往只有首屏一两个模块。在初始渲染的时候不可见区域的模块也会执行和渲染,带来一些额外的性能开销。

使用组件懒加载在不可见时只需要渲染一个骨架屏,不需要真正渲染组件
3.jpg

你可以对组件直接进行懒加载,对于不可见区域的组件内容,直接不进行加载和初始化,避免初始化渲染运行时的开销。具体可以参考我们之前的专栏文章 性能优化之组件懒加载: Vue Lazy Component 介绍 ,了解如何做到组件粒度的懒加载。

3. Vue 应用加载性能优化建议

利用服务端渲染(SSR)和预渲染(Prerender)来优化加载性能

在一个单页应用中,往往只有一个 html 文件,然后根据访问的 url 来匹配对应的路由脚本,动态地渲染页面内容。单页应用比较大的问题是首屏可见时间过长。

单页面应用显示一个页面会发送多次请求,第一次拿到 html 资源,然后通过请求再去拿数据,再将数据渲染到页面上。而且由于现在微服务架构的存在,还有可能发出多次数据请求才能将网页渲染出来,每次数据请求都会产生 RTT(往返时延),会导致加载页面的时间拖的很长。

服务端渲染、预渲染和客户端渲染的对比
4.png
这种情况下可以采用服务端渲染(SSR)和预渲染(Prerender)来提升加载性能,这两种方案,用户读取到的直接就是网页内容,由于少了节省了很多 RTT(往返时延),同时,还可以对一些资源内联在页面,可以进一步提升加载的性能。

可以参考专栏文章 优化向:单页应用多路由预渲染指南 了解如何利用预渲染进行优化。

服务端渲染(SSR)可以考虑使用 Nuxt 或者按照 Vue 官方提供的 Vue SSR 指南 来一步步搭建。

通过组件懒加载优化超长应用内容加载性能

在上面提到的超长应用内容的场景中,通过组件懒加载方案可以优化初始渲染的运行性能,其实,这对于优化应用的加载性能也很有帮助。

组件粒度的懒加载结合异步组件和 webpack 代码分片,可以保证按需加载组件,以及组件依赖的资源、接口请求等,比起通常单纯的对图片进行懒加载,更进一步的做到了按需加载资源。

使用组件懒加载之前的请求瀑布图
5.jpg

使用组件懒加载之后的请求瀑布图
6.jpg

使用组件懒加载方案对于超长内容的应用初始化渲染很有帮助,可以减少大量必要的资源请求,缩短渲染关键路径,具体做法请参考我们之前的专栏文章 性能优化之组件懒加载: Vue Lazy Component 介绍

4. 总结

上面部分总结了 Vue 应用运行时以及加载时的一些性能优化措施,下面做一个回顾和概括:

  • Vue 应用运行时性能优化措施

    • 引入生产环境的 Vue 文件
    • 使用单文件组件预编译模板
    • 提取组件的 CSS 到单独到文件
    • 利用Object.freeze()提升性能
    • 扁平化 Store 数据结构
    • 合理使用持久化 Store 数据
    • 组件懒加载
  • Vue 应用加载性能优化措施

    • 服务端渲染 / 预渲染
    • 组件懒加载

文章总结的这些性能优化手段当然不能覆盖所有的 Vue 应用性能问题,我们也会不断总结和补充其他问题及优化措施,希望文章中提到这些实践经验能给你的 Vue 应用性能优化工作带来小小的帮助。

四、其他方面优化补充

  • webpack模块打包和JavaScript 压缩(如gzip压缩)
  • 利用CDN
  • 按需加载资源
  • 在使用 DOM 操作库时用上 array-ids
  • 缓存优化
  • 避免重定向
  • 启用 HTTP/2
  • 应用性能分析
  • 使用负载均衡方案
  • 为了更快的启动时间考虑一下同构
  • 使用索引加速数据库查询
  • 使用更快的转译方案
  • 避免或最小化 JavaScript 和 CSS 的使用而阻塞渲染
  • 用于未来的一个建议:使用 service workers + 流
  • 图片编码优化,尽量使用svg和字体图标

结语

本文参考/引用:
从 8 道面试题看浏览器渲染过程与性能优化 - null仔 - 掘金
Vue 应用性能优化指南 - 迅雷前端 - 掘金
前端性能优化的常用手段 - 无名小贝勒 - 掘金
前端性能优化之关键路径渲染优化
网页页面性能优化总结 - 由小菜 - 掘金
推荐阅读:
【专题:JavaScript进阶之路】
JavaScript中各种源码实现(前端面试笔试必备)
深入理解 ES6 Promise
JavaScript之函数柯理化
ES6 尾调用和尾递归


我是Cloudy,现居上海,年轻的前端攻城狮一枚,爱专研,爱技术,爱分享。
个人笔记,整理不易,感谢关注阅读点赞收藏
文章有任何问题欢迎大家指出,也欢迎大家一起交流各种前端问题!
查看原文

harden 收藏了文章 · 2020-03-10

React入门看这篇就够了

react - JSX

React 背景介绍

React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。

什么是React

  • A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES

    • 用来构建UI的 JavaScript库
    • React 不是一个 MVC 框架,仅仅是视图(V)层的库
  • React 官网
  • React 中文文档

特点

  • 1 使用 JSX语法 创建组件,实现组件化开发,为函数式的 UI 编程方式打开了大门
  • 2 性能高的让人称赞:通过 diff算法虚拟DOM 实现视图的高效更新
  • 3 HTML仅仅是个开始
> JSX --TO--> EveryThing

- JSX --> HTML
- JSX --> native ios或android中的组件(XML)
- JSX --> VR
- JSX --> 物联网

为什么要用React

  • 1 使用组件化开发方式,符合现代Web开发的趋势
  • 2 技术成熟,社区完善,配件齐全,适用于大型Web项目(生态系统健全)
  • 3 由Facebook专门的团队维护,技术支持可靠
  • 4 ReactNative - Learn once, write anywhere: Build mobile apps with React
  • 5 使用方式简单,性能非常高,支持服务端渲染
  • 6 React非常火,从技术角度,可以满足好奇心,提高技术水平;从职业角度,有利于求职和晋升,有利于参与潜力大的项目

React中的核心概念

  • 1 虚拟DOM(Virtual DOM)
  • 2 Diff算法(虚拟DOM的加速器,提升React性能的法宝)

虚拟DOM(Vitural DOM)

React将DOM抽象为虚拟DOM,虚拟DOM其实就是用一个对象来描述DOM,通过对比前后两个对象的差异,最终只把变化的部分重新渲染,提高渲染的效率

为什么用虚拟dom,当dom反生更改时需要遍历 而原生dom可遍历属性多大231个 且大部分与渲染无关 更新页面代价太大

VituralDOM的处理方式

  • 1 用 JavaScript 对象结构表示 DOM 树的结构,然后用这个树构建一个真正的 DOM 树,插到文档当中
  • 2 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
  • 3 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了

Diff算法

当你使用React的时候,在某个时间点 render() 函数创建了一棵React元素树,
在下一个state或者props更新的时候,render() 函数将创建一棵新的React元素树,
React将对比这两棵树的不同之处,计算出如何高效的更新UI(只更新变化的地方)
<!-- 了解:

有一些解决将一棵树转换为另一棵树的最小操作数算法问题的通用方案。然而,树中元素个数为n,最先进的算法 的时间复杂度为O(n3) 。
如果直接使用这个算法,在React中展示1000个元素则需要进行10亿次的比较。这操作太过昂贵,相反,React基于两点假设,实现了一个O(n)算法,提升性能: -->
  • React中有两种假定:

    • 1 两个不同类型的元素会产生不同的树(根元素不同结构树一定不同)
    • 2 开发者可以通过key属性指定不同树中没有发生改变的子元素

Diff算法的说明 - 1

  • 如果两棵树的根元素类型不同,React会销毁旧树,创建新树
// 旧树
<div>
  <Counter />
</div>

// 新树
<span>
  <Counter />
</span>

执行过程:destory Counter -> insert Counter

Diff算法的说明 - 2

  • 对于类型相同的React DOM 元素,React会对比两者的属性是否相同,只更新不同的属性
  • 当处理完这个DOM节点,React就会递归处理子节点。
// 旧
<div className="before" title="stuff" />
// 新
<div className="after" title="stuff" />
只更新:className 属性

// 旧
<div style={{color: 'red', fontWeight: 'bold'}} />
// 新
<div style={{color: 'green', fontWeight: 'bold'}} />
只更新:color属性

Diff算法的说明 - 3

  • 1 当在子节点的后面添加一个节点,这时候两棵树的转化工作执行的很好
// 旧
<ul>
  <li>first</li>
  <li>second</li>
</ul>

// 新
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

执行过程:
React会匹配新旧两个<li>first</li>,匹配两个<li>second</li>,然后添加 <li>third</li> tree
  • 2 但是如果你在开始位置插入一个元素,那么问题就来了:
// 旧
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

// 新
<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

在没有key属性时执行过程:
React将改变每一个子删除重新创建,而非保持 <li>Duke</li> 和 <li>Villanova</li> 不变

key 属性

为了解决以上问题,React提供了一个 key 属性。当子节点带有key属性,React会通过key来匹配原始树和后来的树。
// 旧
<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

// 新
<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>
执行过程:
现在 React 知道带有key '2014' 的元素是新的,对于 '2015' 和 '2016' 仅仅移动位置即可 
  • 说明:key属性在React内部使用,但不会传递给你的组件
  • 推荐:在遍历数据时,推荐在组件中使用 key 属性:<li key={item.id}>{item.name}</li>
  • 注意:key只需要保持与他的兄弟节点唯一即可,不需要全局唯一
  • 注意:尽可能的减少数组index作为key,数组中插入元素的等操作时,会使得效率底下

React的基本使用

  • 安装:npm i -S react react-dom
  • react:react 是React库的入口点
  • react-dom:提供了针对DOM的方法,比如:把创建的虚拟DOM,渲染到页面上
// 1. 导入 react
import React from 'react'
import ReactDOM from 'react-dom'

// 2. 创建 虚拟DOM
// 参数1:元素名称  参数2:元素属性对象(null表示无)  参数3:当前元素的子元素string||createElement() 的返回值
const divVD = React.createElement('div', {
  title: 'hello react'
}, 'Hello React!!!')

// 3. 渲染
// 参数1:虚拟dom对象  参数2:dom对象表示渲染到哪个元素内 参数3:回调函数
ReactDOM.render(divVD, document.getElementById('app'))

createElement()的问题

  • 说明:createElement()方式,代码编写不友好,太复杂
var dv = React.createElement(
  "div",
  { className: "shopping-list" },
  React.createElement(
    "h1",
    null,
    "Shopping List for "
  ),
  React.createElement(
    "ul",
    null,
    React.createElement(
      "li",
      null,
      "Instagram"
    ),
    React.createElement(
      "li",
      null,
      "WhatsApp"
    )
  )
)
// 渲染
ReactDOM.render(dv, document.getElementById('app'))

JSX 的基本使用

  • 注意:JSX语法,最终会被编译为 createElement() 方法
  • 推荐:使用 JSX 的方式创建组件
  • JSX - JavaScript XML
  • 安装:npm i -D babel-preset-react (依赖与:babel-core/babel-loader)
注意:JSX的语法需要通过 babel-preset-react 编译后,才能被解析执行
/* 1 在 .babelrc 开启babel对 JSX 的转换 */
{
  "presets": [
    "env", "react"
  ]
}

/* 2 webpack.config.js */
module: [
  rules: [
    { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/ },
  ]
]

/* 3 在 js 文件中 使用 JSX */
const dv = (
  <div title="标题" className="cls container">Hello JSX!</div>
)

/* 4 渲染 JSX 到页面中 */
ReactDOM.render(dv, document.getElementById('app'))

JSX的注意点

  • 注意 1: 如果在 JSX 中给元素添加类, 需要使用 className 代替 class

    • 类似:label 的 for属性,使用htmlFor代替
  • 注意 2:在 JSX 中可以直接使用 JS代码,直接在 JSX 中通过 {} 中间写 JS代码即可
  • 注意 3:在 JSX 中只能使用表达式,但是不能出现 语句!!!
  • 注意 4:在 JSX 中注释语法:{/* 中间是注释的内容 */}

React组件

React 组件可以让你把UI分割为独立、可复用的片段,并将每一片段视为相互独立的部分。
  • 组件是由一个个的HTML元素组成的
  • 概念上来讲, 组件就像JS中的函数。它们接受用户输入(props),并且返回一个React对象,用来描述展示在页面中的内容

React创建组件的两种方式

  • 1 通过 JS函数 创建(无状态组件)
  • 2 通过 class 创建(有状态组件)
函数式组件 和 class 组件的使用场景说明:
1 如果一个组件仅仅是为了展示数据,那么此时就可以使用 函数组件
2 如果一个组件中有一定业务逻辑,需要操作数据,那么就需要使用 class 创建组件,因为,此时需要使用 state

JavaScript函数创建

  • 注意:1 函数名称必须为大写字母开头,React通过这个特点来判断是不是一个组件
  • 注意:2 函数必须有返回值,返回值可以是:JSX对象或null
  • 注意:3 返回的JSX,必须有一个根元素
  • 注意:4 组件的返回值使用()包裹,避免换行问题
function Welcome(props) {
  return (
    // 此处注释的写法 
    <div className="shopping-list">
      {/* 此处 注释的写法 必须要{}包裹 */}
      <h1>Shopping List for {props.name}</h1>
      <ul>
        <li>Instagram</li>
        <li>WhatsApp</li>
      </ul>
    </div>
  )
}

ReactDOM.render(
  <Welcome name="jack" />,
  document.getElementById('app')
)

class创建

在es6中class仅仅是一个语法糖,不是真正的类,本质上还是构造函数+原型 实现继承
// ES6中class关键字的简单使用

// - **ES6中的所有的代码都是运行在严格模式中的**
// - 1 它是用来定义类的,是ES6中实现面向对象编程的新方式
// - 2 使用`static`关键字定义静态属性
// - 3 使用`constructor`构造函数,创建实例属性
// - [参考](http://es6.ruanyifeng.com/#docs/class)

// 语法:
class Person {
  // 实例的构造函数 constructor
  constructor(age){
    // 实例属性
    this.age = age
  }
  // 在class中定义方法 此处为实例方法 通过实例打点调用
  sayHello () {
    console.log('大家好,我今年' + this.age + '了');
  }

  // 静态方法 通过构造函数打点调用 Person.doudou()
  static doudou () {
    console.log('我是小明,我新get了一个技能,会暖床');
  }
}
// 添加静态属性
Person.staticName = '静态属性'
// 实例化对象
const p = new Person(19)
 
 
// 实现继承的方式
 
class American extends Person {
  constructor() {
    // 必须调用super(), super表示父类的构造函数
    super()
    this.skin = 'white'
    this.eyeColor = 'white'
  }
}

// 创建react对象
// 注意:基于 `ES6` 中的class,需要配合 `babel` 将代码转化为浏览器识别的ES5语法
// 安装:`npm i -D babel-preset-env`
 
//  react对象继承字React.Component
class ShoppingList extends React.Component {
  constructor(props) { 
    super(props)
  }
  // class创建的组件中 必须有rander方法 且显示return一个react对象或者null
  render() {
    return (
      <div className="shopping-list">
        <h1>Shopping List for {this.props.name}</h1>
        <ul>
          <li>Instagram</li>
          <li>WhatsApp</li>
        </ul>
      </div>
    )
  }
}

给组件传递数据 - 父子组件传递数据

  • 组件中有一个 只读的对象 叫做 props,无法给props添加属性
  • 获取方式:函数参数 props
  • 作用:将传递给组件的属性转化为 props 对象中的属性
function Welcome(props){
  // props ---> { username: 'zs', age: 20 }
  return (
    <div>
      <div>Welcome React</div>
      <h3>姓名:{props.username}----年龄是:{props.age}</h3>
    </div>
  )
}

// 给 Hello组件 传递 props:username 和 age(如果你想要传递numb类型是数据 就需要向下面这样)
ReactDOM.reander(<Hello username="zs" age={20}></Hello>, ......)

封装组件到独立的文件中

// 创建Hello2.js组件文件
// 1. 引入React模块
// 由于 JSX 编译后会调用 React.createElement 方法,所以在你的 JSX 代码中必须首先拿到React。
import React from 'react'

// 2. 使用function构造函数创建组件
function Hello2(props){
  return (
    <div>
      <div>这是Hello2组件</div>
      <h1>这是大大的H1标签,我大,我骄傲!!!</h1>
      <h6>这是小小的h6标签,我小,我傲娇!!!</h6>
    </div>
  )
}
// 3. 导出组件
export default Hello2

// app.js中   使用组件:
import Hello2 from './components/Hello2'

props和state

props

  • 作用:给组件传递数据,一般用在父子组件之间
  • 说明:React把传递给组件的属性转化为一个对象并交给 props
  • 特点:props是只读的,无法给props添加或修改属性
  • props.children:获取组件的内容,比如:

    • <Hello>组件内容</Hello> 中的 组件内容
// props 是一个包含数据的对象参数,不要试图修改 props 参数
// 返回值:react元素
function Welcome(props) {
  // 返回的 react元素中必须只有一个根元素
  return <div>hello, {props.name}</div>
}

class Welcome extends React.Component {
  constructor(props) {
    super(props)
  }

  render() {
    return <h1>Hello, {this.props.name}</h1>
  }
}

state

状态即数据
  • 作用:用来给组件提供组件内部使用的数据
  • 注意:只有通过class创建的组件才具有状态
  • 注意:状态是私有的,完全由组件来控制
  • 注意:不要在 state 中添加 render() 方法中不需要的数据,会影响渲染性能!

    • 可以将组件内部使用但是不渲染在视图中的内容,直接添加给 this
  • 注意:不要在 render() 方法中调用 setState() 方法来修改state的值

    • 但是可以通过 this.state.name = 'rose' 方式设置state(不推荐!!!!)
// 例:
class Hello extends React.Component {
  constructor() {
    // es6继承必须用super调用父类的constructor
    super()

    this.state = {
      gender: 'male'
    }
  }

  render() {
    return (
      <div>性别:{ this.state.gender }</div>
    )
  }
}

JSX语法转化过程

// 1、JSX
const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
)

// 2、JSX -> createElement
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
)

// React elements: 使用对象的形式描述页面结构
// Note: 这是简化后的对象结构
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
  },
  children: ['Hello, world']
}

评论列表案例

  • 巩固有状态组件和无状态组件的使用
  • 两个组件:<CommentList></CommentList><Comment></Comment>
[
  { user: '张三', content: '哈哈,沙发' },
  { user: '张三2', content: '哈哈,板凳' },
  { user: '张三3', content: '哈哈,凉席' },
  { user: '张三4', content: '哈哈,砖头' },
  { user: '张三5', content: '哈哈,楼下山炮' }
]

// 属性扩展
<Comment {...item} key={i}></Comment>

style样式

// 1. 直接写行内样式:
<li style={{border:'1px solid red', fontSize:'12px'}}></li>

// 2. 抽离为对象形式
var styleH3 = {color:'blue'}
var styleObj = {
  liStyle:{border:'1px solid red', fontSize:'12px'},
  h3Style:{color:'green'}
}

<li style={styleObj.liStyle}>
  <h3 style={styleObj.h3Style}>评论内容:{props.content}</h3>
</li>

// 3. 使用样式表定义样式:
import '../css/comment.css'
<p className="pUser">评论人:{props.user}</p>

相关文章

组件的生命周期

  • 简单说:一个组件从开始到最后消亡所经历的各种状态,就是一个组件的生命周期

组件生命周期函数的定义:从组件被创建,到组件挂载到页面上运行,再到页面关闭组件被卸载,这三个阶段总是伴随着组件各种各样的事件,那么这些事件,统称为组件的生命周期函数!

组件生命周期函数总览

  • 组件的生命周期包含三个阶段:创建阶段(Mounting)、运行和交互阶段(Updating)、卸载阶段(Unmounting)
  • Mounting:
constructor()
componentWillMount()
render()
componentDidMount()
  • Updating
componentWillReceiveProps()
shouldComponentUpdate()
componentWillUpdate()
render()
componentDidUpdate()
  • Unmounting
componentWillUnmount()

组件生命周期 - 创建阶段(Mounting)

  • 特点:该阶段的函数只执行一次

constructor()

  • 作用:1 获取props 2 初始化state
  • 说明:通过 constructor() 的参数props获取
  • 设置state和props
class Greeting extends React.Component {
  constructor(props) {
    // 获取 props
    super(props)
    // 初始化 state
    this.state = {
      count: props.initCount
    }
  }
}

// 初始化 props
// 语法:通过静态属性 defaultProps 来初始化props
Greeting.defaultProps = {
  initCount: 0
};

componentWillMount()

  • 说明:组件被挂载到页面之前调用,其在render()之前被调用,因此在这方法里同步地设置状态将不会触发重渲染
  • 注意:无法获取页面中的DOM对象
  • 注意:可以调用 setState() 方法来改变状态值
  • 用途:发送ajax请求获取数据
componentWillMount() {
  console.warn(document.getElementById('btn')) // null
  this.setState({
    count: this.state.count + 1
  })
}

render()

  • 作用:渲染组件到页面中,无法获取页面中的DOM对象
  • 注意:不要在render方法中调用 setState() 方法,否则会递归渲染

    • 原因说明:状态改变会重新调用render()render()又重新改变状态
render() {
  console.warn(document.getElementById('btn')) // null

  return (
    <div>
      <button id="btn" onClick={this.handleAdd}>打豆豆一次</button>
      {
        this.state.count === 4
        ? null
        : <CounterChild initCount={this.state.count}></CounterChild>
      }
    </div>
  )
}

componentDidMount()

  • 1 组件已经挂载到页面中
  • 2 可以进行DOM操作,比如:获取到组件内部的DOM对象
  • 3 可以发送请求获取数据
  • 4 可以通过 setState() 修改状态的值
  • 注意:在这里修改状态会重新渲染
componentDidMount() {
  // 此时,就可以获取到组件内部的DOM对象
  console.warn('componentDidMount', document.getElementById('btn'))
}

组件生命周期 - 运行阶段(Updating)

  • 特点:该阶段的函数执行多次
  • 说明:每当组件的props或者state改变的时候,都会触发运行阶段的函数

componentWillReceiveProps()

  • 说明:组件接受到新的props前触发这个方法
  • 参数:当前组件props
  • 可以通过 this.props 获取到上一次的值
  • 使用:若你需要响应属性的改变,可以通过对比this.propsnextProps并在该方法中使用this.setState()处理状态改变
  • 注意:修改state不会触发该方法
componentWillReceiveProps(nextProps) {
  console.warn('componentWillReceiveProps', nextProps)
}

shouldComponentUpdate()

  • 作用:根据这个方法的返回值决定是否重新渲染组件,返回true重新渲染,否则不渲染
  • 优势:通过某个条件渲染组件,降低组件渲染频率,提升组件性能
  • 说明:如果返回值为false,那么,后续render()方法不会被调用
  • 注意:这个方法必须返回布尔值!!!
  • 场景:根据随机数决定是否渲染组件
// - 参数:
//   - 第一个参数:最新属性对象
//   - 第二个参数:最新状态对象
shouldComponentUpdate(nextProps, nextState) {
  console.warn('shouldComponentUpdate', nextProps, nextState)

  return nextState.count % 2 === 0
}

componentWillUpdate()

  • 作用:组件将要更新
  • 参数:最新的属性和状态对象
  • 注意:不能修改状态 否则会循环渲染
componentWillUpdate(nextProps, nextState) {
  console.warn('componentWillUpdate', nextProps, nextState)
}

render() 渲染

  • 作用:重新渲染组件,与Mounting阶段的render是同一个函数
  • 注意:这个函数能够执行多次,只要组件的属性或状态改变了,这个方法就会重新执行

componentDidUpdate()

  • 作用:组件已经被更新
  • 参数:旧的属性和状态对象
componentDidUpdate(prevProps, prevState) {
  console.warn('componentDidUpdate', prevProps, prevState)
}

组件生命周期 - 卸载阶段(Unmounting)

  • 组件销毁阶段:组件卸载期间,函数比较单一,只有一个函数,这个函数也有一个显著的特点:组件一辈子只能执行依次!
  • 使用说明:只要组件不再被渲染到页面中,那么这个方法就会被调用( 渲染到页面中 -> 不再渲染到页面中 )

componentWillUnmount()

  • 作用:在卸载组件的时候,执行清理工作,比如

    • 1 清除定时器
    • 2 清除componentDidMount创建的DOM对象

React - createClass(不推荐)

  • React.createClass({}) 方式,创建有状态组件,该方式已经被废弃!!!
  • 通过导入 require('create-react-class'),可以在不适用ES6的情况下,创建有状态组件
  • getDefaultProps() 和 getInitialState() 方法:是 createReactClass() 方式创建组件中的两个函数
  • React without ES6
  • React 不适用ES6
var createReactClass = require('create-react-class');
var Greeting = createReactClass({
  // 初始化 props
  getDefaultProps: function() {
    console.log('getDefaultProps');
    return {
      title: 'Basic counter!!!'
    }
  },

  // 初始化 state
  getInitialState: function() {
    console.log('getInitialState');
    return {
      count: 0
    }
  },

  render: function() {
    console.log('render');
    return (
      <div>
        <h1>{this.props.title}</h1>
        <div>{this.state.count}</div>
        <input type='button' value='+' onClick={this.handleIncrement} />
      </div>
    );
  },

  handleIncrement: function() {
    var newCount = this.state.count + 1;
    this.setState({count: newCount});
  },

  propTypes: {
    title: React.PropTypes.string
  }
});

ReactDOM.render(
  React.createElement(Greeting),
  document.getElementById('app')
);

state和setState

  • 注意:使用 setState() 方法修改状态,状态改变后,React会重新渲染组件
  • 注意:不要直接修改state属性的值,这样不会重新渲染组件!!!
  • 使用:1 初始化state 2 setState修改state
// 修改state(不推荐使用)
// https://facebook.github.io/react/docs/state-and-lifecycle.html#do-not-modify-state-directly
this.state.test = '这样方式,不会重新渲染组件';
constructor(props) {
  super(props)

  // 正确姿势!!!
  // -------------- 初始化 state --------------
  this.state = {
    count: props.initCount
  }
}

componentWillMount() {
  // -------------- 修改 state 的值 --------------
  // 方式一:
  this.setState({
    count: this.state.count + 1
  })

  this.setState({
    count: this.state.count + 1
  }, function(){
    // 由于 setState() 是异步操作,所以,如果想立即获取修改后的state
    // 需要在回调函数中获取
    // https://doc.react-china.org/docs/react-component.html#setstate
  });

  // 方式二:
  this.setState(function(prevState, props) {
    return {
      counter: prevState.counter + props.increment
    }
  })

  // 或者 - 注意: => 后面需要带有小括号,因为返回的是一个对象
  this.setState((prevState, props) => ({
    counter: prevState.counter + props.increment
  }))
}

组件绑定事件

  • 1 通过React事件机制 onClick 绑定
  • 2 JS原生方式绑定(通过 ref 获取元素)

    • 注意:ref 是React提供的一个特殊属性
    • ref的使用说明:react ref

React中的事件机制 - 推荐

  • 注意:事件名称采用驼峰命名法
  • 例如:onClick 用来绑定单击事件
<input type="button" value="触发单击事件"
  onClick={this.handleCountAdd}
  onMouseEnter={this.handleMouseEnter}
/>

JS原生方式 - 知道即可

  • 说明:给元素添加 ref 属性,然后,获取元素绑定事件
// JSX
// 将当前DOM的引用赋值给 this.txtInput 属性
<input ref={ input => this.txtInput = input } type="button" value="我是豆豆" />

componentDidMount() {
  // 通过 this.txtInput 属性获取元素绑定事件
  this.txtInput.addEventListener(() => {
    this.setState({
      count:this.state.count + 1
    })
  })
}

事件绑定中的this

  • 1 通过 bind 绑定
  • 2 通过 箭头函数 绑定

通过bind绑定

  • 原理:bind能够调用函数,改变函数内部this的指向,并返回一个新函数
  • 说明:bind第一个参数为返回函数中this的指向,后面的参数为传给返回函数的参数
// 自定义方法:
handleBtnClick(arg1, arg2) {
  this.setState({
    msg: '点击事件修改state的值' + arg1 + arg2
  })
}

render() {
  return (
    <div>
      <button onClick={
        // 无参数
        // this.handleBtnClick.bind(this)

        // 有参数
        this.handleBtnClick.bind(this, 'abc', [1, 2])
      }>事件中this的处理</button>
      <h1>{this.state.msg}</h1>
    </div>
  )
}
  • 在构造函数中使用bind
constructor() {
  super()

  this.handleBtnClick = this.handleBtnClick.bind(this)
}

// render() 方法中:
<button onClick={ this.handleBtnClick }>事件中this的处理</button>

通过箭头函数绑定

  • 原理:箭头函数中的this由所处的环境决定,自身不绑定this
<input type="button" value="在构造函数中绑定this并传参" onClick={
  () => { this.handleBtnClick('参数1', '参数2') }
} />

handleBtnClick(arg1, arg2) {
  this.setState({
    msg: '在构造函数中绑定this并传参' + arg1 + arg2
  });
}

受控组件

在HTML当中,像input,textareaselect这类表单元素会维持自身状态,并根据用户输入进行更新。
在React中,可变的状态通常保存在组件的state中,并且只能用 setState() 方法进行更新.
React根据初始状态渲染表单组件,接受用户后续输入,改变表单组件内部的状态。
因此,将那些值由React控制的表单元素称为:受控组件。
  • 受控组件的特点:

    • 1 表单元素
    • 2 由React通过JSX渲染出来
    • 3 由React控制值的改变,也就是说想要改变元素的值,只能通过React提供的方法来修改
  • 注意:只能通过setState来设置受控组件的值
// 模拟实现文本框数据的双向绑定
<input type="text" value={this.state.msg} onChange={this.handleTextChange}/>

// 当文本框内容改变的时候,触发这个事件,重新给state赋值
handleTextChange = event => {
  console.log(event.target.value)

  this.setState({
    msg: event.target.value
  })
}

评论列表案例

[
  {name: '小明', content: '沙发!!!'},
  {name: '小红', content: '小明,居然是你'},
  {name: '小刚', content: '小明,放学你别走!!!'},
]

props校验

  • 作用:通过类型检查,提高程序的稳定性
  • 命令:npm i -S prop-types
  • 类型校验文档
  • 使用:给类提供一个静态属性 propTypes(对象),来约束props
// 引入模块
import PropTypes from 'prop-types'

// ...以下代码是类的静态属性:
// propTypes 静态属性的名称是固定的!!!
static propTypes = {
  initCount: PropTypes.number, // 规定属性的类型
  initAge: PropTypes.number.isRequired // 规定属性的类型,且规定为必传字段
}

React 单向数据流

  • React 中采用单项数据流
  • 数据流动方向:自上而下,也就是只能由父组件传递到子组件
  • 数据都是由父组件提供的,子组件想要使用数据,都是从父组件中获取的
  • 如果多个组件都要使用某个数据,最好将这部分共享的状态提升至他们最近的父组件当中进行管理
  • 单向数据流
  • 状态提升
react中的单向数据流动:
1 数据应该是从上往下流动的,也就是由父组件将数据传递给子组件
2 数据应该是由父组件提供,子组件要使用数据的时候,直接从子组件中获取

在我们的评论列表案例中:数据是由CommentList组件(父组件)提供的
子组件 CommentItem 负责渲染评论列表,数据是由 父组件提供的
子组件 CommentForm 负责获取用户输入的评论内容,最终也是把用户名和评论内容传递给了父组件,由父组件负责处理这些数据( 把数据交给 CommentItem 由这个组件负责渲染 )

组件通讯

  • 父 -> 子:props
  • 子 -> 父:父组件通过props传递回调函数给子组件,子组件调用函数将数据作为参数传递给父组件
  • 兄弟组件:因为React是单向数据流,因此需要借助父组件进行传递,通过父组件回调函数改变兄弟组件的props
  • React中的状态管理: flux(提出状态管理的思想) -> Redux -> mobx
  • Vue中的状态管理: Vuex
  • 简单来说,就是统一管理了项目中所有的数据,让数据变的可控
  • 组件通讯

Context特性

  • 注意:如果不熟悉React中的数据流,不推荐使用这个属性

    • 这是一个实验性的API,在未来的React版本中可能会被更改
  • 作用:跨级传递数据(爷爷给孙子传递数据),避免向下每层手动地传递props
  • 说明:需要配合PropTypes类型限制来使用
class Grandfather extends React.Component {
  // 类型限制(必须),静态属性名称固定
  static childContextTypes = {
    color: PropTypes.string.isRequired
  }

  // 传递给孙子组件的数据
  getChildContext() {
    return {
      color: 'red'
    }
  }

  render() {
    return (
      <Father></Father>
    )
  }
}

class Child extends React.Component {
  // 类型限制,静态属性名字固定
  static contextTypes = {
    color: PropTypes.string
  }

  render() {
    return (
      // 从上下文对象中获取爷爷组件传递过来的数据
      <h1 style={{ color: this.context.color }}>爷爷告诉文字是红色的</h1>
    )
  }
}

class Father extends React.Component {
  render() {
    return (
      <Child></Child>
    )
  }
}

react-router

基本概念说明

  • Router组件本身只是一个容器,真正的路由要通过Route组件定义

使用步骤

  • 1 导入路由组件
  • 2 使用 <Router></Router> 作为根容器,包裹整个应用(JSX)

    • 在整个应用程序中,只需要使用一次
  • 3 使用 <Link to="/movie"></Link> 作为链接地址,并指定to属性
  • 4 使用 <Route path="/" compoent={Movie}></Route> 展示路由内容
// 1 导入组件
import {
  HashRouter as Router,
  Link, Route
} from 'react-router-dom'

// 2 使用 <Router>
<Router>

    // 3 设置 Link
    <Menu.Item key="1"><Link to="/">首页</Link></Menu.Item>
    <Menu.Item key="2"><Link to="/movie">电影</Link></Menu.Item>
    <Menu.Item key="3"><Link to="/about">关于</Link></Menu.Item>

    // 4 设置 Route
    // exact 表示:绝对匹配(完全匹配,只匹配:/)
    <Route exact path="/" component={HomeContainer}></Route>
    <Route path="/movie" component={MovieContainer}></Route>
    <Route path="/about" component={AboutContainer}></Route>

</Router>

注意点

  • <Router></Router>:作为整个组件的根元素,是路由容器,只能有一个唯一的子元素
  • <Link></Link>:类似于vue中的<router-link></router-link>标签,to 属性指定路由地址
  • <Route></Route>:类似于vue中的<router-view></router-view>,指定路由内容(组件)展示位置

路由参数

  • 配置:通过Route中的path属性来配置路由参数
  • 获取:this.props.match.params 获取
// 配置路由参数
<Route path="/movie/:movieType"></Route>

// 获取路由参数
const type = this.props.match.params.movieType

路由跳转

  • react router - history
  • history.push() 方法用于在JS中实现页面跳转
  • history.go(-1) 用来实现页面的前进(1)和后退(-1)
this.props.history.push('/movie/movieDetail/' + movieId)

fetch

  • 作用:Fetch 是一个现代的概念, 等同于 XMLHttpRequest。它提供了许多与XMLHttpRequest相同的功能,但被设计成更具可扩展性和高效性。
  • fetch() 方法返回一个Promise对象

fetch 基本使用

/*
  通过fetch请求回来的数据,是一个Promise对象.
  调用then()方法,通过参数response,获取到响应对象
  调用 response.json() 方法,解析服务器响应数据
  再次调用then()方法,通过参数data,就获取到数据了
*/
fetch('/api/movie/' + this.state.movieType)
  // response.json() 读取response对象,并返回一个被解析为JSON格式的promise对象
  .then((response) => response.json())
  // 通过 data 获取到数据
  .then((data) => {
    console.log(data);
    this.setState({
      movieList: data.subjects,
      loaing: false
    })
  })

跨域获取数据的三种常用方式

  • 1 JSONP
  • 2 代理
  • 3 CORS

JSONP

  • 安装:npm i -S fetch-jsonp
  • 利用JSONP实现跨域获取数据,只能获取GET请求
  • fetch-jsonp
  • fetch-jsonp
  • 限制:1 只能发送GET请求 2 需要服务端支持JSONP请求
/* movielist.js */
fetchJsonp('https://api.douban.com/v2/movie/in_theaters')
  .then(rep => rep.json())
  .then(data => { console.log(data) })

代理

  • webpack-dev-server 代理配置如下:
  • 问题:webpack-dev-server 是开发期间使用的工具,项目上线了就不再使用 webpack-dev-server
  • 解决:项目上线后的代码,也是会部署到一个服务器中,这个服务器配置了代理功能即可(要求两个服务器中配置的代理规则相同)
// webpack-dev-server的配置
devServer: {
  // https://webpack.js.org/configuration/dev-server/#devserver-proxy
  // https://github.com/chimurai/http-proxy-middleware#http-proxy-options
  // http://www.jianshu.com/p/3bdff821f859
  proxy: {
    // 使用:/api/movie/in_theaters
    // 访问 ‘/api/movie/in_theaters’ ==> 'https://api.douban.com/v2/movie/in_theaters'
    '/api': {
      // 代理的目标服务器地址
      target: 'https://api.douban.com/v2',
      // https请求需要该设置
      secure: false,
      // 必须设置该项
      changeOrigin: true,
      // '/api/movie/in_theaters' 路径重写为:'/movie/in_theaters'
      pathRewrite: {"^/api" : ""}
    }
  }
}

/* movielist.js */
fetch('/api/movie/in_theaters')
  .then(function(data) {
    // 将服务器返回的数据转化为 json 格式
    return data.json()
  })
  .then(function(rep) {
    // 获取上面格式化后的数据
    console.log(rep);
  })

CORS - 服务器端配合

// 通过Express的中间件来处理所有请求
app.use('*', function (req, res, next) {
  // 设置请求头为允许跨域
  res.header('Access-Control-Allow-Origin', '*');

  // 设置服务器支持的所有头信息字段
  res.header('Access-Control-Allow-Headers', 'Content-Type,Content-Length, Authorization,Accept,X-Requested-With');
  // 设置服务器支持的所有跨域请求的方法
  res.header('Access-Control-Allow-Methods', 'POST,GET');
  // next()方法表示进入下一个路由
  next();
});

redux

  • 状态管理工具,用来管理应用中的数据

核心

  • Action:行为的抽象,视图中的每个用户交互都是一个action

    • 比如:点击按钮
  • Reducer:行为响应的抽象,也就是:根据action行为,执行相应的逻辑操作,更新state

    • 比如:点击按钮后,添加任务,那么,添加任务这个逻辑放到 Reducer 中
    • 1 创建State
  • Store:

    • 1 Redux应用只能有一个store
    • 2 getState():获取state
    • 3 dispatch(action):更新state
/* action */

// 在 redux 中,action 就是一个对象
// action 必须提供一个:type属性,表示当前动作的标识
// 其他的参数:表示这个动作需要用到的一些数据
{ type: 'ADD_TODO', name: '要添加的任务名称' }

// 这个动作表示要切换任务状态
{ type: 'TOGGLE_TODO', id: 1 }
/* reducer */

// 第一个参数:表示状态(数据),我们需要给初始状态设置默认值
// 第二个参数:表示 action 行为
function todo(state = [], action) {
  switch(action.type) {
    case 'ADD_TODO':
      state.push({ id: Math.random(), name: action.name, completed: false })
      return state
    case 'TOGGLE_TODO':
      for(var i = 0; i < state.length; i++) {
        if (state[i].id === action.id) {
          state[i].completed = !state[i].completed
          break
        }
      }
      return state
    default:
      return state
  }
}

// 要执行 ADD_TODO 这个动作:
dispatch( { type: 'ADD_TODO', name: '要添加的任务名称' } )

// 内部会调用 reducer
todo(undefined, { type: 'ADD_TODO', name: '要添加的任务名称' })

同事的博客各种干货值得收藏

查看原文

harden 收藏了文章 · 2020-01-17

Umi Hooks - 助力拥抱 React Hooks

这是蚂蚁金服内部技术分享的文字稿,本次分享主要介绍了为什么要用 Hooks?以及如何使用 Umi Hooks 提效?

Umi Hooks http://github.com/umijs/hooks

开场

image-20200116191741370

大家好,我叫尽龙,来自体验技术部。社区名称叫 brickspert,砖家的意思。

自从 React 推出 React Hooks 后,就开始尝试使用 Hooks,并逐渐喜欢上了它。目前,几乎 100% 的组件都是使用 Hooks 开发。经过大半年的实践,在 Hooks 使用方面沉淀了一些经验,很高兴今天有机会能分享给大家。

image-20200116191759286

在分享开始之前,我想了解下:“有多少同学目前已经在项目中大量使用 Hooks 了?”

嗯嗯,谢谢。看举手的同学,大概一半一半吧。没关系,听完今天的分享,我相信你一定有兴趣尝试下 Hooks 的。

React Hooks 是 react v16.8 的一个新特性,很佩服这么重磅的功能,在一个小版本中发布,说明 React 团队有足够的信心向上兼容。

Why Hooks?

image-20200116191817331

为什么要放弃 Class,转用 Hooks 呢?在内部外部有很多争论,包括知乎也有类似提问。我们也不免俗套的要对比下 Class 和 Hooks 了。当然为了保证今天的分享效果,我肯定会偏向 Hooks 的(哈哈哈哈)。

image-20200116192324872

Class 学习成本高

Class 学习成本很高。首当其中的就是生命周期,多,太多了。不仅多,还会变!React v15 和 v16 就不一样。下面是我在网上随便找的一张图。

image-a5b927b35025

这个是 React v15 的生命周期,你都掌握了吗?你知道 v16 有什么变化吗?

之前无论你去哪里面试,基本都会有几个必问问题:

  • 讲讲 React 生命周期?React v15 和 React v16 生命周期有啥变化?
  • 如何优化 Class 组件?shouldComponentUpdate 是做什么的?如何用?
  • 一般在哪个生命周期发送网络请求?为什么?
  • ......

生命周期最重要,但是有很高的学习成本,需要大量实践才能积累足够的经验。当然,这几个问题回答不好,百分之八十以上的几率会挂掉。

当然不止是生命周期,this 也是一个很大的问题。你有没有在组件写很多 bind?或者所有的函数都用箭头函数定义?

this.someFunction = this.someFunction.bind(this);

// 或
someFunction = ()=>{}

为什么要这样写呢?如果不写会有什么问题?哎呦,又多了一个面试题,你会吗?

Hooks 学习成本低

对比 Class,Hooks 的学习成本可就太低了!掌握了 useState 和 useEffect,80% 的事情就搞定了。

image-7cbf7879e7cf

Class 业务逻辑分散

Class 业务逻辑分散,实现一个功能,我要写在不同的生命周期里面,不聚合~

比如,如果你有个定时器,你一定要在 componentWillUnMount 去卸载。

image-67b7d915f6af

再比如,我们要写一个请求用户信息的组件,当userId 变化时,要重新发起请求。我们就要在两个生命中期中写请求的逻辑。

image-4059e72aa129

相信上面的逻辑,大家也是经常会写的吧。

奥奥,sorry,上面的 componentWillReceiveProps 已经被废弃了,我们应该用 componentDidUpdate 来代替。

“咦,这是为啥呢?好好的为什么要废弃,不让这么用了?”

又来一个面试题!你知道答案吗?

Hooks 业务逻辑聚合

而 Hooks 的业务逻辑就非常聚合了。上面的两个例子,改成 Hooks 你会写吗?

image-3780dc60b735

image-88e9ba8a7add

简直不要太简单!香啊!我可以提前下班了。

Class 逻辑复用困难

说到逻辑复用,很多同学会说 Class 的 Render Props 和 HOC(高阶组件)可以做逻辑复用!那我们看看 Class 的逻辑复用有多么的惨不忍睹。

首先我们看看 Render Props。

首先我们想复用监听 window size 变化的逻辑,开开心心的写了下面的代码。

image-f9273eefa2ef

然后,我又想复用监听鼠标位置的逻辑,我只能这么写了。

image-d60b6492b570

到这里你应该看到了问题所在。这简直就是地狱!我不忍心复用其它逻辑了。

我们放过 Render Props,来看看 HOC 吧。

如果你要问什么是 HOC,那我不得不推荐我的另外一篇文章《助你完全理解React高阶组件(Higher-Order Components)》。

哪怕你不知道 HOC 是啥,你也一定用过。比如 redux 的 connect。

image-20200116200932301

上面的代码,我用了三个 HOC,分别是 redux 的 connect,react-intl 的 injectIntl,以及 AntD 的 Form.create()。

这是一个非常常见的用法。如果你光看代码,大概已经懵圈了。“我是谁?我在哪?我要干什么?”

这会我仿佛听见 HOC 在说:“我不仅让你看不懂我,我还很容易出各种问题。”

是的,HOC 很容易出问题。大家都往组件的 props 上面挂属性,万一有个重名的,那就只能说一句“不好意思,GG思密达”!

Hooks 逻辑复用简单

Hooks 来了,它表示,我要一个打五个!Render Props 和 HOC 联合起来也被我秒杀!

image-5a6f5d648ca9

Hooks 表示,来十个,来一百个我也能打。

Hooks 最强的能力就是逻辑复用了,这是我最最最爱的能力了。

Hooks 会产生很多闭包问题

是的,我也不偏袒 Hooks,由于 React Hooks 的机制,如果用法不正确,会导致各种奇怪的闭包问题。

如果你要问 React Hooks 的机制是什么的话,我又要给你推荐一篇我之前写的文章了:《React Hooks 原理》。

那面对这个问题,怎么解呢?说实话,我也没有很好的解决办法。

但是,这可能也有好处。如果碰到想不明白的问题,那 99% 是由于闭包导致的,我们有很确定的方向去排查问题。

image-4636b47be14f

记住这句话,你可以少走很多弯路。

Show Case

image-20200116203233594

当然,说再多,吹再好,也没多大用。我上面讲的 Class 和 Hooks 的优缺点,网上的也有很多人讲,大家也肯定都看过。

用程序员的交流方式,就是“Talk is cheap,Show me the code.”。

亮剑吧!

接下来,我会用一个例子,让你折服,拜倒在 Hooks 的石榴裙下。如果你不服,咱们单独撕~

网络请求组件实现

image-20200116214124140

接下来,我们来实现一个最最最常见的组件。该组件接收 userId,然后发起网络请求,获得用户信息。

说白了,就是最简单的发起网络请求的组件。我们先用 Class 来实现看看。

image-20200116214639300

这段代码,是最简单的网络请求。

  • 定义一个 username 状态。
  • componentDidMount 的时候发起网络请求。
  • 网络请求结束,更新 username。

美滋滋。但是少了点东西。网络请求,我们肯定要维护一个 loading 状态,保证用户体验比较好。

那我们加上吧。

image-20200116214918755

这张图,我们增加了 loading 状态,在网络请求发起前,置为 true,在网络请求结束后,置为 false。

美滋滋。但是还是少点东西。userId 变化后,我要重新发起网络请求吧。

我们再加点代码吧。

image-20200116215101730

我们增加了对 userId 变化的监听,如果 userId 变化后,重新发起请求。

这次稳了吧?

不不不,还不够。如果 userId 连续变化了 5 次,发送了 5 个网络请求,我们要保证总是最后一次网络请求有效。也就是经常说的的“竞态处理”或者“时序控制”。

我加!加还不行吗!

image-20200116215409524

其实到这里,有些同学已经懵了。“你说的时序控制,听着很有道理,但我平时都没处理过这个问题,我看下你怎么实现的。”

确实,时序控制不算一个简单的问题,很多新手都不会解决这个问题。

稳了!到这里你觉得稳了吧。

还是年轻啊,小伙子。

image-20200116220003295

如果用上面的代码来玩,你可能会偶尔碰到上面的警告。这个警告是怎么造成的呢?我说一下你就明白了。下面四个步骤执行,必会报警告:

  1. 组件加载
  2. 发起网络请求
  3. 组件卸载
  4. 网络请求请求成功,触发 setState

看出问题了吗?组件已经卸载了,还去 setState,造成了内存溢出。

怎么解决呢?

image-20200116220311200

在组件卸载的时候,放弃最后一次请求。

到这里为止,我们就完成了一个完美的网络请求。这次真结束了!

看下写了多少行代码。

image-20200116220531399

除去空格,我们写了 38 行代码。实话说,38 行代码我能忍,但是这些逻辑我忍不了!回想下我们处理了多少逻辑:

  • 网络请求
  • loading
  • userId 变化重新发起请求
  • 竞态处理
  • 组件卸载放弃网络请求

关键这些逻辑是没办法复用的!每个项目可能有数十上百个组件会发网络请求,我就要写几十,几百遍这样的逻辑。想想我都难受。

说实话,我在写项目的时候经常会偷懒。要不就不写 loading,要不就不管竞态,要不就不管最后的内存溢出警告。

你有没有和我一样呢?嘿嘿。

言归正传,接下来就邀请 Hooks 登场了。

image-20200116221123031

三下五除二,我们用 Hooks 实现了刚才所有的逻辑。

image-20200116221212124

17 行!代码量减少了 50% 以上。好像还行!

但是,别忘了,Hooks 最重要的能力就是逻辑复用!这些逻辑我们完全可以封装起来!我们把刚才的逻辑全部封装起来!

image-20200116221359407

useAsync 封装了刚才我们说的所有功能,一行代码完成了网络请求。

最后整个组件会长这样。

image-20200116221605411

哇!我自己都佩服自己!简直了!美呆了,帅毙了,感觉自己无敌了!提前完成工作,下班回家!

image-20200116221755193

通过这个例子,我想证明一个论点:“使用 Hooks 封装逻辑的能力,可以极大提高开发效率”。

Umi Hooks

这时候你肯定要问,useAsync 在哪里?给我瞧瞧?

image-20200116221941442

useAsync

useAsync 在这里,快来瞧,快来看啦!

useAsync 是 Umi Hooks 库的核心 Hooks 之一,Umi Hooks 提供了大量提炼自业务的 Hooks。一行代码真的可以实现很多功能!

Umi Hooks 在这里在这里!你懂的~~

image-20200116222352082

当然,useAsync 不止包含上面说的功能,还支持“手动触发”执行,还支持“轮询”!

只要简单的配置一个 pollingInterval ,就能轮询发送请求了。快去试试啦

接下来我们会介绍几个更牛的 Hooks 给大家认识!

useAntdTable

image-cc2f4b087aca

AntD 的 Table 组件,想必大家在项目中经常用到吧!除了刚才异步请求的所有逻辑外,你还得处理其它的逻辑。

image-20200116232857059

比如维护 page、pageSize、sorter、filter 的状态,还得考虑搜索条件变化后,重置 page 到第一页。这些逻辑光想想就头疼了,别说写了。

现在一行代码就可以实现了!useAntdTable,封装了所有的逻辑,只要一行代码!如图上所示,你只要 ...tableProps,就可以了。这也许就是幸福的味道吧~

useLoadMore

加载更多的场景,比如下面动图的场景,想必大家在工作中都写过。

image-22fa47992b6f

这样一个加载更多的场景,我们要维护多少状态?写多少行逻辑?本来我打算写个 Class 实现的例子贴出来的,但是我放弃了,因为太难了~~

随便想想要处理的逻辑:

  • 第一次加载时候的 loading
  • 加载更多时候的 loading
  • 维护 page 和 pageSize
  • 网络请求
  • 是不是加载全了
  • 搜索条件变化后,重置到第一页。
  • .....

脑壳疼,真的脑壳疼。我会写,但是写起来真的好累。

还没完,一般产品同学还会要求,上拉加载更多......

image-cf629db68ebc

这时候我们还得监听滚动位置,如果快到底了,触发加载更多。脑壳更疼了!

image-20200116235013101

Umi Hooks 听到了你的求救,派出 useLoadMore 来拯救你了。一行代码就可以实现所有的功能!一个小时变一分钟,又可以早点下班了。

useDynamicList

image-20200116235455431

还有更好用的,比如 useDynamicList,下面的动态列表,一行代码搞定。

image-16556dfcf0e8

useBoolean

不仅是上面讲到的各种复杂逻辑可以封装。简单的逻辑封装起来也是极其好用的,比如 Boolean 值的管理。

我们一般控制 Modal,Popover 等显示隐藏的时候,都要维护一个 visible 状态。大概会是这样。

image-20200116235651552

这样的逻辑,你写过多少遍?没有几千也有几百吧!

image-20200116235850057

以后你就可以用 useBoolean 咯!

More

image-20200116235957789

不仅是上面讲到的这些,我们还有很多很多的 Hooks。

比如 useSearch,就封装了通常异步搜索场景的逻辑,比如 debounce。

比如 useVirtualList,就封装了虚拟列表的逻辑。

image-20200117000249183

比如 useMouse,封装了监听鼠标位置的逻辑。

比如 useKeyPress,封装了监听键盘按键的逻辑。

image-20200117000412951

30+ Hooks 供您选择,并且我们仍然处于婴儿期,快速发展中。我们的愿景就是:封装别人的逻辑,让别人无逻辑可写。

未来规划

image-20200117001130397

更多的 Hooks 开发

如上面所述,我们现在还处于婴儿期,需要不断汲取能量,更多的 Hooks 正在路上!要实现“让别人无逻辑可写”的目标,还需继续奋斗。

更强大的 useRequest

image-20200117001209280

大家应该都听过 useSWR 吧?是 zeit 公司开发的一个专门做网络请求的 Hooks,提供了很多新颖的思路,给了我们非常大的启发,github star 就像坐火箭一样。但在实际项目使用中,还是会有很多地方不符合蚂蚁内部的体系。但是它给我们非常大的启发,基于 swr 的思路,我们可以实现更强大的 useRequest!图上的能力,我们都要!

useRequest 目前已经处于内测期了,下个版本将会与大家见面!我们的目标是:所有的网络请求,只用 useRequest 就够了!

Hooks 生态

目前社区上 Hooks 相关的基础教程、进阶教程、原理深入、常见问题等文档都比较分散,我们准备向 Hooks 生态发展,提供各式各样的文章。以后学习 Hooks,使用 Hooks,找 Umi Hooks 就对了。

当然,生态方面目前正在规划中,预计年后启动。

总结

image-20200117001711649

Umi Hooks,你值得拥有。

我们目前处于发展阶段,欢迎大家一起共建。

你可以提 idea,我们负责实现。

你可以提 issue,我们负责改 bug。

你可以提 PR,将你封装的 Hooks 分享给大家,让更多人收益。

❤️期待您的参与。

查看原文

harden 收藏了文章 · 2019-09-07

前端工程师自检清单73答

开篇

本文参考文章《一名【合格】前端工程师的自检清单》, 并对其中的部分题目进行了解答,若有遗漏或错误之处望大家指出纠正,共同进步。(点击题目展开答案!)

此文章 Markdown 源文件地址:https://github.com/zxpsuper/blog...

一、JavaScript基础

前端工程师吃饭的家伙,深度、广度一样都不能差。

变量和类型

1. JavaScript 规定了几种语言类型?

JavaScript中的每一个值都有它自己的类型,JavaScript规定了七种语言类型,他们是:

Undefined Null Boolean String Number Symbol Object

2. JavaScript 对象的底层数据结构是什么?

对象数据被存储于堆中 (如对象、数组、函数等,它们是通过拷贝和new出来的)。

引用类型的数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据。

3. Symbol 类型在实际开发中的应用、可手动实现一个简单的 Symbol?

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。

symbol类型的 key 不能被 Object.keysfor..of 循环枚举。因此可当作私有变量使用。

let mySymbol = Symbol('key');
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
  [mySymbol]: 'Hello!'
};

4. JavaScript 中的变量在内存中的具体存储形式

JavaScript 中的变量分为基本类型和引用类型:

基本类型: 保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问

引用类型: 保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript 不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用

5. 基本类型对应的内置对象,以及他们之间的装箱拆箱操作

String(), Number(), Boolean()

装箱:就是把基本类型转变为对应的对象。装箱分为隐式和显示

  // 隐式装箱: 每当读取一个基本类型的值时,后台会创建一个该基本类型所对应的对象。
  // 在这个基本类型上调用方法,其实是在这个基本类型对象上调用方法。
  // 这个基本类型的对象是临时的,它只存在于方法调用那一行代码执行的瞬间,执行方法后立刻被销毁。
  let num=123;
  num.toFixed(2); // '123.00'//上方代码在后台的真正步骤为
  var c = new Number(123);
  c.toFixed(2);
  c = null;
  // 显式装箱: 通过内置对象 Boolean、Object、String 等可以对基本类型进行显示装箱。
  var obj = new String('123');

拆箱: 拆箱与装箱相反,把对象转变为基本类型的值。

  Number([1]); //1
  // 转换演变:
  [1].valueOf(); // [1];
  [1].toString(); // '1';Number('1'); //1 

6. 理解值类型和引用类型

JavaScript中的变量分为基本类型和引用类型:

基本类型: 保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问

引用类型: 保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript 不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用

7. null 和 undefined 的区别

  1. Number 转换的值不同,Number(null) 输出为 0, Number(undefined) 输出为 NaN
  2. null 表示一个值被定义了,但是这个值是空值
  3. undefined 表示缺少值,即此处应该有值,但是还没有定义

8. 至少可以说出三种判断 JavaScript 数据类型的方式,以及他们的优缺点,如何准确的判断数组类型

  1. typeof —— 返回给定变量的数据类型,可能返回如下字符串:

      'undefined'——Undefined
      'boolean'——Boolean
      'string'——String
      'number'——Number
      'symbol'——Symbol
      'object'——Object / Null (Null 为空对象的引用)
      'function'——Function
      // 对于一些如 error() date() array()无法判断,都是显示object类型
  2. instanceof 检测 constructor.prototype 是否存在于参数 object 的原型链上,是则返回 true,不是则返回 false

      alert([1,2,3] instanceof Array) // true
      alert(new Date() instanceof Date) // true
      alert(function(){this.name="22";} instanceof Function) //true
      alert(function(){this.name="22";} instanceof function) //false
      // instanceof 只能用来判断两个对象是否属于实例关系,而不能判断一个对象实例具体属于哪种类型。
  3. constructor —— 返回对象对应的构造函数。

      alert({}.constructor === Object);  =>  true
      alert([].constructor === Array);  =>  true
      alert('abcde'.constructor === String);  =>  true
      alert((1).constructor === Number);  =>  true
      alert(true.constructor === Boolean);  =>  true
      alert(false.constructor === Boolean);  =>  true
      alert(function s(){}.constructor === Function);  =>  true
      alert(new Date().constructor === Date);  =>  true
      alert(new Array().constructor === Array);  =>  true
      alert(new Error().constructor === Error);  =>  true
      alert(document.constructor === HTMLDocument);  =>  true
      alert(window.constructor === Window);  =>  true
      alert(Symbol().constructor);    =>    undefined 
      // null 和 undefined 是无效的对象,没有 constructor,因此无法通过这种方式来判断。
  4. Object.prototype.toString() 默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,是一个字符串,其中 Xxx 就是对象的类型。

      Object.prototype.toString.call(new Date);//[object Date]
      Object.prototype.toString.call(new String);//[object String]
      Object.prototype.toString.call(Math);//[object Math]
      Object.prototype.toString.call(undefined);//[object Undefined]
      Object.prototype.toString.call(null);//[object Null]
      Object.prototype.toString.call('') ;   // [object String]
      Object.prototype.toString.call(123) ;    // [object Number]
      Object.prototype.toString.call(true) ; // [object Boolean]
      Object.prototype.toString.call(Symbol()); //[object Symbol]
      Object.prototype.toString.call(new Function()) ; // [object Function]
      Object.prototype.toString.call(new Date()) ; // [object Date]
      Object.prototype.toString.call([]) ; // [object Array]
      Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
      Object.prototype.toString.call(new Error()) ; // [object Error]
      Object.prototype.toString.call(document) ; // [object HTMLDocument]
      Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用
      // 比较全面

9. 可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用

隐式转换一般说的是 Boolean 的转换

if 语句中,null""undefinded, 0, false 都会被转化为 false

一般应用于对接口数据判空时使用

10. 出现小数精度丢失的原因,JavaScript 可以存储的最大数字、最大安全数字,JavaScript处理大数字的方法、避免精度丢失的方法

  • 精度丢失原因,说是 JavaScript 使用了 IEEE 754 规范,二进制储存十进制的小数时不能完整的表示小数
  • 能够表示的最大数字 Number.MAX_VALUE 等于 1.7976931348623157e+308 ,最大安全数字 Number.MAX_SAFE_INTEGER 等于 9007199254740991
  • 避免精度丢失

    • 计算小数时,先乘 1001000,变成整数再运算
    • 如果值超出了安全整数,有一个最新提案,BigInt 大整数,它可以表示任意大小的整数,注意只能表示整数,而不受安全整数的限制

原型和原型链

1. 理解原型设计模式以及 JavaScript 中的原型规则

A. 所有的引用类型(数组、对象、函数),都具有对象特性,即可自由扩展属性;
B. 所有的引用类型(数组、对象、函数),都有一个`__proto__`属性(隐式原型),属性值是一个普通的对象;
C. 所有的函数,都具有一个 `prototype`(显式原型),属性值也是一个普通对象;
D. 所有的引用类型(数组、对象、函数),其隐式原型指向其构造函数的显式原型;`(obj._proto_ === Object.prototype)`;
E. 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的 `__proto__` (即它的构造函数的 `prototype`)中去寻找;

2. instanceof 的底层实现原理,手动实现一个 instanceof

简单说就是判断实例对象的__proto__是不是强等于对象的prototype属性,如果不是继续往原型链上找,直到 __proto__null 为止。

function instanceOf(obj, object) {//obj 表示实例对象,object 表示对象
  var O = object.prototype;
  obj = obj.__proto__;
  while (true) { 
      if (obj === null) 
          return false; 
      if (O === obj) // 这里重点:当 O 严格等于 obj 时,返回 true 
          return true; 
      obj = obj.__proto__; 
  } 
}

3. 理解 JavaScript 的执行上下文栈,可以应用堆栈信息快速定位问题

执行上下文 就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。

执行上下文总共有三种类型:全局执行上下文, 函数执行上下文, Eval 函数执行上下文

执行栈,在其他编程语言中也被叫做调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。

4. 实现继承的几种方式以及他们的优缺点

详情请点击:《继承的几种实现方式》

5. 可以描述 new 一个对象的详细过程,手动实现一个 new 操作符

  • new一个对象的详细过程:

    function Test() {}
    const test = new Test();
  1. 创建一个对象 const obj = {}
  2. 设置新对象的 constructor 属性为构造函数的名称,设置新对象的__proto__属性指向构造函数的 prototype 对象

    obj.constructor = Test;
    obj.__proto__ = Test.prototype;
  3. 使用新对象调用函数,函数中的 this 被指向新实例对象 Test.call(obj)
  4. 将初始化完毕的新对象地址,保存到等号左边的变量中
  • 实现一个new操作符

    function myNew(Obj,...args){
        var obj = Object.create(Obj.prototype);//使用指定的原型对象及其属性去创建一个新的对象
        Obj.apply(obj,args); // 绑定 this 到obj, 设置 obj 的属性
        return obj; // 返回实例
    }

6. 理解 es6 class 构造以及继承的底层实现原理

  • ES6 类的底层还是通过构造函数去创建的。

    // es6 Parent类实现
    class Parent {
      constructor(name,age){
          this.name = name;
          this.age = age;
      }
      speakSomething(){
          console.log("I can speek chinese");
      }
    }
    // 转化为
    var Parent = function () {
      function Parent(name, age) {
          _classCallCheck(this, Parent); // 判断实例 Parent instanceof Parent(函数)是否为true
    
          this.name = name;
          this.age = age;
      }
      // 此方法通过使用 Object.defineProperty 为 function Parent 的 prototype 添加属性值
      _createClass(Parent, [{
          key: "speakSomething",
          value: function speakSomething() {
              console.log("I can speek chinese");
          }
      }]);
    
      return Parent;
    }();
  • ES6 的继承实现

    //定义子类,继承父类
    class Child extends Parent {
      static width = 18
      constructor(name,age){
          super(name,age);
      }
      coding(){
          console.log("I can code JS");
      }
    }
    // 转化为
    var Child = function (_Parent) {
      _inherits(Child, _Parent);
    
      function Child(name, age) {
          _classCallCheck(this, Child);
    
          return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name, age));
      }
    
      _createClass(Child, [{
          key: "coding",
          value: function coding() {
              console.log("I can code JS");
          }
      }]);
    
      return Child;
    }(Parent);

这里其实就是多了一个 _inherits(Child, _Parent); 方法,实现了以下功能,具体可看文章《ES6类以及继承的实现原理》

  //实现的结果是:
  subClass.prototype.__proto__ = superClass.prototype
  subClass.__proto__ = superClass // 实现静态属性的继承

作用域和闭包

1. 理解词法作用域和动态作用域

词法作用域也称静态作用域,javascript 采用静态作用域

静态作用域 —— 函数的作用域基于函数创建的位置。

动态作用域 —— 函数的作用域基于函数的使用位置。

var value = 1;

function foo() {
  console.log(value);
}

function bar() {
  var value = 2;
  foo();
}

bar(); // 输出 1 。JavaScript 采用的是词法作用域,也称为静态作用域。相同的,动态作用域此代码应该输出 2

2. 理解 JavaScript 的作用域和作用域链

作用域(scope)就是变量访问规则的有效范围。

JavaScript 中全局变量的作用域是全局的,在代码的任何地方都是有定义的。然而函数的参数和局部变量只在函数体内有定义。另外局部变量的优先级要高于同名的全局变量,也就是说当局部变量与全局变量重名时,局部变量会覆盖全局变量。

3. this的原理以及几种不同使用场景的取值

this的几种不同使用场景的取值 +

<a href="http://www.ruanyifeng.com/blog/2018/06/javascript-this.html" target="_blank">JavaScript 的 this 原理</a>

4. 闭包的实现原理和作用,可以列举几个开发中闭包的实际应用

原理:闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

作用:闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

应用:1. 匿名自执行函数 2. 结果缓存 3. 封装局部变量

参考链接:《学习Javascript闭包(Closure)》

5. 理解堆栈溢出和内存泄漏的原理,如何防止

堆栈溢出 的产生是由于过多的函数调用,导致调用堆栈无法容纳这些调用的返回地址,一般在递归中产生。堆栈溢出很可能由无限递归(Infinite recursion)产生,但也可能仅仅是过多的堆栈层级.

参考链接:《内存泄漏与避免》

6. 如何处理循环的异步操作

  1. 将异步操作变同步,使用 async/await.
  2. 去掉循环,将循环变成递归

执行机制

1. 为何 try 里面放 return,finally 还会执行,理解其内部机制

try 语句中,在执行 return 语句时,要返回的结果已经准备好了,就在此时,程序转到 finally 执行了。

在转去之前,try 中先把要返回的结果存放到局部变量中去,执行完 finally 之后,在从中取出返回结果。

因此,即使finally 中对返回的结果进行了改变,但是不会影响返回结果。

它应该使用栈保存返回值。

2. JavaScript 如何实现异步编程,可以详细描述 EventLoop 机制

JavaScript 如何实现异步编程:

  1. callback (回调函数)

回调函数代表着,当某个任务处理完,然后需要做的事。比如读取文件,连接数据库,等文件准备好,或数据库连接成功执行编写的回调函数,又比如像一些动画处理,当动画走完,然后执行回调。

  1. 发布订阅模式

顾名思义,便是先订阅了事件,有人一发布事件你就知道了,接着执行后面的操作。

  1. Promise

Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件的结果,相比回调函数,Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

  1. Generator (生成器)函数

Generator 函数是 ES6 提供的一种异步编程解决方案,其行为类似于状态机。

  1. async/await

async/await 本质上还是基于 Generator 函数,可以说是 Generator 函数的语法糖,async 就相当于之前写的run函数(执行Generator函数的函数),而 await 就相当于 yield ,只不过 await 表达式后面只能跟着 Promise 对象,如果不是 Promise 对象的话,会通过 Promise.resolve 方法使之变成 Promise 对象。async 修饰 function,其返回一个 Promise 对象。

《浏览器 Event Loop 机制》

3. 宏任务和微任务分别有哪些

宏任务: setTimeout,setInterval,setImmediate (Node独有),requestAnimationFrame (浏览器独有),I/O,UI rendering (浏览器独有)

微任务: process.nextTick (Node独有),Promise,Object.observe,MutationObserver

4. 可以快速分析一个复杂的异步嵌套逻辑,并掌握分析方法

// 执行顺序,先微队列,后宏队列。
console.log(1);
setTimeout(() => {
  console.log(2);
  setTimeout(() => {
    console.log(8);
  })
  Promise.resolve().then(() => {
    console.log(3)
  });
});
new Promise((resolve, reject) => {
  console.log(4)
  setTimeout(() => {
    console.log(10);
  })
  resolve()
}).then(() => {
  console.log(5);
  Promise.resolve().then(() => {
    console.log(11)
  });
  setTimeout(() => {
    console.log(13);
  })
})
setTimeout(() => {
  Promise.resolve().then(() => {
    console.log(9)
  });
  console.log(6);
  setTimeout(() => {
    console.log(12);
  })
})
console.log(7);

从头至尾执行一次代码,根据上面分类规则分至不同队列, new promise( function ) 也是立即执行。setTimeout 的回调函数属于宏队列(macrotask)resolve 的回调函数属于微队列

// 栈区(stack)
console.log(1);
console.log(4);
console.log(7);
// 宏队列
() => {
  console.log(2);
  setTimeout(() => {
    console.log(8);
  })
  Promise.resolve().then(() => {
    console.log(3)
  });
}
() => {
  console.log(10);
}
() => {
  Promise.resolve().then(() => {
    console.log(9)
  });
  console.log(6);
  setTimeout(() => {
    console.log(12);
  })
}
// 微队列
() => {
  console.log(5);
  Promise.resolve().then(() => {
    console.log(11)
  });
  setTimeout(() => {
    console.log(13);
  })
}

优先执行微队列,微队列执行过程中产生的微队列和宏队列置于队列末尾排序执行,而宏队列产生的微队列和宏队列于新的队列中等待。。

执行微队列:(分类)

// 栈区(stack)
console.log(1);
console.log(4);
console.log(7);
//////////
console.log(5);
// 微队列
() => {
  console.log(11)
});
// 宏队列
() => {
  console.log(2);
  setTimeout(() => {
    console.log(8);
  })
  Promise.resolve().then(() => {
    console.log(3)
  });
}
() => {
  console.log(10);
}
() => {
  Promise.resolve().then(() => {
    console.log(9)
  });
  console.log(6);
  setTimeout(() => {
    console.log(12);
  })
}
() => {
    console.log(13);
}

此时新增了一个微队列console.log(11),因为是微队列产生的,继续执行:

// 栈区(stack)
console.log(1);
console.log(4);
console.log(7);
//////////
console.log(5);
/////////
console.log(11)
// 微队列-空
// 宏队列
() => {
  console.log(2);
  setTimeout(() => {
    console.log(8);
  })
  Promise.resolve().then(() => {
    console.log(3)
  });
}
() => {
  console.log(10);
}
() => {
  Promise.resolve().then(() => {
    console.log(9)
  });
  console.log(6);
  setTimeout(() => {
    console.log(12);
  })
}
() => {
    console.log(13);
}

执行完微队列后执行宏队列:

// 栈区(stack)
console.log(1);
console.log(4);
console.log(7);
//////////
console.log(5);
/////////
console.log(11);
/////////
console.log(2);
console.log(10);
console.log(6);
console.log(13);
// 微队列
() => {
  console.log(3)
}
() => {
  console.log(9)
}
// 宏队列
() => {
  console.log(8);
}
() => {
  console.log(12);
}

接下来执行微队列后宏队列,即:

// 栈区(stack)
console.log(1);
console.log(4);
console.log(7);
//////////
console.log(5);
/////////
console.log(11);
/////////
console.log(2);
console.log(10);
console.log(6);
console.log(13);
////////
console.log(3)
console.log(9)
////////
console.log(8);
console.log(12);

5. 使用 Promise 实现串行

// 一个 promise 的 function
function delay(time) {
 return new Promise((resolve, reject) => {
   console.log(`wait ${time}s`)
   setTimeout(() => {
     console.log('execute');
     resolve()
   }, time * 1000)
 })
}
const arr = [3, 4, 5];
  1. reduce

    arr.reduce((s, v) => {
     return s.then(() => delay(v))
    }, Promise.resolve())
  2. async + 循环 + await

    (
     async function () {
       for (const v of arr) {
         await delay(v)
       }
     }
    )()
  3. 普通循环

    let p = Promise.resolve()
    for (const i of arr) {
     p = p.then(() => delay(i))
    }
  4. 递归

    function dispatch(i, p = Promise.resolve()) {
     if (!arr[i]) return Promise.resolve()
     return p.then(() => dispatch(i + 1, delay(arr[i])))
    }
    dispatch(0)

6. Node 与浏览器 EventLoop 的差异

《JavaScript 运行机制详解:再谈Event Loop》

《带你彻底弄懂Event Loop》

7. 如何解决页面加载海量数据而页面不卡顿

  1. 分治思想,在一定的时间内多次加载数据,直至渲染完成,使用 window.requestAnimationFramedocument.createDocumentFragment() 实现, 可参考文章【如何解决页面加载海量数据而不冻结前端UI】
  2. 局部显示,毕竟用户能看到的就一屏内容,监听用户的滚动行为,改变显示元素,可使 DOM 结构最简单化。可参考文章【大数据如何在前端流畅展示】,不过他的 Demo有点问题.

语法和API

1. 理解 ECMAScript 和 JavaScript 的关系

ECMAScriptJavaScript 的规范,JavaScriptECMAScript 的实现。

2. 熟练运用 es5、es6 提供的语法规范

【JavaScript 标准参考教程(alpha)】

【ECMAScript 6 入门】

3. setInterval 需要注意的点,使用 settimeout 实现 setInterval

  • setInterval 需要注意的点:

在使用 setInterval 方法时,每一次启动都需要对 setInterval 方法返回的值做一个判断,判断是否是空值,若不是空值,则要停止定时器并将值设为空,再重新启动,如果不进行判断并赋值,有可能会造成计时器循环调用,在同等的时间内同时执行调用的代码,并会随着代码的运行时间增加而增加,导致功能无法实现,甚至占用过多资源而卡死奔溃。因此在每一次使用setInterval方法时,都需要进行一次判断。

let timer = setInterval(func, 1000)
// 在其他地方再次用到setInterval(func, 1000)
if (timer !== null) {
    clearInterval(timer)
    timer = null
}
timer = setInterval(func, 1000)
  • 使用 settimeout 实现 setInterval

    setIntervalFunc = () =>{
      console.log(1) //使用递归
      setTimeout(setIntervalFunc, 1000);
    };
    setInterval()

4. JavaScript 提供的正则表达式 API、可以使用正则表达式(邮箱校验、URL解析、去重等)解决常见问题

邮箱校验:

function isEmail(emailStr) {
    return /^[a-zA-Z0-9]+([._-]*[a-zA-Z0-9]*)*@[a-zA-Z0-9]+.[a-zA-Z0-9{2,5}$]/.test(emailStr);
}

URL解析:

function isUrl(urlStr) {
    return /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\*\+,;=.%]+$/.test(value)
}

数组去重:

// set结构
let arr = [1, 1, 2, 2, 3, 3]
arr2 = [...new Set(arr)]
console.log(arr2) // [1,2,3]

// Object.keys(), 利用属性 key 的唯一性
let arrObj = [1, 1, 2, 2, 3, 3]
arrObj2 = {}
for (i in arrObj) {
    arrObj2[arrObj[i]] = true
}
let arrObj3 = Object.keys(arrObj2)
console.log(arrObj3)

// 利用 indexOf() 查询数组内是否已经包含该元素
var arrIndexOf = ['a','c','b','d','a','b']
var arrIndexOf2 = [];
for(var i = 0;i<arrIndexOf.length;i++){
    if(arrIndexOf2.indexOf(arrIndexOf[i])<0){
        arrIndexOf2.push(arrIndexOf[i]);
    }
}
console.log(arrIndexOf2)// ['a', 'c', 'b', 'd']

二、HTML和CSS

HTML

1. 从规范的角度理解 HTML,从分类和语义的角度使用标签

语义化标签<header> <footer> <nav> <section> <article> <aside> 等

  • 让页面呈现清晰的结构
  • 屏幕阅读器(如果访客有视障)会完全根据你的标记来“读”你的网页
  • 搜索引擎的爬虫依赖标签确定上下文和权重问题
  • 便于团队开发和维护

标签分类

  • 文档标签(10 个):`<html>、<head>、<body>、<title>、<meta>、<base>

、<style>、<link>、<script>、<noscript>`

  • 表格标签(10 个):`<table>、<thead>、<tbody>、<tfoot>、<tr>、<td>、<th>

、<col>、<colgroup>、<caption>`

  • 表单标签(10 个):`<from>、<input>、<textarea>、<button>、<select>

、<optgroup>、<option>、<label>、<fieldset>、<legend>`

  • 列表标签(6个):<ul>、<ol>、<li>、<dl>、<dt>、<dd>
  • 多媒体标签(5个):<img>、<map>、<area>、<object>、<param>
  • 文章标签:<h1> - <h6> 、<p>、<br>、<span>、<bdo>、<pre>、<acronym>、<abbr>、<blockquote>、<q>、<ins>、<del>、<address>
  • 字体样式标签:<tt>、<i>、<b>、<big>、<small>、<em>、<strong>、<dfn>、<code>、<samp>、<kbd>、<var>、<cite>、<sup>、<sub>

    在不同的场景使用不同的标签,更能显示清晰的结构。

2. 元信息类标签 (head、title、meta) 的使用目的和配置方法

<head> 标签用于定义文档的头部,它是所有头部元素的容器。<head> 中的元素可以引用脚本、指示浏览器在哪里找到样式表、提供元信息等等。可以包含的标签有: <base>, <link>, <meta>, <script>, <style>, 以及 <title>。

<title> 定义文档的标题,它是 head 部分中唯一必需的元素。

<meta>元素可提供有关页面的元信息(meta-information),比如针对搜索引擎和更新频度的描述和关键词。使用方法参考【meta标签详解】

3. HTML5 离线缓存原理

  • [ ] 待补充

4. 可以使用 Canvas API、SVG 等绘制高性能的动画

CSS

1. CSS 盒模型,在不同浏览器的差异

  • 标准 w3c 盒子模型的范围包括 margin、border、padding、content,并且 content 部分不包含其他部分
  • ie 盒子模型的范围也包括 margin、border、padding、content,和标准 w3c 盒子模型不同的是:ie 盒子模型的 content 部分包含了 borderpading

</details>

<details>
<summary>2. CSS 所有选择器及其优先级、使用场景,哪些可以继承,如何运用at规则</summary>

不同级别优先级!important > 行内样式 > ID选择器 > 类选择器 > 元素 > 通配符 > 继承 > 浏览器默认属性

相同级别优先级内联(行内)样式 > 内部样式表 > 外部样式表 > 导入样式(@import)。

可继承属性

字体系列属性, font-family, font-weight, font-size, font-style...
文本系列属性, text-indent, text-align, line-heigh, word-spacing, letter-spacing, text-transform, color
元素可见性:visibility, 光标属性:cursor

AT rule:

一、什么是 at-rules

eg:@charset "utf-8";

at-ruleCSS 样式声明,以 @ 开头,紧跟着是标识符(charset),最后以分号(;)结尾。

二、几个 at-rules

1、@charset —定义被样式表使用的字符集

2、@import ——告诉 CSS 引擎包含外部的 CSS 样式表

3、@namespace——告诉 CSS 引擎所有的内容都必须考虑使用 XML 命名空间前缀

4、嵌套at-rules

(1)@media——条件组规则。如果设备符合标准定义的条件查询则使用该媒体

(2)@font-face——描述了一个将从外部下载的字体

(3)@keyframes——描述了中间步骤在 CSS 动画的序列

(4)@page——描述了文件的布局变化,当要打印文档时。

(5)@supports——条件组规则,如果浏览器满足给出的规则,则把它应用到内容中

(6)@document——条件组规则,如果被用到文档的 CSS 样式表满足了给定的标准,那么将被应用到所有的内容中。
</details>

<details>
<summary>3. CSS 伪类和伪元素有哪些,它们的区别和实际应用</summary>

伪类:用于向某些选择器添加特殊的效果. :active, :focus, :link, :visited, :hover, :first-child

伪元素:用于将特殊的效果添加到某些选择器. :before, :after, :first-line, :first-letter

伪类和伪元素的根本区别在于:它们是否创造了新的元素(抽象)。从我们模仿其意义的角度来看,如果需要添加新元素加以标识的,就是伪元素,反之,如果只需要在既有元素上添加类别的,就是伪类。

</details>

<details>
<summary>4. HTML 文档流的排版规则,CSS 几种定位的规则、定位参照物、对文档流的影响,如何选择最好的定位方式,雪碧图实现原理</summary>

HTML 文档流的排版规则: 把元素按从上而下,从左到右的顺序默认排列。不在一行的元素从上而下,在一行的从左到右排列。

CSS 几种定位的规则:

  • static 定位(普通流定位)
  • float 定位(浮动定位), 有两个取值:left (左浮动)和 right (右浮动)。

浮动元素会在没有浮动元素的上方,效果上看是遮挡住了没有浮动的元素,有float样式规则的元素是脱离文档流的,它的父元素的高度并不能有它撑开。

  • relative 定位(相对定位), 相对本元素的左上角进行定位,top,left,bottom,right 都可以有值。虽然经过定位后,位置可能会移动,但是本元素并没有脱离文档流,还占有原来的页面空间。
  • absolute 定位(绝对定位), 相对于祖代中有 relative (相对定位)并且离本元素层级关系上是最近的元素的左上角进行定位,如果在祖代元素中没有有 relative定位的,就默认相对于body进行定位。绝对定位是脱离文档流的
  • fixed 定位(固定定位),这种定位方式是相对于整个文档的,只需设置它相对于各个方向的偏移值,就可以将该元素固定在页面固定的位置,通常用来显示一些提示信息,脱离文档流;

雪碧图实现原理CSS Sprite,是一种 CSS 图像合并技术,该方法是将小图标和背景图像合并到一张图片上,然后利用 css 的背景定位来显示需要显示的图片部分。
</details>

<details>
<summary>5. 水平垂直居中的方案、可以实现6种以上并对比它们的优缺点</summary>

参考文章: 【CSS实现水平垂直居中的1010种方式】
</details>

<details>
<summary>6. BFC 实现原理,可以解决的问题,如何创建BFC</summary>

BFC(Block formatting context) 直译为"块级格式化上下文"。它是一个独立的渲染区域,只有块级元素参与, 它规定了内部的块级元素如何布局,并且与这个区域外部毫不相干。

BCF 可以解决的问题:浮动定位,消除外边距折叠,清除浮动,自适应多栏布局

BFC的创建:根元素或包含根元素的元素,浮动元素(float 不为none),绝对定位元素( positionabsolute 或者 fixed),displayinline-block,table-cell,table-caption,overflow 值不为 visible,弹性元素( flex 布局),网格元素( grid 布局)
</details>

<details>
<summary>7. CSS模块化方案、如何配置按需加载、如何防止 CSS 阻塞渲染</summary>

CSS模块化方案: 文件细化,命名约定,CSS Modules , css in js

如何防止 CSS 阻塞渲染:

CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。

有一些 CSS 样式只在特定条件下(例如显示网页或将网页投影到大型显示器上时)使用,我们可以通过 CSS“媒体类型”和“媒体查询”来解决这类用例:

<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 40em)">

首屏相关的关键 `CSS` 使用阻塞渲染的方式加载,所有的非关键 `CSS` 在首屏渲染完成后加载。

</details>

<details>
<summary>8. 手写图片瀑布流效果</summary>

参考文章:【瀑布流布局的实现】
</details>

<details>
<summary>9. 使用CSS绘制几何图形(圆形、三角形、扇形、菱形等)</summary>

// 圆形
.circle{
    width:100px;
    height:100px;
    border-radius:50%;
    background:blue;
}
// 三角形
.triangle {
    width: 0;
    height: 0;
    border: 50px solid blue;
    /* 通过改变边框颜色,可以改变三角形的方向 */
    border-color: blue transparent transparent transparent;
}
// 扇形,扇形是由一个圆形和一个矩形进行组合得到的,用矩形遮住圆形的一部分就形成了扇形。
.sector {
    width: 142px;
    height: 142px;
    background: #fff;
    border-radius: 50%;
    background-image: linear-gradient(to right, transparent 50%, #655 0);
}

.sector::before {
    content: '';
    display: block;
    margin-left: 50%;
    height: 100%;
    width: 100%;
    background-color: inherit;
    transform-origin: left;
    /*调整角度,改变扇形大小*/
    transform: rotate(230deg);
}
// 菱形
.rhombus {
    width: 200px;
    height: 200px;
    transform: rotateZ(45deg) skew(30deg, 30deg);
    background: blue;
}

10. 使用纯 CSS 实现曲线运动(贝塞尔曲线)

CSS3 新增了 transition-timing-function 属性,它的取值就可以设置为一个三次贝塞尔曲线方程。

参考文章: 【贝塞尔曲线的css实现——淘宝加入购物车基础动画】

11. 实现常用布局(三栏、圣杯、双飞翼、吸顶),说出多种方式并理解其优缺点

圣杯布局, 两边顶宽,中间自适应的三栏布局。

  • [ ] 期待评论补充

三、计算机基础

关于编译原理,不需要理解非常深入,但是最基本的原理和概念一定要懂,这对于学习一门编程语言非常重要

编译原理

1. 理解代码到底是什么,计算机如何将代码转换为可以运行的目标程序

代码就是程序员用开发工具所支持的语言写出来的源文件,是一组由字符、符号或信号码元以离散形式表示信息的明确的规则体系。

计算机源代码最终目的是将人类可读文本翻译成为计算机可执行的二进制指令,这种过程叫编译,它由通过编译器完成。

2. 正则表达式的匹配原理和性能优化

  • [ ] 待补充

3. 如何将JavaScript代码解析成抽象语法树(AST)

  • [ ] 待补充

4. base64 的编码原理

  • [ ] 待补充

5. 几种进制的相互转换计算方法,在 JavaScript 中如何表示和转换

parseInt(str, radix) 将一个 radix 进制的 str 转化为十进制,parseInt('23',8) // 19,将八进制的‘23’转化为10进制的‘19’

number.toString(radix) 将一个数字转化为 radix 进制的数字字符串

0x11.toString(8) // 21
0x11.toString(10) // 17
0x11.toString(2) // 10001

网络协议

1. 理解什么是协议,了解 TCP/IP 网络协议族的构成,每层协议在应用程序中发挥的作用

协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定。如怎么样建立连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流。它的三要素是:语法、语义、时序。

TCP/IP 网络协议族的构成: TCP/IP 协议是 Internet 最基本的协议。由传输层的 TCP 协议和网络层的 IP 协议组成。

TCP 负责发现传输的问题,一有问题就发出信号,要求重新传输,直到所有数据安全正确地传输到目的地。而 IP 是给因特网的每一台联网设备规定一个地址。

应用层

应用层决定了向用户提供应该服务时通信的活动。

TCP/IP 协议族内预存了各类通用的应用服务。比如,FTP(File Transfer Protocol,文件传输协议)和 DNS(Domain Name System,域名系统)服务就是其中的两类。HTTP 协议也处于该层。

传输层

传输层对上层应用层,提供处于网络连接中两台计算机之间的数据传输。

在传输层有两个性质不同的协议:TCP(Transmission Control Protocol,传输控制协议)和 UDP(User Data Protocol,用户数据报协议)。

网络层(又名网络互连层)

网络层用来处理在网络上流动的数据包。数据包是网络传输的最小数据单位。该层规定了通过怎样的路径(所谓的传输路线)到达对方计算机,并把数据包传送给对方。

与对方计算机之间通过多台计算机或网络设备进行传输时,网络层所起的所用就是在众多的选项内选择一条传输路线。

链路层(又名数据链路层,网络接口层)

用来处理连接网络的硬件部分。包括控制操作系统、硬件的设备驱动、NIC(Network Interface Card,网络适配器,即网卡),及光纤等物理可见部分(还包括连接器等一切传输媒介)。硬件上的范畴均在链路层的作用范围之内。

2. 三次握手和四次挥手详细原理,为什么要使用这种机制

三次握手和四次挥手详细原理:

三次握手:避免连接请求的数据包丢失,数据传输过程因为网络并发量很大在某结点被阻塞

四次挥手: TCP连接是全双工通道,需要双向关闭。

参考文章: 【TCP/IP协议族】

3. 有哪些协议是可靠,TCP有哪些手段保证可靠交付

TCP的协议:FTP(文件传输协议)、Telnet(远程登录协议)、SMTP(简单邮件传输协议)、POP3(和 SMTP 相对,用于接收邮件)、HTTP 协议等。

TCP 提供可靠的、面向连接的数据传输服务。使用 TCP 通信之前,需要进行“三次握手”建立连接,通信结束后还要使用“四次挥手”断开连接。

4. DNS的作用、DNS解析的详细过程,DNS优化原理

DNS 的作用:DNS是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。

DNS解析过程

1、在浏览器中输入 www.qq.com 域名,操作系统会先检查自己本地的 hosts 文件是否有这个网址映射关系,如果有,就先调用这个 IP 地址映射,完成域名解析。

2、如果 hosts 里没有这个域名的映射,则查找本地 DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。

3、如果 hosts本地DNS解析器缓存 都没有相应的网址映射关系,首先会找 TCP/ip 参数中设置的 首选DNS服务器,在此我们叫它 本地DNS服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。

4、如果要查询的域名,不由 本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个 IP 地址映射,完成域名解析,此解析不具有权威性。

5、如果 本地DNS服务器 本地区域文件与缓存解析都失效,则根据 本地DNS服务器 的设置(是否设置转发器)进行查询,如果未用转发模式,本地 DNS 就把请求发至 13 台根 DNS,根 DNS 服务器收到请求后会判断这个域名 (.com) 是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP本地DNS服务器 收到 IP 信息后,将会联系负责 .com 域的这台服务器。这台负责 .com 域的服务器收到请求后,如果自己无法解析,它就会找一个管理 .com 域的下一级 DNS 服务器地址(http://qq.com)本地DNS服务器。当 本地DNS服务器 收到这个地址后,就会找 http://qq.com 域服务器,重复上面的动作,进行查询,直至找到 www.qq .com 主机。

6、如果用的是转发模式,此 DNS 服务器就会把请求转发至上一级 DNS 服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根 DNS 或把转请求转至上上级,以此循环。不管是 本地DNS服务器 用是是转发,还是根提示,最后都是把结果返回给 本地DNS服务器,由此 DNS 服务器再返回给客户机。

DNS 优化:减少DNS的请求次数;进行DNS预获取 。

减少DNS的请求次数————在项目中减少不同域名的http请求,尽量少的域名减少DNS的请求数

DNS预获取————减少用户的等待时间,提升用户体验 。

默认情况下浏览器会对页面中和当前域名(正在浏览网页的域名)不在同一个域的域名进行预获取,并且缓存结果,这就是隐式的 DNS Prefetch。如果想对页面中没有出现的域进行预获取,那么就要使用显示的 DNS Prefetch 了。

<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//www.itechzero.com">
<link rel="dns-prefetch" href="//api.share.baidu.com">
<link rel="dns-prefetch" href="//bdimg.share.baidu.com">

5. CDN的作用和原理

  • CDN的作用

CDN 的全称是 Content Delivery Network,即内容分发网络。其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。

  • CDN 的原理

1、多域名加载资源

一般情况下,浏览器都会对单个域名下的并发请求数(文件加载)进行限制,通常最多有 4 个,那么第 5 个加载项将会被阻塞,直到前面的某一个文件加载完毕。因为 CDN 文件是存放在不同区域(不同 IP)的,所以对浏览器来说是可以同时加载页面所需的所有文件(远不止 4 个),从而提高页面加载速度。

2、文件可能已经被加载过并保存有缓存

一些通用的 js 库或者是 css 样式库,如 jQuery ,在网络中的使用是非常普遍的。当一个用户在浏览你的某一个网页的时候,很有可能他已经通过你网站使用的 CDN 访问过了其他的某一个网站,恰巧这个网站同样也使用了 jQuery,那么此时用户浏览器已经缓存有该 jQuery 文件(同 IP 的同名文件如果有缓存,浏览器会直接使用缓存文件,不会再进行加载),所以就不会再加载一次了,从而间接的提高了网站的访问速度。

3、分布式的数据中心

假如你的站点布置在北京,当一个香港或者更远的用户访问你的站点的时候,他的数据请求势必会很慢很慢。而 CDN 则会让用户从离他最近的节点去加载所需的文件,所以加载速度提升就是理所当然的了。

6. HTTP 请求报文和响应报文的具体组成,能理解常见请求头的含义,有几种请求方式,区别是什么

参考文章:【HTTP 请求详解】

HTTP协议的六种请求方法

  1. GET: 发送请求来获得服务器上的资源,请求体中不会包含请求数据,请求数据放在协议头中
  2. POST: 和 get 一样很常见,向服务器提交资源让服务器处理,比如提交表单、上传文件等,可能导致建立新的资源或者对原有资源的修改。提交的资源放在请求体中。不支持快取。非幂等
  3. HEAD: 本质和 get 一样,但是响应中没有呈现数据,而是 http 的头信息,主要用来检查资源或超链接的有效性或是否可以可达、检查网页是否被串改或更新,获取头信息等,特别适用在有限的速度和带宽下。
  4. PUT: 和 post 类似,html 表单不支持,发送资源与服务器,并存储在服务器指定位置,要求客户端事先知道该位置;比如 post 是在一个集合上(/province),而 put 是具体某一个资源上(/province/123)。所以 put 是安全的,无论请求多少次,都是在 123 上更改,而 post 可能请求几次创建了几次资源。幂等
  5. DELETE: 请求服务器删除某资源。和 put 都具有破坏性,可能被防火墙拦截
  6. CONNECT: HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。就是把服务器作为跳板,去访问其他网页然后把数据返回回来,连接成功后,就可以正常的 getpost 了。

7: OPTIONS: 获取 http 服务器支持的 http 请求方法,允许客户端查看服务器的性能,比如 ajax 跨域时的预检等。
8: TRACE: 回显服务器收到的请求,主要用于测试或诊断。一般禁用,防止被恶意攻击或盗取信息。

7. HTTP 所有状态码的具体含义,看到异常状态码能快速定位问题

  • 1XX:信息状态码

    • 100 Continue 继续,一般在发送post请求时,已发送了http header之后服务端将返回此信息,表示确认,之后发送具体参数信息
  • 2XX:成功状态码

    • 200 OK 正常返回信息
    • 201 Created 请求成功并且服务器创建了新的资源
    • 202 Accepted 服务器已接受请求,但尚未处理
  • 3XX:重定向

    • 301 Moved Permanently 请求的网页已永久移动到新位置。
    • 302 Found 临时性重定向。
    • 303 See Other 临时性重定向,且总是使用 GET 请求新的 URI。
    • 304 Not Modified 自从上次请求后,请求的网页未修改过。
  • 4XX:客户端错误

    • 400 Bad Request 服务器无法理解请求的格式,客户端不应当尝试再次使用相同的内容发起请求。
    • 401 Unauthorized 请求未授权。
    • 403 Forbidden 禁止访问。
    • 404 Not Found 找不到如何与 URI 相匹配的资源。
  • 5XX: 服务器错误

    • 500 Internal Server Error 最常见的服务器端错误。
    • 503 Service Unavailable 服务器端暂时无法处理请求(可能是过载或维护)。

8. HTTP1.1、HTTP2.0带来的改变

缓存处理,在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。

带宽优化及网络连接的使用HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。

错误通知的管理,在 HTTP1.1 中新增了 24 个错误状态响应码,如 409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。

Host头处理,在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名 (hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个 IP 地址。HTTP1.1 的请求消息和响应消息都应支持 Host 头域,且请求消息中如果没有 Host 头域会报告一个错误(400 Bad Request)

长连接HTTP 1.1 支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个 TCP 连接上可以传送多个 HTTP 请求和响应,减少了建立和关闭连接的消耗和延迟,在 HTTP1.1 中默认开启 Connection: keep-alive,一定程度上弥补了 HTTP1.0 每次请求都要创建连接的缺点。

HTTP2.0和HTTP1.X相比的新特性

  • 新的二进制格式(Binary Format),HTTP1.x 的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认 01 的组合。基于这种考虑 HTTP2.0 的协议解析决定采用二进制格式,实现方便且健壮。
  • 多路复用(MultiPlexing),即连接共享,即每一个 request 都是是用作连接共享机制的。一个 request 对应一个 id,这样一个连接上可以有多个 request,每个连接的 request 可以随机的混杂在一起,接收方可以根据 requestidrequest 再归属到各自不同的服务端请求里面。
  • header 压缩,如上文中所言,对前面提到过 HTTP1.xheader 带有大量信息,而且每次都要重复发送,HTTP2.0 使用 encoder 来减少需要传输的 header 大小,通讯双方各自 cache 一份 header fields 表,既避免了重复 header 的传输,又减小了需要传输的大小。
  • 服务端推送(server push),HTTP2.0具有 server push 功能。

9. HTTPS 的加密原理,如何开启 HTTPS,如何劫持 HTTPS 请求

参考文章:【一个故事讲完 https】

设计模式

1. 熟练使用前端常用的设计模式编写代码,如单例模式、装饰器模式、代理模式等

参考文章:【设计模式】

2. 发布订阅模式和观察者模式的异同以及实际应用

观察者模式和发布订阅模式最大的区别就是发布订阅模式有个事件调度中心。

// 观察者模式
class Subject{
  constructor(){
    this.subs = [];
  }
  addSub(sub){
    this.subs.push(sub);
  }
  notify(){
    this.subs.forEach(sub=> {
      sub.update();
    });
  }
}
class Observer{
  update(){
    console.log('update');
  }
}
let subject = new Subject();
let ob = new Observer();
//目标添加观察者了
subject.addSub(ob);
//目标发布消息调用观察者的更新方法了
subject.notify();   //update
// 发布订阅者模式
class PubSub {
    constructor() {
        this.subscribers = {}
    }
    subscribe(type, fn) {
        if (!Object.prototype.hasOwnProperty.call(this.subscribers, type)) {
          this.subscribers[type] = [];
        }
        
        this.subscribers[type].push(fn);
    }
    unsubscribe(type, fn) {
        let listeners = this.subscribers[type];
        if (!listeners || !listeners.length) return;
        this.subscribers[type] = listeners.filter(v => v !== fn);
    }
    publish(type, ...args) {
        let listeners = this.subscribers[type];
        if (!listeners || !listeners.length) return;
        listeners.forEach(fn => fn(...args));        
    }
}

let ob = new PubSub();
ob.subscribe('add', (val) => console.log(val));
ob.publish('add', 1);

四、数据结构和算法

据我了解的大部分前端对这部分知识有些欠缺,甚至抵触,但是,如果突破更高的天花板,这部分知识是必不可少的,而且我亲身经历——非常有用!

JavaScript编码能力

1. 多种方式实现数组去重、扁平化、对比优缺点

参考文章:【JS 数组去重方法整理】

【5种方式实现数组扁平化】

2. 多种方式实现深拷贝、对比优缺点

参考文章:【递归实现深拷贝】

3. 手写函数柯里化工具函数、并理解其应用场景和优势

  • [ ] 待补充

4.手写防抖和节流工具函数、并理解其内部原理和应用场景

参考文章:【函数的防抖与节流】

5.实现一个 sleep 函数

function sleep(time) {
  return new Promise((resolve,reject) => setTimeout(resolve, time))
}
sleep(3000).then(() => {console.log('沉睡3000ms')})

手动实现前端轮子

1. 手动实现call、apply、bind

call

  1. 判断当前 this 是否为函数,防止 Function.prototype.myCall() 直接调用
  2. context 为可选参数,如果不传的话默认上下文为 window
  3. context 创建一个 Symbol(保证不会重名)属性,将当前函数赋值给这个属性
  4. 处理参数,传入第一个参数后的其余参数
  5. 调用函数后即删除该 Symbol 属性
Function.prototype.myCall = function(context = window, ...args) {
    if (this === Function.prototype) {
        return undefined; // 用于防止 Function.prototype.myCall() 直接调用
    }
    context = context || window;
    const fn = Symbol();
    context[fn] = this;
    const result = context[fn](...args);
    delete context[fn];
    return result;
};

apply

apply 实现类似 call,参数为数组

Function.prototype.myApply = function(context = window, args) {
    if (this === Function.prototype) {
        return undefined; // 用于防止 Function.prototype.myCall() 直接调用
    }
    const fn = Symbol();
    context[fn] = this;
    let result;
    if (Array.isArray(args)) {
        result = context[fn](...args);
    } else {
        result = context[fn]();
    }
    delete context[fn];
    return result;
};

bind

因为 bind() 返回一个方法需手动执行,因此利用闭包实现。

Function.prototype.myBind = function(context, ...args1) {
    if (this === Function.prototype) {
        throw new TypeError('Error');
    }
    const _this = this;
    return function F(...args2) {
        // 判断是否用于构造函数
        if (this instanceof F) {
            return new _this(...args1, ...args2);
        }
        return _this.apply(context, args1.concat(args2));
    };
};

2.手动实现符合 Promise/A+ 规范的 Promise

参考文章:【手动实现 promise】

3. 手写一个 EventEmitter 实现事件发布、订阅

    function EventEmitter() {
      this._events = Object.create(null);
    }

    // 向事件队列添加事件
    // prepend为true表示向事件队列头部添加事件
    EventEmitter.prototype.addListener = function (type, listener, prepend) {
      if (!this._events) {
        this._events = Object.create(null);
      }
      if (this._events[type]) {
        if (prepend) {
          this._events[type].unshift(listener);
        } else {
          this._events[type].push(listener);
        }
      } else {
        this._events[type] = [listener];
      }
    };

    // 移除某个事件
    EventEmitter.prototype.removeListener = function (type, listener) {
      if (Array.isArray(this._events[type])) {
        if (!listener) {
          delete this._events[type]
        } else {
          this._events[type] = this._events[type].filter(e => e !== listener && e.origin !== listener)
        }
      }
    };

    // 向事件队列添加事件,只执行一次
    EventEmitter.prototype.once = function (type, listener) {
      const only = (...args) => {
        listener.apply(this, args);
        this.removeListener(type, listener);
      }
      only.origin = listener;
      this.addListener(type, only);
    };

    // 执行某类事件
    EventEmitter.prototype.emit = function (type, ...args) {
      if (Array.isArray(this._events[type])) {
        this._events[type].forEach(fn => {
          fn.apply(this, args);
        });
      }
    };
    // 测试一下
    var emitter = new EventEmitter();

    var onceListener = function (args) {
      console.log('我只能被执行一次', args, this);
    }

    var listener = function (args) {
      console.log('我是一个listener', args, this);
    }

    emitter.once('click', onceListener);
    emitter.addListener('click', listener);

    emitter.emit('click', '参数');
    emitter.emit('click');

    emitter.removeListener('click', listener);
    emitter.emit('click');

4.可以说出两种实现双向绑定的方案、可以手动实现

参考文章:【Vue 双向数据绑定原理】

5.手写JSON.stringify、JSON.parse

let Myjson = {
      parse: function(jsonStr) {
          return eval('(' + jsonStr + ')');
      },
      stringify: function(jsonObj) {
          var result = '',
              curVal;
          if (jsonObj === null) {
              return String(jsonObj);
          }
          switch (typeof jsonObj) {
              case 'number':
              case 'boolean':
                  return String(jsonObj);
              case 'string':
                  return '"' + jsonObj + '"';
              case 'undefined':
              case 'function':
                  return undefined;
          }

          switch (Object.prototype.toString.call(jsonObj)) {
              case '[object Array]':
                  result += '[';
                  for (var i = 0, len = jsonObj.length; i < len; i++) {
                      curVal = JSON.stringify(jsonObj[i]);
                      result += (curVal === undefined ? null : curVal) + ",";
                  }
                  if (result !== '[') {
                      result = result.slice(0, -1);
                  }
                  result += ']';
                  return result;
              case '[object Date]':
                  return '"' + (jsonObj.toJSON ? jsonObj.toJSON() : jsonObj.toString()) + '"';
              case '[object RegExp]':
                  return "{}";
              case '[object Object]':
                  result += '{';
                  for (i in jsonObj) {
                      if (jsonObj.hasOwnProperty(i)) {
                          curVal = JSON.stringify(jsonObj[i]);
                          if (curVal !== undefined) {
                              result += '"' + i + '":' + curVal + ',';
                          }
                      }
                  }
                  if (result !== '{') {
                      result = result.slice(0, -1);
                  }
                  result += '}';
                  return result;

              case '[object String]':
                  return '"' + jsonObj.toString() + '"';
              case '[object Number]':
              case '[object Boolean]':
                  return jsonObj.toString();
          }
      }
  };

6. 手写懒加载效果

参考文章:【图片懒加载】

浏览器原理

1. 可详细描述浏览器从输入URL到页面展现的详细过程

参考文章:【输入URL至页面渲染】

2. 浏览器的垃圾回收机制,如何避免内存泄漏

参考文章:【浏览器内存回收机制】

参考文章:【内存泄漏与避免】

资源推荐

语言基础

计算机基础

数据结构和算法

运行环境

框架和类库

前端工程

项目和业务

学习提升

博客推荐

技术之外

此文章 Markdown 源文件地址:https://github.com/zxpsuper/blog...

查看原文

harden 发布了文章 · 2019-08-02

学习计划

正在学习:

1.深入react技术栈;
2.巩固es6基础;
3.力扣算法题;

准备学习:

1.单元测试;
2.blog:前端自检清单等;
3.webpack配置;
4.JavaScript数据结构与算法;

已完成

深入react与redux技术栈

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 18 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-06-14
个人主页被 460 人浏览