Last Surprise Lyn
  1. 1 Last Surprise Lyn
  2. 2 Life Will Change Lyn
  3. 3 Flower Of Life 发热巫女
  4. 4 Hypocrite Nush
  5. 5 Libertus Chen-U
  6. 6 かかってこいよ NakamuraEmi
  7. 7 Warcry mpi
  8. 8 One Last You Jen Bird
  9. 9 Time Bomb Veela
  10. 10 BREAK IN TO BREAK OUT Lyn
2017-10-16 18:23:58

Vue原理解析——从Vue对象到DOM

我们都知道Vue的基本使用是从new一个Vue对象开始,挂载到真实DOM元素并实现数据的响应式绑定。那么在这个过程中究竟发生了什么呢?


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.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-init:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    /*一个防止vm实例自身被观察的标志位*/
    vm._isVue = true
    // merge 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
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    /*初始化生命周期*/
    initLifecycle(vm)
    /*初始化事件*/
    initEvents(vm)
    /*初始化render*/
    initRender(vm)
    /*调用beforeCreate钩子函数并且触发beforeCreate钩子事件*/
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    /*初始化props、methods、data、computed与watch*/
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    /*调用created钩子函数并且触发created钩子事件*/
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      /*格式化组件名*/
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      /*挂载组件*/
      vm.$mount(vm.$options.el)
    }
  }

在初始化过程中,会依次进行一些Vue对象特性的注册,例如生命周期,响应式数据等。可以看到在数据初始化完毕后会调用created钩子函数,然后通过$mount方法去将Vue对象挂载到真实DOM元素上。

模板编译

$mount方法就是触发组件的DOM渲染的开端了。根据源码可知渲染模式有3种:自定义render函数,template和el,也就是分别对应我们常用的3种挂载写法了,在这一步的作用是解析HTML模板,转化为可以生成对应DOM的render函数。

需要知道的是虽然写法有3种,但无论使用哪种调用方式最终都要生成一个render函数。区别在于,通过template和el编译的模板是通过AST解析优化得到的,属于声明式渲染,理解容易但是灵活度比较低。而自定义的render函数相当于人为的用逻辑创建模板,灵活性高,但可读性比较差。具体使用哪一种,要根据业务需求决定。

VNode

在模板编译过程中,我们得到了一个可以生成对应DOM结构的render函数,但这个render函数并不是生成真实的DOM并插入到HTML中。要知道每当数据变化时都会重新调用render函数生成最新的DOM结构,而这样的JS操作DOM进行重绘的操作是非常消耗性能的。所以这个render函数生成的是以JS对象为节点的虚拟DOM树,每次修改数据时DOM的变化都在这颗虚拟DOM上执行,这就是VNode了。

Patch

每一次更新VNode后,会通过名为patch的方法对新旧VNode使用diff算法寻找差异,根据不同状态对VNode进行合理的修改,这部分算法由于篇幅原因在今后的文章中再专门总结。

映射

到这一步Vue的核心功能部分已经完成了,可以看到由于使用了VNode,只要在支持JS的环境下都可以使用Vue,那么根据环境的不同,将VNode映射到真实DOM的操作也不一样。Vue根据不同的环境做了一层适配层,例如浏览器环境和移动端的weex环境都会通过这层适配层对VNode提供相同接口去映射真实DOM。

小结

回头梳理一下,整体流程并不复杂,但沿着整个流程对我们学习Vue的相关知识非常有帮助,最后再梳理一下核心部分:

  1. new Vue初始化Vue核心功能。
  2. $mount挂载生成render函数。
  3. 数据变化时render函数生成新的VNode对象。
  4. 通过diff算法比对VNode并更新。
  5. 根据适配层将VNode变化映射到真实DOM。

-- EOF --

添加在分类「 前端开发 」下,并被添加 「Vue」 标签。