Vue.js源碼(1):Hello World的背後

下面的代碼會在頁面上輸出Hello World,可是在這個new Vue()到頁面渲染之間,到底發生了什麼。這篇文章但願經過最簡單的例子,去了解Vue源碼過程。這裏分析的源碼版本是Vue.version = '1.0.20'html

<div id="mountNode">{{message}}</div>
var vm = new Vue({
    el: '#mountNode',
    data: function () {
        return {
            message: 'Hello World'
        };
    }
});

這篇文章將要解決幾個問題:vue

  1. new Vue()的過程當中,內部到底有哪些步驟node

  2. 如何收集依賴react

  3. 如何計算表達式正則表達式

  4. 如何表達式的值如何反應在DOM上的算法

簡單來講過程是這樣的:express

  1. observe: 把{message: 'Hello World'}變成是reactive的api

  2. compile: compileTextNode "{{message}}",解析出指令(directive = v-text)和表達式(expression = message),建立fragment(new TextNode)準備替換數組

  3. link:實例化directive,將建立的fragment和directive連接起來,將fragment替換在DOM上app

  4. bind: 經過directive對應的watcher獲取依賴(message)的值("Hello World"),v-text去update值到fragment上

詳細過程,接着往下看。

構造函數

文件路徑:src/instance/vue.js

function Vue (options) {
  this._init(options)
}

初始化

這裏只拿對例子理解最關鍵的步驟分析。
文件路徑:src/instance/internal/init.js

Vue.prototype._init = function (options) {
    ...
    // merge options.
    options = this.$options = mergeOptions(
      this.constructor.options,
      options,
      this
    )
    ...
    // initialize data observation and scope inheritance.
    this._initState()
    ...
    // if `el` option is passed, start compilation.
    if (options.el) {
      this.$mount(options.el)
    }
}

merge options

mergeOptions()定義在src/util/options.js文件中,這裏主要定義options中各類屬性的合併(merge),例如:props, methods, computed, watch等。另外,這裏還定義了每種屬性merge的默認算法(strategy),這些strategy均可以配置的,參考Custom Option Merge Strategy

在本文的例子中,主要是data選項的merge,在merge以後,放到$options.data中,基本至關於下面這樣:

vm.$options.data = function mergedInstanceDataFn () {
      var parentVal = undefined
      
      // 這裏就是在咱們定義的options中的data
      var childVal = function () {
          return {
              message: 'Hello World'
          }
      }
      
      // data function綁定vm實例後執行,執行結果: {message: 'Hello World'}
      var instanceData = childVal.call(vm)
      
      // 對象之間的merge,相似$.extend,結果確定就是:{message: 'Hello World'}
      return mergeData(instanceData, parentVal)
}

init data

_initData()發生在_initState()中,主要作了兩件事:

  1. 代理data中的屬性

  2. observe data

文件路徑:src/instance/internal/state.js

Vue.prototype._initState = function () {
    this._initProps()
    this._initMeta()
    this._initMethods()
    this._initData() // 這裏
    this._initComputed()
  }

屬性代理(proxy)

把data的結果賦值給內部屬性:
文件路徑:src/instance/internal/state.js

var dataFn = this.$options.data // 上面咱們獲得的mergedInstanceDataFn函數
var data = this._data = dataFn ? dataFn() : {}

代理(proxy)data中的屬性到_data,使得vm.message === vm._data.message
文件路徑:src/instance/internal/state.js

/**
  * Proxy a property, so that
  * vm.prop === vm._data.prop
  */
Vue.prototype._proxy = function (key) {
    if (!isReserved(key)) {
      var self = this
      Object.defineProperty(self, key, {
        configurable: true,
        enumerable: true,
        get: function proxyGetter () {
          return self._data[key]
        },
        set: function proxySetter (val) {
          self._data[key] = val
        }
      })
    }
  }

observe

這裏是咱們的第一個重點,observe過程。在_initData()最後,調用了observe(data, this)對數據進行observe。在hello world例子裏,observe()函數主要是針對{message: 'Hello World'}建立了Observer對象。
文件路徑:src/observer/index.js

var ob = new Observer(value) // value = data = {message:'Hello World'}

observe()函數中還作了些可否observe的條件判斷,這些條件有:

  1. 沒有被observe過(observe過的對象都會被添加__ob__屬性)

  2. 只能是plain object(toString.call(ob) === "[object Object]")或者數組

  3. 不能是Vue實例(obj._isVue !== true

  4. object是extensible的(Object.isExtensible(obj) === true

Observer

官網的Reactivity in Depth上有這麼句話:

When you pass a plain JavaScript object to a Vue instance as its data option, Vue.js will walk through all of its properties and convert them to getter/setters

The getter/setters are invisible to the user, but under the hood they enable Vue.js to perform dependency-tracking and change-notification when properties are accessed or modified

Observer就是幹這個事情的,使data變成「發佈者」,watcher是訂閱者,訂閱data的變化。

圖片描述

在例子中,建立observer的過程是:

  1. new Observer({message: 'Hello World'})

  2. 實例化一個Dep對象,用來收集依賴

  3. walk(Observer.prototype.walk())數據的每個屬性,這裏只有message

  4. 將屬性變成reactive的(Observer.protoype.convert())

convert()裏調用了defineReactive(),給data的message屬性添加reactiveGetter和reactiveSetter
文件路徑:src/observer/index.js

export function defineReactive (obj, key, value) {
    ...
    Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      ...
      if (Dep.target) {
        dep.depend() // 這裏是收集依賴
        ...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      ...
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      ...
      dep.notify() // 這裏是notify觀察這個數據的依賴(watcher)
    }
  })
}

關於依賴收集和notify,主要是Dep
文件路徑:src/observer/dep.js

export default function Dep () {
  this.id = uid++
  this.subs = []
}

這裏的subs是保存着訂閱者(即watcher)的數組,當被觀察數據發生變化時,即被調用setter,那麼dep.notify()就循環這裏的訂閱者,分別調用他們的update方法。

可是在getter收集依賴的代碼裏,並無看到watcher被添加到subs中,何時添加進去的呢?這個問題在講到Watcher的時候再回答。

mount node

按照生命週期圖上,observe data和一些init以後,就是$mount了,最主要的就是_compile
文件路徑:src/instance/api/lifecycle.js

Vue.prototype.$mount = function (el) {
    ...
    this._compile(el)
    ...
  }

_compile裏分兩步:compile和link

compile

compile過程是分析給定元素(el)或者模版(template),提取指令(directive)和建立對應離線的DOM元素(document fragment)。

文件路徑:src/instance/internal/lifecycle.js

Vue.prototype._compile = function (el) {
    ...
    var rootLinker = compileRoot(el, options, contextOptions)
    ...
    var rootUnlinkFn = rootLinker(this, el, this._scope)
    ...
    var contentUnlinkFn = compile(el, options)(this, el)
    ...
}

例子中compile #mountNode元素,大體過程以下:

  1. compileRoot:因爲root node(<div id="mountNode"></div>)自己沒有任何指令,因此這裏compile不出什麼東西

  2. compileChildNode:mountNode的子node,即內容爲"{{message}}"的TextNode

  3. compileTextNode:
    3.1 parseText:其實就是tokenization(標記化:從字符串中提取符號,語句等有意義的元素),獲得的結果是tokens
    3.2 processTextToken:從tokens中分析出指令類型,表達式和過濾器,並建立新的空的TextNode
    3.3 建立fragment,將新的TextNode append進去

parseText的時候,經過正則表達式(/\{\{\{(.+?)\}\}\}|\{\{(.+?)\}\}/g)匹配字符串"{{message}}",得出的token包含這些信息:「這是個tag,並且是文本(text)而非HTML的tag,不是一次性的插值(one-time interpolation),tag的內容是"message"」。這裏用來作匹配的正則表達式是會根據delimitersunsafeDelimiters的配置動態生成的。

processTextToken以後,其實就獲得了建立指令須要的全部信息:指令類型v-text,表達式"message",過濾器無,而且該指令負責跟進的DOM是新建立的TextNode。接下來就是實例化指令了。

link

每一個compile函數以後都會返回一個link function(linkFn)。linkFn就是去實例化指令,將指令和新建的元素link在一塊兒,而後將元素替換到DOM tree中去。
每一個linkFn函數都會返回一個unlink function(unlinkFn)。unlinkFn是在vm銷燬的時候用的,這裏不介紹。

實例化directive:new Directive(description, vm, el)

description是compile結果token中保存的信息,內容以下:

description = {
    name: 'text', // text指令
    expression: 'message',
    filters: undefined,
    def: vTextDefinition
}

def屬性上的是text指令的定義(definition),和Custome Directive同樣,text指令也有bind和update方法,其定義以下:

文件路徑:src/directives/public/text.js

export default {

  bind () {
    this.attr = this.el.nodeType === 3
      ? 'data'
      : 'textContent'
  },

  update (value) {
    this.el[this.attr] = _toString(value)
  }
}

new Directive()構造函數裏面只是一些內部屬性的賦值,真正的綁定過程還須要調用Directive.prototype._bind,它是在Vue實例方法_bindDir()中被調用的。
在_bind裏面,會建立watcher,並第一次經過watcher去得到表達式"message"的計算值,更新到以前新建的TextNode中去,完成在頁面上渲染"Hello World"。

watcher

For every directive / data binding in the template, there will be a corresponding watcher object, which records any properties 「touched」 during its evaluation as dependencies. Later on when a dependency’s setter is called, it triggers the watcher to re-evaluate, and in turn causes its associated directive to perform DOM updates.

每一個與數據綁定的directive都有一個watcher,幫它監聽表達式的值,若是發生變化,則通知它update本身負責的DOM。一直說的dependency collection就在這裏發生。

Directive.prototype._bind()裏面,會new Watcher(expression, update),把表達式和directive的update方法傳進去。

Watcher會去parseExpression
文件路徑:src/parsers/expression.js

export function parseExpression (exp, needSet) {
  exp = exp.trim()
  // try cache
  var hit = expressionCache.get(exp)
  if (hit) {
    if (needSet && !hit.set) {
      hit.set = compileSetter(hit.exp)
    }
    return hit
  }
  var res = { exp: exp }
  res.get = isSimplePath(exp) && exp.indexOf('[') < 0
    // optimized super simple getter
    ? makeGetterFn('scope.' + exp)
    // dynamic getter
    : compileGetter(exp)
  if (needSet) {
    res.set = compileSetter(exp)
  }
  expressionCache.put(exp, res)
  return res
}

這裏的expression是"message",單一變量,被認爲是簡單的數據訪問路徑(simplePath)。simplePath的值如何計算,怎麼經過"message"字符串得到data.message的值呢?
獲取字符串對應的變量的值,除了用eval,還能夠用Function。上面的makeGetterFn('scope.' + exp)返回:

var getter = new Function('scope', 'return ' + body + ';') // new Function('scope', 'return scope.message;')

Watch.prototype.get()獲取表達式值的時候,

var scope = this.vm
getter.call(scope, scope) // 即執行vm.message

因爲initState時對數據進行了代理(proxy),這裏的vm.message即爲vm._data.message,便是data選項中定義的"Hello World"。

值拿到了,那何時將message設爲依賴的呢?這就要結合前面observe data裏說到的reactiveGetter了。
文件路徑:src/watcher.js

Watcher.prototype.get = function () {
  this.beforeGet()        // -> Dep.target = this
  var scope = this.scope || this.vm
  ...
  var value value = this.getter.call(scope, scope)
  ...
  this.afterGet()         // -> Dep.target = null
  return value
}

watcher獲取表達式的值分三步:

  1. beforeGet:設置Dep.target = this

  2. 調用表達式的getter,讀取(getter)vm.message的值,進入了message的reactiveGetter,因爲Dep.target有值,所以執行了dep.depend()將target,即當前watcher,收入dep.subs數組裏

  3. afterGet:設置Dep.target = null

這裏值得注意的是Dep.target,因爲JS的單線程特性,同一時刻只能有一個watcher去get數據的值,因此target在全局下只須要有一個就能夠了。
文件路徑:src/observer/dep.js

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null

就這樣,指令經過watcher,去touch了表達式中涉及到的數據,同時被該數據(reactive data)保存爲其變化的訂閱者(subscriber),數據變化時,經過dep.notify() -> watcher.update() -> directive.update() -> textDirective.update(),完成DOM的更新。

到這裏,「Hello World」怎麼渲染到頁面上的過程基本就結束了。這裏針對最簡單的使用,挑選了最核心的步驟進行分析,更多內部細節,後面慢慢分享。

相關文章
相關標籤/搜索