Vue.js 源碼學習筆記2 數據驅動

數據驅動

Vue.js 一個核心思想就是數據驅動

類比:

jquery缺點:直接操作dom 增加內存使用(把DOM與javascript各自想象成為島嶼,它們之間用收費橋梁鏈接的。訪問DOM次數越多,費用就越高,推薦的做法是盡可能的減少過橋的次數,努力呆在ECMscript島嶼上。)

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)

}

new初始化vue后? 再調用? 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 等等

在初始化的最后,檢測到如果有?el?屬性,則調用?vm.$mount?方法掛載?vm,掛載的目標就是把模板渲染成最終的 DOM

initState() 做了什么事情

initData 拿出 vue 中data里的數據? 賦值給 vm._data 同時拿到props 和 methods。?

最終使用proxy函數將 數據進行代理? 例如:this.message =>? this._data.message

function initData (vm: Component) {

? let data = vm.$options.data

? data = vm._data = typeof data === 'function'

? ? ? getData(data, vm)

? ? : data || {}

? if (!isPlainObject(data)) {

? ? data = {}

? ? process.env.NODE_ENV !== 'production' && warn(

? ? ? 'data functions should return an object:\n' +

? ? ? 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',

? ? ? vm

? ? )

? }

? // proxy data on instance

? const keys = Object.keys(data)

? const props = vm.$options.props

? const methods = vm.$options.methods

? let i = keys.length

? while (i--) {

? ? const key = keys[i]

? ? if (process.env.NODE_ENV !== 'production') {

? ? ? if (methods && hasOwn(methods, key)) {

? ? ? ? warn(

? ? ? ? ? `Method "${key}" has already been defined as a data property.`,

? ? ? ? ? vm

? ? ? ? )

? ? ? }

? ? }

? ? if (props && hasOwn(props, key)) {

? ? ? process.env.NODE_ENV !== 'production' && warn(

? ? ? ? `The data property "${key}" is already declared as a prop. ` +

? ? ? ? `Use prop default value instead.`,

? ? ? ? vm

? ? ? )

? ? } else if (!isReserved(key)) {

? ? ? proxy(vm, `_data`, key)

? ? }

? }

? // observe data

? observe(data, true /* asRootData */)

}

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?這個方法的實現是和平臺、構建方式都相關的。

當 拋開 webpack 的 vue-loader 。??compiler? 是最接近我們本地開發的構建? 在入口文件init.js中 使用$mounted() 方法進行掛載DOM時 就使用了這個方法

const mount = Vue.prototype.$mount

// 之前已經定義過的$mount方法 在文件入口處再次重新定義

Vue.prototype.$mount = function (

? el?: string | Element,

? hydrating?: boolean

): Component {

? el = el && query(el)


? /* istanbul ignore if */

? //?它對?el?做了限制,Vue 不能掛載在?body、html?這樣的根節點上 vue本事會帶html和body節點 會導致覆蓋節點 提示報錯? 這也就是平常開發中為何使用<div id="app"></div>的方式來掛載vue

? if (el === document.body || el === document.documentElement) {

??? process.env.NODE_ENV !== 'production' && warn(

????? `Do not mount Vue to or - mount to normal elements instead.`

??? )

??? return this

? }


? const options = this.$options

? // resolve template/el and convert to render function

? //?如果沒有定義?render?方法,則會把?el?或者?template?字符串轉換成?render?方法

? //?在 Vue 2.0 版本中,所有 Vue 的組件的渲染最終都需要?render?方法,無論我們是用單文件 .vue 方式開發組件,還是寫了?el?或者?template?屬性,最終都會轉換成?render?方法,那么這個過程是 Vue 的一個“在線編譯”的過程,它是調用?compileToFunctions?方法實現的

? if (!options.render) {

??? let template = options.template

? ? // 如果么有render 那么就讀取template 并且對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是DOM對象 則獲取html

??????? template = template.innerHTML

????? } else {

??????? if (process.env.NODE_ENV !== 'production') {

????????? warn('invalid template option:' + template, this)

??????? }

??????? return this

????? }

??? } else if (el) { // 如果template 木有DOM innerHTML 則會在外面包一個

????? 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?方法在?src/platform/web/runtime/index.js?中定義,之所以這么設計完全是為了復用,因為它是可以被?runtimeonly?版本的 Vue 直接使用的。

Vue.prototype.$mount = function (

? el?: string | Element,

? hydrating?: boolean

): Component {

? el = el && inBrowser ? query(el) : undefined

? return mountComponent(this, el, hydrating)

}

$mount?方法支持傳入 2 個參數,第一個是?el,它表示掛載的元素,可以是字符串,也可以是 DOM 對象,如果是字符串在瀏覽器環境下會調用?query?方法轉換成 DOM 對象的。第二個參數是和服務端渲染相關,在瀏覽器環境下我們不需要傳第二個參數。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Vue 是通過$mount實例方法去掛載vm的,$mount方法在多個文件中都有定義,如src/platform/...
    famingng閱讀 7,175評論 0 3
  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內容,還有我對于 Vue 1.0 印象不深的內容。關于...
    云之外閱讀 5,080評論 0 29
  • # 傳智播客vue 學習## 1. 什么是 Vue.js* Vue 開發手機 APP 需要借助于 Weex* Vu...
    再見天才閱讀 3,629評論 0 6
  • 沒有人不是一座空城 到了夜晚總叫人心疼 心房空蕩四肢冰冷 盡管開著全部的燈 我還是感到恐懼冰冷 我是一座空蕩蕩的新...
    若風在野閱讀 170評論 0 2
  • 許多初學寫作者都有這樣的體會,心里想的挺好,然而一提筆滿不是那么回事,無論怎樣攪盡腦汁,挖空心思也寫不出幾個字來,...
    海天明月閱讀 331評論 2 7