Vue3 源碼解析
vue3 出來有一段時間了。今天正式開始記錄一下
vue 3.0.0-beta
源碼學習心得。
本文編寫於
2020-06-10
,腳手架使用 vite-app
版本 0.20.0
,內置 vue 3.0.0-beta.14
。
ps: 可能大部分人都不清楚 vue3
的開發api,將源碼之前先講述 使用方法
環境搭建
最容易搭建 vue3
的方式就是使用作者的 vite
通過 npm
安裝
$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev
也可以通過 yarn
安裝
$ yarn create vite-app <project-name>
$ cd <project-name>
$ yarn
$ yarn dev
安裝的過程中你可能遇到以下問題(反正本菜遇到了)
- 異常1:
No valid exports main found for' C:\xxx\xxx\node_ modules\@rollup\pluginutils'
- 異常2:
The engine "node" is incompatible with this module. Expected version ">= 10.16.0". Got "10.15.3
異常1:本菜翻閱了 vite
的 issue,然後 google + baidu 一無所獲, 最後發現是因爲本菜 node
版本爲 13.5.0
導致的(版本過高),
異常2:很明顯啦,node
版本太低了。
最後的解決方式是:本菜通過 nvm
將 node 版本切換到 12.12.0
,至於 nvm
沒使用過的童鞋們可以去嘗試下哦。特別好用
vite 原理解析
當瀏覽器識別 type="module"
引入js文件的時候,內部的 import 就會發起一個網絡請求,嘗試去獲取這個文件。
那麼就可以通過通過攔截路由 /
和 .js
結尾的請求。然後通過 node 去加載對應的 .js
文件
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const app = new Koa()
app.use(async ctx=>{
const {request:{url} } = ctx
// 首頁
if(url=='/'){n
ctx.type="text/html"
ctx.body = fs.readFileSync('./index.html','utf-8')
}else if(url.endsWith('.js')){
// js文件
const p = path.resolve(__dirname,url.slice(1))
ctx.type = 'application/javascript'
const content = fs.readFileSync(p,'utf-8')
ctx.body = content
}
})
app.listen(3001, ()=>{
console.log('聽我口令,3001端口,起~~')
})
如果只是簡單的代碼,這樣加載就可以了。完全是按需加載,比起 webpack 的語法解析性能當然會快非常多。
但是遇到第三方庫以上代碼就會找不到 .js
文件的位置了,此時 vite
會用 es-module-lexer
把文件解析成 ast
,拿到 import
的地址。
通過分析 import
的內容,識別是不是第三方庫(這個主要是看前面是不是相對路徑)
如果是第三方庫就去 node_modules
中查找,vite
中通過在第三方庫中添加前綴 /@modules/
,然後發現了 /@modules/
後走 第三方庫邏輯
if(url.startsWith('/@modules/')){
// 這是一個node_module裏的東西
const prefix = path.resolve(__dirname,'node_modules',url.replace('/@modules/',''))
const module = require(prefix+'/package.json').module
const p = path.resolve(prefix,module)
const ret = fs.readFileSync(p,'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(ret)
}
這樣第三方庫也可以解析了。然後是 .vue
單文件解析。
首先 xx.vue
返回的格式大概是這樣的
const __script = {
setup() {
...
}
}
import {render as __render} from "/src/App.vue?type=template&t=1592389791757"
__script.render = __render
export default __script
然後可以用 @vue/compiler-dom
把 html
解析成 render
解析 .css
就更加簡單了。通過 document.createElement('style')
然後再注入就好了
ps:具體的源碼還沒看(先搞點 Vue3 吧)
reactive
正式進入正題。
作爲 vue2
的使用者最想知道的肯定是 vue3
的數據劫持和雙向綁定了。在 vue3中,雙向綁定和可選項,如果需要使用雙向綁定的需要通過 reactive
方法進行數據劫持。
在這之前呢還需要知道一個函數 setup
-
setup
是使用Composition API
的入口 -
setup
可以返回一個對象,該對象的屬性會被合併到渲染上下文,並可以在模板中直接使用 -
setup
也可以返回render
函數
現在開始寫一個簡單的 vue
<template>
<div>
<div>{{ count }}</div>
<button @click="increment">count++</button>
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
let count = reactive({
num: 0
})
const increment = () => count.num++
return {
count,
increment
}
}
}
</script>
emmm。這樣點擊按鈕就可以動態改變 dom 中的 count 值了。
現在開始解讀 reactive
源碼。
首先找到 reactivity.esm-browser.js
文件,找到 626
行。
function reactive(target) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && target.__v_isReadonly) {
return target;
}
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers);
}
上面的 __v_isReadonly
其實是一個 typescript 的枚舉值
export const enum ReactiveFlags {
skip = '__v_skip',
isReactive = '__v_isReactive',
isReadonly = '__v_isReadonly',
raw = '__v_raw',
reactive = '__v_reactive',
readonly = '__v_readonly'
}
不同的枚舉值對應了不同的數據劫持方式,例如 reactive、 shallowReactive 、readonly、 shallowReadonly
然後進入 createReactiveObject
在 649
行,意思就是:創建響應式對象
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
// 略...
// 如果target已經代理了, 返回target
if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
return target;
}
// target already has corresponding Proxy
if (hasOwn(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */)) {
return isReadonly ? target.__v_readonly : target.__v_reactive;
}
if (!canObserve(target)) {
return target;
}
// 重點...
// collectionHandlers:對引用類型的劫持,
// baseHandlers: 對進行基本類型的劫持
const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers);
def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed);
return observed;
}
createReactiveObject
做了以下幾件事
- 防止重複劫持
- 只讀劫持
- 根據不同類型選擇不同的劫持方式(
collectionHandlers
或baseHandlers
)
實現劫持的主要方法是通過 Proxy
方法,(Proxy 使用可以看看阮老師的博客),順騰摸瓜找到 mutableHandlers
定義的地方。在 338
行
const mutableHandlers = {
get,
set,
deleteProperty,
has,
ownKeys
};
// 229行
const get = /*#__PURE__*/ createGetter();
// 251 行
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
// 一些 __v_isReactive、__v_isReadonly、__v_raw的處理
// 略...
// 數組操作
const targetIsArray = isArray(target);
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
// 非數組
const res = Reflect.get(target, key, receiver);
// 其他 調用 track 返回 res 的情況
// 略...
// 如果可寫,那麼會調用 track
!isReadonly && track(target, "get" /* GET */, key);
// 如果是對象呢。那麼遞歸
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res;
};
}
mutableHandlers
主要是一個含有 Proxy
各種方法的常量。
get 指向了方法 createGetter
, 創建 get 劫持
createGetter
主要做了以下事情
- 異常處理
- 如果是數組且
hasOwn(arrayInstrumentations, key)
則調用arrayInstrumentations
獲取值 - 調用
track
- 對象迭代
reactive
那麼數組的 arrayInstrumentations
是什麼呢? 我們來到源碼的 第 234
行。
const arrayInstrumentations = {};
['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
arrayInstrumentations[key] = function (...args) {
//
const arr = toRaw(this);
for (let i = 0, l = this.length; i < l; i++) {
track(arr, "get" /* GET */, i + '');
}
// we run the method using the original args first (which may be reactive)
// 我們首先 以原始args 運行該方法(可能是反應性的)
const res = arr[key](...args);
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
// 如果那不起作用,則使用原始值再次運行它。
return arr[key](...args.map(toRaw));
}
else {
return res;
}
};
});
通過 arrayInstrumentations
得到 hasOwn(arrayInstrumentations, key)
就是指 ['includes', 'indexOf', 'lastIndexOf']
arrayInstrumentations
中還是調用了 track
方法,那麼 track
方法就更加神祕了。來看看它的源碼吧? 源碼在 126
行
function track(target, type, key) {
if (!shouldTrack || activeEffect === undefined) {
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
if ( activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
});
}
}
}
首先 track
需要 shouldTrack
和 activeEffect
爲真。
在不考慮 activeEffect
的情況下。track
所做的事情就是
- 創建包含自身的
map
- 將
activeEffect
塞到map
中 - 觸發
onTrack
然後 activeEffect
又是什麼呢?找到 3687
行,這裏有個 createReactiveEffect
函數。
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect(...args) {
return run(effect, fn, args);
};
effect._isEffect = true;
effect.active = true;
effect.raw = fn;
effect.deps = [];
effect.options = options;
return effect;
}
createReactiveEffect
是在 effect
中被調用的
而 effect
分別在以下地方被使用了
-
trigger
通過scheduleRun
調用effect
:源碼3756
行 -
mountComponent
通過setupRenderEffect
調用effect
:源碼 6235 行- PS 該階段在
createComponentInstance
之後
- PS 該階段在
-
doWatch
通過scheduler
調用effect
先開始講述 trigget
相關的代碼(核心哦)
function trigger(target, type, key, extraInfo) {
const depsMap = targetMap.get(target);
// 略...
const effects = new Set();
const computedRunners = new Set();
if (type === "clear" /* CLEAR */) {
// collection being cleared, trigger all effects for target
depsMap.forEach(dep => {
addRunners(effects, computedRunners, dep);
});
}
// 略...
const run = (effect) => {
scheduleRun(effect, target, type, key, extraInfo);
};
computedRunners.forEach(run);
effects.forEach(run);
}
trigger
最終是在 set
函數中被使用,源碼 3855
行,這個 set
就是數據劫持所用的 set
function set(target, key, value, receiver) {
value = toRaw(value);
const oldValue = target[key];
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
}
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
{
const extraInfo = { oldValue, newValue: value };
if (!hadKey) {
trigger(target, "add" /* ADD */, key, extraInfo);
}
else if (hasChanged(value, oldValue)) {
trigger(target, "set" /* SET */, key, extraInfo);
}
}
}
return result;
}
在源碼 3900
行中,被 mutableHandlers
、readonlyHandlers
等函數中被使用。
還記得嗎? mutableHandlers
是什麼? 可以回到文章開頭部分 reactive
源碼講解之初的 createReactiveObject
方法。在通過 Proxy
劫持數據的時候用的就是 mutableHandlers
reactive 總結
所以,這裏就成環了。
- 其實
effect
纔是響應式的核心,在mountComponent
、doWatch
、reactive
中被調用。 - 因爲在
reactive
中 通過Proxy
實現劫持。 - 在
Proxy
劫持set
時調用trigger
。 - 然後在
targger
中清除收集並觸發目標的所有effects
- 最終觸發
patch
遊戲結束。
最後
- 覺得有用的請點個贊
- 本文內容出自 https://github.com/zhongmeizhi/FED-note
- 歡迎關注公衆號「前端進階課」認真學前端,一起進階。回覆
全棧
或Vue
有好禮相送哦