淺談vue的第一個commit分析
為什麼寫這篇vue的分析文章?
對於天資愚鈍的前端(我)來說,閱讀原始碼是件不容易的事情,畢竟有時候看原始碼分析的文章都看不懂。每次看到大佬們用了1~2年的vue就能掌握原理,甚至精通原始碼,再看看自己用了好幾年都還在基本的使用階段,心中總是羞愧不已。如果一直滿足於基本的業務開發,怕是得在初級水平一直待下去了吧。所以希望在學習原始碼的同時記錄知識點,可以讓自己的理解和記憶更加深刻,也方便將來查閱。
目錄結構
本文以vue的第一次 commit a879ec06 作為分析版本
├── build │ └── build.js // `rollup` 打包配置 ├── dist │ └── vue.js ├── package.json ├── src // vue原始碼目錄 │ ├── compiler // 將vue-template轉化為render函式 │ │ ├── codegen.js // 遞迴ast提取指令,分類attr,style,class,並生成render函式 │ │ ├── html-parser.js // 通過正則匹配將html字串轉化為ast │ │ ├── index.js // compile主入口 │ │ └── text-parser.js // 編譯{{}} │ ├── config.js // 對於vue的全域性配置檔案 │ ├── index.js // 主入口 │ ├── index.umd.js // 未知(應該是umd格式的主入口) │ ├── instance // vue例項函式 │ │ └── index.js // 包含了vue例項的初始化,compile,data代理,methods代理,watch資料,執行渲染 │ ├── observer // 資料訂閱釋出的實現 │ │ ├── array.js // 實現array變異方法,$set $remove 實現 │ │ ├── batcher.js // watch執行佇列的收集,執行 │ │ ├── dep.js // 訂閱中心實現 │ │ ├── index.js // 資料劫持的實現,收集訂閱者 │ │ └── watcher.js // watch實現,訂閱者 │ ├── util // 工具函式 │ │ ├── component.js │ │ ├── debug.js │ │ ├── dom.js │ │ ├── env.js // nexttick實現 │ │ ├── index.js │ │ ├── lang.js │ │ └── options.js │ └── vdom │ ├── dom.js // dom操作的封裝 │ ├── h.js // 節點資料分析(元素節點,文字節點) │ ├── index.js // vdom主入口 │ ├── modules // 不同屬性處理函式 │ │ ├── attrs.js // 普通attr屬性處理 │ │ ├── class.js // class處理 │ │ ├── events.js // event處理 │ │ ├── props.js // props處理 │ │ └── style.js // style處理 │ ├── patch.js // node樹的渲染,包括節點的加減更新處理,及對應attr的處理 │ └── vnode.js // 返回最終的節點資料 └── webpack.config.js // webpack配置
從template到html的過程分析
我們的程式碼是從new Vue()開始的,Vue的建構函式如下:
constructor (options) { // options就是我們對於vue的配置 this.$options = options this._data = options.data // 獲取元素html,即template const el = this._el = document.querySelector(options.el) // 編譯模板 -> render函式 const render = compile(getOuterHTML(el)) this._el.innerHTML = '' // 例項代理data資料 Object.keys(options.data).forEach(key => this._proxy(key)) // 將method的this指向例項 if (options.methods) { Object.keys(options.methods).forEach(key => { this[key] = options.methods[key].bind(this) }) } // 資料觀察 this._ob = observe(options.data) this._watchers = [] // watch資料及更新 this._watcher = new Watcher(this,render,this._update) // 渲染函式 this._update(this._watcher.value) }
當我們初始化專案的時候,即會執行建構函式,該函式向我們展示了vue初始化的主線:編譯template字串 => 代理data資料/methods的this繫結 => 資料觀察 => 建立watch及更新渲染
1. 編譯template字串
const render = compile(getOuterHTML(el))
其中compile的實現如下:
export function compile (html) { html = html.trim() // 對編譯結果快取 const hit = cache[html] // parse函式在parse-html中定義,其作用是把我們獲取的html字串通過正則匹配轉化為ast,輸出如下 {tag: 'div',attrs: {},children: []} return hit || (cache[html] = generate(parse(html))) }
接下來看看generate函式,ast通過genElement的轉化生成了構建節點html的函式,在genElement將對if for 等進行判斷並轉化( 指令的具體處理將在後面做分析,先關注主流程程式碼),最後都會執行genData函式
// 生成節點主函式 export function generate (ast) { const code = genElement(ast) // 執行code程式碼,並將this作為code的global物件。所以我們在template中的變數將指向為例項的屬性 {{name}} -> this.name return new Function (`with (this) { return $[code]}`) } // 解析單個節點 -> genData function genElement (el,key) { let exp // 指令的實現,實際就是在模板編譯時實現的 if (exp = getAttr(el,'v-for')) { return genFor(el,exp) } else if (exp = getAttr(el,'v-if')) { return genIf(el,exp) } else if (el.tag === 'template') { return genChildren(el) } else { // 分別為 tag 自身屬性 子節點資料 return `__h__('${ el.tag }',${ genData(el,key) },${ genChildren(el) })` } }
我們可以看看在genData中都做了什麼。上面的parse函式將html字串轉化為ast,而在genData中則將節點的attrs資料進一步處理,例如class -> renderClass style class props attr 分類。在這裡可以看到 bind 指令的實現,即通過正則匹配 : 和 bind,如果匹配則把相應的 value值轉化為 (value)的形式,而不匹配的則通過JSON.stringify()轉化為字串('value')。最後輸出attrs的(key-value),在這裡得到的物件是字串形式的,例如(value)等也僅僅是將變數名,而在generate中通過new Function進一步通過(this.value)得到變數值。
function genData (el,key) { // 沒有屬性返回空物件 if (!el.attrs.length) { return '{}' } // key let data = key ? `{key:${ key },` : `{` // class處理 if (el.attrsMap[':class'] || el.attrsMap['class']) { data += `class: _renderClass(${ el.attrsMap[':class'] },"${ el.attrsMap['class'] || '' }"),` } // attrs let attrs = `attrs:{` let props = `props:{` let hasAttrs = false let hasProps = false for (let i = 0,l = el.attrs.length; i < l; i++) { let attr = el.attrs[i] let name = attr.name // bind屬性 if (bindRE.test(name)) { name = name.replace(bindRE,'') if (name === 'class') { continue // style處理 } else if (name === 'style') { data += `style: ${ attr.value },` // props屬性處理 } else if (mustUsePropsRE.test(name)) { hasProps = true props += `"${ name }": (${ attr.value }),` // 其他屬性 } else { hasAttrs = true attrs += `"${ name }": (${ attr.value }),` } // on指令,未實現 } else if (onRE.test(name)) { name = name.replace(onRE,'') // 普通屬性 } else if (name !== 'class') { hasAttrs = true attrs += `"${ name }": (${ JSON.stringify(attr.value) }),` } } if (hasAttrs) { data += attrs.slice(0,-1) + '},' } if (hasProps) { data += props.slice(0,' } return data.replace(/,$/,'') + '}' }
而對於genChildren,我們可以猜到就是對ast中的children進行遍歷呼叫genElement,實際上在這裡還包括了對文字節點的處理。
// 遍歷子節點 -> genNode function genChildren (el) { if (!el.children.length) { return 'undefined' } // 對children扁平化處理 return '__flatten__([' + el.children.map(genNode).join(',') + '])' } function genNode (node) { if (node.tag) { return genElement(node) } else { return genText(node) } } // 解析{{}} function genText (text) { if (text === ' ') { return '" "' } else { const exp = parseText(text) if (exp) { return 'String(' + escapeNewlines(exp) + ')' } else { return escapeNewlines(JSON.stringify(text)) } } }
genText處理了text及換行,在parseText函式中利用正則解析{{}},輸出字串(value)形式的字串。
現在我們再看看__h__('${ el.tag }',${ genChildren(el) })中__h__函式
// h 函式利用上面得到的節點資料得到 vNode物件 => 虛擬dom export default function h (tag,b,c) { var data = {},children,text,i if (arguments.length === 3) { data = b if (isArray(c)) { children = c } else if (isPrimitive(c)) { text = c } } else if (arguments.length === 2) { if (isArray(b)) { children = b } else if (isPrimitive(b)) { text = b } else { data = b } } if (isArray(children)) { // 子節點遞迴處理 for (i = 0; i < children.length; ++i) { if (isPrimitive(children[i])) children[i] = VNode(undefined,undefined,children[i]) } } // svg處理 if (tag === 'svg') { addNS(data,children) } // 子節點為文字節點 return VNode(tag,data,undefined) }
到此為止,我們分析了const render = compile(getOuterHTML(el)),從el的html字串到render函式都是怎麼處理的。
2. 代理data資料/methods的this繫結
// 例項代理data資料 Object.keys(options.data).forEach(key => this._proxy(key)) // 將method的this指向例項 if (options.methods) { Object.keys(options.methods).forEach(key => { this[key] = options.methods[key].bind(this) }) }
例項代理data資料的實現比較簡單,就是利用了物件的setter和getter,讀取this資料時返回data資料,在設定this資料時同步設定data資料
_proxy (key) { if (!isReserved(key)) { // need to store ref to self here // because these getter/setters might // be called by child scopes via // prototype inheritance. 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 } }) } }
3. Obaerve的實現
Observe的實現原理在很多地方都有分析,主要是利用了Object.defineProperty()來建立對資料更改的訂閱,在很多地方也稱之為資料劫持。下面我們來學習從零開始建立這樣一個數據的訂閱釋出體系。
從簡單處開始,我們希望有個函式可以幫我們監聽資料的改變,每當資料改變時執行特定回撥函式
function observe(data,callback) { if (!data || typeof data !== 'object') { return } // 遍歷key Object.keys(data).forEach((key) => { let value = data[key]; // 遞迴遍歷監聽深度變化 observe(value,callback); // 監聽單個可以的變化 Object.defineProperty(data,get() { return value; },set(val) { if (val === value) { return } value = val; // 監聽新的資料 observe(value,callback); // 資料改變的回撥 callback(); } }); }); } // 使用observe函式監聽data const data = {}; observe(data,() => { console.log('data修改'); })
上面我們實現了一個簡單的observe函式,只要我們將編譯函式作為callback傳入,那麼每次資料更改時都會觸發回撥函式。但是我們現在不能為單獨的key設定監聽及回撥函式,只能監聽整個物件的變化執行回撥。下面我們對函式進行改進,達到為某個key設定監聽及回撥。同時建立排程中心,讓整個訂閱釋出模式更加清晰。
// 首先是訂閱中心 class Dep { constructor() { this.subs = []; // 訂閱者陣列 } addSub(sub) { // 新增訂閱者 this.subs.push(sub); } notify() { // 釋出通知 this.subs.forEach((sub) => { sub.update(); }); } } // 當前訂閱者,在getter中標記 Dep.target = null; // 訂閱者 class Watch { constructor(express,cb) { this.cb = cb; if (typeof express === 'function') { this.expressFn = express; } else { this.expressFn = () => { return new Function(express)(); } } this.get(); } get() { // 利用Dep.target存當前訂閱者 Dep.target = this; // 執行表示式 -> 觸發getter -> 在getter中新增訂閱者 this.expressFn(); // 及時置空 Dep.taget = null; } update() { // 更新 this.cb(); } addDep(dep) { // 新增訂閱 dep.addSub(this); } } // 觀察者 建立觀察 class Observe { constructor(data) { if (!data || typeof data !== 'object') { return } // 遍歷key Object.keys(data).forEach((key) => { // key => dep 對應 const dep = new Dep(); let value = data[key]; // 遞迴遍歷監聽深度變化 const observe = new Observe(value); // 監聽單個可以的變化 Object.defineProperty(data,{ configurable: true,get() { if (Dep.target) { const watch = Dep.target; watch.addDep(dep); } return value; },set(val) { if (val === value) { return } value = val; // 監聽新的資料 new Observe(value); // 資料改變的回撥 dep.notify(); } }); }); } } // 監聽資料中某個key的更改 const data = { name: 'xiaoming',age: 26 }; const observe = new Observe(data); const watch = new Watch('data.age',() => { console.log('age update'); }); data.age = 22
現在我們實現了訂閱中心,訂閱者,觀察者。觀察者監測資料的更新,訂閱者通過訂閱中心訂閱資料的更新,當資料更新時,觀察者會告訴訂閱中心,訂閱中心再逐個通知所有的訂閱者執行更新函式。到現在為止,我們可以大概猜出vue的實現原理:
- 建立觀察者觀察data資料的更改 (new Observe)
- 在編譯的時候,當某個程式碼片段或節點依賴data資料,為該節點建議訂閱者,訂閱data中某些資料的更新(new Watch)
- 當dada資料更新時,通過訂閱中心通知資料更新,執行節點更新函式,新建或更新節點(dep.notify())
上面是我們對vue實現原理訂閱釋出模式的基本實現,及編譯到更新過程的猜想,現在我們接著分析vue原始碼的實現:
在例項的初始化中
// ... // 為資料建立資料觀察 this._ob = observe(options.data) this._watchers = [] // 新增訂閱者 執行render 會觸發 getter 訂閱者訂閱更新,資料改變觸發 setter 訂閱中心通知訂閱者執行 update this._watcher = new Watcher(this,this._update) // ...
vue中資料觀察的實現
// observe函式 export function observe (value,vm) { if (!value || typeof value !== 'object') { return } if ( hasOwn(value,'__ob__') && value.__ob__ instanceof Observer ) { ob = value.__ob__ } else if ( shouldConvert && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { // 為資料建立觀察者 ob = new Observer(value) } // 儲存關聯的vm if (ob && vm) { ob.addVm(vm) } return ob } // => Observe 函式 export function Observer (value) { this.value = value // 在陣列變異方法中有用 this.dep = new Dep() // observer例項存在__ob__中 def(value,'__ob__',this) if (isArray(value)) { var augment = hasProto ? protoAugment : copyAugment // 陣列遍歷,新增變異的陣列方法 augment(value,arrayMethods,arrayKeys) // 對陣列的每個選項呼叫observe函式 this.observeArray(value) } else { // walk -> convert -> defineReactive -> setter/getter this.walk(value) } } // => walk Observer.prototype.walk = function (obj) { var keys = Object.keys(obj) for (var i = 0,l = keys.length; i < l; i++) { this.convert(keys[i],obj[keys[i]]) } } // => convert Observer.prototype.convert = function (key,val) { defineReactive(this.value,val) } // 重點看看defineReactive export function defineReactive (obj,val) { // key對應的的訂閱中心 var dep = new Dep() var property = Object.getOwnPropertyDescriptor(obj,key) if (property && property.configurable === false) { return } // 相容原有setter/getter // cater for pre-defined getter/setters var getter = property && property.get var setter = property && property.set // 實現遞迴監聽屬性 val = obj[key] // 深度優先遍歷 先為子屬性設定 reactive var childOb = observe(val) // 設定 getter/setter Object.defineProperty(obj,{ enumerable: true,configurable: true,get: function reactiveGetter () { var value = getter ? getter.call(obj) : val // Dep.target 為當前 watch 例項 if (Dep.target) { // dep 為 obj[key] 對應的排程中心 dep.depend 將當前 wtcher 例項新增到排程中心 dep.depend() if (childOb) { // childOb.dep 為 obj[key] 值 val 對應的 observer 例項的 dep // 實現array的變異方法和$set方法訂閱 childOb.dep.depend() } // TODO: 此處作用未知? if (isArray(value)) { for (var e,i = 0,l = value.length; i < l; i++) { e = value[i] e && e.__ob__ && e.__ob__.dep.depend() } } } return value },set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val // 通過 getter 獲取 val 判斷是否改變 if (newVal === value) { return } if (setter) { setter.call(obj,newVal) } else { val = newVal } // 為新值設定 reactive childOb = observe(newVal) // 通知key對應的訂閱中心更新 dep.notify() } }) }
訂閱中心的實現
let uid = 0 export default function Dep () { this.id = uid++ // 訂閱排程中心的watch陣列 this.subs = [] } // 當前watch例項 Dep.target = null // 新增訂閱者 Dep.prototype.addSub = function (sub) { this.subs.push(sub) } // 移除訂閱者 Dep.prototype.removeSub = function (sub) { this.subs.$remove(sub) } // 訂閱 Dep.prototype.depend = function () { // Dep.target.addDep(this) => this.addSub(Dep.target) => this.subs.push(Dep.target) Dep.target.addDep(this) } // 通知更新 Dep.prototype.notify = function () { // stablize the subscriber list first var subs = this.subs.slice() for (var i = 0,l = subs.length; i < l; i++) { // subs[i].update() => watch.update() subs[i].update() } }
訂閱者的實現
export default function Watcher (vm,expOrFn,cb,options) { // mix in options if (options) { extend(this,options) } var isFn = typeof expOrFn === 'function' this.vm = vm // vm 的 _watchers 包含了所有 watch vm._watchers.push(this) this.expression = expOrFn this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers // deps 一個 watch 例項可以對應多個 dep this.deps = [] this.newDeps = [] this.depIds = Object.create(null) this.newDepIds = null this.prevError = null // for async error stacks // parse expression for getter/setter if (isFn) { this.getter = expOrFn this.setter = undefined } else { warn('vue-lite only supports watching functions.') } this.value = this.lazy ? undefined : this.get() this.queued = this.shallow = false } Watcher.prototype.get = function () { this.beforeGet() var scope = this.scope || this.vm var value try { // 執行 expOrFn,此時會觸發 getter => dep.depend() 將watch例項新增到對應 obj[key] 的 dep value = this.getter.call(scope,scope) } if (this.deep) { // 深度watch // 觸發每個key的getter watch例項將對應多個dep traverse(value) } // ... this.afterGet() return value } // 觸發getter,實現訂閱 Watcher.prototype.beforeGet = function () { Dep.target = this this.newDepIds = Object.create(null) this.newDeps.length = 0 } // 新增訂閱 Watcher.prototype.addDep = function (dep) { var id = dep.id if (!this.newDepIds[id]) { // 將新出現的dep新增到newDeps中 this.newDepIds[id] = true this.newDeps.push(dep) // 如果已在排程中心,不再重複新增 if (!this.depIds[id]) { // 將watch新增到排程中心的陣列中 dep.addSub(this) } } } Watcher.prototype.afterGet = function () { // 切除key的getter聯絡 Dep.target = null var i = this.deps.length while (i--) { var dep = this.deps[i] if (!this.newDepIds[dep.id]) { // 移除不在expOrFn表示式中關聯的dep中watch的訂閱 dep.removeSub(this) } } this.depIds = this.newDepIds var tmp = this.deps this.deps = this.newDeps // TODO: 既然newDeps最終會被置空,這邊賦值的意義在於? this.newDeps = tmp } // 訂閱中心通知訊息更新 Watcher.prototype.update = function (shallow) { if (this.lazy) { this.dirty = true } else if (this.sync || !config.async) { this.run() } else { // if queued,only overwrite shallow with non-shallow,// but not the other way around. this.shallow = this.queued ? shallow ? this.shallow : false : !!shallow this.queued = true // record before-push error stack in debug mode /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.debug) { this.prevError = new Error('[vue] async stack trace') } // 新增到待執行池 pushWatcher(this) } } // 執行更新回撥 Watcher.prototype.run = function () { if (this.active) { var value = this.get() if ( ((isObject(value) || this.deep) && !this.shallow) ) { // set new value var oldValue = this.value this.value = value var prevError = this.prevError // ... this.cb.call(this.vm,value,oldValue) } this.queued = this.shallow = false } } Watcher.prototype.depend = function () { var i = this.deps.length while (i--) { this.deps[i].depend() } }
wtach回撥執行佇列
在上面我們可以發現,watch在收到資訊更新執行update時。如果非同步情況下會執行pushWatcher(this)將例項推入執行池中,那麼在何時會執行回撥函式,如何執行呢?我們一起看看pushWatcher的實現。
// batch.js var queueIndex var queue = [] var userQueue = [] var has = {} var circular = {} var waiting = false var internalQueueDepleted = false // 重置執行池 function resetBatcherState () { queue = [] userQueue = [] // has 避免重複 has = {} circular = {} waiting = internalQueueDepleted = false } // 執行執行佇列 function flushBatcherQueue () { runBatcherQueue(queue) internalQueueDepleted = true runBatcherQueue(userQueue) resetBatcherState() } // 批量執行 function runBatcherQueue (queue) { for (queueIndex = 0; queueIndex < queue.length; queueIndex++) { var watcher = queue[queueIndex] var id = watcher.id // 執行後置為null has[id] = null watcher.run() // in dev build,check and stop circular updates. if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > config._maxUpdateCount) { warn( 'You may have an infinite update loop for watcher ' + 'with expression "' + watcher.expression + '"',watcher.vm ) break } } } } // 新增到執行池 export function pushWatcher (watcher) { var id = watcher.id if (has[id] == null) { if (internalQueueDepleted && !watcher.user) { // an internal watcher triggered by a user watcher... // let's run it immediately after current user watcher is done. userQueue.splice(queueIndex + 1,watcher) } else { // push watcher into appropriate queue var q = watcher.user ? userQueue : queue has[id] = q.length q.push(watcher) // queue the flush if (!waiting) { waiting = true // 在nextick中執行 nextTick(flushBatcherQueue) } } } }
4. patch實現
上面便是vue中資料驅動的實現原理,下面我們接著回到主流程中,在執行完watch後,便執行this._update(this._watcher.value)開始節點渲染
// _update => createPatchFunction => patch => patchVnode => (dom api) // vtree是通過compile函式編譯的render函式執行的結果,返回了當前表示當前dom結構的物件(虛擬節點樹) _update (vtree) { if (!this._tree) { // 第一次渲染 patch(this._el,vtree) } else { patch(this._tree,vtree) } this._tree = vtree } // 在處理節點時,需要針對class,props,style,attrs,events做不同處理 // 在這裡注入針對不同屬性的處理函式 const patch = createPatchFunction([ _class,// makes it easy to toggle classes props,style,attrs,events ]) // => createPatchFunction返回patch函式,patch函式通過對比虛擬節點的差異,對節點進行增刪更新 // 最後呼叫原生的dom api更新html return function patch (oldVnode,vnode) { var i,elm,parent var insertedVnodeQueue = [] // pre hook for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]() if (isUndef(oldVnode.sel)) { oldVnode = emptyNodeAt(oldVnode) } if (sameVnode(oldVnode,vnode)) { // someNode can patch patchVnode(oldVnode,vnode,insertedVnodeQueue) } else { // 正常的不復用 remove insert elm = oldVnode.elm parent = api.parentNode(elm) createElm(vnode,insertedVnodeQueue) if (parent !== null) { api.insertBefore(parent,vnode.elm,api.nextSibling(elm)) removeVnodes(parent,[oldVnode],0) } } for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]) } // hook post for (i = 0; i < cbs.post.length; ++i) cbs.post[i]() return vnode }
結尾
以上分析了vue從template 到節點渲染的大致實現,當然也有某些地方沒有全面分析的地方,其中template解析為ast主要通過正則匹配實現,及節點渲染及更新的patch過程主要通過節點操作對比來實現。但是我們對編譯template字串 => 代理data資料/methods的this繫結 => 資料觀察 => 建立watch及更新渲染的大致流程有了個比較完整的認知。
到此這篇關於淺談vue的第一個commit分析的文章就介紹到這了,更多相關vue commit內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!