likes
comments
collection
share

new Vue()到底干了什么?🚀

作者站长头像
站长
· 阅读数 20

前言

本文不会对Vue源码进行过多细致的解读,主要串联起new Vue()之后,八大生命周期 是如何触发的,以及Vue是如何进行dom更新渲染的。

注:本文结合Vue源码与Vue.js技术揭秘整理而来;为了缩减篇幅,文中代码均有删减

入口文件

代码地址

new Vue()之后调用 _init 方法,开始相关初始化工作

function Vue(options) 
  this._init(options)
}

// 以下几个方法皆为在Vue原型链上挂载相应的方法
initMixin(Vue) // _init
stateMixin(Vue) // $data、$props、$set、$delete、$watch
eventsMixin(Vue) // $on、$once、$off、$emit
lifecycleMixin(Vue) // _update、$forceUpdate、$destroy
renderMixin(Vue) // $nextick、_render

beforeCreate & created

代码地址

Vue.prototype._init = function (options?: Object) {
  // ...
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')
  // ...
}

可以看到 beforeCreate 和 created 的钩子调用是在 initState 的前后。那么显然 beforeCreate 的钩子函数中就不能获取到 propsdata 中定义的值,也不能调用 methods 中定义的函数。

在这两个钩子函数执行的时候,并没有渲染 DOM,所以我们也不能够访问 DOM,一般来说,如果组件在加载的时候需要和后端有交互,放在这俩个钩子函数执行都可以,如果是需要访问 propsdata 等数据的话,就需要使用 created 钩子函数。之后我们会介绍 vue-router 和 vuex 的时候会发现它们都混合了 beforeCreate 钩子函数

initState

源码地址

initState 的作用是初始化 propsdatamethodswatchcomputed 等属性

export function initState(vm: Component) {
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)

  // Composition API
  initSetup(vm)

  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    const ob = observe((vm._data = {}))
    ob && ob.vmCount++
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initData

源码地址

initData主要调用observe方法,该方法会生成一个__ob__实例挂载到对象数据上,并递归的将对象属性变为响应式

function initData(vm: Component) {
  let data: any = vm.$options.data
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {};
  // ...
  // observe data
  const ob = observe(data)
  ob && ob.vmCount++
}

observer

  constructor(public value: any, public shallow = false, public mock = false) {
    // this.value = value
    this.dep = mock ? mockDep : new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (isArray(value)) {
     // ...
    } else {
      const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i]
        defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
      }
    }
  }

defineReactive

export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean
) {
  const dep = new Dep();
  // ...
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
      }
      return isRef(value) && !shallow ? value.value : value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (!hasChanged(value, newVal)) {
        return
      }
      // ...
      val = newVal
      dep.notify()
    }
  })

  return dep
}

create阶段流程总结

_init() -> beforeCreate -> initState() -> initData() -> observe() -> defineReactive() -> getter、setter -> created

数据准备阶段完成,准备进行依赖收集。

beforeMount & mounted

前置api $mount

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

还是在Vue.prototype._init里面,执行完created生命周期后执行$mount进行组件挂载。

callHook(vm, 'created')
...
if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

顾名思义,beforeMount 钩子函数发生在 mount,也就是 DOM 挂载之前,它的调用时机是在 mountComponent 函数中,定义在 src/core/instance/lifecycle.ts 中:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // ...
  callHook(vm, 'beforeMount')

  // 根据初始化还是更新,选择不同的函数
  let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    // 挂载
    updateComponent = () => {
        const vnode = vm._render()
        vm._update(vnode, hydrating)
    }
  } else {
    // 更新
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

其中vm._render()用于生成当前组件的vnode,同时触发模板上的依赖收集。

在执行 vm._render() 函数渲染 VNode 之前,执行了 beforeMount 钩子函数,在执行完 vm._update() 把 vnode patch 到真实 DOM 后,执行 mounted 钩子。

注意:这里对 mounted 钩子函数执行有一个判断逻辑,vm.$vnode 如果为 null,则表明这不是一次组件的初始化过程,而是我们通过外部 new Vue 初始化过程。那么对于组件,它的 mounted 时机在哪儿呢?

为了精简内容,具体看Vue.js技术揭秘中的解释。

mount阶段中的_update

_update源码

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    vm._vnode = vnode
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
  }

patch

return function patch(oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue: any[] = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {}
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

这里的__patch__就是patch,用于生成虚拟DOM树,执行后会返回真实dom节点。然后在_update里通过vm.$el进行挂载,完成挂载后触发mounted生命周期。

Wacher源码

初始化Wacher实例

constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    if (isRenderWatcher) {
      vm._watcher = this
    }
    // ...
    // parse expression for getter
    if (isFunction(expOrFn)) {
      this.getter = expOrFn
    } else {
     // ...
    }
    this.value = this.lazy ? undefined : this.get()
  }

这里的expOrFn就是mount中的updateComponent,从而执行vm._update(vm._render(), hydrating),生成虚拟dom,并挂载真实dom。

update阶段中的_update

当响应式数据更新后,会调用数据对象挂载的__ob__.dep.notify()方法,进而触发以下操作(update): wacher.update() -> watcher.run() -> watcher.get() -> getter执行(mount中的updateComponent),进而重新进行依赖收集。


mount阶段流程总结

vm.$mount() -> beforeMount -> new Watcher() -> watcher.get() -> updateComponent() -> vm._render() -> vm._update() -> vm.$el = patch() -> mounted

beforeUpdate & updated

更新阶段从某个响应式数据发生变化的时候开始,它的setter函数会通知闭包中的Dep,Dep则会调用它管理的所有Watch对象,触发Watch对象的update方法:

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

Vue异步执行dom更新,将watcher放进队列里,从而触发queueWatcher

export function queueWatcher(watcher: Watcher) {
  const id = watcher.id
  if (has[id] != null) {
    return
  }
  has[id] = true
  if (!flushing) {
    queue.push(watcher)
  }
  // queue the flush
  if (!waiting) {
    waiting = true
    nextTick(flushSchedulerQueue)
  }
}

下一个事件循环(nexTick),清空队列,执行flushSchedulerQueue 函数,它的定义在 src/core/observer/scheduler.ts 中:

function flushSchedulerQueue () {
  // ...
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before() // 触发beforeUpdate生命周期
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // ...
  }
  // 获取到 updatedQueue
  callUpdatedHooks(updatedQueue)
}

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}

同时更新阶段watcher会执行mounted阶段传进去的options(watcher.before),从而触发beforeUpdate生命周期

const watcherOptions: WatcherOptions = {
  before() {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}

同时,mountComponent中实例化Watcher,还把当前 wathcer 实例 push 到 vm._watchers 中,vm._watcher 是专门用来监听 vm 上数据变化然后重新渲染的,所以它是一个渲染相关的 watcher,因此在 callUpdatedHooks 函数中,只有 vm._watcher 的回调执行完毕后,才会执行 updated 钩子函数。

重点:一个组件实例只有一个视图watcher,配合nextick机制,组件在一次事件循环中,无论绑定的数据更新多少遍,始终只有一个watcher生效。

dom树如何更新

我们现在知道了模板编译后会生成render函数,render函数执行后会触发模板(视图)上的依赖收集,同时生成组件的vnode。因此当相应的数据变化的时候,会触发wacher调用get()方法,从而触发updateComponent->_update(diff算法)。

但是我们缺少了一点,那就是父组件通过props传进来的数据是怎样添加响应式并进行子组件的重新渲染的?

initProps

function initProps(vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  // ...
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    // ...
    defineReactive(props, key, value)
    // ...
}

组件初始化时,将props初始化为响应式数据。

子组件更新

首先,prop 数据的值变化在父组件,我们知道在父组件的 render 过程中会访问到这个 prop 数据,所以当 prop 数据变化一定会触发父组件的重新渲染,那么重新渲染是如何更新子组件对应的 prop 的值呢?

在父组件重新渲染的最后,会执行 patch 过程,进而执行 patchVnode 函数,patchVnode 通常是一个递归过程,当它遇到组件 vnode 的时候,会执行组件更新过程的 prepatch 钩子函数,在 src/core/vdom/patch.ts 中:

function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // ...

  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }
  // ...
}

这里的i,就是prepatch

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = vnode.componentOptions
  const child = vnode.componentInstance = oldVnode.componentInstance
  updateChildComponent(
    child,
    options.propsData, // updated props
    options.listeners, // updated listeners
    vnode, // new parent vnode
    options.children // new children
  )
}

这里调用了updateChildComponent,在这个方法里会对子组件的props进行更新,从而触发setter-> watcher.get() -> _update() -> patch()、diff

export function updateChildComponent(
  vm: Component,
  propsData: Record<string, any> | null | undefined,
  listeners: Record<string, Function | Array<Function>> | undefined,
  parentVnode: MountedComponentVNode,
  renderChildren?: Array<VNode> | null
) {
  // ...
  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }
}

注意: 其实子组件的重新渲染有 2 种情况,一个是 prop 值被修改,另一个是对象类型的 prop 内部属性的变化。

先来看一下 prop 值被修改的情况,当执行 props[key] = validateProp(key, propOptions, propsData, vm) 更新子组件 prop 的时候,会触发 prop 的 setter 过程,只要在渲染子组件的时候访问过这个 prop 值,那么根据响应式原理,就会触发子组件的重新渲染。

再来看一下当对象类型的 prop 的内部属性发生变化的时候,这个时候其实并没有触发子组件 prop 的更新。但是在子组件的渲染过程中,访问过这个对象 prop,所以这个对象 prop 在触发 getter 的时候会把子组件的 render watcher 收集到依赖中,然后当我们在父组件更新这个对象 prop 的某个属性的时候,会触发 setter 过程,也就会通知子组件 render watcher 的 update,进而触发子组件的重新渲染。

以上就是当父组件 props 更新,触发子组件重新渲染的 2 种情况。


更新阶段流程总结

响应式数据更新(props、data) -> wacher.update() -> queueWatcher() -> flushSchedulerQueue() -> beforeUpdate -> watcher.run() -> watcher.get() -> updateComponent() -> vm._render() -> vm._update() -> vm.$el = patch() -> callUpdatedHooks() -> updated

beforeDestroy & destroyed

顾名思义,beforeDestroy 和 destroyed 钩子函数的执行时机在组件销毁的阶段,最终会调用 $destroy 方法,它的定义在 src/core/instance/lifecycle.ts 中:

Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // teardown scope. this includes both the render watcher and other
    // watchers created
    vm._scope.stop()
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }

流程总结

beforeCreate -> created:

_init() -> beforeCreate -> initState() -> initData() -> observe() -> defineReactive() -> getter、setter -> created

beforeMount -> mounted

vm.$mount() -> beforeMount -> new Watcher() -> watcher.get() -> updateComponent() -> vm._render() -> vm._update() -> vm.$el = patch() -> mounted

beforeUpdate -> updated

响应式数据更新(props、data) -> wacher.update() -> queueWatcher() -> flushSchedulerQueue() -> beforeUpdate -> watcher.run() -> watcher.get() -> updateComponent() -> vm._render() -> vm._update() -> vm.$el = patch() -> callUpdatedHooks() -> updated

beforeDestroy -> destroyed

vm.$destroy() -> beforeDestroy -> 节点卸载、数据清除、移除虚拟dom等 -> destroyed

本来想简短的把整个流程讲完,奈何内容实在太多,也是一次对Vue生命周期探索的有趣体验吧!

纸上得来终觉浅 绝知此事要躬行

参考文献:

转载自:https://juejin.cn/post/7235591859669827640
评论
请登录