深刻Vue.js從源碼開始(三)

數據驅動

Vue.js 一個核心思想是數據驅動。所謂數據驅動,是指視圖是由數據驅動生成的,咱們對視圖的修改,不會直接操做 DOM,而是經過修改數據。它相比咱們傳統的前端開發,如使用 jQuery 等前端庫直接修改 DOM,大大簡化了代碼量。特別是當交互複雜的時候,只關心數據的修改會讓代碼的邏輯變的很是清晰,由於 DOM 變成了數據的映射,咱們全部的邏輯都是對數據的修改,而不用碰觸 DOM,這樣的代碼很是利於維護。
在 Vue.js 中咱們能夠採用簡潔的模板語法來聲明式的將數據渲染爲 DOM:html

<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 中。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 只能經過 new 關鍵字初始化,而後會調用 this._init 方法, 該方法在 src/core/instance/init.js 中定義node

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 等等webpack

tips: 關於vue源碼的調試技巧web

在webpack的vue工程中有如下配置文件
webpack.base.conf.jsjson

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 文件中定義:weex

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 的實例。

相關文章
相關標籤/搜索