vue源碼學習筆記(一): 數據驅動

雖然已經完成了underscore和zepto的源碼學習,但是還是很有必要講講自己對源碼學習的理解:根據"刻意練習"理論, 人的學習過程分爲: “輸入-練習-反饋-修正"四個步驟,而我們的實際工作中,學習方式一般分爲兩種,一種是經驗式學習,很多時候由於項目時間緊,我們匆匆看下文檔瞭解50%的東西就上手工作了,具體是邊做邊查,這種方法的好處是所見即所得,很容易有成就感,但也會有一些問題: 一是使用的方法很可能不是"best practice”,而僅僅是happen to make it work. 二是很容易侷限與思維定勢中,只使用自己熟悉的方法去解決問題.第二種學習方法是"系統的學習",這種方法耗時長,見效慢,看起來吃力不討好,但是對長遠有幫助,看書,看源碼屬於這種方法,如果把學寫代碼比作練習書法之類的技能的話,學習的過程肯定是免不了臨摹自己練習得多了,心中有字,才能下筆如有神.對於源碼學習,個人的感受是,underscore加強了對js語言以及函數式編程的理解,zepto加深了我對瀏覽器api的理解,另外還有設計模式,算法的實際運用.
本系列是慕課網上的<vue源碼解析>課程的學習筆記,(400軟妹幣,20h+課程,另有配套電子書),之所以購買課程是有幾個原因:一是:Vue源碼代碼量太大,按照以往一個個函數看的方式容易導致迷失在細節中,忽略了整體的脈絡的把握.二是網上Vue的源碼解析系列不多,本課程號稱是第一個全方位解析Vue源碼的, 三是買課程當天剛發了工資,一咬牙就剁了手.總體而言,課程物有所值,廢話不多說,開始本系列的第一篇: 數據驅動

一. 引入vue的時候做了什麼

在這裏插入圖片描述

可以看到的是, vue在開發的時候將各個功能模塊進行了拆分,在使用的時候通過Mixin的方法將各個模塊相關的方法掛載在Vue.prototype上,好處是權責分明,條理清晰, 下面看看vue的目錄
在這裏插入圖片描述

二. new Vue時候做了什麼

在這裏插入圖片描述
new Vue的時候調用了私有的_init方法,通過圖片可以看到,init方法的邏輯也是十分清晰的, 但是裏面涉及了許多過程,而目前我們要關注的就是我們是如何將data中定義參數插入到template中的,先來看一個簡單的例子

//模板html
<div id="app">
  {{ message }}
</div>

//js文件
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

第一種是沒有compliler的runtime-only版本,也就是用戶必須手寫render函數(什麼?你沒寫過render函數?那很正常,因爲寫template的方式更加符合我們的開發習慣),或者藉助webpack的vue-loader打包工具將template轉化爲render函數,我們使用vue-cli在開發SPA的時候使用的就是runtime-only版本,第二種是runtime only + compiler版本,該版本下vue內部的編譯器會經過Parse-optimise-codegen三道步驟生成render函數,上述的簡單代碼用手寫的render函數的話會是這樣:

//js文件
render: function (createElement) {
  return createElement('div', {
     attrs: {
        id: 'app'
      },
  }, this.message)
}

兩種方式最終都會調用_update函數生成真實的dom

vm._update(vm._render(), hydrating)

在這裏插入圖片描述
結合上圖再回到我們的$mount方法

//緩存原型鏈上的定義好的$mount
const mount = Vue.prototype.$mount
//,重新定義$mount, hydrate參數爲true表明是服務端
Vue.prototype.$mount = function (el, hydrating){
 //拿到要進行掛載的錨點,一般是#app
  el = el && query(el)
//錨點元素會被整個替換,因此要判斷錨點元素是否爲html或者body,爲這兩者時報錯
  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
  }
//判斷傳入的options是否有render函數
const options = this.$options
if (!options.render) {
 //直接在Vue函數裏面定義template的情況
  let template = options.template
  if (template) {
    if (typeof template === 'string') {
      if (template.charAt(0) === '#') {
      //通過id拿到html字符串
        template = idToTemplate(template)
        if (process.env.NODE_ENV !== 'production' && !template) {
          warn(
            `Template element not found or is empty: ${options.template}`,
            this
          )
        }
      }
    } else if (template.nodeType) {
    //模板是一個dom對象,也拿到html字符串
      template = template.innerHTML
    } else {
    //無template,無render函數,報錯
      if (process.env.NODE_ENV !== 'production') {
        warn('invalid template option:' + template, this)
      }
      return this
    }
  } else if (el) {
  //無template參數,無render參數,而是像上面的例子一樣在html中直接使用插值
    template = getOuterHTML(el)
  }

//如果模板存在的話,進入編譯的過程
if (template) {
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    mark('compile')
  }
  
 //傳入特定的平臺相關的參數,利用閉包特性編譯生成平臺相關的compileToFunction函數,好處是在數據發生變化需要重新
 //生成dom的時候不必再次創建相同的函數,提高了性能
  const { render, staticRenderFns } = compileToFunctions(template, {
    shouldDecodeNewlines,
    shouldDecodeNewlinesForHref,
    delimiters: options.delimiters,
    comments: options.comments
  }, this)
  
  //將生成的render和staticRenderFun掛載到options上
  options.render = render
  options.staticRenderFns = staticRenderFns
  
  //和性能相關的錨點,可暫時不管
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    mark('compile end')
    measure(`vue ${this._name} compile`, 'compile', 'compile end')
    }
  }
} 
//調用了原型鏈上緩存的$mount 
return mount.call(this, el, hydrating)}

爲什麼要緩存$mount然後又定義一遍呢?原因是vue最終只認render函數,在runtime-only版本是不需要走這段邏輯的,webpack打包工具已經生成了render函數,而compiler版本則需要將模板編譯成render函數

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean //參數是和服務端渲染相關,在瀏覽器環境下我們不需要傳第二個參數
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

下面來看看mountComponent方法,這個方法定義在 src/core/instance/lifecycle.js 文件中

export function mountComponent(
  vm: Component,
  el: ? Element,
  hydrating ? : boolean 
): Component {
  vm.$el = el
  //首先還是判斷template或者render函數是否存在
  if (!vm.$options.render) {
      vm.$options.render = createEmptyVNode
      if (process.env.NODE_ENV !== 'production') {
          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
   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)
   	      //調用_render函數生成virtual dom
   	      const vnode = vm._render()
   	      mark(endTag)
   	      measure(`vue ${name} render`, startTag, endTag)
   	
   	      mark(startTag)
   	       //調用_update方法根據virtual dom生成真實的dom
   	      vm._update(vnode, hydrating)
   	      mark(endTag)
   	      measure(`vue ${name} patch`, startTag, endTag)
       }
   } else {
       updateComponent = () => {
           vm._update(vm._render(), hydrating)
       }
   }
   //渲染watcher,與響應式原理有關,後面會仔細講
   new Watcher(vm, updateComponent, noop, {
       before() {
           if (vm._isMounted) {
               callHook(vm, 'beforeUpdate')
           }
       }
   }, true /* 是否爲renderWatcher*/ )wa
   hydrating = false
   if (vm.$vnode == null) {
       vm._isMounted = true
       callHook(vm, 'mounted')
   }
   return vm
}

可以看到除了和性能相關的錨點之外,兩段邏輯都執行了vm._render方法生成vnode, 然後調用update方法生成真實的dom

const vnode = vm._render()

vm._update(vm._render(), hydrating)

接下來看看我們的vnode以及vdom的生成過程,首先要了解的是瀏覽器中生成dom是很昂貴的,如圖
在這裏插入圖片描述
而vnode實際上就是通過一個js對象去描述dom,在渲染的之前通過differ算法判斷vnode是否發生了變化進而確定是否要生成這個dom,vue的vdom借鑑了snabbdom的實現

export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;


// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node

constructor (
 tag?: string,
 data?: VNodeData,
 children?: ?Array<VNode>,
 text?: string,
 elm?: Node,
 context?: Component,
 componentOptions?: VNodeComponentOptions,
 asyncFactory?: Function
) {
 this.tag = tag
 this.data = data
 this.children = children
 this.text = text
 this.elm = elm
 this.ns = undefined

}

// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
 return this.componentInstance
}
}

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章