GithubHelp home page GithubHelp logo

cyril-blog's Issues

批量更新DOM(Vue.nextTick与React的setState分析笔记)

Vue中的批量更新DOM

关于批量更新DOM文档中有这么一段介绍:

默认情况下, Vue 的 DOM 更新是异步执行的。理解这一点非常重要。当侦测到数据变化时, Vue 会打开一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。假如一个 watcher 在一个事件循环中被触发了多次,它只会被推送到队列中一次。然后,在进入下一次的事件循环时, Vue 会清空队列并进行必要的 DOM 更新。在内部,Vue 会使用 MutationObserver 来实现队列的异步处理,如果不支持则会回退到 setTimeout(fn, 0)

这么做可以有效避免无效的更新,比如一个数据在一个事件循环中多次改变,则如果按正常的策略会多次触发watcher中的回调,重新构建虚拟DOM进行diff处理,而事实上这是不必要的,其实重复的watcher只要执行一次。所以Vue利用js中的事件循环进行优化减少不必要的计算和DOM操作。
我主要阅读的关于Vue.nextTick这一块源码,来看一下Vue是如何利用事件循环的。在此之前需要明白两个东西,microtaskMutationObserver前者可以看知乎上的一个讨论Promise的队列与setTimeout的队列的有何关联。后者直接看看MDN就好,MutationObserver

首先看如何使用:

Vue.nextTick(function () {
  console.log('nextTick')
})

即传递一个回调函数,该函数在事件循环结束时调用,下面来看一下这块的源码(/src/core/util/env.js):

export const nextTick = (function () {
  const callbacks = [] //存放所有回调
  let pending = false //记录当前是否有回调在执行
  let timerFunc //一个函数,包装了一个能添加到microtask的函数

  // 在这里执行所有的回调,而这个函数的执行时机在下一个事件循环中
  function nextTickHandler () {
    pending = false
    // 将callbacks数组复制执行,因为如果在nextTick的回调函数中继续执行Vue.nextTick()
    // 则cb会不断被push到callbacks中,导致callbacks一直执行
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }

  // the nextTick behavior leverages the microtask queue, which can be accessed
  // via either native Promise.then or MutationObserver.
  // MutationObserver has wider support, however it is seriously bugged in
  // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
  // completely stops working after triggering a few times... so, if native
  // Promise is available, we will use it:
  /* istanbul ignore if */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    // 如果能用Promise则通过Promise.then把nextTickHandler添加到microtask
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      if (isIOS) setTimeout(noop)
    }
  } else if (typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
   // 如果能用MutationObserver,则人为的创建一个textNode,
   // 并让MutationObserver监听这个textNode,在timerFunc中改变这个textNode,
   // 由此触发MutationObserver的回调(这里涉及MutationObserver的工作方式,看看MDN文档就好),
   // 实现在下一次事件循环执行nextTickHandler的目的
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    //都不行只能用setTimeout实现,将nextTickHandler添加到macrotask中
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  return function queueNextTick (cb?: Function, ctx?: Object) {
    let _resolve
    callbacks.push(() => {
      if (cb) cb.call(ctx)
      if (_resolve) _resolve(ctx)
    })
    if (!pending) {
      pending = true
      timerFunc()
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(resolve => {
        _resolve = resolve
      })
    }
  }
})()

首先nextTick是一个自执行函数,返回值是queueNextTick()函数(主要利用闭包来保存一些变量)。当Vue.nextTick()执行时,执行的就是queueNextTick()函数,在这里将回调放入callbacks数组保存。并且用了一个pending标志位做了一次判断,它的主要作用是保证timerFunc()这个函数在一轮事件循环中只执行一次(因为在执行timerFunc()前将它置为true,而只有下一轮事件循环开始时它才能被置为false),timerFunc()这个函数仅仅包装了一个能添加到microtask的函数(如promise.then,MutationObserver),具体注释中有。关于选用哪种方式利用事件循环,从代码中可以看出,优先使用Promise,不存在则使用MutationObserver,都不存在则用setTimeout。

清楚这个函数的工作原理差不多就明白了Vue异步更新DOM的原理了,因为Vue会把一轮事件循环(即一次task)中所有触发的watcher去重后添加到一个队列里,然后将这个队列交由Vue.nextTick(),即将这个队列添加到microtask中,这样在本次task结束后,按照规则就会取出所有的microtask执行它们,实现DOM的更新。

React是如何做的?

react通过setState这个API改变state,那么它是如何工作的?

React 源码剖析系列 - 解密 setState这篇文章提到一个例子:

componentDidMount() {
  this.setState({val: this.state.val + 1});
  console.log(this.state.val);    // 第 1 次 log

  this.setState({val: this.state.val + 1});
  console.log(this.state.val);    // 第 2 次 log

  setTimeout(() => {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 第 3 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 第 4 次 log
  }, 0);
}

输出结果是0 0 2 3原因文章中也有提到。
此外Change Detection And Batch Update这篇文章也有一个总结:

在React调用的方法中连续setState走的是批量更新,此外走的是连续更新

就是说如果方法是通过React调用的比如生命周期函数,React的事件处理等,那么会进行批量更新,自己调用的方法,比如setTimeout,xhr等则是连续更新。

  • 2017-07-07更新
    最近在看Under the hood: ReactJS,part-1又提到了事务这个概念,回过头来又看了一遍发现之前一直好奇的问题原来如此简单。关于react将需要更新的组件放到dirtyComponents这里可以理解,但是是在什么时机去更改batchingStrategy.isBatchingUpdates的值,或者说执行ReactUpdates.flushBatchedUpdates的时机,因为对于vue来说就是一个microtask,很好理解。现在才知道原来如果在react生命周期如componentDidMounted调用setState,其实componentDidMounted就处在一个事务中,那么当它执行完的时候就该执行transaciton.closeAll,在这里处理批量更新

可以参看这张图,来自Under the hood: ReactJS

参考链接

Vue源码详解之nextTick
vue早期源码学习系列之五:批处理更新DOM

Redux与Vuex源码笔记(一)

虽然网上能搜到各种关于vuex与redux的源码分析文章,而且有很多写得很详细,但我还是决定按照我的思路写几篇博客,重点将放在整体的工作流程以及vuex与redux的一些对比。

Redux与React-redux工作流程梳理

第一篇是关于React-redux(5.0.3)的源码分析笔记,因为重在流程整理,所以其中只截取了部分源码

首先看看通常的用法:

const firstState =(state = {}, action){
  switch (action.type) {
    case 'ACTION_ONE':
      return {
        // 返回一个新的state
      }
    default:
      return {
        // 返回一个默认的state
      }
  }
}
// ...secondState类似

// 合并
const rootReducer = combineReducers({
    firstState,
    secondState
})

let store = createStore(
    rootReducer,
    // 关于中间件放到第二篇
    applyMiddleware(thunk)
);

<Provider store={store}>
  // 放一些自己的组件,如:
  <MyComponent></MyComponent>
</Provider>

store如何生成暂时先跳过,暂时只需要知道它是这样的结构:

store = {
    dispatch, // 即常用的dispatch函数
    subscribe,
    getState, // 通过它可以获取整个state
    replaceReducer
}

Provider开始:

export default class Provider extends Component {
  // react为实现跨级组件通信提供的api,从这一层往下的子组件都可以通过context.store直接拿到store,类似于一个全局变量
  getChildContext() {
    return { store: this.store, storeSubscription: null }
  }

  constructor(props, context) {
    super(props, context)
    // this.store来自于props
    this.store = props.store
  }

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

Provider的功能就是将store传给子孙组件。下面进入子组件MyComponent:

export default class MyComponent extends Component{
  // ...省略
  render() {
    // 这里this.props已经包含firstState
  }
}

// 这里传入的state是通过store.getState()获取到的值,即整个应用的state
function mapStateToProps(state) {
  // 在这里可以从整个state中提取一些在该组件需要用到的属性,比如这里的firstState
    const {firstState} = state
    // 通常需要返回一个对象,对象中的属性将通过下面的connect函数注入到组件的props中,
    return {
        firstState
    }
}

export default connect(mapStateToProps)(MyComponent)

关于connect我在看文档和使用的时候就挺好奇的,是怎么通过connect就可以把mapStateToProps里返回的对象注入到组件中的?关于这一段的流程我专门写了个最小化Demo打断点调试了一下,得出结果如下:

执行connect(mapStateToProps)对应的源码是:

function connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps
  ) {
    // match函数中对mapStateToProps做一些处理,
    // 定义了mapStateToProps缺失或者mapStateToProps是一个函数时如何处理,
    // 如果mapStateToProps是一个函数,将对它进行一个包装,返回
    // function initProxySelector(dispatch, { displayName }) {...}
    // 即initMapStateToProps指向function initProxySelector(dispatch, { displayName }) {...}
    // 至于为什么这么做我还没搞懂
    const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps')
    const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps')
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
    // 这里返回connectHOC()的执行结果,传递了一系列参数包括mapStateToProps,
    // 这个函数其实就是 /src/components/connectAdvanced.js中的connectAdvanced()
    // 在这个connectAdvanced里定义了一些contextTypes(猜测是为了从context上获取store)
    // 然后返回了wrapWithConnect(WrappedComponent){}函数
    // 至此connect(mapStateToProps)就执行完了,返回wrapWithConnect(WrappedComponent){}函数
    return connectHOC(selectorFactory, {
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
    })
  }

个人感觉connect(mapStateToProps)就是对mapStateToProps的包装和利用闭包保存一些变量引用还有初始化一些变量。 上面注释中提到connect(mapStateToProps)返回的结果是wrapWithConnect(WrappedComponent){}函数,所以connect(mapStateToProps)(MyComponent)相当于执行wrapWithConnect(MyComponent)

wrapWithConnect源码:

// 只截取了部分

// 重点关注run方法
function makeSelectorStateful(sourceSelector, store) {
  // wrap the selector in an object that tracks its results between runs.
  const selector = {
    // 下面会多次调用run方法,将在这里比较新旧props,并设置 selector.shouldComponentUpdate
    run: function runComponentSelector(props) {
      try {
        const nextProps = sourceSelector(store.getState(), props)
        if (nextProps !== selector.props || selector.error) {
          selector.shouldComponentUpdate = true
          selector.props = nextProps
          selector.error = null
        }
      } catch (error) {
        selector.shouldComponentUpdate = true
        selector.error = error
      }
    }
  }

  return selector
}

function wrapWithConnect(WrappedComponent) {
    // 高阶组件的通常用法,创建一个Connect组件,用来包装我们传入的WrappedComponent
    class Connect extends Component {
      constructor(props, context) {
        super(props, context)
        // 获取到Provider中的store
        this.store = props[storeKey] || context[storeKey]

        this.initSelector()
        // 初始化Subscription,用于发布订阅模式
        this.initSubscription()
      }

      componentDidMount() {
        if (!shouldHandleStateChanges) return

        // 订阅store,一个发布订阅模式,当store发生变化时调用onStateChange
        this.subscription.trySubscribe()
      }

      componentWillReceiveProps(nextProps) {
        this.selector.run(nextProps)
      }

      shouldComponentUpdate() {
        // 通过this.selector.shouldComponentUpdate属性避免不必要的更新,
        // 所以使用了redux之后是不需要自己通过shouldComponentUpdate优化的
        return this.selector.shouldComponentUpdate
      }

      initSelector() {
        const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)

        this.selector = makeSelectorStateful(sourceSelector, this.store)
        // 将在run方法中调用mapStateToProps,返回值赋给this.selector.props
        this.selector.run(this.props)
      }

      initSubscription() {
        if (!shouldHandleStateChanges) return

        const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
        this.subscription = new Subscription(this.store, parentSub, this.onStateChange.bind(this))
      }
      // store发生变化时调用
      onStateChange() {
        this.selector.run(this.props)
        // setState触发react的更新,这里dummyState是一个空对象,
        // 只是为了触发react的更新机制,在更新机制中会从通过store.getState()获取最新的state
        this.setState(dummyState)
      }


      render() {
        // 在这里将额外的props(包括dispatch和mapStateToProps返回值)合并到WrappedComponent(即这里的MyComponent组件)
        return createElement(WrappedComponent, this.addExtraProps(selector.props))
      }
    }

    return hoistStatics(Connect, WrappedComponent)
  }
}

现在可以做个总结:

  1. connect内部调用了我们定义的mapStateToProps(),返回值将作为props注入组件
  2. 注入的方式类似于高阶组件
  3. connect中调用store.subscribe(这个方法在redux中定义,详情可以看第二篇)传入onStateChange作为回调,使得state变化时能够执行onStateChange触发DOM更新

参考链接

探索react-redux的小秘密
Redux入坑进阶-源码解析
解读redux工作原理

常见排序算法及动画演示js实现

部分动画展示在这里源码在这,动画是直接操作DOM用CSS3实现的,每次运行时排序程序直接运行完,但会在每一步记录一个状态并将状态push进一个数组,排序完成后根据数组还原排序过程。

选择排序

比较简单,过程如下,首先找到数组最小的那个元素,将它与数组第一个元素交换,然后在剩下的元素中找到最小的元素将它与数组第二个元素交换,这样循环直至结束。其最优、平均、最差时间复杂度均为 О(n²),几乎用不到。

function selectionSort(arr) {
    for (let i = 0, len = arr.length; i < len; i++) {
        let min = i;
        for (let j = i + 1; j < len; j++) {
            if (arr[j] < arr[min]) {
                min = j
            }
        }
        if (min !== i) {
            swap(arr, min, i) //交换
        }
    }
}

冒泡排序

平均时间复杂度О(n²),最坏О(n²),最优О(n)

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
function bubbleSort(arr) {
    for (let i = 0, len = arr.length; i < len - 1; i++) {
        for (let j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr, j + 1, j);
            }
        }
    }
}

插入排序

与冒泡有相同的时间复杂度,不过交换次数变少,整体过程类似于整理扑克牌。

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5
function insertionSorting(arr) {
    let temp,j
    for (let i = 1, len = arr.length; i < len; i++) {
        temp = arr[i]
        j = i - 1
        while (j >= 0 && arr[j] > temp) {
            arr[j + 1] = arr[j]
            j--
        }
        arr[j + 1] = temp
    }
}

快速排序

最常用的排序,平均Ο(nlogn),最坏О(n²),最优Ο(nlogn),但快速排序通常明显比其他Ο(nlogn)算法更快

  1. 从数列中挑出一个元素,称为"基准"(pivot),
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
// 原地快排
function quickSort(arr, left = 0, len = arr.length) {
    let pivot
    if (left < len) {
        pivot = patition(arr, left, len)
        quickSort(arr, left, pivot - 1)
        quickSort(arr, pivot, len)
    }
    function patition(arr, left = 0, len = arr.length) {
        let pivot = left
        for (let right = left + 1; right < len; right++) {
            if (arr[pivot] >= arr[right]) {
                left++
                swap(arr, left, right)
            }
        }
        swap(arr, left, pivot)
        return left + 1
    }
}

// js版实现
function quickSort(arr){
  if (arr.length <= 1)  return arr
  let pivotIndex = Math.floor(arr.length / 2)
  let pivot = arr.splice(pivotIndex, 1)[0]
  let left = []
  let right = []
  for (let i = 0; i < arr.length; i++){
    if (arr[i] < pivot) {
      left.push(arr[i])
    } else {
      right.push(arr[i])
    }
  }
  return quickSort(left).concat([pivot], quickSort(right))
}

归并排序

所有情况均为Ο(nlogn),主要策略是分治。

  1. 将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素
  2. 将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素
  3. 重复步骤2,直到所有元素排序完毕
// 递归版
function mergeSort(arr) {
    function merge(left, right) {
        let result = []
        while (left.length && right.length) {
            result.push(left[0] <= right[0] ? left.shift() : right.shift())
        }
        return result.concat(left.concat(right))
    }

    let len = arr.length, mid = parseInt(len / 2)
    if (len < 2) return arr
    return merge(mergeSort(arr.slice(0, mid)), mergeSort(arr.slice(mid)))
}

堆排序

时间复杂度所有情况均为Ο(nlogn)

function heapSort(arr) {
    let len = arr.length

    function maxHeapify(start, end) {
        let dad = start, son = dad * 2 + 1
        if (son >= end) return
        if (son + 1 < end && arr[son] < arr[son + 1]) // 比较两个子节点大小,选择较大的
            son++
        if (arr[dad] <= arr[son]) {
            swap(arr, dad, son)
            maxHeapify(son, end) // 递归调整
        }
    }

    // 创建最大堆,从最后一个父节点来时调整
    for (let i = Math.floor(len / 2) - 1; i >= 0; i--) {
        maxHeapify(i, len);
    }
    // 堆排序
    for (let i = len - 1; i > 0; i--) {
        swap(arr, 0, i);
        maxHeapify(0, i);
    }
}

小结

虽然实际开发中排序这项工作基本不要自己手写,因为大部分语言都有现成的接口可以使用比如js的Array.prototype.sort(),它是用快排实现的并做了一些优化,但是其中有些**还是值得借鉴的,比如快排中的patition,可能在其他算法问题中有所运用。

关于圣杯布局

最早听说圣杯布局是刚接触前端时刷百度ife看到的,当时给出的方案是这样的双飞翼布局介绍。当时啥都不懂,觉得怎么这么麻烦扫了一遍直接跳过。时隔半年,js学了不少,CSS感觉没多大进步,看网格布局这篇博客的时候看到这样一个需求:

圣杯布局是一种网页布局,由四部分组成:一个页眉,页脚和一个主要内容区域,有两个侧边,每边一个。布局遵循一下规则:
1.两边带有固定宽度中间可以流动(fluid)
2.中心列最先出现在标记中
3.所有三列不管其中内容如何变化,都应该是相同的高度
4.页脚应该总是在浏览器视窗的底部,即便内容不填满整个适口的宽度
5.响应式布局,在较小的视口中,各部分要进行折叠,宽度100%显示

想了一会,居然想不出很好的解决方案,因为按照一般的三栏式布局只需要在html中把left,right放在main前面,然后分别让left,right左右浮动,并设置main的margin就可以实现。但这题需要将中心列最先出现在标记中,上面的方法显然是不行的。搜了一下前三个需求基本都是用float,position,negative margin解决,思路都和双飞翼布局介绍这里介绍的一样,属于兼容性很好的方案。至于第四个需求,传统方法可以使用绝对定位或负边距,较灵活的方案可以用《CSS揭秘》提到了两个解决方案:CSS calc和CSS flex。最后一个需求直接媒体查询改一些float,position问题应该不大。

然后感觉flex应该可以实现这些需求,不考虑兼容性的话应该没必要用那么麻烦的方法。试了一下,果然简单很多,代码如下:

html:

<body>
    <div class="header">header</div>
    <div class="container">
        <div class="main">main</div>
        <div class="left">left</div>
        <div class="right">right</div>
    </div>
    <div class="footer">footer</div>
</body>

CSS:

* {
    margin: 0;
    padding: 0;
}

body {
    display: flex;
    flex-flow: column;
    min-height: 100vh;
}

.header {
    height: 3em;
    background-color: red;
}

.container {
    display: flex;
    flex: 1;
}

.main {
    flex: 1;
    background-color: green;
}

.left {
    width: 200px;
    background-color: aqua;
    order: -1; //定义项目的排列顺序。数值越小,排列越靠前,默认为0。
}

.right {
    width: 230px;
    background-color: blue;
}

.footer {
    background-color: gray;
}

@media (max-width: 800px) {
    .container {
        flex-flow: column;
    }

    .left,
    .right {
        width: 100%;
    }
}

效果:
screenshot from 2017-01-13 11-45-00

screenshot from 2017-01-13 11-52-47

用flex只要这么点代码就把这几个需求全部实现。
网格布局这篇博客介绍了网格布局的用法,并举了如何用网格布局实现这些需求,虽说现在兼容性基本等于0,但用它来进行整体布局确实会简单很多。

网络是怎样连接的

最近把《网络是怎样连接的》这本书看了一遍,发现讲得挺清楚的,作为科普还是不错的。所以对于原来我不知道的东西做一些记录、总结。具体内容是关于“从输入URL到页面加载完成的过程中都发生了什么事情?”这个问题回答。虽然这个问题网上有很多答案,但这确实是涉及面太广,很难完全说明白的一个问题。

第一步:浏览器解析URL

这一步主要与HTTP相关,包括生成HTTP信息(请求行,消息头,消息体),平时遇到的比较多,书中提到的基本都是已经知道的了。

第二步:DNS域名解析

具体过程如下:浏览器调用操作系统Socket库,将域名传入Socket库中的域名解析函数,该函数根据域名生成一些信息(包括域名,Class,记录类型),并将这些信息打包传给操作系统内部协议栈,由操作系统协议栈发起网络请求(此时目标服务器为DNS服务器,IP一般已经指定,或自动获取,不需要再查询IP),DNS服务器收到信息后,如果有缓存则直接返回结果,如果没有就从根域名开始查找,一级一级查找,直到得到结果。结果也由操作系统内部协议栈接收,并写入内存保存。顺便提一下,DNS查询是基于UDP协议的。

第三步:建立连接,发起请求(TCP/IP)

先整理大体过程:获得IP后浏览器就可以向目标服务器请求数据,不过这个过程依然是委托给是Socket库。首先调用Socket库的socket组件创建客户端套接字,此时会返回一个描述符(主要为了区别不同的套接字),然后调用connect组件,将描述符,服务器IP,端口作为参数传入。此时协议栈会执行连接操作,成功后协议栈会将对方的IP地址和端口号等信息保存在套接字中。现在套接字连接就建立起来了,剩下的只要将数据送入套接字,这些数据就会传到对方的套接字中。调用Socket库的write,传入描述符和要发送的数据,经过网络,服务器就会收到我们发的数据,做出响应后返回数据给客户端,此时客户端调用Socket库中的read程序委托协议栈完成数据的接收,写入内存,待所有内容接收完毕后后断开连接。这里只是大体流程,具体内容涉及到TCP/IP协议。

下面具体讨论这一过程。上面提到的第一步是调用Socket库的socket组件创建客户端套接字,套接字其实就是通信控制信息,如通信对象的IP地址,端口号,通信的进行状态等,协议栈在执行操作时需要参阅这些信息才能正确通信。可以用netstat -ano命令获取当前系统套接字内容。至于调用socket就是在协议栈开辟内存并写入初始状态,最后会返回一个描述符。应用程序拿到了描述符就可以确定套接字(也就确定了通信信息),接下来只要在创建连接时带上这个描述符,协议栈就知道该去和谁通信。下面一步是调用connect,传入描述符,服务器IP端口号(http默认是80)等信息,协议栈根据这些信息创建TCP头部,其中包括发送方和接收方的端口号,控制位SYN设为1(表示这是建立连接的包),设置适当的序号(在判断数据是否成功送达时用到)和窗口大小(用于滑动窗口模式),并将信息传递给IP模块委托它进行发送。服务器接收到后根据TCP头信息提供的端口号找到相应的套接字,服务器做一系列操作后(包括将套接字的状态改为正在连接),然后返回响应,这个过程和客户端一样,此外还要将ACK位设位1,表示已经收到相应的网络包。客户端收到服务器的响应后还要再返回一个ACK表示确认。其实整个流程就是TCP/IP三次握手的过程。下面一步是调用write,这时TCP会将请求信息切分成一定大小的块,并在每一块上加上TCP头部(包含序号,表示当前发送的是第几个字节的数据),服务器收到后会返回ACK号表示确认收到,(TCP就是依靠序号和ACK号来确保数据传输可靠,因为如果中间某个包丢失,客户端检测ACK号就可以知道丢了哪个包,直接重发就可以了)这个过程将持续到所有数据收发完毕。最后一步是关闭连接删除套接字,与前面连接过程类似,不过是通过FIN标志位表示关闭连接,双方都确认后才关闭连接,然后过一段时间就可以删除套接字。

TCP模块处理完后的包还要交由IP模块,IP模块所做的就是继续在包上添加IP头部(包含发送,接收双方的IP地址)和MAC头部(包含接收,发送方的MAC地址,其中接收方的MAC地址一开始客户端并不知道,可以用ARP查询),这些信息在后面经过交换机,路由器时都要用到,这时一个完整的以太网包就构建好了,IP模块将这个完整的包传递到网卡驱动,网卡驱动会把包交由MAC模块做最后的处理(添加报头起始帧分界符和校验位),然后经由电路将数字信号转换为电信号,发送出去。

顺便记一下用UDP与TCP传输时的区别:TCP会在发送每一个包时都进行确认,而UDP直接把所有数据全部发送出去,接收方只返回一次确认,如果接收方没有确认,发送方直接重发所有的包(显然比较低效)。这种机制比较适用于控制用的短数据,比如一共只有一个包,或者是视频音频数据。

关于传输过程的一些细节:

  • 硬件交互

关于硬件不是很感兴趣,所以这块只是大致看了一下,包括如何将电信号转化成数字信号,如何抑制传输过程中的噪声,交换机如何进行包转发(内部维护一张MAC地址-端口的表),路由器包转发操作(维护一张路由表)等。

  • 关于服务端

包括防火墙,负载均衡,内容分发服务(CDN)。防火墙通过配置包过滤规则对网络包进行过滤;使用CDN时,通过查询路由表可大致估算出客户端距离缓存服务器的距离,进而选择最近的缓存服务器。
而且服务端的TCP模块工作过程也和客户端有所不同,客户端用的是connect发起连接,服务端则是用bind和listen使服务器处于等待连接状态,然后调用accept,待请求到来后立即进行处理。

总结

网络涉及到的东西太多了,各种协议…… 这本书也只是概括着讲了整个流程,不过补了一下这些基础知识之后还是有些好处的,解决了原来的一些困惑。

第13届D2参会感受

看了一下上次博客的更新时间,还是大概一年前。。。自己居然已经一年没有写过什么东西,反思了一下主要有两个原因:一是这一年里确实对前端技术本身关注不够,没有深入哪一块去研究,二是投入工作之后平时已经比较忙了,空闲时间不太想学习。现状急需改变,就从今天开始吧。

上周参加了 D2 前端技术论坛,本应该参加完就进行一些总结,但工作日比较忙,没来及做这件事,一直拖到现在才进行整理。
每次听完这些分享其实感受都是一个:前端有趣的东西还有这么多,每一个都想去玩一玩,然后一天过后就忘了这些,依然写着类似的业务代码。我觉得还是要每周花点时间在脱离业务的场景下学点别的东西。

正文

本次有三个分会场,每个会场主题不同,同一时间只能选择自己感兴趣的某一个话题听了。
第一场听的设计稿智能生成代码平台
这个话题记得去年参加JSConf的时候就在提,其实到现在也没有什么成熟的方案,这次讲的个人感觉都是一些比较虚的东西,最终也没给出切合标题的实际方案,大概只详细讲了怎么自动生成iconfont。
最近也在了解一些中后台提效方案,很大的一个方向就是智能搭建,目前这种搭建方式尚不能很好的满足业务需求,更不用说设计稿自动生成代码了,还有很长的路要走啊。关于提效这件事,两年前大家谈的大多是组件库,于是当时各种不同风格的组件库纷纷出现,直至组件库规范形成,大家开始基于这些组件库组装更复杂的模板、物料,比如阿里的飞冰,已经可以快速搭建一个后台系统。在此之上可能就是设计稿转代码了,期待这一天的到来。

第二场听的探索海量数据的实时渲染
感受还挺深的,虽然我没有接触过这类似的业务场景,但想象一下利用浏览器渲染一些极其复杂的图形,一定会有很多瓶颈。讲师从WebWorker,讲到WebAssembly,再到WebGL2,虽然都是之前知道的东西,但对这几个东西都没有深入了解。中间还举了优化一段代码的例子,总结下来其实最重要的还是一句话:你写代码的水平对性能的影响远大于工具。确实是这样,比如我在一些业务代码中有时为了图方便直接就写出了O(N^2)复杂度的代码,主要我知道数据量不大这么写没什么问题,如果是面对海量数据那么其实每一行代码都得想下可不可以先从算法层面进行优化。

第三场听的Migration to React Suspense
本以为会讲一些 Suspense 的代码实现,结果比较失望,仅仅是介绍了 Suspense 这个特性出现的原因和如何使用。但 demo 准备的不错,一步一步算是把 Suspense 讲的很清楚了。

第四场听的Web 渲染引擎中兴之路之技术大揭秘
主要将 U4 内核的演进,现在 U4 内核已经有不弱于原生组件的性能了。移动端写的比较少,感觉原生的组件主要好在视觉、动画效果,看起来很舒服,h5 即便性能不是问题了,但在这些动效算法上还是不如原生的啊。

第五场听的ElectronJS in 2019
没太多技术干货,主要再讲 ElectronJS 是如何发展的,解释了为何 ElectronJS 更新 Chromium 版本如此艰难,原来他们在 Chromium 之上打了很多补丁来绕过 Chromium 的一些限制,于是 Chromium 每次更新加入新特性他们都得回归一遍,这样的版本迭代效率可想而知,但目前也没有什么好的办法。之前我有体验过 ElectronJS,这个项目的想法很赞的,将 Chromium 和 node 结合开发桌面端项目,但总感觉以后桌面端 PWA 是一个比较好的方向。

总的来说质量还是不错的,毕竟没多少广告就已经很赞啦,而且至少又听到了一些新名词。

项目中遇到一些坑(长期记录)

  • animation动画在某些手机浏览器没有效果
在支持animation属性情况下没有效果尝试更改animation属性参数位置。发现还有一个原因,如果是用vue写的那么不能把@keyframes写在<style scoped>中,去掉scoped或者添加一个全局的animation.css
  • h5页面键盘弹出时,页面抖动
可能页面高度不足,键盘弹出时把页面向上挤,解决方法是设置页面高度。
  • 如何在不影响其他元素布局的情况下扩大可点击的区域
使用伪元素
content: '';
position: absolute;
top: -20px;
right: -20px;
bottom: -20px;
left: -20px;
  • 正则问题
对于全局匹配的正则表达式对于`test`和`exec`方法要注意存在lastIndex
例如 let re = /^./g
re.test('.a') //true
re.test('.a') //false
多次执行结果不同其实是因为lastIndex存在

Redux与Vuex源码笔记(二)

store的生成及中间件

上一篇从入口Provider开始总结了state的注入过程,其中state来自于store,这篇就看一下store是如何生成的,主要关注combineReducerscreateStoreapplyMiddleware,这几个API。(redux版本3.6.0)

combineReducers基本用法:

const firstState =(state = {}, action){
  switch (action.type) {
    case 'ACTION_ONE':
      return {
        // 返回一个新的state
      }
    default:
      return {
        // 返回一个默认的state
      }
  }
}
// ...secondState类似

// 合并
const rootReducer = combineReducers({
    firstState,
    secondState
})

刚上手使用的时候有两点困惑,

  1. combineReducers传入的firstStatesecondState是如何变成state对象上的key的?
  2. firstState function中传入的参数state并不是整个state,而是拆分过的。即文档上的说明:每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。关于这个redux是如何做的? 其实关于以上两点认真看文档再仔细想想的话会发现确实没有什么黑魔法,很容易实现这两个需求。

源码:

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  // 筛选出reducer是function的部分保存到finalReducers中
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  // 返回一个function,它的调用时机在每一次dispatch中
  // 所以combineReducers只是对传入的reducers进行了一些筛选,
  // 然后利用闭包把这些reducers保存下来
  return function combination(state = {}, action) {

    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      // 这里key就是firstState,secondState
      const key = finalReducerKeys[i]
      // 对应的reducer function
      const reducer = finalReducers[key]
      // 根据key获取前一个state
      const previousStateForKey = state[key]
      // 执行reducer function,这里可以看到上面提到的困惑中的第二点是如何实现的
      // 返回值作为下一个state
      const nextStateForKey = reducer(previousStateForKey, action)
      // reducer function必须有返回值
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 返回新的state
    return hasChanged ? nextState : state
  }
}

比较简单,一看就明白了,注释中提到combination将在dispatch中执行,这个方法在createStore中定义,下面是它的源码(里面很多英文注释,其实已经写的非常清楚了):

// 第一个参数reducer在这个例子里就是combineReducers执行结果,即combination函数
export default function createStore(reducer, preloadedState, enhancer) {
  // 进行一些参数转化,比如preloadedState缺失,只传入了enhancer的情况
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    // 如果有传入合法的enhance,则通过enhancer再调用一次createStore
    // 比如在分析applyMiddleware时会用到
    return enhancer(createStore)(reducer, preloadedState)
  }

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 暴露出获取全局state的方法
  function getState() {
    return currentState
  }

  /**
   * Adds a change listener. It will be called any time an action is dispatched,
   * and some part of the state tree may potentially have changed. You may then
   * call `getState()` to read the current state tree inside the callback.
   *
   * You may call `dispatch()` from a change listener, with the following
   * caveats:
   *
   * 1\. The subscriptions are snapshotted just before every `dispatch()` call.
   * If you subscribe or unsubscribe while the listeners are being invoked, this
   * will not have any effect on the `dispatch()` that is currently in progress.
   * However, the next `dispatch()` call, whether nested or not, will use a more
   * recent snapshot of the subscription list.
   *
   * 2\. The listener should not expect to see all state changes, as the state
   * might have been updated multiple times during a nested `dispatch()` before
   * the listener is called. It is, however, guaranteed that all subscribers
   * registered before the `dispatch()` started will be called with the latest
   * state by the time it exits.
   *
   * @param {Function} listener A callback to be invoked on every dispatch.
   * @returns {Function} A function to remove this change listener.
   */
   // 配合react-redux的话这里的listener传入的就是onStateChange
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

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

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  /**
   * Dispatches an action. It is the only way to trigger a state change.
   *
   * The `reducer` function, used to create the store, will be called with the
   * current state tree and the given `action`. Its return value will
   * be considered the **next** state of the tree, and the change listeners
   * will be notified.
   *
   * The base implementation only supports plain object actions. If you want to
   * dispatch a Promise, an Observable, a thunk, or something else, you need to
   * wrap your store creating function into the corresponding middleware. For
   * example, see the documentation for the `redux-thunk` package. Even the
   * middleware will eventually dispatch plain object actions using this method.
   *
   * @param {Object} action A plain object representing “what changed”. It is
   * a good idea to keep actions serializable so you can record and replay user
   * sessions, or use the time travelling `redux-devtools`. An action must have
   * a `type` property which may not be `undefined`. It is a good idea to use
   * string constants for action types.
   *
   * @returns {Object} For convenience, the same action object you dispatched.
   *
   * Note that, if you use a custom middleware, it may wrap `dispatch()` to
   * return something else (for example, a Promise you can await).
   */
  function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
        'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
        'Have you misspelled a constant?'
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      // 将在这里执行传入的reducer,
      // 在这个例子中reducer就是上面提到combination函数,可以看到传入的参数是当前整个state和action
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = currentListeners = nextListeners
    // 执行订阅的回调
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }


  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates
  // the initial state tree.
  // 英文注释很清楚了,不解释了
  dispatch({ type: ActionTypes.INIT })
  // 返回的就是一个store对象,即上一篇一直提到的store
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

总结:

  1. createStore调用完成后返回一个store对象,上面挂了一些需要暴露的方法,常用的dispatch,subscribe,getState
  2. 在我们调用dispatch的时候会对传入的action做检测,存在action.type属性时会调用所有reducer function,更新state,并且调用所有listener
  3. subscribe提供一个订阅接口,结合react-redux时,connect函数内部帮我们做了订阅这件事
  4. 在调用createStore的最后会自动dispatch一个特殊的action来确保每个reducer都有默认的输出

关于applyMiddleware的实现,这篇文章Redux入坑进阶-源码解析讲的不错,就不总结了。可能比较难懂的部分就是函数currying和compose这些属于函数式编程中的**。重点就是利用compose将所有中间件串起来,比如compose(f, g, h)将返回(...args) => f(g(h(...args)))。并且返回的dispatch也与普通的(不传入applyMiddleware)不同,经过applyMiddleware改造的dispatch在调用的时候就是在调用f(g(h(...args))),由此实现了middleware的功能:

它提供了一个分类处理action的机会,在middleware中,你可以检阅每一个流过的action,挑出特定类型的action进行相应的操作,给你一次改变action的机会。

小结

Redux的实现这块基本研究完了,虽然文档中多次强调没有黑魔法,但我记得在我刚上手使用的的时候总有种奇怪的感觉,处处都想log一下看看这里传入的是什么,根本没有感受到什么数据流更加清晰这种事。但看完源码整理了它的工作流程后再写起代码感觉好多了,基本每一行代码我都知道它在执行时会做哪些事,改变哪些东西。

学习前端一年都学到了什么

学习前端一年都学到了什么

标题叫学习前端一年,但准确的说是一年零两个月,因为刚好从去年暑假开始接触、学习前端,其实这篇文章早就打算写,但一直忙着实习(其实是懒)没有去思考和总结。但最近正好实习快结束,主管也让我写一篇实习期总结,所以我也借此思考一下一年都学到了什么,有哪些不足,一下年有什么打算。准备主要分为三个部分:

关于学习态度

关于自己的一年里的进步与成长,第一个想到的就是学习态度。态度两个字其实包含很多可能,在我这里更多的是兴趣。记得去年暑假刚开始那会我刚把常用html元素名称记住,常用css属性都没记住还要查文档,js就会基本语法,看红宝书讲this,原型继承这些还是稀里糊涂的,不知道这些有啥用。当时跟着把百度前端学院的几个task一点一点完成,其中有一个task是写一个类似于todo list的工作计划管理页我印象非常深刻,当时居然用动态生成dom去拼接字符串的方法把这个需求完成的差不多了,现在想想自己当时真是在瞎搞。再看一下一年后的现在,我觉得目前的水平处理一般的业务基本没有问题,react,redux,vue,vuex,webpack,babel,node,koa,ES6&7&8,pwa,数据流,组件化,异步,模块化,函数式,响应式,前端真是随便一列就是一堆关键词(重点还都是近几年出现&流行的),由这个关键词又能展开扯一堆,各种库、工具、概念基本都熟悉或者了解。其实进步的不仅仅是前端,更重要的是把计算机相关的核心学科也补了一遍,数据结构,计算机网络,操作系统等。这些进步的取得很大程度在于学习态度,其实我一开始接触计算机纯粹是看这个行业工资高,学习周期又比电气短,既然可以花更少的时间赚更多的钱我为什么不学这个,后来学了之后才发现这个确实很适合我,刚开始是学的Android应用开发,好像基本上是从上午9点搞到晚上10点吧,记得当时学习效率真是很低,经常要花一天时间才能搞懂一个原生组件怎么用,现在想来当时自己没有打好java的基础就开始直接学Android是错误的,浪费了很多时间。不过也有可能这是初学者的必经之路,毕竟成长不是一蹴而就的,现在我如果再来学习一个我没接触过的东西,比如ios开发,我觉得语言也好,开发方式也好,我都可以很快上手,因为编程这个东西是相通的,从语言的角度来看大概就是各种语言特性相互抄抄所以学起来都挺快的(Haskell例外……真心看了语法就想放弃的语言……);从开发模式来看也是相通的,核心就是抽象、低耦合高内聚;从很多概念来看也是相通的,基本都是抄的操作系统,比如多线程,异步,模块,抽象这些操作系统里早就有了。我觉得坚持一年半的不断学习的结果就是现在已经把学习当成一种习惯,很难让自己什么也不学的度过一天(刷知乎也算),我并不认为这是个好习惯。
这段写的比较乱,基本想到什么就写什么,其实我想表达的就是一个人的学习态度决定着他的成长。

关于实习收获

很幸运自己的第一份工作就遇到一个好老板,好团队。虽然我没有待过其他团队,很难有个对比,但我的理解是能让人快乐工作的团队就是个好团队。收获可以列出以下几点:

  • 前端工程化

未实习之前,我所理解的前端工程化仅仅局限于组件化,模块化,打包构建这些。现在才发现工程化还可以包括很多东西,如项目开发规范,自动化部署,监控,持续集成,前端埋点等等。尤其是自动化构建、部署,自己以前都是手动上传新资源到服务器,或是在服务器上从github拉取新资源,手动操作总是麻烦的,虽然也听说过一些持续集成/持续发布的工具,如jenkins但没有亲自使用过。现在通过gitlab hook或是一些内部工具进行前端静态资源发布不仅简化了很多操作而且可以有效避免出错。
工程化是一个很大的话题,可以延伸到软件工程这个经典话题,之前也翻过这方面的书,感受是没有什么实际工作经验就去看这种东西那真的可以当成玄学来看了,如今有了实践之后对软件工程又多了一些理解,计划实习结束回学校再看看这方面的书。

  • 持续迭代

因为实习期间做的项目有好几个是基于原有项目开发新功能,所以对于持续迭代,如何修改不熟悉的代码,快速实现新需求这方面的能力也有所提高。一个团队人员的变动是不可避免的,所以基于别人代码快速实现新需求是一种必备技能,关于这个我觉得更多的是一个经验的问题,写的代码多了,看的代码多了,自然能很快定位到我需要动哪块代码,比如我现在面对这种需求已经不像一开始那样感到比较混乱了。当然持续迭代还有另一种要求,自己设计的代码要具有可扩展性,规范的目录结构等。可扩展性在前端一般就是组件化,模块化,其实就是抽象。关于这一点我还有个比较深刻的体会,就是不管一开始多简单的项目都要学会抽象、模块化,不能只为快速实现当前需求就简单的把代码都耦合到一起,要考虑到项目的后续迭代,而且往往还要经过其他人修改,迭代次数多了估计就面目全非了吧,后续的维护者(比如我)看到这样的代码一般就按原有代码风格写了,很难提起心情进行优化重构,这样就是一个恶性循环。当然关于这一点还需要把握好度,也不能过度设计。

  • 团队协作

因为之前一直是自己独立开发一些demo级别的项目,前后端都自己写,想怎么写就怎么写,对于团队协作模式基本是一无所知。如今经过一两个完整项目之后对于团队协作开发有了比较全面的认识,比如一个项目周期:PRD评审,交互评审,进入开发,前后端联调自测,测试评审,提测并修复bug,发布上线,各个阶段是干嘛的,作为前端在这些阶段要发挥哪些作用,现在已经基本明白。

  • 阿里味

没有工作之前就明白企业文化、价值观的重要性,但亲身体验过后才会明白它为什么重要,能给企业带来什么,能给我们个人带来什么。记得刚来的第一天就非常惊讶,这里的后勤工作人员永远都这么的有礼貌,技术人员从我接触到的其他实习生和团队成员来看,每一位都是非常优秀的人,聪明、乐观、皮实、自省是阿里的人才观,这也是每一个阿里人所拥有的品质。最近在看《浪潮之巅》这本书,里面讲述的一个个企业的兴衰,没有一家能一直兴盛,在一个时代处在浪潮之巅,下一个时代不抓住机会可能就由此衰败,其中企业的基因、文化起着很重要的作用。企业的文化和价值观,短短两个月我只是有了一些初步的认识,这是一个需要不断感受、思考的东西。

最后,我觉得我是一个不善于表达感谢的人,但其实在心里我是十分感谢在实习期间每一个帮助过我的人。

关于未来

很早之前就没有那种快速成长的感觉了,现在的进步是缓慢的,如果每天都做一个总结会发现今天好像什么也没学到,但如果一个月、两个月再来回顾一下,其实发现还是学到了不少东西的。在这种情况下我觉得可以做一些知识的沉淀,读一些经典计算机书籍,尤其实习两个月之后我更加感觉到经典的书籍一定要在大学去读,因为工作后很难有这个心情去读经(枯)典(燥)书籍了。而幸好我的大学生活还没有结束,我希望接下来利用在学校的时间去读一些书,暂定的计划有SICP,软件工程相关的书,这是短期的计划。至于长期的计划就比较难了,究竟是走业务路线还是技术路线?是专注前端还是转向其他方向?这涉及到我现在不断思考的一个问题:我的竞争力在哪?如何不那么容易被人替代?刚开始我的答案是我几年后肯定要转行,因为前端的水还是太浅,或者说比较深的地方我们做业务的很难接触到。现在我的答案是希望自己能一直保持一颗学习的心,其实这是一个显而易见的答案,能保持一直学习的人未来的成就一定不会太低,但做到怕是很难的,这便是IT行业与传统行业的不同吧,不过既然已经做出了选择就走到底吧,无需多想。

期待一年后的自己

JSBridge总结

在native调用js方法

Android:

webview.getSettings().setJavaScriptEnabled(true);
// 需要webview加载完成调用
// 格式javascript:jsMethodName
// 相当于在页面控制台(console)直接调用jsMethodName()
webview.loadUrl("javascript:jsFuc()"); // some message

HTML:

<script>
  function jsFuc(){
    alert('some message')
  }
</script>

在js中调用native方法

主要有两种,注入API和拦截URL SCHEME

注入API(addJavascriptInterface)

注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。

Android:

class JSInterface {  
    @JavascriptInterface
    public void callNative() {
        // call native method
    }
}
// 相当与在window对象上挂了JSBridge对象
// 通过JSBridge.callNative()调用native方法
webView.addJavascriptInterface(new JSInterface(), "JSBridge");

HTML:

<script>
  JSBridge.callNative() 
</script>

在4.2以前addJavascriptInterface有漏洞,所以4.2以上引入注解@JavascriptInterface来解决,对于4.2一下版本可以使用拦截prompt实现:

Android:

class myClient extends WebChromeClient{
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 接收到js中的prompt中的参数,根据参数这里可以调用相应的native方法
        Log.d("TAG", message); // ok
        result.confirm();
        return true;
    }
}
webview.setWebChromeClient(new myClient());

HTML:

<script>
  prompt('ok')
</script>

拦截URL SCHEME

拦截 URL SCHEME 的主要流程是:Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作。

优点是可以兼容IOS6,所以现在基本可以忽略这种方法了

HTML:

<script>
  let iframe = document.createElement('iframe');
  iframe.src = 'jsbridge://namespace.method?[...args]';
</script>

Android:

webview.setWebViewClient(new WebViewClient() {
  @Override
  public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
      return super.shouldInterceptRequest(view, request);
  }
});

总结

Hybrid中的JSBridge就是这几种,实际生产环境还需要进行封装,定义传参格式等等,但基本原理是不变的。更加详细的内容:移动混合开发中的 JSBridge

关于React Native或Weex中的JSBridge,还是比较复杂的,因为它们都将html页面映射成了原生组件,不在基于webview提供的那几个API。它们是通过JNI,让C++作为一个中间层,实现Java与JS的绑定。这里有几篇详细的文章:Weex SDK Android 源码解析React Native Android版核心层js-bridge实现浅析

vue插件机制及模板渲染

接触Vue也有一段时间了,Demo也写过几个,个人还是比较喜欢Vue的。记得当时用的时候就对三个问题比较感兴趣——1.响应式原理;2.插件如何工作;3.组件的实现。其中关于探究响应式原理的文章网上不少,包括模仿Vue基于Object.defineProperty实现数据绑定的简单实现也有不少,比如这几个

不过这两个的Compiler部分都是基于1.x的,2.x引入了虚拟DOM,关于这个的整个流程,下面会有简单总结。至于第二问题,大概搜了一下,估计是太简单了(弄清楚之后发现确实不复杂,就是在生命周期中去调用自定义的插件,复杂的是整个生命周期流程)没什么人写这方面的东西,只好对着文档源码(2.0.0版的)自己去看看了。

Vue插件机制

使用插件前要调用Vue.use(MyPlugin),这就相当于调用MyPlugin.install(Vue)

Vue.use = function (plugin: Function | Object) {
   /* istanbul ignore if */
   if (plugin.installed) {
     return
   }
   // additional parameters
   const args = toArray(arguments, 1)
   args.unshift(this) //使第一个参数变为Vue,剩下的直接传过去
   if (typeof plugin.install === 'function') {
     plugin.install.apply(plugin, args)
   } else {
     plugin.apply(null, args)
   }
   plugin.installed = true
   return this
 }

所以插件都要有install方法。

文档举了这几种开发插件的方式

MyPlugin.install = function (Vue, options) {
  // 1. 添加全局方法或属性
  Vue.myGlobalMethod = function () {
    // 逻辑...
  }
  // 2. 添加全局资源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 逻辑...
    }
    ...
  })
  // 3. 注入组件
  Vue.mixin({
    created: function () {
      // 逻辑...
    }
    ...
  })
  // 4. 添加实例方法
  Vue.prototype.$myMethod = function (options) {
    // 逻辑...
  }
}

第一种和第四种比较简单,直接把方法挂在Vue或是Vue原型上,作为全局方法或实例方法,和jQuery差不多(不过jQuery的extend方法还有其他功能),在vue里似乎用的很少。第三种通常通过全局mixin方法在vue生命周期中注入一些初始化插件的代码,如vuex。

Vue.mixin = function (mixin: Object) {
    Vue.options = mergeOptions(Vue.options, mixin)
}

可以看到mixin方法可以传入一个对象,如传入{ beforeCreate: vuexInit },则经过mergeOptions后会将vuexInit函数与vue的beforeCreate生命周期钩子函数关联起来,mergeOptions并不是简单的替换,因为那样会覆盖原来的钩子函数,对于混合对象与组件存在同名生命周期方法时,vue会将他们都保存在一个数组中,并且混合对象的方法会先执行。例如vuex是这么用的:

  const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
  //1.0用钩子函数init,2.0用beforeCreate
  Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }

这样Vue生命周期走到beforeCreate的时候就会调用vuexInit,完成$store的挂载(vue-router也有类似的操作)。

感觉功能最强大用得最多的是第二种,也就是自定义指令,很多组件库都带有一些自定义指令,之前我自己也写了个Demo,关于图片懒加载指令的,指令用起来确实很方便。

调用Vue.directive()的时候仅仅把自定义指令添加到Vue.options.directives上,这样后面生成vnode的过程中将所有的自定义指令转换成一个对象才能顺利处理,然后根据生命周期调用相应的钩子函数。

关于全局方法的初始化,如directive,component包括之前的use,mixin都在 src/core/global-api 下

_assetTypes: [
  'component',
  'directive',
  'filter'
]

config._assetTypes.forEach(type => {
  Vue[type] = function (
    id: string,
    definition: Function | Object
  ): Function | Object | void {
    if (!definition) {
      return this.options[type + 's'][id]
    } else {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production') {
        if (type === 'component' && config.isReservedTag(id)) {
          warn(
            'Do not use built-in or reserved HTML elements as component ' +
            'id: ' + id
          )
        }
      }
      if (type === 'component' && isPlainObject(definition)) {
        definition.name = definition.name || id
        definition = this.options._base.extend(definition)
      }
      if (type === 'directive' && typeof definition === 'function') {
        definition = { bind: definition, update: definition }
      }
      this.options[type + 's'][id] = definition //这里
      return definition
    }
  }
})

模板到DOM大致流程

关于模板到真正DOM的大致流程,写了个demo跑了一下,大概是这样的,首先template模板经过parse处理后返回一棵AST,即

/**
 * Convert HTML string to AST.
 */
export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  //....省略大量代码,具体实现在src/compiler/parser 
}

获得一棵AST后再经过generate()生成渲染函数

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): {
  render: string,
  staticRenderFns: Array<string>
} {
  //...省略,在src/compiler/codegen/index.js
}

执行渲染函数后会获得一个VNode,即虚拟DOM,然后把它交给patch函数,负责把虚拟DOM变为真正DOM。

例如在chrome打断点跑一下这个模板

<body>
<div id="app">
    <h1 v-test="message"></h1>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!'
        }
    });
</script>
</body>

parse后返回
screenshot from 2017-01-14 21-12-41

generate后返回

return {
  // 渲染函数,执行后生成vdom
    render: ("with(this){return " + code + "}"), //code =_c('div',{attrs:{"id":"app"}},[_c('h1',{directives:[{name:"test",rawName:"v-test",value:(message),expression:"message"}]})])
    staticRenderFns: currentStaticRenderFns
}

执行渲染函数后生成的VNode
screenshot from 2017-01-15 22-04-39

然后经过patch变成真正DOM,期间还有个很重要的Wather,用来收集依赖,数据发生变动时触发diff,这块的原理基本和vue1.x没什么变化。

小结

其实关于Vue的实现过程我现在只想了解个大概,对整个设计思路有个印象,这样选择其他库 框架的时候也有个比较。而且毕竟水平有限,全部看源码太费力又没那么多时间。

参考资料

记一次webpack配置过程

虽说知道webpack这个东西已经挺久了,但从来没有自己动手配置过,因为一般都是用的脚手架工具(vue-cli)。而且前几天刷2017百度ife的时候,发现习惯了ES6的import后再回到普通的开发方式,引一堆script标签很不适应(而且修改代码后没有热更新)。所以就准备自己搞个模板,这样以后写一些demo的时候就可以直接用模板生成,省的每次都要配webpack。但后来想每次还要从github clone 模板,还是不爽啊,干脆写个简单的脚手架发布到npm上,这样就和vue-cli基础用法差不多了,比如全局安装了脚手架之后直接执行

cyril init

就可以在当前目录生成一个配置好的项目,然后就可以愉快的开发了。关于这个"简陋的"脚手架的更多信息点这里,我配的模板在这里

下面进入主题(基于webpack2)

新建一个配置文件webpack.config.js

// webpack运行时可以提供参数,比如区分是生产环境还是开发环境
module.exports = (options = {}) => {
  return {
    // 在这里写配置
  }
}

配置中比较常见的就是entry(入口文件,webpack会从入口文件开始分析,根据规则选择loader,打包所有依赖),output(输出文件),rules(一些匹配规则),plugins(用到的插件),具体内容如下

entry: {
    main: './src/index.js',
},
output: {
   // 开发环境的话就没必要用chunk了,直接输出默认名字,
    // 生产环境打包时webpack会给文件名加上一段hash,代码改动hash值就会变
    filename: options.dev ? '[name].js' : '[chunkhash].[name].js',
    // 打包到dist目录下
    path: path.resolve(__dirname, 'dist')
},
module: {
    // 配置一些loader
    rules: [
        {
            test: /\.js$/,
            // babel转码时把node_modules中的js排除
            exclude: /node_modules/,
            use: [{
                loader: 'babel-loader',
                // babel配置
                options: {
                    // 暂时只配置转码ES6,并且将modules设为false防止babel将ES6的import转成CommonJS,
                    //因为webpack2是可以自己处理import的
                    presets: [['es2015', {modules: false}]],
                    // 用了几个babel插件,为了使用async/await
                    plugins: [
                        'transform-async-to-generator',
                        'transform-regenerator',
                        'transform-runtime'
                    ]
                }
            }]
        },
        {
            test: /\.html$/,
            use: 'html-loader'
        },
        {
            test: /\.css$/,
            // 将CSS分离打包
            use: ExtractTextPlugin.extract({
                use: 'css-loader'
            })

        },

        {
            test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/,
            use: [
                {
                    loader: 'url-loader',
                    options: {
                        limit: 10000
                    }
                }
            ]
        }
    ]
},
// 在这里注册用到的插件
plugins: [
    // 分离CSS文件
    new ExtractTextPlugin('styles.css'),
    // 处理html的插件,会根据打包结果自动生成script,style标签,
    new HtmlWebpackPlugin({
        template: './index.html'
    }),
    // 以下这段代码直接从vue-cli复制来的,将依赖库和自己开发的文件分开打包,
   // 因为如果打包到一起的话每次本地代码有变动都会重新打包生成新的hash文件名,
    // 这样无法利用浏览器缓存,而其实依赖库的代码一般是没有变动的,分开打包可以利用浏览器缓存
    // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
        name: 'vendor',
        minChunks: function (module, count) {
            // any required modules inside node_modules are extracted to vendor
            return (
                module.resource &&
                /\.js$/.test(module.resource) &&
                module.resource.indexOf(
                    path.join(__dirname, './node_modules')
                ) === 0
            )
        }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    new webpack.optimize.CommonsChunkPlugin({
        name: 'manifest',
        chunks: ['vendor']
    })
],
// webpack-dev-server的配置
devServer: {
    // 配置本地监听端口
    port: 3000,
    historyApiFallback: true,
    // 在这里可以配置反向代理
    proxy: {
        '/api': {
            target: 'http://localhost:3001',
            secure: false,
            changeOrigin: true,
        }
    }
}

最后还需要改下package.json,使npm run build/dev工作和安装依赖

"devDependencies": {
  "webpack": "^2.2.1",
  "babel-core": "^6.23.1",
  "babel-loader": "^6.3.2",
  "babel-plugin-syntax-dynamic-import": "^6.18.0",
  "babel-plugin-transform-async-to-generator": "^6.22.0",
  "babel-plugin-transform-regenerator": "^6.22.0",
  "babel-plugin-transform-runtime": "^6.23.0",
  "babel-preset-es2015": "^6.22.0",
  "css-loader": "^0.26.2",
  "extract-text-webpack-plugin": "^2.0.0",
  "html-loader": "^0.4.5",
  "html-webpack-plugin": "^2.28.0",
  "rimraf": "^2.6.1",
  "webpack-dev-server": "^2.4.1"
},
"scripts": {
  "build": "rimraf dist && webpack -p",
  "dev": "webpack-dev-server -d --hot --env.dev" 
},

小结

这只是个我为了平时写demo配的webpack,结果就花了我一下午看文档,查资料……虽说也没遇到什么大坑。简单看了一下vuejs-templates的配置,还是挺复杂了。感觉没有做过比较大型的项目,自己对前端工程化这块的理解还是比较浅的。keep learning……

实现自定义事件

一个比较常见的功能,非常有利于代码解耦,直接上代码。

实现的接口

  • on(eventName,handler),绑定事件
  • emit(eventName,arguments),触发事件
  • once(eventName,handler),绑定只执行一次的事件
  • off(eventName),解除事件绑定

具体实现

;(function (window) {
     function EventProxy() {
        this.handlers = {} // 存放所有添加的事件
    }

    EventProxy.prototype = {
        on: function (eventName, handler) {
            this.handlers[eventName] = this.handlers[eventName] || [];
            this.handlers[eventName].push(handler);
        },
        once: function (eventName, handler) {
            var self = this
            // 改写原回调,使执行一次后删除
            var wrapper = function () {
                handler.apply(self, arguments);
                self.off(eventName, wrapper);
            };
            this.on(eventName, wrapper)
        },
        emit: function (eventName) {
            var args = Array.prototype.slice.call(arguments, 1) // 取出传入的参数
            for (var i = 0,len = this.handlers[eventName].length; i < len; i++) {
                this.handlers[eventName][i].apply(this, args);
            }
        },
        off: function (eventName, handler) {
            var handlers = this.handlers[eventName],i
            if (handlers && handler) {
                i = handlers.indexOf(handler)
                handlers.splice(i, 1)
            }else {
                this.handlers[eventName] = []
            }
        }
    }   

    window.EventProxy =  EventProxy
})(window)

测试

var ep = new EventProxy ()
ep.on("event1",function () {
    console.log("event1 emit 1")
})

ep.emit("event1")  // event1 emit 1

ep.on("event1",function (value) {
    console.log("event1 emit " + value)
})

ep.emit("event1","一些参数") // event1 emit 1
                            // event1 emit 一些参数

ep.once("event2",function (value) {
    console.log("event2 emit once " + value)
})

ep.emit("event2", "一些参数") // event2 emit once 一些参数
ep.emit("event2", "一些参数") 
ep.off("event1")
ep.emit("event1")  

其他需求

回调函数的this指向可以通过bind指定

……

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.