現在VUE3已經有一段時間了,也慢慢普及起來了。不過因為一直還在使用VUE2的原因還是去瞭解和學了下它的源碼,畢竟VUE2也不會突然就沒了是吧,且VUE3中很多原理之類的也是類似的。然後就準備把VUE3搞起來了是吧。VUE2源碼使用的是roullup進行打包的,還使用了Flow進行靜態類型檢測(該庫 ...
現在VUE3已經有一段時間了,也慢慢普及起來了。不過因為一直還在使用VUE2的原因還是去瞭解和學了下它的源碼,畢竟VUE2也不會突然就沒了是吧,且VUE3中很多原理之類的也是類似的。然後就準備把VUE3搞起來了是吧。VUE2源碼使用的是roullup進行打包的,還使用了Flow進行靜態類型檢測(該庫使用的已經不多了,且VUE3已經使用TypeScript進行開發了,有類型檢測了)。若是沒怎麼接觸過Vue2,直接Vue3會更划算些,結構之類的也更清晰了。
篇幅有限只探討了核心的一些過程。
VUE2項目結構與入口
主要目錄結構:
vue2源碼倉庫:https://github.com/vuejs/vue
clone後可以看到大概如下結構:
|----benchmarks 性能測試
|----scripts 腳本文件
|----scr 源碼
| |----compiler 模板編譯相關
| |----core vue2核心代碼
| |----platforms 平臺相關
| |----server 服務端渲染
| |----sfc 解析單文件組件
| |----shared 模塊間共用屬性和方法
package.json入口:
// package.json 中指定了roullup的配置文件及打包參數
"scripts": {
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
"dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
"dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
}
// 在/scripts/config.js 可以看到在接受到參數後 打包入口最終在 /src/platforms下文件中
構建參數與版本的說明
可以看到rollup
打包或者調試的時候後面更了很多參數,不同參數就能生成不同內容的版本,參數說明如下:
- web-runtime: 運行時,無法解析傳入的template
- web-full:運行時 + 模板編譯
- web-compiler:僅模板編譯
- web-runtime-cjs web-full-cjs:cjsCommonJS打包
- web-runtime-esm web-full-esm :esm 語法(支持import export)
- web-full-esm-browser:瀏覽器中使用
- web-server-renderer:服務端渲染
註:在使用CLI腳手架開發時,一般都是選擇web-runtime
是因為,腳手架中有vue-loader會將模板轉為render函數了,所以不需要再模板編譯了。
入口深入與源碼的構建,調試
我們可以在/platforms
目錄下找到,最外層的入口。但這個入口有經過層層包裝,添加了些方法後,最後才會到創建VUE實例的入口。以entry-runtime-with-compiler.js
為例,
entry-runtime-with-compiler 重寫了$mount
,主要增加了對模板的處理方法。:
- 沒有
template
則嘗試從el中取dom作template
- 有
template
則直接使用傳入的template
- 沒則將
template
轉化為render
函數,放在$options
上
它的Vue又是從./runtime/index
導進來的。runtime/index.js有公共的$mount
方法,還增加了:
- directives (全局指令:model,show)
- components (全局組件:transition,transitionGroup)
- patch(瀏覽器環境)
詳細流程如下圖:
開啟調試:
在package.json
項中增加sourcemap配置,如:
"scripts": {
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
.......
}
然後npm run dev
就可以在源碼中debugger進行調試了。
VUE基本代碼的執行在源碼中的核心流程
比如在頁面中有如下代碼,它主要涉及到Vue中的技術有:模板語法,數據雙向綁定,計算屬性,偵聽器。
點擊查看主要代碼
<div id="app">
<p>{{fullName}}:{{fullName}}-{{formBY}}</p>
</div>
const vm = new Vue({
el: "#app",
data() {
return {
firstName: "Shiina",
lastName: "Mashiro",
formBY: "flytree-cnblogs",
arr: [1, 2, 3, ["a"]],
};
},
computed: {
fullName() {
return this.firstName + this.lastName;
}
},
watch: {
firstName(newValue, oldValue) {
console.log(newValue, oldValue)
}
}
});
setTimeout(() => {
vm.firstName = 'flytree'
}, 1000);
我們可以把核心(細節後面再展開,先有個整體把握)的執行流程梳理下如下圖:
創建響應式數據
要實現數據的雙向綁定,就要創建響應式數據,原理就是重寫了data
中每項數據的getter
和setter
,這樣就可以攔截到每次的取值或者改值的操作了,取值的時候收集依賴,改值的時候通知notify
:
點擊查看代碼
// 路徑 /scr/core/observer/index.js
export function defineReactive() {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
const 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 (process.env.NODE_ENV !== 'production' && 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()
}
})
}
模板編譯
compileToFunctions
進行模板編譯,主要流程就是:
- 使用正則解析模板,然後將其轉化衛AST抽象語法樹。
- 然後根據AST抽象語法樹拼裝
render
函數。
比如上面的代碼
template: "<div id=\"app\">\n <p>{{fullName}}:{{fullName}}-{{formBY}}</p>\n
生成的render函數:
"with(this){
return _c('div',{attrs:{"id ":"app "}},
[_c('p',[_v(_s(fullName)+": "+_s(fullName)+" - "+_s(formBY))])])
}"
使用with
,vue實列執行到這個方法時,則會去找當前實例的屬性。
而_c
,_s
,_v
等函數是用來將對應類型節點轉換位虛擬dom的,render
執行後就能生成對應的虛擬dom樹了。
依賴收集
在看依賴收集前,可以想下以下問題:
問 | 答 |
---|---|
什麼時候進行依賴收集? | data中項被取值(其getter執行) |
什麼時候執行getter? | _render函數執行 |
什麼時候執行_render? | _update函數執行 |
什麼時候執行_update? | data項中getter執行 |
什麼時候執行data項中get方法? | 模板中取值 |
這時我們再看下get
的來源和去處,看下具體的流程:
可以看到:
1.取值:在模板中取值的時候它就會進行依賴收集,執行dep.depend()
, 最後會去重的watcher存在依賴的subs[]
中。去重是,如果模板中重覆取了兩次值,那也不會重覆收集watcher
。
2.改值:在值發生變更的時候,就會觸發dep.notify()
,會遍歷執行其dep.subs
中的所有watcher.update()
,最後還是會執行到watcher.get()
,那麼就執行了_update(_render())
把變化更新到dom上了。
Dep類源碼:
點擊查看代碼
export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !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((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Watcher類源碼:
點擊查看主要代碼
export default class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
// 渲染watcher時就gettr就傳入了 _update(_render())
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
// 在計算屬性創建watcher的時候lazy為true
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
*/
addDep(dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps() {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run() {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate() {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown() {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
更新到dom樹的細節
從上面步驟分析下來,一般情況下,watcher實例的中的get()執行了,就能觸發,dom更新了。就是走了updateComponent
// 此方法在 core/instance/lifecycle.js
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
_render
執行後會生成虛擬dom,而_update
就會執行patch(__patch__)
更新對比後更新dom了。
_update源碼:
點擊查看代碼
export function lifecycleMixin (Vue) {
Vue.prototype._update = function (vnode, hydrating) {
const vm = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
}
patch進行diff優化
patch導出:
// platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
最後createPatchFunction的源碼在core/vdom/patch.js
Diff大概的流程:
判斷是否是相同節點:sameVnode
判斷標簽和key
是否相同。
diff演算法是用來比較兩個虛擬dom的更新情況的,而且是同級比較的。
在diff演算法中有四個指針,
在新的虛擬dom中的兩個指針,新前(在前面的指針),新後(在後面的指針)。
在舊的虛擬dom中的兩個指針,舊前(在前面的指針),舊後(在後面的指針)。
前指針的特點:
- 初始位置在最前面,也就是說children數組中的第0位。
- 前指針只能向後移動。
後指針的特點:
- 初始位置在最後面,也就是說在children數組中的第length-1位。
- 後指針只能向前移動。
每次比較可能進行以下四種比較:
- 新前和舊前。匹配則,前指針後移一位,後指針前移一位。
- 新後和舊後。匹配則,前指針後移一位,後指針前移一位。
- 新後和舊前。匹配則,將所匹配的節點的dom移動到舊後之後,虛擬dom中將其設位undefined,指針移動。
- 新前和舊後。匹配則,將所匹配的節點的dom移動到舊前之前,虛擬dom中將其設位undefined,指針移動。
匹配的步驟是按此順序從一到四進行匹配,但若之中有匹配成功的則不進行之後的匹配,比如第2種情況匹配,則不會進行3,4的匹配了。
上面四種匹配是對push, shift, pop, unshift ,reveres ,sort 操作進行優化,但若以上的四種情況都未曾匹配到,則會以新虛擬dom中為匹配的這項當作查找的目標,在舊虛擬dom中進行遍歷查找:
- 若查找到,則將dom中找到這項移動舊前之前,其虛擬dom中位置則設為undefined。然後新前指針移動一位。
- 若未找到,則將新前所指的這項(也是查找的目標項),生成dom節點,插入到舊前之前上,而後新前指針移動一位。