一、摘要:
Vuejs是一款前端MVVM框架,利用Vuejs、webpack以及周邊一系列生態工具我們可以快速的構建起一個前端應用,網上對於Vue的分析大都是基於各個模塊,理解起來不夠順暢,本文將從整個執行過程出發,講一下Vuejs的核心原理。
二、版本說明:
Vuejs有兩種版本,一種是runtime、一種是runtime-with-compiler,對應的渲染有兩種寫法:
1、render渲染函數:直接寫render函數,渲染時將會調用render函數渲染DOM
<div id="demo"></div> //runtime寫法 var instance = new Vue({ data: { hi: "monrning ", name: "Mr zhang" }, render(h){ //h vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); }; return h('div', this.hi + " " + this.name); } }); instance.$mount(document.getElementById("demo"));
2、HTML模板語法:
<div id="demo">{{hi}} {{name}}</div> new Vue({ el: "#demo" data: { hi: "monrning ", name: "Mr zhang" } });
runtime就是Vue運行時,很多框架的核心都是改自Vue的運行時,比如mpvue的運行部分,以后會講。
runtime與runtime-with-compiler的區別:
顧名思義,前者沒有compiler,后者有compiler。ompiler是Vue的編譯模塊,可以將HTML模板轉換成對應的AST以及render渲染函數提供給Vue使用,所以本質上可以認為Vue的渲染就是在調用render渲染函數,compiler的作用就是在構造渲染函數。本文不講compiler部分,只需要知道compiler會將模板構造成render函數即可,后面理解會用到。
三、模塊分解:
為了便於理解,本人將Vue的核心分成以下幾個部分:數據初始化、數據更新、異步隊列、DOM渲染(虛擬DOM)
數據初始化:初始化調用Object.defineProperty對數據進行劫持,進而監聽數據的變化,后續的更新、渲染都會依賴這一部分;
數據更新:數據監聽實際在數據初始化階段已經完成了,將這一部分獨立出來的原因是數據初始化只做了對數據的set、get進行監聽,邏輯的部分需要在數據更新以及渲染中來看;
異步隊列:異步隊列主要是為了解決在數據更新上觸發多個Watcher如何進行更新的問題;
DOM渲染:這一部分包含虛擬DOM,單獨作為一個部分,其中虛擬DOM高效的diff算法、patch的概念都很重要;
說明一下,這部分的分解是按照我個人的思路總結出來的幾部分(非按順序執行部分),如果有無法下手的情況可以有意識的按照這幾部分來思考一下,僅供大家借鑒使用。
ps:注意上面的初始化和更新僅僅是“數據”的部分,不要跟下面的分析弄混淆。
四、核心原理:
接下來以實際的執行過程來講一下Vue和核心原理,主要包括兩個階段:初始化階段、數據更新階段
以下面的代碼為例:
<html lang="en"> <head> <meta charset="UTF-8"> <title></title> <script src="../dist/vue.js"></script> </head> <body> <div id="demo"> <div > {{title}} {{title2}} </div> <input type="button" @click="testClick" value="click"/> </div> </body> <script> // 這種情況不需要 var instance = new Vue({ el:"#demo", data: { title: "input MVVM", title2: "input MVVM title2" }, methods: { testClick(){ this.title = Math.random(); this.title2 = Math.random(); } } }); </script> </html>
首先講一下初始化階段
1、首先進行“數據的初始化”,代碼如下:
function initData (vm) { var data = vm.$options.data; data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}; if (!isPlainObject(data)) { data = {}; 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 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 ); } } 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)) { proxy(vm, "_data", key); //將data代理到Vue實例上 } } // observe data observe(data, true /* asRootData */); }
可以看到數據初始化包括兩個過程:
(1)、將 data 代理到Vue實例上,之后 data 中的數據可以直接通過Vue實例 this 來設置或者獲取,代理過程如下:
vm._data = typeof data === 'function' ? getData(data, vm) : data || {}; //遍歷data,設置代理 proxy(vm, "_data", key); //proxy 過程 function proxy (target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] }; sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); }
ps: 這里先將數據掛在了vm._data 上,之后再代理到 vm 實例上,前者主要是為了暴漏 $data 使用,最終可以獲取數據的地方有三個: vm (實例)、vm._data、vm.$data;
(2)、劫持數據,進行數據的初始化,底層使用大家了解最多Object.defineProperty(官方宣稱3.0后這部分會使用Proxy來代替)
上面的 observe(data, true /* asRootData */) 最終會調用 defineReactive 進行初始化,直接看這部分:
//{title: 'MVVM'}
function defineReactive$$1 ( obj,//{title: 'MVVM'} key,//title val, customSetter, shallow ) { var dep = new Dep();//依賴收集器 每個key一個 var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; if ((!getter || setter) && arguments.length === 2) { val = obj[key]; } var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { //進行劫持 enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); //依賴收集器將當前watcher收集到依賴中 if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (customSetter) { customSetter(); } // #7981: for accessor properties without setter if (getter && !setter) { return } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); //通知訂閱當前key的所有watcher } }); }
這部分是最基本的部分,必須理解,直接上圖:
①、Observer會調用defineReactive對數據的每個key進行劫持;
②、defineReactive會為當前key定義get、set方法,以及創建一個Dep實例,Dep可以稱為依賴收集器;
③、當(watcher)在獲取數據時,如: let title = data.title,此時就會觸發 'title' 對應的 get方法,如果Dep.target有對應的watcher,那么通過dep.depend() 將當前watcher加入到 'title' 對應的dep中,最后返回 title 的值。
這里有必要看下Dep的源碼以及解釋下Dep.target的含義
var uid = 0; /** * A dep is an observable that can have multiple * directives subscribing to it. */ var Dep = function Dep () { this.id = uid++; this.subs = []; }; Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub); }; Dep.prototype.removeSub = function removeSub (sub) { remove(this.subs, sub); }; Dep.prototype.depend = function depend () { if (Dep.target) { Dep.target.addDep(this); } }; Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); if (!config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort(function (a, b) { return a.id - b.id; }); } for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } }; // The current target watcher being evaluated. // This is globally unique because only one watcher // can be evaluated at a time. Dep.target = null; var targetStack = []; function pushTarget (target) { targetStack.push(target); Dep.target = target; } function popTarget () { targetStack.pop(); Dep.target = targetStack[targetStack.length - 1]; }
這里Dep.target相當於一個上下文的作用(公共變量),用來存儲當前正在執行的watcher實例。獲取數據時判斷如果Dep.target(當前watcher)存在,dep.depend() 就會將當前正在獲取數據的watcher加入到依賴之中;
④、當數據發生變化時,如:data.title = 'xxxx',此時會觸發 'title' 對應的set方法,通過dep.notity() 通知對應dep中的watcher,watcher再進行更新;
以上就是數據初始化的過程,總結來說就是對數據進行劫持,並為每個key建立一個依賴,獲取數據時依賴收集對應的watcher,數據變化時通知對應的watcher進行更新。
2.之后進行數據的掛載,主要包括兩個部分:模版解析、創建渲染watcher完成渲染
模版解析:compiler部分
調用compiler中的compiletoFunctions將模版解析為render渲染函數,(模板解析是運行時比較消耗性能的部分,如果已經編譯過Vue會將結果緩存起來使用)
render函數將會傳遞給下面的渲染watcher渲染DOM使用(獲取數據、創建對應的DOM、綁定事件),直觀的看一下render的邏輯。
(function anonymous( ) { with(this){return _c('div',{attrs:{"id":"demo"}},[_c('div',[_v("\n "+_s(title)+"\n "+_s(title2)+"\n ")]),_v(" "),_c('input',{attrs:{"type":"button","value":"click"},on:{"click":testClick}})])} })
創建渲染watcher,渲染DOM:渲染watcher划重點,Vue有很多個watcher,但只有一個watcher負責渲染,就是渲染watcher
首先講一下Watcher,看下源碼:
var uid$2 = 0; /** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */ var Watcher = function Watcher ( vm, expOrFn, cb, options, isRenderWatcher ) { this.lazy = options.lazy; .....this.id = ++uid$2; // uid for batching ..... this.expression = expOrFn.toString(); // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); ..... } this.value = this.lazy ? undefined : this.get(); }; /** * Evaluate the getter, and re-collect dependencies. */ Watcher.prototype.get = function get () { pushTarget(this);//將當前watcher 賦值到 Dep.target
var value; var vm = this.vm; ...... value = this.getter.call(vm, vm);//調用watcher傳入的回調函數
...... popTarget(); this.cleanupDeps(); return value }; /** * Add a dependency to this directive. */ Watcher.prototype.addDep = function addDep (dep) { var id = dep.id; if (!this.newDepIds.has(id)) { this.newDepIds.add(id); this.newDeps.push(dep); if (!this.depIds.has(id)) { dep.addSub(this); } } }; Watcher.prototype.update = function update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this);//將 watcher加入異步隊列中
} }; /** * Scheduler job interface. * Will be called by the scheduler. */ Watcher.prototype.run = function run () { if (this.active) { var value = this.get();
......
}
......
}
Watcher接受五個參數,現在只關注前兩個即可,一個是vm實例,一個是執行函數expOrFn。
Watcher在構造函數初始化時會執行 this.get() 方法,this.get()會執行兩個操作:pushTargt(this)、執行回調函數expOrFn
①、pushTargt(this):將當前的watcher實例賦值給Dep.target(見上面Object.defineProperty中的get方法),也即是此時 Dep.target = 'renderWatcher';
②、執行回調函數expOrFn:如果此時回調函數中如果有屬獲取數據的動作,將會先觸發Object.defineProperty中的get方法,將Dep.target(當前watcher) 加入到依賴中,之后整個回調函數執行完畢。
過程如圖:
這里的 pushTarget 以及 Dep.target 在看源碼的時候是比較難懂的,主要是語義上沒有寫清(如果對應寫成 pushWatcher 以及 globalWatcher 可能會更清楚一些,作者應該是想寫的高內聚一些)。
接下來看一下渲染watcher,渲染watcher顧名思義就是負責渲染的watcher,說白了就是回調函數會執行上面的render渲染函數進行DOM渲染
updateComponent = function () {
vm._update(vm._render(), hydrating); //_render 會調用上面render進行DOM的繪制
};
new Watcher(vm, updateComponent, noop, { before: function before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate'); } } }, true /* isRenderWatcher */);
那么根據上面Watcher邏輯,此處渲染watcher初始化時首先會將Dep.target切換到當前watcher上,之后執行回調updateComponent。updateComponent實際上就是執行render函數,render函數獲取並訂閱數據,之后創建DOM完成渲染。
由於已經訂閱了數據,數據在發生變化時就會通知渲染watcher重新進行渲染,之后反映到DOM上。
下面是 updateComponent -> render 執行的邏輯,createElement 將會使用虛擬DOM來創建,最后映射到真實DOM,虛擬DOM的技術有機會單獨來講,不在此處展開。
with(this){return _c('div',{attrs:{"id":"demo"}},[_c('div',[_v("\n "+_s(title)+"\n "+_s(title2)+"\n ")]),_v(" "),_c('input',{attrs:{"type":"button","value":"click"},on:{"click":testClick}})])}
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
總結一下:watcher說白了就是用了哪個數據就會訂閱哪個數據,數據變化時就會收到對應通知,未使用到的數據變化就不會受到通知。如渲染watcher,模版中使用了title、title2,那么當title、title2變化時就會通知渲染watcher進行更新,其它各種watcher同理(lazy的有些特殊,后面再講);
接下來講一下數據更新階段
當數據發生變化時,如例子中testClick觸發數據變化,過程為:
testClick(){ this.title = Math.random(); this.title2 = Math.random(); }
1.數據更新觸發data中的set,依賴收集器會通知Watcher進行更新:
此處首先執行的是this.title = Math.random(),由於渲染watcher使用了title進行渲染,那么此處title的變化就會通知渲染watcher進行更新
//defineReactive中數據的set dep.notify(); Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); if (!config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort(function (a, b) { return a.id - b.id; }); } for (var i = 0, l = subs.length; i < l; i++) { subs[i].update();//watcher更新 } };
這里只有一個watcher使用了title,所以subs中就是一個渲染watcher,渲染watcher就會更新。
接下來看下watcher的更新邏輯:
Watcher.prototype.update = function update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); } };
這里可以看到watcher的update不會直接執行watcher的邏輯(例子中為渲染DOM),而是執行了一個queueWatcher方法(前面的邏輯判斷先忽略),queueWatcher會將watcher放入一個隊列queue中,之后通過nextTick來執行,這就是我前面講到的異步隊列
function queueWatcher (watcher) { var id = watcher.id; console.log('watcherId='+ id + 'exporession=' + watcher.expression); if (has[id] == null) { //console.log('watcherId='+ id + 'exporession=' + watcher.expression); has[id] = true; if (!flushing) { queue.push(watcher); } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. var i = queue.length - 1; while (i > index && queue[i].id > watcher.id) { i--; } queue.splice(i + 1, 0, watcher); } // queue the flush if (!waiting) { waiting = true; if (!config.async) { flushSchedulerQueue(); return } nextTick(flushSchedulerQueue); } } }
nextTick可以簡單當成一個異步函數來看,比如setTimeout,主要目的就是把要執行的操作放到下一個執行周期中,比如例子中testClick整個執行周期完成后才會執行此處的邏輯,watcher中的邏輯才會真正執行,這樣就避免了多次觸發watcher以及觸發了多個watcher更新造成的重復渲染問題。
var queue = [ watcher ]; //nextTick flushSchedulerQueue setTimetout(function(){ for(var i = 0; i < queue.length; i++){ queue[i].run(); //watcher.run() } } , 0);
ps:這里的異步隊列中涉及到了nextTick以及多個watcher執行順序的問題,本文為了方便理解只講了一種watcher--渲染watcher,后面講其它watcher的時候一起來講。
以上就是數據更新階段的邏輯。
五、總結:
本文從Vue的初始化以及更新兩個方面出發講了下Vue的核心邏輯,主要目的是為了幫助大家了解整個流程,介紹過程屏蔽了不少內容,后面有機會再展開。另外,如果有沒看過源碼可以從我上面划分的幾個部分來開,可能會事半功倍。