小松的技术博客

六和敬

若今生迷局深陷,射影含沙。便许你来世袖手天下,一幕繁华。 你可愿转身落座,掌间朱砂,共我温酒煮茶。

Vue源码浅析(二)-生命周期

这篇博文主要分析Vue整个生命周期的实现,Vue的生命周期是非常明确的,用官方的一张图就可以清晰的了解整个生命周期:

Vue实例的诞生

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

Vue的构造非常简单,直接调用了this.init()方法,而this.init()存在于Vue的原型链中,Vue的模块拆解的相当不错,原型上的方法根据功能的不同拆分到不同的文件中,然后在index文件将它们mixin到Vue的原型链中:

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

因此在index文件我们可以清晰的知道Vue的模块划分,然后再逐个解刨就好了。

我们继续到Vue.prototype._init的实现:

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    vm._uid = uid++
    // 如果是Vue的实例,则不需要被observe
    vm._isVue = true 
    // 第一步: options参数的处理
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // 第二步: renderProxy
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 第三步: vm的生命周期相关变量初始化
    initLifecycle(vm)
    // 第四步: vm的事件监听初始化
    initEvents(vm)
    callHook(vm, 'beforeCreate')
    // 第五步: vm的状态初始化,prop/data/computed/method/watch都在这里完成初始化,因此也是Vue实例create的关键
    initState(vm)
    callHook(vm, 'created')
    // 第六步:render & mount
    initRender(vm)
}

看完整个方法,我们也就对其整体面貌有了大概的了解,接下来就是继续走入每一步去探索其详细的实现。

组件继承树与组件实例树

在继续下面的分析之前,需要清楚标题上涉及的两颗树(自己YY出来的)。

组件继承树:这个基于组件定义而来的,Vue的继承是用Vue.extend来实现的,如:var MutiInput = Vue.extend({...}), 这里我们定义了MutiInput这个组件且MutiInput.super = Vue,而MutiInput可以继续extend,这样就可以形成一颗继承树

组件实例树: 这是基于组件使用的,例如

<muti-input>
   <muti-input></muti-input>
</muti-input>

上面是实例了两个MutiInput,内部的MutiInput的parent为外部MutiInput,很多组件实例相互嵌套,变成了一颗组件实例树。

options参数的处理

我们new一个Vue时,会传递一个对象,这个对象就是Vue构造时的options参数:

new Vue({
    data:{},
    props: {},
    methods: {},
    computed: {},
    watch: {},
    //...其它一些hook函数
})

日常中的业务逻辑基本都分散在options里面的各个子对象里面了。我想稍微了解过Vue的人都知道上述各个部分的用途了,这里就不再阐述怎么使用了。那我们来看看Vue是如何处理options参数的?

因为Vue是一套组件化的系统,子组件的options必然受到父组件的影响,即使是同一个组件,我们也有公用的options(挂接在构造器上,如Vue.options)和差异的options(实例化时传入的options),因此处理options时我们要处理四个相关的optons:

  • 父组件构造器上的options
  • 父组件实例上的options
  • 当前组件构造器上的options
  • 当前组件实例化传入的options

我们暂且不讨论Vue内部Component的实现,只关注root组件的options处理:

vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
)

因为是root组件,所以我们只需要merge构造器上的options和传入的options, 但root组件也可能是Vue的子类,它自身也会merge自己的options与父类的options(要区分组件继承树和组件实例树),其merge实现如下:

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    // Ctor parent的optoions
    const superOptions = Ctor.super.options
    const cachedSuperOptions = Ctor.superOptions
    // Ctor本身的optoions
    const extendOptions = Ctor.extendOptions
    // 这里对superOptions做了cache,如果没有变化,就不用merge生成Ctor.options
    if (superOptions !== cachedSuperOptions) {
      // super option changed
      Ctor.superOptions = superOptions
      extendOptions.render = options.render
      extendOptions.staticRenderFns = options.staticRenderFns
      options = Ctor.options = mergeOptions(superOptions, extendOptions)
      // 方便调试,建议开发时加上name属性
      if (options.name) {
       options.components[options.name] = Ctor
      }
    }
  }
  return options
}

接下来就重点看mergeOptions的实现了:

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  //...

  // 统一props格式
  normalizeProps(child)
  // 统一directives的格式
  normalizeDirectives(child)

  // 如果存在child.extends
  // ...
  // 如果存在child.mixins
  // ...

  // 针对不同的键值,采用不同的merge策略
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

上面采取了对不同的field采取不同的策略,Vue提供了一个strats对象,其本身就是一个hook,如果strats有提供特殊的逻辑,就走strats,否则走默认merge逻辑。

const strats = config.optionMergeStrategies
strats.el  = strats.propsData = ...
strats.data = ...
strats.watch  ...
....

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

用这种hook的方式就能很好的区分对待公共处理逻辑与特殊处理逻辑,我们以data为例去看看它们做了什么特殊处理:

strats.data = function (parentVal: any, childVal: any, vm?: Component): ?Function {
if (!vm) {
    // 在Vue的组件继承树上的merge是不存在vm的
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      return parentVal
    }
    if (typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    return function mergedDataFn () {
      return mergeData(
        childVal.call(this),
        parentVal.call(this)
      )
    }
  } else if (parentVal || childVal) {
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm)
        : undefined
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

这里告诉我们,options.data经过merge之后,实际上是一个function,在真正调用function才会进行真正的merge,其它的merge都会根据自身特点而又不同的操作,这里就不贴代码了。

走到这一步,我们终于把业务逻辑以及组件的一些特性全都放到了vm.$options中了,后续的操作我们都可以从vm.$options拿到可用的信息。框架基本上都是对输入宽松,对输出严格,vue也是如此,不管使用者添加了什么代码,最后都规范的收入vm.$options中。

renderProxy

这一步比较简单,主要是定义了vm._renderProxy,这是后期为render做准备的,作用是在render中将this指向vm._renderProxy。一般而言,vm._renderProxy是等于vm的,但在开发环境,Vue动用了Proxy这个新API,有关Proxy,大家可以读读深入浅出ES6(十二):代理 Proxies, 这里不再展开

vm的事件监听

每一个框架,如果要能处理事件,必定具备其基本功能:$on,$off,$once,$emit,其在任何框架的实现都是类似的,开发者应该非常熟悉,这里只看看$on的实现:

Vue.prototype.$on = function (event: string, fn: Function): Component {
    const vm: Component = this
    ;(vm._events[event] || (vm._events[event] = [])).push(fn)
    return vm
}

之所以展示这段代码,只是为了体现出Vue事件存放位置vm._events

接下来看看init时做的事情:

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  // init parent attached events
  const listeners = vm.$options._parentListeners
  const on = bind(vm.$on, vm)
  const off = bind(vm.$off, vm)
  vm._updateListeners = (listeners, oldListeners) => {
    updateListeners(listeners, oldListeners || {}, on, off, vm)
  }
  if (listeners) {
    vm._updateListeners(listeners)
  }
}

它主要定义了vm._updateListeners方法用于更新事件监听队列

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  vm: Component
) {
  let name, cur, old, fn, event, capture
  for (name in on) {
    cur = on[name]
    old = oldOn[name]
    if (!cur) {
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${name}": got ` + String(cur),
        vm
      )
    } else if (!old) {
      // 新添加的listener
      capture = name.charAt(0) === '!'
      event = capture ? name.slice(1) : name
      if (Array.isArray(cur)) {
        // 同一事件有多个listener
        add(event, (cur.invoker = arrInvoker(cur)), capture)
      } else {
        if (!cur.invoker) {
          fn = cur
          cur = on[name] = {}
          cur.fn = fn
          cur.invoker = fnInvoker(cur)
        }
        add(event, cur.invoker, capture)
      }
    } else if (cur !== old) {
      // 替换旧的事件监听
      if (Array.isArray(old)) {
        old.length = cur.length
        for (let i = 0; i < old.length; i++) old[i] = cur[i]
        on[name] = old
      } else {
        old.fn = cur
        on[name] = old
      }
    }
  }
  // 删除已经无用的listenrs
  for (name in oldOn) {
    if (!on[name]) {
      event = name.charAt(0) === '!' ? name.slice(1) : name
      remove(event, oldOn[name].invoker)
    }
  }
}

vm._updateListeners是用于管理事件监听者,可以新增、替换、删除。

vm的状态初始化

vm的状态初始化时整个初始化中最复杂的异步,其data、props、methods、computed、watch都在这一步进行初始化,因此这一步也是Vue真正的创建。

export function initState (vm: Component) {
  vm._watchers = []
  initProps(vm)
  initData(vm)
  initComputed(vm)
  initMethods(vm)
  initWatch(vm)
}

那我们一步一步的看其是如何处理的吧:

props

function initProps (vm: Component) {
  // vm.$options.props是我们为组件定义的属性
  const props = vm.$options.props
  if (props) {
    // 取出使用者传入的propsData
    const propsData = vm.$options.propsData || {}
    const keys = vm.$options._propKeys = Object.keys(props)
    const isRoot = !vm.$parent
    // root instance props should be converted
    // 这里用来标志是否将要propsData的value转换为监控对象,因为propsData可能指向其它对象,也许不能够被监控,因而除了propsData默认的value可以被监控,其它用户传入的值都不可信,因此也就不转换
    observerState.shouldConvert = isRoot
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      // 监控prop的改变
      defineReactive(vm, key, validateProp(key, props, propsData, vm))
    }
    // 还原observerState.shouldConvert
    observerState.shouldConvert = true
  }
}

data

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? data.call(vm)
    : data || {}
  if (!isPlainObject(data)) {
    // 保证data必须为纯对象
    data = {}
  }
  // 将属性代理的vm上, 可以通过vm.xx访问到vm._data.xx
  const keys = Object.keys(data)
  const props = vm.$options.props
  let i = keys.length
  while (i--) {
    if (props && hasOwn(props, keys[i])) {
      // 是props,则不代理
    } else {
      proxy(vm, keys[i])
    }
  }
  // 将data转换为监控对象,这一步和前一批文章接轨
  observe(data)
  data.__ob__ && data.__ob__.vmCount++
}

在data初始化时,就见到了上一篇文章提到的主入口observe(data),如果对observe不熟悉的话,就去回顾一下上一篇文章吧。框架还用vm代理了data的属性访问与赋值,使得调用者使用更加方便。

function proxy (vm: Component, key: string) {
  if (!isReserved(key)) {
    // 依旧是Object.defineProperty的使用
    Object.defineProperty(vm, key, {
      configurable: true,
      enumerable: true,
      get: function proxyGetter () {
        return vm._data[key]
      },
      set: function proxySetter (val) {
        vm._data[key] = val
      }
    })
  }
}

computed

function initComputed (vm: Component) {
  const computed = vm.$options.computed
  if (computed) {
    for (const key in computed) {
      const userDef = computed[key]
      if (typeof userDef === 'function') {
        computedSharedDefinition.get = makeComputedGetter(userDef, vm)
        computedSharedDefinition.set = noop
      } else {
        computedSharedDefinition.get = userDef.get
          ? userDef.cache !== false
            ? makeComputedGetter(userDef.get, vm)
            : bind(userDef.get, vm)
          : noop
        computedSharedDefinition.set = userDef.set
          ? bind(userDef.set, vm)
          : noop
      }
      Object.defineProperty(vm, key, computedSharedDefinition)
    }
  }
}

computed其实本身也是一种特殊的并且lazy的watcher,在get时它作为所计算的属性依赖而被收集,同时它把依赖自己的watcher也添加到属性的依赖中去,这样当原属性变化时,就会通知到依赖computed的依赖重新获取最新值。

function makeComputedGetter (getter: Function, owner: Component): Function {
  const watcher = new Watcher(owner, getter, noop, {
    lazy: true
  })
  return function computedGetter () {
    if (watcher.dirty) {
      // 将自己添加到属性的依赖列表中去
      watcher.evaluate()
    }
    if (Dep.target) {
      // 将依赖watcher的依赖也收集到属性的依赖列表中去
      watcher.depend()
    }
    return watcher.value
  }
}

methods

function initMethods (vm: Component) {
  const methods = vm.$options.methods
  if (methods) {
    for (const key in methods) {
      将this绑定到vm
      vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
      //...
    }
  }
}

methods的初始化比较简单,就是作用域的重新绑定。

watch

const watch = vm.$options.watch
  if (watch) {
    for (const key in watch) {
      const handler = watch[key]
      // 可以是数组,为key创建多个watcher
      if (Array.isArray(handler)) {
        for (let i = 0; i < handler.length; i++) {
          createWatcher(vm, key, handler[i])
        }
      } else {
        createWatcher(vm, key, handler)
      }
    }
  }

对于watcher,必须是一个对象,key是你想要监听的属性,value是属性变化时执行回调,value可以是数组,一次执行多个回调。

function createWatcher (vm: Component, key: string, handler: any) {
  let options
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 如果handle传入为字符串,则直接找vm上的方法,一般是methods中定义的方法,这也是methods的初始化要先于watch初始化的原因
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  vm.$watch(key, handler, options)
}

createWatcher最终走入原型方法$watch中:

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ): Function {
    const vm: Component = this
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    // 最后返回的是unwatch函数,用于在你不需要的时候销毁watcher
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}

$watch的实现与computed在原理上是相同的,都是用Watcher来做中转,像avalon就去掉了computed的支持,直接用watcher替代了computed。

经过这些初始化的步骤,一个组件就被创造出来了,那么接下来就是把我们的组件放入网页中,呈现给用户了

vm的render初始化

export function initRender (vm: Component) {
  vm.$vnode = null // the placeholder node in parent tree
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null
  vm._renderContext = vm.$options._parentVnode && vm.$options._parentVnode.context
  vm.$slots = resolveSlots(vm.$options._renderChildren, vm._renderContext)
  // bind the public createElement fn to this instance
  // so that we get proper render context inside it.
  vm.$createElement = bind(createElement, vm)
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

render的初始化很内容并不多,主要是最后几句,如果在options中提供了el,那么就需要把组件挂接到el上,如果没有提供el,那么就要后期自己去调用vm.$mount了。

Vue.prototype.$mount的实现会有多种,在Vue的web-runtime.jsweb-runtime-with-compiler.js就能看到实现上会有差异,产生差异的原因是可以用不同的方式去生成render函数(render函数就是用于生成VNode树的函数,可以是开发者去写render函数,也可以通过template编译成render函数,甚至可以通过JSX去编译)。我现在只关注用template去生成render函数,但是render函数的生成我要放到下一篇文章去讲解,所以暂时我们跳过render函数的生成。不管以调用何种$mount方法,最终都会走进Vue.prototype._mount的内部方法,此时render函数已经准备就绪并赋值给了vm.$options.render

Vue.prototype._mount = function (
    el?: Element | void,
    hydrating?: boolean
  ): Component {
    const vm: Component = this
    vm.$el = el
    if (!vm.$options.render) {
      // 如果没有render函数,就给一个空的render函数
      vm.$options.render = emptyVNode
      // ...
    }
    callHook(vm, 'beforeMount')
    // 重点:新建一个Watcher并赋值给vm._watcher
    vm._watcher = new Watcher(vm, () => {
      vm._update(vm._render(), hydrating)
    }, noop)
    hydrating = false
    // manually mounted instance, call mounted on self
    // mounted is called for render-created child components in its inserted hook
    if (vm.$vnode == null) {
      vm._isMounted = true
      callHook(vm, 'mounted')
    }
    return vm
  }

我们重点关注上述中的新建的watcher.它并不是lazy的,因此在构造的时候就会执行vm._update(vm._render(), hydrating)vm._render()会根据数据生成一个vdom, vm.update()则会对比新的vdom和当前vdom,并把差异的部分渲染到真正的dom树上, 在vm.render()执行过程中,这个watcher会作为依赖被添加到vm的data中去,如果data发生变化,就会通知这个watcher重新执行vm._update(vm._render(), hydrating)。这样整个响应式系统就完全建立起来了。

到此,vm的整个诞生流程就分析完了,接下来就是重点探讨如下环节了:

  • vm的render函数
  • vue的vdom的diff以及patch

下一篇文章我将重点分析vm的render函数,再会~~~

←支付宝← →微信 →
comments powered by Disqus