摘要:構造函數(shù)文件路徑初始化這里只拿對例子理解最關鍵的步驟分析。在最后,調用了對數(shù)據(jù)進行。每個函數(shù)之后都會返回一個。就是去實例化指令,將指令和新建的元素在一起,然后將元素替換到中去。
</>復制代碼
下面的代碼會在頁面上輸出Hello World,但是在這個new Vue()到頁面渲染之間,到底發(fā)生了什么。這篇文章希望通過最簡單的例子,去了解Vue源碼過程。這里分析的源碼版本是Vue.version = "1.0.20"
</>復制代碼
{{message}}
</>復制代碼
var vm = new Vue({
el: "#mountNode",
data: function () {
return {
message: "Hello World"
};
}
});
這篇文章將要解決幾個問題:
new Vue()的過程中,內部到底有哪些步驟
如何收集依賴
如何計算表達式
如何表達式的值如何反應在DOM上的
簡單來說過程是這樣的:
observe: 把{message: "Hello World"}變成是reactive的
compile: compileTextNode "{{message}}",解析出指令(directive = v-text)和表達式(expression = message),創(chuàng)建fragment(new TextNode)準備替換
link:實例化directive,將創(chuàng)建的fragment和directive鏈接起來,將fragment替換在DOM上
bind: 通過directive對應的watcher獲取依賴(message)的值("Hello World"),v-text去update值到fragment上
詳細過程,接著往下看。
構造函數(shù)文件路徑: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實例后執(zhí)行,執(zhí)行結果: {message: "Hello World"}
var instanceData = childVal.call(vm)
// 對象之間的merge,類似$.extend,結果肯定就是:{message: "Hello World"}
return mergeData(instanceData, parentVal)
}
init data
_initData()發(fā)生在_initState()中,主要做了兩件事:
代理data中的屬性
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函數(shù)
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)對數(shù)據(jù)進行observe。在hello world例子里,observe()函數(shù)主要是針對{message: "Hello World"}創(chuàng)建了Observer對象。
文件路徑:src/observer/index.js
</>復制代碼
var ob = new Observer(value) // value = data = {message:"Hello World"}
在observe()函數(shù)中還做了些能否observe的條件判斷,這些條件有:
沒有被observe過(observe過的對象都會被添加__ob__屬性)
只能是plain object(toString.call(ob) === "[object Object]")或者數(shù)組
不能是Vue實例(obj._isVue !== true)
object是extensible的(Object.isExtensible(obj) === true)
Observer官網(wǎng)的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變成“發(fā)布者”,watcher是訂閱者,訂閱data的變化。
在例子中,創(chuàng)建observer的過程是:
new Observer({message: "Hello World"})
實例化一個Dep對象,用來收集依賴
walk(Observer.prototype.walk())數(shù)據(jù)的每一個屬性,這里只有message
將屬性變成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觀察這個數(shù)據(jù)的依賴(watcher)
}
})
}
關于依賴收集和notify,主要是Dep類
文件路徑:src/observer/dep.js
</>復制代碼
export default function Dep () {
this.id = uid++
this.subs = []
}
這里的subs是保存著訂閱者(即watcher)的數(shù)組,當被觀察數(shù)據(jù)發(fā)生變化時,即被調用setter,那么dep.notify()就循環(huán)這里的訂閱者,分別調用他們的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
compilecompile過程是分析給定元素(el)或者模版(template),提取指令(directive)和創(chuàng)建對應離線的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元素,大致過程如下:
compileRoot:由于root node(
)本身沒有任何指令,所以這里compile不出什么東西compileChildNode:mountNode的子node,即內容為"{{message}}"的TextNode
compileTextNode:
3.1 parseText:其實就是tokenization(標記化:從字符串中提取符號,語句等有意義的元素),得到的結果是tokens
3.2 processTextToken:從tokens中分析出指令類型,表達式和過濾器,并創(chuàng)建新的空的TextNode
3.3 創(chuàng)建fragment,將新的TextNode append進去
parseText的時候,通過正則表達式(/{{{(.+?)}}}|{{(.+?)}}/g)匹配字符串"{{message}}",得出的token包含這些信息:“這是個tag,而且是文本(text)而非HTML的tag,不是一次性的插值(one-time interpolation),tag的內容是"message"”。這里用來做匹配的正則表達式是會根據(jù)delimiters和unsafeDelimiters的配置動態(tài)生成的。
processTextToken之后,其實就得到了創(chuàng)建指令需要的所有信息:指令類型v-text,表達式"message",過濾器無,并且該指令負責跟進的DOM是新創(chuàng)建的TextNode。接下來就是實例化指令了。
link每個compile函數(shù)之后都會返回一個link function(linkFn)。linkFn就是去實例化指令,將指令和新建的元素link在一起,然后將元素替換到DOM tree中去。
每個linkFn函數(shù)都會返回一個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()構造函數(shù)里面只是一些內部屬性的賦值,真正的綁定過程還需要調用Directive.prototype._bind,它是在Vue實例方法_bindDir()中被調用的。
在_bind里面,會創(chuàng)建watcher,并第一次通過watcher去獲得表達式"message"的計算值,更新到之前新建的TextNode中去,完成在頁面上渲染"Hello World"。
</>復制代碼
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.
每個與數(shù)據(jù)綁定的directive都有一個watcher,幫它監(jiān)聽表達式的值,如果發(fā)生變化,則通知它update自己負責的DOM。一直說的dependency collection就在這里發(fā)生。
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",單一變量,被認為是簡單的數(shù)據(jù)訪問路徑(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) // 即執(zhí)行vm.message
由于initState時對數(shù)據(jù)進行了代理(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獲取表達式的值分三步:
beforeGet:設置Dep.target = this
調用表達式的getter,讀取(getter)vm.message的值,進入了message的reactiveGetter,由于Dep.target有值,因此執(zhí)行了dep.depend()將target,即當前watcher,收入dep.subs數(shù)組里
afterGet:設置Dep.target = null
這里值得注意的是Dep.target,由于JS的單線程特性,同一時刻只能有一個watcher去get數(shù)據(jù)的值,所以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了表達式中涉及到的數(shù)據(jù),同時被該數(shù)據(jù)(reactive data)保存為其變化的訂閱者(subscriber),數(shù)據(jù)變化時,通過dep.notify() -> watcher.update() -> directive.update() -> textDirective.update(),完成DOM的更新。
到這里,“Hello World”怎么渲染到頁面上的過程基本就結束了。這里針對最簡單的使用,挑選了最核心的步驟進行分析,更多內部細節(jié),后面慢慢分享。
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/90930.html
摘要:最后舉兩個例子,回顧上面的內容例一改變的是數(shù)組元素中屬性,由于創(chuàng)建的的指令,因此這里直接由指令更新對應元素的內容。 下面例子來自官網(wǎng),雖然看上去就比Hello World多了一個v-for,但是內部多了好多的處理過程。但是這就是框架,只給你留下最美妙的東西,讓生活變得簡單。 {{ todo.text }} ...
摘要:本文是小羊根據(jù)文檔進行解讀的第一篇文章,主要內容涵蓋的基礎部分的知識的,文章順序基本按照官方文檔的順序,每個知識點現(xiàn)附上代碼,然后根據(jù)代碼給予個人的一些理解,最后還放上在線編輯的代碼以供練習和測試之用在最后,我參考上的一篇技博,對進行初入的 本文是小羊根據(jù)Vue.js文檔進行解讀的第一篇文章,主要內容涵蓋Vue.js的基礎部分的知識的,文章順序基本按照官方文檔的順序,每個知識點現(xiàn)附上代...
摘要:本文是小羊根據(jù)文檔進行解讀的第一篇文章,主要內容涵蓋的基礎部分的知識的,文章順序基本按照官方文檔的順序,每個知識點現(xiàn)附上代碼,然后根據(jù)代碼給予個人的一些理解,最后還放上在線編輯的代碼以供練習和測試之用在最后,我參考上的一篇技博,對進行初入的 本文是小羊根據(jù)Vue.js文檔進行解讀的第一篇文章,主要內容涵蓋Vue.js的基礎部分的知識的,文章順序基本按照官方文檔的順序,每個知識點現(xiàn)附上代...
摘要:無數(shù)的模板語言和框架應運而生但是技術始終被分割為前端和后端。這意味著一個頁面可以有很多的這并不會對其余的頁面有任何影響。提前綁定和編譯預測是一個非常有效的部署方式。最后,這是我們對于這個特定問題的貢獻。 Next.js 原文地址 Naoyuki Kanezawa (@nkzawa), Guillermo Rauch (@rauchg) 和 Tony Kovanen (@tonykova...
摘要:今日最佳對于程序員而言,所謂的二八定律指的是花百分之八十的時間去學習日常研發(fā)中不常見的那百分之二十的原理。 【今日最佳】對于程序員而言,所謂的二八定律指的是 花百分之八十的時間去學習日常研發(fā)中不常見的那百分之二十的原理。 據(jù)說阿里某程序員對書法十分感興趣,退休后決定在這方面有所建樹。于是花重金購買了上等的文房四寶。 一日,飯后突生雅興,一番磨墨擬紙,并點上了上好的檀香,頗有王羲之風范,...
閱讀 650·2021-09-22 10:02
閱讀 6414·2021-09-03 10:49
閱讀 573·2021-09-02 09:47
閱讀 2160·2019-08-30 15:53
閱讀 2938·2019-08-30 15:44
閱讀 909·2019-08-30 13:20
閱讀 1824·2019-08-29 16:32
閱讀 896·2019-08-29 12:46