Vue3 源碼逐行解析 Vue3 源碼解析

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-domhtml 解析成 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

然後進入 createReactiveObject649 行,意思就是:創建響應式對象

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 做了以下幾件事

  1. 防止重複劫持
  2. 只讀劫持
  3. 根據不同類型選擇不同的劫持方式(collectionHandlersbaseHandlers

實現劫持的主要方法是通過 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 主要做了以下事情

  1. 異常處理
  2. 如果是數組且hasOwn(arrayInstrumentations, key) 則調用 arrayInstrumentations 獲取值
  3. 調用 track
  4. 對象迭代 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 需要 shouldTrackactiveEffect 爲真。

在不考慮 activeEffect 的情況下。track 所做的事情就是

  1. 創建包含自身的 map
  2. activeEffect 塞到 map
  3. 觸發 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 之後
  • 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 行中,被 mutableHandlersreadonlyHandlers 等函數中被使用。

還記得嗎? mutableHandlers 是什麼? 可以回到文章開頭部分 reactive 源碼講解之初的 createReactiveObject 方法。在通過 Proxy 劫持數據的時候用的就是 mutableHandlers

reactive 總結

所以,這裏就成環了。

  1. 其實 effect 纔是響應式的核心,在 mountComponentdoWatchreactive 中被調用。
  2. 因爲在 reactive 中 通過 Proxy 實現劫持。
  3. Proxy 劫持set時調用 trigger
  4. 然後在 targger 中清除收集並觸發目標的所有 effects
  5. 最終觸發 patch 遊戲結束。

最後

  1. 覺得有用的請點個贊
  2. 本文內容出自 https://github.com/zhongmeizhi/FED-note
  3. 歡迎關注公衆號「前端進階課」認真學前端,一起進階。回覆 全棧Vue 有好禮相送哦
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章