從這一小節開始,正式進入
Vue
源碼的核心,也是難點之一,響應式系統的構建。這一節將做爲分析響應式構建過程源碼的入門,主要分爲兩大塊,第一塊是針對響應式數據props,methods,data,computed,wather
初始化過程的分析,另外一塊則是在保留源碼設計理念的前提下,嘗試手動構建一個基礎的響應式系統。有了這兩個基礎內容的鋪墊,下一篇進行源碼具體細節的分析會更加駕輕就熟。vue
回顧一下以前的內容,咱們對Vue
源碼的分析是從初始化開始,初始化_init
會執行一系列的過程,這個過程包括了配置選項的合併,數據的監測代理,最後纔是實例的掛載。而在實例掛載前還有意忽略了一個重要的過程,數據的初始化(即initState(vm)
)。initState
的過程,是對數據進行響應式設計的過程,過程會針對props,methods,data,computed
和watch
作數據的初始化處理,並將他們轉換爲響應式對象,接下來咱們會逐步分析每個過程。node
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
// 初始化props
if (opts.props) { initProps(vm, opts.props); }
// 初始化methods
if (opts.methods) { initMethods(vm, opts.methods); }
// 初始化data
if (opts.data) {
initData(vm);
} else {
// 若是沒有定義data,則建立一個空對象,並設置爲響應式
observe(vm._data = {}, true /* asRootData */);
}
// 初始化computed
if (opts.computed) { initComputed(vm, opts.computed); }
// 初始化watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
複製代碼
簡單回顧一下props
的用法,父組件經過屬性的形式將數據傳遞給子組件,子組件經過props
屬性接收父組件傳遞的值。算法
// 父組件
<child :test="test"></child>
var vm = new Vue({
el: '#app',
data() {
return {
test: 'child'
}
}
})
// 子組件
Vue.component('child', {
template: '<div>{{test}}</div>',
props: ['test']
})
複製代碼
所以分析props
須要分析父組件和子組件的兩個過程,咱們先看父組件對傳遞值的處理。按照以往文章介紹的那樣,父組件優先進行模板編譯獲得一個render
函數,在解析過程當中遇到子組件的屬性,:test=test
會被解析成{ attrs: {test: test}}
並做爲子組件的render
函數存在,以下所示:數組
with(){..._c('child',{attrs:{"test":test}})}
複製代碼
render
解析Vnode
的過程遇到child
這個子佔位符節點,所以會進入建立子組件Vnode
的過程,建立子Vnode
過程是調用createComponent
,這個階段咱們在組件章節有分析過,在組件的高級用法也有分析過,最終會調用new Vnode
去建立子Vnode
。而對於props
的處理,extractPropsFromVNodeData
會對attrs
屬性進行規範校驗後,最後會把校驗後的結果以propsData
屬性的形式傳入Vnode
構造器中。總結來講,props
傳遞給佔位符組件的寫法,會以propsData
的形式做爲子組件Vnode
的屬性存在。下面會分析具體的細節。瀏覽器
// 建立子組件過程
function createComponent() {
// props校驗
var propsData = extractPropsFromVNodeData(data, Ctor, tag);
···
// 建立子組件vnode
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
}
複製代碼
先看檢測props
規範性的過程。**props
編譯後的結果有兩種,其中attrs
前面分析過,是編譯生成render
函數針對屬性的處理,而props
是針對用戶自寫render
函數的屬性值。**所以須要同時對這兩種方式進行校驗。app
function extractPropsFromVNodeData (data,Ctor,tag) {
// Ctor爲子類構造器
···
var res = {};
// 子組件props選項
var propOptions = Ctor.options.props;
// data.attrs針對編譯生成的render函數,data.props針對用戶自定義的render函數
var attrs = data.attrs;
var props = data.props;
if (isDef(attrs) || isDef(props)) {
for (var key in propOptions) {
// aB 形式轉成 a-b
var altKey = hyphenate(key);
{
var keyInLowerCase = key.toLowerCase();
if (
key !== keyInLowerCase &&
attrs && hasOwn(attrs, keyInLowerCase)
) {
// 警告
}
}
}
}
}
複製代碼
重點說一下源碼在這一部分的處理,HTML對大小寫是不敏感的,全部的瀏覽器會把大寫字符解釋爲小寫字符,所以咱們在使用DOM
中的模板時,cameCase(駝峯命名法)的props
名須要使用其等價的 kebab-case
(短橫線分隔命名) 命代替。 即: <child :aB="test"></child>
須要寫成<child :a-b="test"></child>
框架
剛纔說到分析props
須要兩個過程,前面已經針對父組件對props
的處理作了描述,而對於子組件而言,咱們是經過props
選項去接收父組件傳遞的值。咱們再看看子組件對props
的處理:async
子組件處理props
的過程,是發生在父組件_update
階段,這個階段是Vnode
生成真實節點的過程,期間會遇到子Vnode
,這時會調用createComponent
去實例化子組件。而實例化子組件的過程又回到了_init
初始化,此時又會經歷選項的合併,針對props
選項,最終會統一成{props: { test: { type: null }}}
的寫法。接着會調用initProps
, initProps
作的事情,簡單歸納一句話就是,將組件的props
數據設置爲響應式數據。函數
function initProps (vm, propsOptions) {
var propsData = vm.$options.propsData || {};
var loop = function(key) {
···
defineReactive(props,key,value,cb);
if (!(key in vm)) {
proxy(vm, "_props", key);
}
}
// 遍歷props,執行loop設置爲響應式數據。
for (var key in propsOptions) loop( key );
}
複製代碼
其中proxy(vm, "_props", key);
爲props
作了一層代理,用戶經過vm.XXX
能夠代理訪問到vm._props
上的值。針對defineReactive
,本質上是利用Object.defineProperty
對數據的getter,setter
方法進行重寫,具體的原理能夠參考數據代理章節的內容,在這小節後半段也會有一個基本的實現。oop
initMethod
方法和這一節介紹的響應式沒有任何的關係,他的實現也相對簡單,主要是保證methods
方法定義必須是函數,且命名不能和props
重複,最終會將定義的方法都掛載到根實例上。
function initMethods (vm, methods) {
var props = vm.$options.props;
for (var key in methods) {
{
// method必須爲函數形式
if (typeof methods[key] !== 'function') {
warn(
"Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
"Did you reference the function correctly?",
vm
);
}
// methods方法名不能和props重複
if (props && hasOwn(props, key)) {
warn(
("Method \"" + key + "\" has already been defined as a prop."),
vm
);
}
// 不能以_ or $.這些Vue保留標誌開頭
if ((key in vm) && isReserved(key)) {
warn(
"Method \"" + key + "\" conflicts with an existing Vue instance method. " +
"Avoid defining component methods that start with _ or $."
);
}
}
// 直接掛載到實例的屬性上,能夠經過vm[method]訪問。
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
}
}
複製代碼
data
在初始化選項合併時會生成一個函數,只有在執行函數時纔會返回真正的數據,因此initData
方法會先執行拿到組件的data
數據,而且會對對象每一個屬性的命名進行校驗,保證不能和props,methods
重複。最後的核心方法是observe
,observe
方法是將數據對象標記爲響應式對象,並對對象的每一個屬性進行響應式處理。與此同時,和props
的代理處理方式同樣,proxy
會對data
作一層代理,直接經過vm.XXX
能夠代理訪問到vm._data
上掛載的對象屬性。
function initData(vm) {
var data = vm.$options.data;
// 根實例時,data是一個對象,子組件的data是一個函數,其中getData會調用函數返回data對象
data = vm._data = typeof data === 'function'? getData(data, vm): data || {};
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
// 命名不能和方法重複
if (methods && hasOwn(methods, key)) {
warn(("Method \"" + key + "\" has already been defined as a data property."),vm);
}
}
// 命名不能和props重複
if (props && hasOwn(props, key)) {
warn("The data property \"" + key + "\" is already declared as a prop. " + "Use prop default value instead.",vm);
} else if (!isReserved(key)) {
// 數據代理,用戶可直接經過vm實例返回data數據
proxy(vm, "_data", key);
}
}
// observe data
observe(data, true /* asRootData */);
}
複製代碼
最後講講observe
,observe
具體的行爲是將數據對象添加一個不可枚舉的屬性__ob__
,標誌對象是一個響應式對象,而且拿到每一個對象的屬性值,重寫getter,setter
方法,使得每一個屬性值都是響應式數據。詳細的代碼咱們後面分析。
和上面的分析方法同樣,initComputed
是computed
數據的初始化,不一樣之處在於如下幾點:
computed
能夠是對象,也能夠是函數,可是對象必須有getter
方法,所以若是computed
中的屬性值是對象時須要進行驗證。computed
的每一個屬性,要建立一個監聽的依賴,也就是實例化一個watcher
,watcher
的定義,能夠暫時理解爲數據使用的依賴自己,一個watcher
實例表明多了一個須要被監聽的數據依賴。除了不一樣點,initComputed
也會將每一個屬性設置成響應式的數據,一樣的,也會對computed
的命名作檢測,防止與props,data
衝突。
function initComputed (vm, computed) {
···
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
// computed屬性爲對象時,要保證有getter方法
if (getter == null) {
warn(("Getter is missing for computed property \"" + key + "\"."),vm);
}
if (!isSSR) {
// 建立computed watcher
watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions);
}
if (!(key in vm)) {
// 設置爲響應式數據
defineComputed(vm, key, userDef);
} else {
// 不能和props,data命名衝突
if (key in vm.$data) {
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
}
}
}
}
複製代碼
顯然Vue
提供了不少種數據供開發者使用,可是分析完後發現每一個處理的核心都是將數據轉化成響應式數據,有了響應式數據,如何構建一個響應式系統呢?前面提到的watcher
又是什麼東西?構建響應式系統還須要其餘的東西嗎?接下來咱們嘗試着去實現一個極簡風的響應式系統。
Vue
的響應式系統構建是比較複雜的,直接進入源碼分析構建的每個流程會讓理解變得困難,所以我以爲在儘量保留源碼的設計邏輯下,用最小的代碼構建一個最基礎的響應式系統是有必要的。對Dep,Watcher,Observer
概念的初步認識,也有助於下一篇對響應式系統設計細節的分析。
咱們以MyVue
做爲類響應式框架,框架的搭建不作贅述。咱們模擬Vue
源碼的實現思路,實例化MyVue
時會傳遞一個選項配置,精簡的代碼只有一個id
掛載元素和一個數據對象data
。模擬源碼的思路,咱們在實例化時會先進行數據的初始化,這一步就是響應式的構建,咱們稍後分析。數據初始化後開始進行真實DOM
的掛載。
var vm = new MyVue({
id: '#app',
data: {
test: 12
}
})
// myVue.js
(function(global) {
class MyVue {
constructor(options) {
this.options = options;
// 數據的初始化
this.initData(options);
let el = this.options.id;
// 實例的掛載
this.$mount(el);
}
initData(options) {
}
$mount(el) {
}
}
}(window))
複製代碼
首先引入一個類Observer
,這個類的目的是將數據變成響應式對象,利用Object.defineProperty
對數據的getter,setter
方法進行改寫。在數據讀取getter
階段咱們會進行依賴的收集,在數據的修改setter
階段,咱們會進行依賴的更新(這兩個概念的介紹放在後面)。所以在數據初始化階段,咱們會利用Observer
這個類將數據對象修改成相應式對象,而這是全部流程的基礎。
class MyVue {
initData(options) {
if(!options.data) return;
this.data = options.data;
// 將數據重置getter,setter方法
new Observer(options.data);
}
}
// Observer類的定義
class Observer {
constructor(data) {
// 實例化時執行walk方法對每一個數據屬性重寫getter,setter方法
this.walk(data)
}
walk(obj) {
const keys = Object.keys(obj);
for(let i = 0;i< keys.length; i++) {
// Object.defineProperty的處理邏輯
defineReactive(obj, keys[i])
}
}
}
複製代碼
咱們能夠這樣理解,一個Watcher
實例就是一個依賴,數據不論是在渲染模板時使用仍是在用戶計算時使用,均可以算作一個須要監聽的依賴,watcher
中記錄着這個依賴監聽的狀態,以及如何更新操做的方法。
// 監聽的依賴
class Watcher {
constructor(expOrFn, isRenderWatcher) {
this.getter = expOrFn;
// Watcher.prototype.get的調用會進行狀態的更新。
this.get();
}
get() {}
}
複製代碼
那麼哪一個時間點會實例化watcher
並更新數據狀態呢?顯然在渲染數據到真實DOM
時能夠建立watcher
。$mount
流程前面章節介紹過,會經歷模板生成render
函數和render
函數渲染真實DOM
的過程。咱們對代碼作了精簡,updateView
濃縮了這一過程。
class MyVue {
$mount(el) {
// 直接改寫innerHTML
const updateView = _ => {
let innerHtml = document.querySelector(el).innerHTML;
let key = innerHtml.match(/{(\w+)}/)[1];
document.querySelector(el).innerHTML = this.options.data[key]
}
// 建立一個渲染的依賴。
new Watcher(updateView, true)
}
}
複製代碼
watcher
若是理解爲每一個數據須要監聽的依賴,那麼Dep
能夠理解爲對依賴的一種管理。數據能夠在渲染中使用,也能夠在計算屬性中使用。相應的每一個數據對應的watcher
也有不少。而咱們在更新數據時,如何通知到數據相關的每個依賴,這就須要Dep
進行通知管理了。而且瀏覽器同一時間只能更新一個watcher
,因此也須要一個屬性去記錄當前更新的watcher
。而Dep
這個類只須要作兩件事情,將依賴進行收集,派發依賴進行更新。
let uid = 0;
class Dep {
constructor() {
this.id = uid++;
this.subs = []
}
// 依賴收集
depend() {
if(Dep.target) {
// Dep.target是當前的watcher,將當前的依賴推到subs中
this.subs.push(Dep.target)
}
}
// 派發更新
notify() {
const subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
// 遍歷dep中的依賴,對每一個依賴執行更新操做
subs[i].update();
}
}
}
Dep.target = null;
複製代碼
咱們看看數據攔截的過程。前面的Observer
實例化最終會調用defineReactive
重寫getter,setter
方法。這個方法開始會實例化一個Dep
,也就是建立一個數據的依賴管理。在重寫的getter
方法中會進行依賴的收集,也就是調用dep.depend
的方法。在setter
階段,比較兩個數不一樣後,會調用依賴的派發更新。即dep.notify
const defineReactive = (obj, key) => {
const dep = new Dep();
const property = Object.getOwnPropertyDescriptor(obj);
let val = obj[key]
if(property && property.configurable === false) return;
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
// 作依賴的收集
if(Dep.target) {
dep.depend()
}
return val
},
set(nval) {
if(nval === val) return
// 派發更新
val = nval
dep.notify();
}
})
}
複製代碼
回過頭來看watcher
,實例化watcher
時會將Dep.target
設置爲當前的watcher
,執行完狀態更新函數以後,再將Dep.target
置空。這樣在收集依賴時只要將Dep.target
當前的watcher push
到Dep
的subs
數組便可。而在派發更新階段也只須要從新更新狀態便可。
class Watcher {
constructor(expOrFn, isRenderWatcher) {
this.getter = expOrFn;
// Watcher.prototype.get的調用會進行狀態的更新。
this.get();
}
get() {
// 當前執行的watcher
Dep.target = this
this.getter()
Dep.target = null;
}
update() {
this.get()
}
}
複製代碼
一個極簡的響應式系統搭建完成。在精簡代碼的同時,保持了源碼設計的思想和邏輯。有了這一步的基礎,接下來深刻分析源碼中每一個環節的實現細節會更加簡單。
這一節內容,咱們正式進入響應式系統的介紹,前面在數據代理章節,咱們學過Object.defineProperty
,這是一個用來進行數據攔截的方法,而響應式系統構建的基礎就是數據的攔截。咱們先介紹了Vue
內部在初始化數據的過程,最終得出的結論是,不論是data,computed
,仍是其餘的用戶定義數據,最終都是調用Object.defineProperty
進行數據攔截。而文章的最後,咱們在保留源碼設計思想和邏輯的前提下,構建出了一個簡化版的響應式系統。完整的功能有助於咱們下一節對源碼具體實現細節的分析和思考。