深切Vue.js从源码最先(三)

数据驱动

Vue.js 一个中间头脑是数据驱动。所谓数据驱动,是指视图是由数据驱动天生的,我们对视图的修正,不会直接操纵 DOM,而是经由历程修正数据。它比拟我们传统的前端开辟,如运用 jQuery 等前端库直接修正 DOM,大大简化了代码量。特别是当交互庞杂的时刻,只体贴数据的修正会让代码的逻辑变的异常清楚,由于 DOM 变成了数据的映照,我们一切的逻辑都是对数据的修正,而不必碰触 DOM,如许的代码异常利于保护。
在 Vue.js 中我们能够采纳简约的模板语法来声明式的将数据衬着为 DOM:

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

终究它会在页面上衬着出 Hello Vue。接下来,我们会从源码角度来剖析 Vue 是怎样完成的,剖析历程会以主线代码为主,主要的分支逻辑会放在以后零丁剖析。数据驱动另有一部分是数据更新驱动视图变化,这一块内容我们也会在以后的章节剖析,这一章我们的目的是弄清楚模板和数据怎样衬着成终究的 DOM。

new Vue 发生了什么

从进口代码最先剖析,我们先来剖析 new Vue 背地发生了哪些事变。我们都晓得,new 症结字在 Javascript 言语中代表实例化是一个对象,而 Vue 实际上是一个类,类在 Javascript 中是用 Function 来完成的,来看一下源码,在src/core/instance/index.js 中。

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 只能经由历程 new 症结字初始化,然后会挪用 this._init 要领, 该要领在 src/core/instance/init.js 中定义

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-start:${vm._uid}`
    endTag = `vue-perf-end:${vm._uid}`
    mark(startTag)
  }

  // a flag to avoid this being observed
  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)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')

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

  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}


Vue 初始化主要就干了几件事变,兼并设置,初始化生命周期,初始化事宜中间,初始化衬着,初始化 data、props、computed、watcher 等等

tips: 关于vue源码的调试技能

在webpack的vue工程中有以下设置文件
webpack.base.conf.js

 resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
    }
  },

指向的实在vue源码是,node_modules 里的vue工程下的途径vue/dist/vue.esm.js。须要调试的代码插进去debugger来断点

Vue 实例挂载的完成

Vue 中我们是经由历程 $mount 实例要领去挂载 vm 的,$mount 要领在多个文件中都有定义,如 src/platform/web/entry-runtime-with-compiler.js、src/platform/web/runtime/index.js、src/platform/weex/runtime/index.js。由于 $mount 这个要领的完成是和平台、构建体式格局都相干的。接下来我们重点剖析带 compiler 版本的 $monut 完成,由于抛开 webpack 的 vue-loader,我们在纯前端浏览器环境剖析 Vue 的事情道理,有助于我们对道理明白的深切。

先来看一下 src/platform/web/entry-runtime-with-compiler.js 文件中定义:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}


这段代码起首缓存了原型上的 $mount 要领,再从新定义该要领,我们先来剖析这段代码。起首,它对 el 做了限定,Vue 不能挂载在 body、html 如许的根节点上。接下来的是很症结的逻辑 —— 假如没有定义 render 要领,则会把 el 或许 template 字符串转换成 render 要领。这里我们要切记,在 Vue 2.0 版本中,一切 Vue 的组件的衬着终究都须要 render 要领,不管我们是用单文件 .vue 体式格局开辟组件,照样写了 el 或许 template 属性,终究都邑转换成 render 要领,那末这个历程是 Vue 的一个“在线编译”的历程,它是挪用 compileToFunctions 要领完成的,编译历程我们以后会引见。末了,挪用本来原型上的 $mount 要领挂载。

本来原型上的 $mount 要领在 src/platform/web/runtime/index.js 中定义,之所以这么设想完整是为了复用,由于它是能够被 runtime only 版本的 Vue 直接运用的。

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

$mount 要领实际上会去挪用 mountComponent 要领,这个要领定义在 src/core/instance/lifecycle.js 文件中:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  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
}

从上面的代码能够看到,mountComponent 中间就是先挪用 vm._render 要领先天生假造 Node,再实例化一个衬着Watcher,在它的回调函数中会挪用 updateComponent 要领,终究挪用 vm._update 更新 DOM。
Watcher 在这里起到两个作用,一个是初始化的时刻会实行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时刻实行回调函数,这块儿我们会在以后的章节中引见。
函数末了推断为根节点的时刻设置 vm._isMounted 为 true, 示意这个实例已挂载了,同时实行 mounted 钩子函数。 这里注重 vm.$vnode 示意 Vue 实例的父假造 Node,所以它为 Null 则示意当前是根 Vue 的实例。

    原文作者:codehunter
    原文地址: https://segmentfault.com/a/1190000017391202
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞