logo

Stay Hungry. Stay Foolish.

vue源码阅读之响应式原理

Vue 中对数据的响应式,即对 propsdatacomputed 的变化进行响应式更改。

其入口是在 src/core/instance/init.js 里的 initState(vm) 里进行实现的。

initState(vm) 里 初始化了 props, methods, data, computed, watch

initData

initData 入手来看下:

function initData (vm: Component) {
  let data = vm.$options.data
  // 将_data作为中间变量,后续 proxy 里会用到
  data = vm._data = typeof data === 'function'
    ? getData(data, vm) // getData 做了错误处理,并控制了 Dep.target,使得避免在获取 data 初始值的过程中意外地把依赖记录下来。
    : data || {}
  // 省略相关 warn 代码...
  // 进行 data proxy methods 重复 key 的检验并抛出 warn msg
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    // 省略相关关于重复 key 检测 warn 代码
    // 对 _data 对象上的每个 key 进行 getter setter 设置
    // 使得访问 this.xxx 代理到 this._data.xxx
    proxy(vm, `_data`, key)
  }
  // observe data
  /**
   * 响应式核心方法
   * 即 new Observer(value)
   * 具体看 new Observer(value) 逻辑
   * */
  observe(data, true /* asRootData */)
}

initData 里首先将原始 data 复制了份 _data,然后遍历了每个 key, 通过 proxy 方法对 _data 里的 key 进行了 getter setter 设置。

使得访问 this.xxx 代理到 this._data.xxx。

接下来执行 observe(data, true /* asRootData */)

该方法结果是 return new Observer(value),返回了一个 Observer 实例。

Observer

来看下 Observer 类的定义,这也是响应式实现的主要地方。

class Observer 源码
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 添加 __ob__ 属性,值为自身,即 this.data.__ob__ = this
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 对数组原生方法进行了劫持,使得push 等能监控到,来触发响应式,但无法监控到 通过下标更改的情况
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 对数组进行遍历执行 observe
      this.observeArray(value)
    } else {
      // 会进入 walk 方法
      this.walk(value)
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 通过 defineReactive 方法,设置 getter setter, 真正的响应代码
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

Observer 在 new 的时候,主要逻辑会先执行 this.dep = new Dep() 来创建依赖实例,然后处理数组/对象的情况。

这中间还对里面的每个元素进行了深度遍历,使得每个元素均是响应式的。

再主要来看 this.walk(value) 方法,该方法通过 defineReactive 方法,设置 getter setter,这是真正的响应代码。

defineReactive 源码
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 缓存依赖
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 这里代码类比 computed 里属性定义的 getter setter 那样
  // 定义了,就用定义的 getter setter 方法
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // childObserve 对象,对 child 进行监听
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 这步主要收集依赖,并返回值
    get: function reactiveGetter () {
      // 兼容使用定义的 getter 情况
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    // 这步触发依赖并设置值
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      // 自己定义了 setter 就调用自己的 setter, 没有就直接赋值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // newVal 是个 object,则继续递归执行监听 observe
      childOb = !shallow && observe(newVal)
      // 触发更新
      dep.notify()
    }
  })
}

defineReactive 代码可以简化如下:

_defineReactive (obj, key, val) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: true,
    get: function reactiveGetter() {
      dep.addSub(Dep.target)
      return val
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value) return
      // 自己定义了 setter 就调用自己的 setter, 没有就直接赋值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      dep.notify()
    }
  })
}

defineReactive 函数内,通过闭包实例化了 Dep 这个订阅者。

Dep

class Dep 源码
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      // Dep.target 即 watcher:watcher.addDep(dep)
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
// 保存当前全局唯一存在的 watcher
Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

Dep 这里可以简化为:

class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

Dep.target = null

Dep 这个依赖订阅者,有自己的 subs 用来缓存 Dep.targetDep.target 其实是一个 watcher 实例,这里先不管 watcher。 回到 defineReactive,在 get 里将 watcher 实例添加到订阅者 Dep 里, 在 set 时遍历缓存在 dep.subs 数组里的 watcher 实例,执行 watcher.update() 方法,来触发更新。

Watcher

来看下 class Watcher

class Watcher 源码
  export default class Watcher {
    vm: Component; // 实例自身
    expression: string;
    cb: Function;
    id: number;
    deep: boolean;
    user: boolean;
    lazy: boolean;
    sync: boolean;
    dirty: boolean;
    active: boolean;
    deps: Array<Dep>;
    newDeps: Array<Dep>;
    depIds: SimpleSet; // ES6 set 类型
    newDepIds: SimpleSet; // ES6 set 类型
    before: ?Function;
    getter: Function;
    value: any;

    constructor (
      vm: Component,
      expOrFn: string | Function, // 表达式本身 [ getter | noop ]
      cb: Function, // [ noop ]
      options?: ?Object, // { lazy: true } // 如果设置为 true 则在第一次 get 的时候才计算值,初始化的时候并不计算。init 时为 true
      isRenderWatcher?: boolean
    ) {
      this.vm = vm
      if (isRenderWatcher) {
        vm._watcher = this
      }
      vm._watchers.push(this)
      // options
      if (options) {
        this.deep = !!options.deep
        this.user = !!options.user
        this.lazy = !!options.lazy
        this.sync = !!options.sync
        /*
        before作用是定义 beforeUpdate 钩子:
        「
          before () {
              if (vm._isMounted) {
                callHook(vm, 'beforeUpdate')
              }
            }
          」
        */
        this.before = options.before
      } else {
        this.deep = this.user = this.lazy = this.sync = false
      }
      this.cb = cb
      this.id = ++uid // uid for batching
      this.active = true
      this.dirty = this.lazy // for lazy watchers
      // 两个数组,
      this.deps = []
      this.newDeps = []
      // 两个id 为 set 实例,在 add 时候,防止重复添加相同 id
      this.depIds = new Set()
      this.newDepIds = new Set()
      this.expression = process.env.NODE_ENV !== 'production'
        ? expOrFn.toString()
        : ''
      // parse expression for getter
      if (typeof expOrFn === 'function') {
        this.getter = expOrFn
      } else {
        this.getter = parsePath(expOrFn)
        if (!this.getter) {
          this.getter = noop
          process.env.NODE_ENV !== 'production' && warn(
            `Failed watching path: "${expOrFn}" ` +
            'Watcher only accepts simple dot-delimited paths. ' +
            'For full control, use a function instead.',
            vm
          )
        }
      }
      this.value = this.lazy
        ? undefined
        : this.get()
    }

    /**
     * Evaluate the getter, and re-collect dependencies.
     */
    // 执行 getter, 并重新收集依赖关系
    get () {
      /* pushTarget 代码:
      「
          Dep.target = null
          const targetStack = []

          export function pushTarget (_target: ?Watcher) {
            if (Dep.target) targetStack.push(Dep.target)
            Dep.target = _target
          }

          export function popTarget () {
            Dep.target = targetStack.pop()
          }
        」
      */
      // 这行代码作用:在进行 get 取值时,使得 Dep.target 为 watcher 实例自身
      pushTarget(this)
      let value
      const vm = this.vm
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {
        if (this.user) {
          handleError(e, vm, `getter for watcher "${this.expression}"`)
        } else {
          throw e
        }
      } finally {
        // "touch" every property so they are all tracked as
        // dependencies for deep watching
        if (this.deep) {
          traverse(value)
        }
        popTarget()
        this.cleanupDeps()
      }
      return value
    }

    /**
     * Add a dependency to this directive.
     */
    addDep (dep: Dep) {
      const id = dep.id
      // if (!this.newDepIds.has(id)) 判断,为了防止如 computed 里
      // `return this.a + this.a` 这种相同值的计算情况
      if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id)
        this.newDeps.push(dep)
        if (!this.depIds.has(id)) {
          dep.addSub(this)
        }
      }
    }

    /**
     * Clean up for dependency collection.
     */
    cleanupDeps () {
      let i = this.deps.length
      while (i--) {
        const dep = this.deps[i]
        if (!this.newDepIds.has(dep.id)) {
          dep.removeSub(this)
        }
      }
      let tmp = this.depIds
      this.depIds = this.newDepIds
      this.newDepIds = tmp
      this.newDepIds.clear()
      tmp = this.deps
      this.deps = this.newDeps
      this.newDeps = tmp
      this.newDeps.length = 0
    }

    /**
     * Subscriber interface.
     * Will be called when a dependency changes.
     */
    update () {
      /* istanbul ignore else */
      if (this.lazy) {
        this.dirty = true
      } else if (this.sync) {
        this.run()
      } else {
        queueWatcher(this)
      }
    }

    /**
     * Scheduler job interface.
     * Will be called by the scheduler.
     */
    run () {
      if (this.active) {
        const value = this.get()
        if (
          value !== this.value ||
          // Deep watchers and watchers on Object/Arrays should fire even
          // when the value is the same, because the value may
          // have mutated.
          isObject(value) ||
          this.deep
        ) {
          // set new value
          const oldValue = this.value
          this.value = value
          if (this.user) {
            try {
              this.cb.call(this.vm, value, oldValue)
            } catch (e) {
              handleError(e, this.vm, `callback for watcher "${this.expression}"`)
            }
          } else {
            this.cb.call(this.vm, value, oldValue)
          }
        }
      }
    }

    /**
     * Evaluate the value of the watcher.
     * This only gets called for lazy watchers.
     */
    evaluate () {
      this.value = this.get()
      this.dirty = false
    }

    /**
     * Depend on all deps collected by this watcher.
     */
    depend () {
      let i = this.deps.length
      while (i--) {
        this.deps[i].depend()
      }
    }

    /**
     * Remove self from all dependencies' subscriber list.
     */
    teardown () {
      if (this.active) {
        // remove self from vm's watcher list
        // this is a somewhat expensive operation so we skip it
        // if the vm is being destroyed.
        if (!this.vm._isBeingDestroyed) {
          remove(this.vm._watchers, this)
        }
        let i = this.deps.length
        while (i--) {
          this.deps[i].removeSub(this)
        }
        this.active = false
      }
    }
  }

上面说到在 set 某个值的时候,实际上会执行 watcher.update() 方法。来看下这个 update 方法:

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

update 方法,同步情况执行 this.run() ,一般情况是异步,会进入到最后个 else 里,执行 queueWatcher(this)

queueWatcher 里主要代码 nextTick(flushSchedulerQueue),它将 watch 队列放到 nextTick 里统一执行,这也使得连续 set 同个数据两次不同值,不会更新两次视图,提升了一定性能。

computed 的原理

注意 watcher 实例化 constructor 最后行有个 this.value:

this.value = this.lazy
  ? undefined
  : this.get()

这个 value,在 initComputed 的时候,会对 computed 里的每个 key new Watcher(...),所以可以说是 computed 的双向绑定实现即是通过 Watcher 来响应变化。

initComputed 源码函数:
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  // 遍历定义在 computed 上的 key
  for (const key in computed) {
    const userDef = computed[key]
    // 处理自定义 getter 情况
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }
    // 非 ssr 情况,对 key 进行 监控,ssr 下就不需要监控了
    if (!isSSR) {
      // create internal watcher for the computed property.
      // 对每个key 设置 Watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      // 将 computed 里定义的 key 在实例上添加进行代理,使得可直接通过 this.xx 来访问
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

从上面传入参数 { lazy: true } 可知,初始化时 value 为 undefineddefineComputed(vm, key, userDef)Object.defineProperty(target, key, sharedPropertyDefinition). 在 sharedPropertyDefinition 里会拿之前 watcher 过的依赖的值:

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

所以 computed 中的值在读取的时候,拿到的结果即 watcher.value。

总结下 data 响应式流程:

this.xx 代理到 this._data.xx 然后 _data 在 Observer 里定义过 setter getter,在初始 render 时会收集依赖到闭包的 Dep 依赖实例的数组里,在 setter 时会通知所有依赖即每个 watcher 实例进行 update 更新。

总结下 computed 响应式流程:

this.xx 初始化时会先创建一个 watcher,然后通过 Object.defineProperty 进行 setter,getter 设置,其中 getter 会得到是 watcher.value 的值。

注意:

Observer 文件下定义了个 arrayMethods,该方法重写了数组方法,使得改变 push 等操作也能触发更新。

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

附: 实现vue精简版响应式代码

阅读全文

vue源码阅读之初始化过程

import阶段

从打包入口文件入手,scripts/config.js, ➡️ resolve('web/entry-runtime.js') ⤵️ 入口文件 src/platforms/entry-runtime.js, platforms 文件夹是跟平台相关的代码 ⤵️ src/platforms/web/runtime/index.jsruntime/index.js 里对该运行时代码做了特殊处理 ⤵️ src/code/index.js,真正入口文件

import Vue from './instance/index'
// ...
initGlobalAPI(Vue)
// ...ssr相关代码
Vue.version = '__VERSION__'

export default Vue

⤵️ src/core/instance/index.js

function Vue (options) {
  // ...
  this._init(options)
}
// 添加 _init 函数
initMixin(Vue)
// 主要是添加了 $data,$props,$watch,$set,$delete 几个属性和方法
stateMixin(Vue)
// 主要是添加了 $on,$off,$once,$emit 三个方法
eventsMixin(Vue)
// 主要添加了 _update, $forceUpdate, $destroy 三个方法
lifecycleMixin(Vue)
// 主要添加了 $nextTick 和 _render 两个方法以及一大堆renderHelpers
renderMixin(Vue)

export default Vue

instance/index.js 作用是按功能模块往 Vue 原型和自身添加了许多属性和方法。 再回到 initGlobalAPI(Vue).

// 一般使用的是实例里的方法
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick

Vue.options = Object.create(null)
// 循环出来的结果其实是三个 `components`,`directives`, `filters`
ASSET_TYPES.forEach(type => {
  Vue.options[type + 's'] = Object.create(null)
})

// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue

// builtInComponents 仅为内置组件 KeepAlive
extend(Vue.options.components, builtInComponents)

initUse(Vue) // 添加 Vue.use
initMixin(Vue) // 添加 Vue.mixin
initExtend(Vue) // 添加 Vue.extend
// 添加 Vue.component, Vue.directive, Vue.filter 方法
initAssetRegisters(Vue)

initGlobalAPI 作用也是继续添加了一些全局方法。 现在 function Vue 大致就变成了如下样子:

//构造函数
function Vue () {
  this._init()
}

//全局config对象,我们几乎不会用到
Vue.config = {
  keyCodes,
  _lifecycleHooks: ['beforeCreate', 'created', ...]
}

// 默认的options配置,我们每个组件都会继承这个配置。
Vue.options = {
  beforeCreate, // 比如 vue-router 就会注册这个回调,因此会每一个组件继承
  components, // 前面提到了,默认组件有三个 `KeepAlive`,`transition`, `transitionGroup`,这里注册的组件就是全局组件,因为任何一个组件中不用声明就能用了。所以全局组件的原理就是这么简单
  directives, // 默认只有 `v-show` 和 `v-model`
  filters
}

//一些全局方法
Vue.use // 注册插件
Vue.component // 注册组件
Vue.directive // 注册指令
Vue.nextTick //下一个tick执行函数
Vue.set/delete // 数据的修改操作
Vue.mixin // 混入mixin用的

//Vue.prototype 上有几种不同作用的方法

//由initMixin 添加的 `_init` 方法,是Vue实例初始化的入口方法,会调用其他的功能初始话函数
Vue.prototype._init

// 由 initState 添加的三个用来进行数据操作的方法
Vue.prototype.$data
Vue.prototype.$props
Vue.prototype.$watch
Vue.prototype.$set
Vue.prototype.$delete

// 由initEvents添加的事件方法
Vue.prototype.$on
Vue.prototype.$off
Vue.prototype.$one
Vue.prototype.$emit

// 由 lifecycle添加的生命周期相关的方法
Vue.prototype._update
Vue.prototype.$forceUpdate
Vue.prototype.$destroy

//在 platform 中添加的生命周期方法
Vue.prototype.$mount

// 由renderMixin添加的`$nextTick` 和 `_render` 以及一堆renderHelper
Vue.prototype.$nextTick
Vue.prototype._render
Vue.prototype._b
Vue.prototype._e
//...

实例化阶段[new Vue({…})]

代码在 src/core/instance/init.js 里。 主要功能代码:

  • 生成自增的唯一ID标识 vm._uid = uid++
  • 合并 vm.constructor 和传入的 options,生成 vm.$options
    • 这一步使得可以在子组件能够使用全局的 directives、filters 等方法
  • 挂载自身 vm._self = vm
  • 初始化生命周期、事件、渲染等相关钩子工作
    • initLifecycle(vm)
      • 定位第一个非抽象父级,并添加到 $children 里,parent.$children.push(vm)
      • 添加很多变量,主要为 $parent、$children,$refs 也在这里定义了,其他 _开头的变量均为生命周期不同阶段状态的 flag
        • vm.$parent = parent
        • vm.$root = parent ? parent.$root : vm
        • vm.$children = []
        • vm.$refs = {}
        • vm._watcher = null
        • vm._inactive = null
        • vm._directInactive = false
        • vm._isMounted = false
        • vm._isDestroyed = false
        • vm._isBeingDestroyed = false
    • initEvents(vm)
      • 注册的是父组件事件

    • initRender(vm)
      • 做 render 的准备工作,并未开始 render,如创建 vm._vnode,vm.$createElement,$attrs 和 $listeners
    • callHook(vm, ‘beforeCreate’)
      • 调用 beforeCreate 生命周期钩子
      • callHook 里定义了:if (vm._hasHookEvent) {vm.$emit('hook:' + hook)},所以有个小技巧实现在在父组件通过 hook 钩子,监听子组件生命周期方法:

      •   <Chind-Component @hook:updated="doSomething" /> 
        
    • initInjections(vm) // resolve injections before data/props
    • initState(vm)
      • data, props, computed 等都是在这里初始化的,常见的面试考点比如Vue是如何实现数据响应化的 答案就在这个函数中寻找
    • initProvide(vm) // resolve provide after data/props
    • callHook(vm, ‘created’)
      • 调用 created 生命周期钩子
  • 存在 el ,则调用 $mountvm.$mount(vm.$options.el)
    • 当然,el 也可以不写,而是在实例化的时候直接调用: new Vue({...}).$mount('#app')

阅读全文

koa中间件原理

koa 功能

  • 封装 http 模块【lib/application 下的 listen 方法】
  • 构建中间件模型【通过 koa-compose 包】
  • 整合了request,response,context【lib/application 下的 createContext 方法】
  • 错误处理【lib/application 下的 onerror 方法】

一个基础模板代码如下:

const Koa = require('koa');
const app = new Koa();

// logger
app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// response
app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

koa 里,中间件函数接收两个参数 context, next, 通过 use 方法,将函数添加到 middleware 数组里【 this.middleware.push(fn) 】, 在 listen 里

http.createServer(this.callback()).listen(...args);

调用了 callback,在 callback 里,通过 compose(this.middleware), 处理之前添加过的中间件,返回 this.handleRequest(ctx, fn) 来执行中间件, handleRequest 是返回 Promise 函数的,所以在编写中间件时,往往通过 async await 来构建,这里主要来解析下 compose,它是引用了 koa-compose 包,主要源码如下:

function compose () {
  return function (context, next) {
    // last called middleware
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

compose 里会返回 dispatch 方法,dispatch 里会先执行第一个fn,并将 middleware 队列里下一个 fn 作为 next 参数,且 dispatch 返回的为 Promise,从而形成了先进后出洋葱式模型。 koa-middleware

阅读全文

Vuex源码分析

Vuex是Vue的数据状态管理插件.将数据放在单例下的store里,对数据进行监控管理.

引入、安装阶段,从入口 index.js开始

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}

导出了 Store 类和一些辅助map开头的函数。 最初在项目里使用Vuex时,Vue.use(Vuex)里便是调用了install方法。

// ...
let Vue
// ...
// 初始install阶段,Vue.use(Vuex)会调用install方法
export function install (_Vue) {
  // Vue为store里定义,_Vue为调用install时传入的Vue
  // 防止重复install
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  // applyMixin函数作用:将store注入到子组件,子组件可通过this.$store访问store
  applyMixin(Vue)
}

store.js里导出了install方法。在store.js开头定义局部 Vue 变量,用于判断是否已经装载和减少全局作用域查找。 这里调用applyMixin方法,它将store注入到子组件,子组件可通过this.$store访问store // applyMixin.js

if (version >= 2) {
  Vue.mixin({ beforeCreate: vuexInit })
} 

applyMixin里对 vue 1.0 和 2.0 版本进行了判断,2.0下在beforeCreate阶段注入store

// 子组件可以通过this.$store来访问store
function vuexInit () {
  const options = this.$options
  // 注入store
  // 初始时,在根组件上,调用者会传入store,所以会进入if逻辑
  if (options.store) {
    // options上的store可以为 function,
    // 这类似于 vue 里的 data return 的是个 function,防止多个 store下,里面的数据被共享
    this.$store = typeof options.store === 'function'
      ? options.store()
      : options.store
  } else if (options.parent && options.parent.$store) {
    // 在子组件上,初始化时未传入store,则从父组件中获取store
    // 公用了一份初始根组件时传入的全局的store
    this.$store = options.parent.$store
  }
}

new Store实例化阶段

  // plugins为外部传入的插件
  // strict为默认不开启严格模式(严格模式:只能在mutation里进行数据更改,在action等其他位置进行数据更改会抛出错误)
  const {
    plugins = [],
    strict = false
  } = options

  // 定义store 内部状态
  // 用于判断是否在commit环节的flag,保证只在mutation环境改变state
  this._committing = false
  // actions操作对象
  this._actions = Object.create(null)
  // 发布订阅模式下的订阅函数集合
  this._actionSubscribers = []
  // mutations操作对象
  this._mutations = Object.create(null)
  // 封装后的getters集合对象
  this._wrappedGetters = Object.create(null)
  /**
   * 格式化 options,也是该源码阅读的核心重点,
   * 在非模块模式(普通用法传入options)或 在按模块开发方式传入store时,存储处理过的modules
   * this._modules = {
   *   root: {
   *     context: {dispatch: ƒ, commit: ƒ},
   *     runtime: false,
   *     state: {…},
   *     _children: {},
   *     _rawModule: {state: {…}, actions: {…}, mutations: {…}, plugins: Array(1)},
   *     namespaced: false
   *   },
   *   __proto__: {
   *    get: ƒ (path)
   *    getNamespace: ƒ getNamespace(path)
   *    register: ƒ register(path, rawModule, runtime)
   *    unregister: ƒ unregister(path)
   *    update: ƒ update$1(rawRootModule)
   *    constructor: ƒ ModuleCollection(rawRootModule)
   *    __proto__: {...}
   *   }
   * }
   */
  this._modules = new ModuleCollection(options)
  // 模块命名空间map
  this._modulesNamespaceMap = Object.create(null)
  // 发布订阅模式下的订阅函数集合
  this._subscribers = []
  // vue实例,用到watch监视变化功能
  this._watcherVM = new Vue()
  // ...
  installModule(this, state, [], this._modules.root)

  // initialize the store vm, which is responsible for the reactivity
  // (also registers _wrappedGetters as computed properties)
  resetStoreVM(this, state)

  // 调用传入的各种插件
  plugins.forEach(plugin => plugin(this))

  const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
  if (useDevtools) {
    devtoolPlugin(this)
  }

constructor 里主要操作为: 格式化modules,安装 module,初始化 store vm, 安装 plugins

格式化modules,即 this._modules = new ModuleCollection(options)

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。 每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  state: { ... },
  modules: {
    a: moduleA,
    b: moduleB
  }
})

代码被分割到 ./module/module-collection 文件夹下。 初始化时,执行 register 方法

this.register([], rawRootModule, false)

register 接收3个参数

  • path:路径,模块tree的路径
  • rawModule:最初 new Store 时传入的 options
  • runtime:是否是运行时创建的模块,在初始化根模块时为 false, 其他情况为 true,理解为 初始化时的 flag

如初始化 register 时,参数为:path: [], rawModule: options, runtime: false

register (path, rawModule, runtime = true) {// path: [], rawModule: options, runtime: false
  if (process.env.NODE_ENV !== 'production') {
    // 对构造器传入的options的getters、mutations、actions进行dev环境的格式校验
    // 非预期的格式会抛出错误
    assertRawModule(path, rawModule)
  }

  // 初始化module
  const newModule = new Module(rawModule, runtime)
  // 第一次初始化时,执行if代码块,path为[]
  if (path.length === 0) {
    this.root = newModule
  } else {
    // 获取当前module的parent
    const parent = this.get(path.slice(0, -1))
    // 添加child
    parent.addChild(path[path.length - 1], newModule)
  }

  // 递归注册嵌套的module
  if (rawModule.modules) {
    forEachValue(rawModule.modules, (rawChildModule, key) => {
      this.register(path.concat(key), rawChildModule, runtime)
    })
  }
}

register 里的逻辑大致为

  • 校验传入的options格式,非预期的格式会抛出错误
  • 实例化 Module,初始化时 模块 tree 的根即为 root,之后 addChild 添加的子模块 即会放入到 root._children 里,逐级对包含的所以模块进行递归 register,以形式 模块 tree

root格式如下:

root: {
  context: {dispatch: ƒ, commit: ƒ},
  runtime: false,
  state: {},
  _children: {},
  _rawModule: {state: {}, actions: {}, mutations: {}, plugins: Array(1)},
  namespaced: false
}

初始化module

// 初始化module
const newModule = new Module(rawModule, runtime)

Module 定义在 ./module/module.js

构造函数:

constructor (rawModule, runtime) {// rawModule: options, runtime: false
  this.runtime = runtime
  // 存储module子项
  this._children = Object.create(null)
  // 存储初始化时传入的原始options
  this._rawModule = rawModule
  const rawState = rawModule.state

  // 兼容module形式里state传入函数形式,类似 组件中的 data,总是return 一个Object,来防止对象被共享
  this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}

屡下 模块初始化 思路:

  • 对初始传入的 options 进行模块 实例化,
  • 里面包含了子模块的话 会被递归 放进 root._children 对象里,形成一颗 模块 tree,
  • 并且 通过 forEachMutation 包装函数等的类似方法,子模块在执行 action、mutation 方法时也只是执行的是子模块里对应的方法,
  • 每个模块的 _rawModule 即为 定义时 传入的 options
  • 每个模块的 state 即为 定义时 传入的 options.state

安装模块

installModule(this, state, [], this._modules.root)

在模块被初始化定义后,通过 installModule 对 state, actions, mutations, getters 进行初始化处理。

function installModule (store, rootState, path, module, hot) {
  // this, state(this._modules.root.state), [], this._modules.root
  // 是root时,path为[]
  const isRoot = !path.length
  //  modules: {
  //     a: moduleA,
  //     b: moduleB
  //   }
  // modules.a 的命名空间为 'a/'
  const namespace = store._modules.getNamespace(path)

  // 存储namespace对应的module于_modulesNamespaceMap
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // 设置 state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }

  const local = module.context = makeLocalContext(store, namespace, path)

  // 拼接namespace,如:
  // modules: {
  //   account: {
  //     namespaced: true,
  //     state: { ... }, // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
  //     getters: {
  //       isAdmin () { ... } // -> getters['account/isAdmin']
  //     },
  //     actions: {
  //       login () { ... } // -> dispatch('account/login')
  //     },
  //     mutations: {
  //       login () { ... } // -> commit('account/login')
  //     }
  //   }
  // }
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  // 递归install子项
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

分析下 registerMutation 的注册

在这里先通过 forEachMutation 方法, 传入对应的模块的 mutation 和 key, registerMutation 里将我们项目里定义的 mutations 方法以 key-value 形式存入 _mutations 里,在传入的时候 将 state 作为第一参数,我们自己传入的为第二参数形式,这里使得如文档里所说一样形式使用:

// ...
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

这里会对命名空间进行处理,假如模块 A 中有名为 add 的 mutation 函数,那么在注册过程中会变成 a/add

相关 Mutation 注册代码: ->>>>>>>>>>>>

module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})
//
forEachMutation (fn) {
  if (this._rawModule.mutations) {
    forEachValue(this._rawModule.mutations, fn)
  }
}
//
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

<<<<<<<<<<

registerAction 的注册

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload, cb) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

这里将 { dispatch commit getters state rootGetters rootState } 作为第一参数,将自己dispatch 时传入的参数作为第二参数,所以有了文档里所说的书写形式:

// 自己项目里的 actions.js
toggleMenu ({ commit }) {
  commit('TOGGLE_MENU')
},
// or
setSidebarMenuList (context, userType) {
  context.commit('SET_SIDEBAR_MENU_LIST', userType)
}

registerAction 里 处理了 promise 情况,所以 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise

所以我们可以这么来写:

login({commit}, user){
  return new Promise((resolve, reject) => {
    commit('auth_request')
    axios({url: '...', data: user, method: 'POST' })
      .then(resp => {
        const token = resp.data.token
        const user = resp.data.user
        localStorage.setItem('token', token)
        axios.defaults.headers.common['Authorization'] = token
        commit('auth_success', token, user)
        resolve(resp)
      })
      .catch(err => {
        commit('auth_error')
        localStorage.removeItem('token')
        reject(err)
      })
  })
}
export default {
  methods: {
    register () {
      this.$store.dispatch('register', {
        name: this.name,
        password: this.password
      })
        .then(() => this.$router.push('/'))
        .catch(err => console.log(err))
    }
  }
}

将处理的请求提取到了vuex里,也使得页面里代码更加简洁了。只保留了业务逻辑。

registerGetter 的注册

// ...
return rawGetter(
  local.state, // local state
  local.getters, // local getters
  store.state, // root state
  store.getters // root getters
)

getter 这里先处理了命名空间,然后这里接收 4 个参数。 前面两个为模块内的 local state ,local getters,后两个参数为根节点上的内容 root state,root getters。 当然,没使用模块形式的话,两者相同。

resetStoreVM(this, state)

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // ...
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  // ...
}

resetStoreVM 的工作是 实例化一个 vue 实例,并将 getters 里定义的方法 通过 Object.defineProperty 来进行绑定到 store.getters 上, 并作为 computed 计算属性,在state变化时,getter 能响应式变化。 在实际调用 即为如下关系:

// 这里使得vm.$store.state 取到 vm._data.$$state
get state () {
  return this._vm._data.$$state
}
store.state = store._vm._data.$$state
store.getters.a = store.a = store._vm._data.$$state.a

Vuex 还有一些map开头的辅助函数,这些 handler 通过 hook 达到了简写的目的。 Vuex 本身是 Vue 的插件形式,其自身继续延续支持插件形式。replaceState,subscribe通常会在插件里用到。

store.subscribe((mutation, state) => {
  console.log(mutation.type)
  console.log(mutation.payload)
})

阅读全文

浏览器相关知识点

浏览器输入url到看到页面的流程

  • DNS解析
    • DNS服务器对域名解析,得到目标服务器的IP后,进行HTTP访问
  • 应用层【HTTP数据】
  • 传输层【TCP协议:确保可靠性】
    • 为了传输方便,在传输层(TCP 协议)把从应用层处收到的数据(HTTP 请求报文)进行分割,并在各个报文上打上标记序号及端口号后转发给网络层。
    • 三次握手🤝
      • 握手过程中使用了TCP的标志 [flag] —— SYN [synchronize] 和 ACK [acknowledgement]
        • 发送端首先发送一个带 SYN 标志的数据包给对方。
        • 接收端收到后,回传一个带有 SYN/ACK 标志的数据包以示传达确认信息。
        • 最后,发送端再回传一个带 ACK 标志的数据包,代表“握手”结束。若在握手过程中某个阶段莫名中断,TCP 协议会再次以相同的顺序发送相同的数据包。
    • 除了上述三次握手,TCP 协议还有其他各种手段来保证通信的可靠性
  • 网络层【IP协议:负责传输、ARP协议:地址解析协议】
    • 增加作为通信目的地的 MAC 地址
    • ARP 是一种用以解析地址的协议,根据通信方的 IP 地址就可以反查出对应的 MAC 地址
  • 数据链路层
  • 服务器
    • 数据链路层接收,按序往上层发送,并把对应的首部消去,TCP按序重组请求报文,HTTP处理请求,返回响应
  • 处理请求,返回响应内容
  • 浏览器得到资源内容/报文,进行解析渲染
    • 检查HTML并构建DOM
      • 字节 -> 字符串 -> node -> DOM
    • 检查CSS并构建CSSOM【CSS Object Model,是一个建立在web页面上的 CSS 样式的映射】
      • 字节 -> 字符串 -> node -> DOM
    • Web浏览器将DOM和CSSOM结合,并构建出渲染树(render tree)
      • 包含节点和节点样式信息
    • 渲染引擎根据 RenderTree 开始渲染和展示
      • 布局(回流):确定节点位置和大小
      • 绘制:调用CPU,合成图层,显示与屏幕
    • 遇到 script、link 会阻塞
  • 断开连接,四次挥手👋



协议对应OSI七层模型位置:

  • 物理层
  • 数据链路层
  • 网络层
    • IP
  • 传输层
    • TCP
    • UDP(User Data Protocal): 用户数据报协议
      • 不可靠,可能丢包而且包的顺序性也不能保证
  • 会话层
  • 表示层
  • 应用层
    • http

重绘、回流有什么区别?

  • 网页生成时,至少会渲染一次,在用户访问过程中,还会不断重新渲染
  • 重绘是当节点需要更改外观而不影响布局,如:color的改变
  • 回流是布局或者几何属性的改变,如:width、height的改变
  • 回流必定发生重绘,重绘未必引发回流

async 和 defer 有什么区别?

  • async 如果已经加载好,就会开始执行
  • defer 不阻塞 HTML 的解析,HTML解析完后,再执行
  • 加载多个JS脚本,async无序加载,而defer有序加载
    • 如加载 谷歌统计代码,使用async
    • 加载 JS相互依赖代码,使用defer,如 jQuery.js、jQuery-plugin.js

为什么操作DOM慢?

  • 相当于不同线程之间的通信
  • 可能带来重绘、回流

强缓存和协商缓存

强缓存是不经过服务器的, 协商缓存是经过服务器的

强缓存可能从本地内存获取,也可能从本地磁盘内读取

  • 强缓存相关字段(Expires(响应头), Cache-Control(响应头)),

    Cache-Control优先级大于Expires

    • Cache-Control
      • no-cache:强制向源服务器再次验证
      • no-store:不缓存请求或响应的任何内容,这才是真的’no-cache’不缓存
  • 协商缓存相关字段(Last-Modified(响应头), If-Modified-Since(请求头), Etag(响应头), If-None-Match(请求头))

HTTP 缓存机制流程图:

catch

为什么建立连接是三次握手,而关闭连接却是四次挥手呢?


参考-图解HTTP
参考-Introduction to the CSS Object Model
参考-掘金文章-深入浅出浏览器渲染原理
参考-掘金文章-TCP的三次握手四次挥手
参考-segmentfault文章-从URL输入到页面展现到底发生什么?
参考-github.com/MuYunyun/blog

ARP协议相关资料:ARP协议百度百科

阅读全文

已知年月,求该月共多少天?

在写日历组件时,曾遇到 已知年月,求该月共多少天? 这样的需求。

最开始思路会是:

  • 先判断该年份是否是闰年,来处理 2 月份情况,闰年 2 月共 29 天,非闰年 2 月共 28 天
  • 再判断其他月份,如 1 月共 31 天,4 月共 30 天

代码就不一一列出了,思路代码啥的没啥问题。

这里其实有种更简便的方法,借助 Date API 处理日期溢出时,会自动往后推延响应时间的规则,直接上代码:

// month 值需对应实际月份减一,如实际 2 月,month 为 1,实际 3 月,month 为 2
function getMonthCountDay (year, month) {
  return 32 - new Date(year, month, 32).getDate()
}

验证下:

// 求闰年的 2 月份总天数
getMonthCountDay(2000, 1) // 29
// 求非闰年的 2 月份总天数
getMonthCountDay(2001, 1) // 28
// 求 1 月份总天数
getMonthCountDay(2000, 0) // 31
getMonthCountDay(2001, 0) // 31
// 求 4 月份总天数
getMonthCountDay(2000, 3) // 30
getMonthCountDay(2001, 3) // 30

阅读全文

CSS里的BFC

BFC概念

块格式化上下文(Block Formatting Context,BFC) 是Web页面的可视化CSS渲染的一部分,是布局过程中生成块级盒子的区域,也是浮动元素与其他元素的交互限定区域。是CSS2.1规范定义的内容。

下列方式会创建块格式化上下文:

  • 根元素或包含根元素的元素
  • 浮动元素(元素的 float 不是 none
  • 绝对定位元素(元素的 positionabsolutefixed
  • 行内块元素(元素的 displayinline-block
  • 表格单元格(元素的 displaytable-cell,HTML表格单元格默认为该值)
  • 表格标题(元素的 displaytable-caption,HTML表格标题默认为该值)
  • 匿名表格单元格元素(元素的 displaytabletable-rowtable-row-group、table-header-grouptable-footer-group(分别是HTML table、row、tbody、thead、tfoot的默认属性)或 inline-table
  • overflow 值不为 visible 的块元素
  • display 值为 flow-root 的元素
  • contain 值为 layoutcontentstrict 的元素
  • 弹性元素(displayflexinline-flex元素的直接子元素)
  • 网格元素(display为 gridinline-grid 元素的直接子元素)
  • 多列容器(元素的 column-countcolumn-width 不为 auto,包括 column-count 为 1)
  • column-spanall 的元素始终会创建一个新的BFC,即使该元素没有包裹在一个多列容器中(标准变更,Chrome bug)。

块格式化上下文包含创建它的元素内部的所有内容.

根元素即为一个BFC

特性

  • 属于同一个BFC的两个相邻Box的margin会发生重叠,与方向无关,仅保留较大的margin值
  • 消除与浮动元素的重叠
  • 可制造内部浮动(计算BFC的高度时,浮动子元素也参与计算)
  • 每个元素的margin-left,与包含块border-left相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此(见demo里的part3)。

demo

demo

总结

BFC是文档内元素显示的一种形式,概念有点抽象,可想像成在文档里为一个密闭的空间,与外部进行了隔离。在默认情况下,根元素即为BFC。所以在根元素内部的元素垂直方向会产生margin重合,浮动元素会产生元素重叠,BFC可消除元素内的子元素浮动产生的不包含情况等特性。

参考MDN链接

阅读全文

简单实现浅拷贝与深拷贝

深浅拷贝区别

浅拷贝只复制第一层可枚举的属性值,深拷贝对每一层里的可枚举的属性值都进行复制。

  • 简单实现浅拷贝

function copy (source) {
  if (source === null || typeof source !== 'object') return source;
  const copy = Array.isArray(source) ? [] : {};

  Object.keys(source).forEach(key => {
    copy[key] = source[key]
  })
  return copy
}

浅拷贝的实现有

  • Object.assign
  • ...展开语法
  • Array.prototype.slice()
  • 数组的concat
  • 简单实现深拷贝

function deepCopy (source) {
  if (source === null || typeof source !== 'object') return source;
  const copy = Array.isArray(source) ? [] : {};

  Object.keys(source).forEach(key => {
    copy[key] = deepCopy(source[key])
  })
  return copy
}

JSON.parse(JSON.stringify(source)) 是对深拷贝的实现

但存在如下问题:

  • 拷贝JSON中不支持的类型会有问题,如Date类型拷贝时,会转化为带T格式的日期字符串

阅读全文

Centos6.8下安装jenkins

本博客使用了jenkins进行持续集成,在配置腾讯云时,顺便记录下配置过程。安装jenkins的方式官网 给出了多种,这里使用了war包文件方式安装。

一、安装前提

1、安装java1.8版本

Jenkins 依赖于1.8版本的java。

使用yum,安装java1.8,执行

yum install -y java-1.8.0-openjdk.x86_64

安装以后执行 java -version 看下是否安装成功。

2、添加环境变量

接着在 /etc/profile文件里添加java的环境变量

vim  /etc/profile

在最后添加如下代码:

JAVA_HOME=/usr/java/jdk1.8.0_74
JRE_HOME=/usr/java/jdk1.8.0_74/jre
PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin
CLASSPATH=:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib

执行修改生效命令:source /etc/profile

查看是否生效命令:echo $PATH

二、jenkins相关

1、下载jenkins war包

java安装的准备工作做完了,接着根据官网提供的Centos下的jenkins下载方式,执行如下命令:

sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat/jenkins.io.key

找到刚下载的安装包,可执行如下命令:

rpm -ql jenkins

结果如下:

[root@VM_160_98_centos ~]# rpm -ql jenkins
/etc/init.d/jenkins
/etc/logrotate.d/jenkins
/etc/sysconfig/jenkins
/usr/lib/jenkins
/usr/lib/jenkins/jenkins.war
/usr/sbin/rcjenkins
/var/cache/jenkins
/var/lib/jenkins
/var/log/jenkins

/usr/lib/jenkins/jenkins.war就是war包的位置了。

2、默认启动端口

这里先顺便修改下jenkins的8080默认启动端口。(查看端口使用情况命令:netstat -ntlp)

修改端口的文件命令为:vim /etc/sysconfig/jenkins,找到里面的JENKINS_PORT="8080",我这里改为了5555,没需求可不修改。

3、运行jenkins

运行jenkins命令且退出命令行不影响程序执行,运行如下命令:

java -jar /usr/lib/jenkins/jenkins.war --httpPort=5555 &

其中 & 符号使得退出命令行不影响程序执行。

4、添加nginx 代理端口设置

接着在nginx里配置代理到5555端口,使得外网能访问。

接着到服务器后台管理(我服务器是腾讯云)添加子域名和在安全组里添加5555端口的入站规则。

这样jenkins就算安装完成了。http://jenkins.tanxchen.com

附:Jenkins 默认插件

默认插件

  • Floders Plugin
  • OWASP markup formatter plugin
  • Build timeout plugin
  • Credentials binding plugin
  • timestamper
  • Workspace cleanup plugin
  • ant plugin
  • Gradle plugin
  • pipeline
  • Github organization floder plugin
  • pipeline stage view plugin
  • Git plugin
  • subversion plugin
  • SSH slaves plugin
  • Matrix authorization stragegy plugin
  • PAM authentication plugin
  • LDAP plugin
  • Email extension plugin
  • Mailer plugin

共19个默认插件

阅读全文