Vue3資料驅動原始碼解讀

社交電商踩坑發表於2019-10-18

前言

DEMO地址,先下載DEMO,開啟index.html體驗一下吧

  • 閱讀的時候最好是先把DEMO過一遍,然後帶著問題來看這篇文章,不然可能會一臉懵逼
  • 我的DEMO原始碼簡化了非常非常多的程式碼,所以功能很基礎,為了方便把流程先理清楚
  • 不要死抓住細節不放,有些問題放一放,等把握全域性之後就可以理解了
  • 可以下載DEMO原始碼,開啟裡面index.html先體驗下

靈魂圖

先上一張靈魂圖,可能看完圖就要跑了。。。為了講清楚畫的比較亂

Vue3資料驅動原始碼解讀

總體介紹

從上圖中可以大致看出,分為兩條線?

第一條線:

new Vue -> reactive[new Proxy -> return proxy] -> baseHandler(set, get) -> effect檔案中的依賴收集(track,trigger)-> trigger觸發任務排程 -> 完了

第一條線遺留幾個問題:

  • 依賴是什麼時候生成的?
  • 依賴(dom和data之間的關係)是如何形成的?

先保留這兩個問題,繼續看第二條線

第二條線

從根路徑app開始解析子節點 -> 解析出當前node,node裡面的指令{{name}} -> 例項化effect(傳入回撥函式:回撥裡面去proxy上面獲取name) -> 呼叫effect

第二條線的重點

呼叫effect主要做兩件事:

  • 把當前effect push進activeReactiveEffectStack
  • 執行回撥,回撥函式裡面從proxy獲取name
  • 【從proxy獲取name】這一步會觸發proxy的get
  • get裡面從activeReactiveEffectStack獲取最後一個依賴,進行依賴收集

原始碼解析

reactive

import { mutableHandlers } from './baseHandler'

// 這個map儲存key: target, value:proxy
// 作用:
//  1.避免重複proxy
const rawToReactive = new WeakMap()
// 這個map儲存key:proxy, value:target
// 作用:
//  1.避免proxy物件再次被proxy
const reactiveToRaw = new WeakMap()
export const targetMap = new WeakMap()

export function reactive(target){
    return createReactiveObject(
        target,
        rawToReactive,
        reactiveToRaw,
        mutableHandlers
    )
}

// 建立響應式物件
function createReactiveObject(target, toProxy, toRaw, handlers){
    // 如果當前物件已被proxy,那麼直接返回
    let observed = toProxy.get(target)
    if (observed !== void 0) {
        return observed
    }

    // 檢測被proxy的物件,即這裡的target,自身是否是個proxy,如果是的話,直接返回
    if (toRaw.has(target)) {
        return target
    }

    // 當前的target既沒有被proxy,也不是個proxy物件,那麼對它proxy
    observed = new Proxy(target, handlers)

    // 例項化之後把它維護到兩個map
    toProxy.set(target, observed)
    toRaw.set(observed, target)

    // 把當前的target維護到targetMap,targetMap的作用 -> 【繼續往下看,先不管】
    if (!targetMap.has(target)) {
        targetMap.set(target, new Map())
    }
    return observed
}

// toRaw函式,傳入proxy物件,獲取target
// 
export function toRaw(observed) {
    return reactiveToRaw.get(observed) || observed
}
複製程式碼

reactive的作用

可以看出reactive的作用主要是:

  • 建立proxy例項並返回
  • 維護一個targetMap佇列

reactive的遺留問題

  • targetMap是幹嘛用的?
  • handler來自於baseHandler

ok,這兩個疑問?️先保留,繼續看baseHandler的原始碼

baseHandler

import { toRaw } from './reactive'
import { track, trigger } from './effect'

// 為了便於理解,這裡只做了get和set的proxy
// 其他的程式碼都是一般的代理,不講,講一下track和trigger
// 從vue 2.0的原始碼其實可以知道:
//  1.get的時候會做依賴收集:即這裡的track
//  2.set的時候會做更新廣播:即這裡的trigger
export const mutableHandlers = {
    get(target, key, receiver){
        const res = Reflect.get(target, key, receiver)
        track(target, 'get', key)
        return res
    },
    set(target, key, value, receiver){
        const oldValue = target[key]
        const result = Reflect.set(target, key, value, receiver)

        // 這裡檢測key是否是target的自有屬性
        const hadKey = target.hasOwnProperty(key)
        // 在reactive維護了一個reactiveToRaw佇列,儲存了[proxy]:[target]這樣的佇列,這裡檢測下是否是使用createReactiveObject新建的proxy
        if (target === toRaw(receiver)) {
            // 判斷是否值改變,才觸發更新
            if (hadKey && value !== oldValue) {
                trigger(target, 'set', key)
            }
        }
        return result
    }
}
複製程式碼

baseHandler的作用

  • get獲取值,其次依賴收集
  • set設定值,其次觸發任務排程

baseHandler的遺留問題

  • 什麼是依賴?
  • 收集的依賴中,dom和data的關係是怎樣的
  • 如何做任務排程?

effect

重點來了,effect就是vue3裡面用於依賴管理的,主要是管理三個東西:

  • 依賴收集
  • 依賴例項化
  • 依賴儲存
import { targetMap } from './reactive'

const activeReactiveEffectStack = []

// 下面這兩個api是初始化effect,就不過於糾結了
export function effect(fn, options){
    const effect = createReactiveEffect(fn, options)
    return effect
}

function createReactiveEffect(fn, options){
    const effect = function(){
        if (!activeReactiveEffectStack.includes(effect)) {
            try {
              activeReactiveEffectStack.push(effect)
              return fn()
            } finally {
              activeReactiveEffectStack.pop()
            }
        }
    }
    effect.scheduler = options.scheduler
    return effect
}

// 作用:
// 1.收集依賴
export function track(target, type, key){
    const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
    // proxy初始化的時候,這個depsMap為new Map
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
        targetMap.set(target, (depsMap = new Map()))
    }

    // 如果是第一次這個dep是沒有的,因為depsMap是new Map
    let dep = depsMap.get(key)
    if (dep === void 0) {
        // 這裡把依賴放進去。依賴是個Set
        depsMap.set(key, (dep = new Set()))
    }
    // 這裡的effect就是依賴。
    // 依賴是啥?可以理解為依賴儲存了data <-> dom的關係
    dep.add(effect)
    // effect.deps.push(dep)
}

// 作用:
//  1.觸發了資料更新,這時候得更新dom了
export function trigger(target, type, key){
    const depsMap = targetMap.get(target)
    const effects = new Set()
    const run = effect => {
        scheduleRun(effect, target, type, key)
    }
    // 解析出依賴中要更新的effect
    addRunners(effects, depsMap.get(key))

    // 任務排程執行
    effects.forEach(run)
}

function addRunners(effects, effectsToAdd){
    effectsToAdd.forEach(effect => {
        effects.add(effect)
    })
}

// 任務排程,就理解為data更新之後,呼叫effect.scheduler去更新dom
function scheduleRun(effect, target, type, key){
    if (effect.scheduler !== void 0) {
        effect.scheduler(effect)
    } else {
        effect()
    }
}
複製程式碼

effect重點

  • 依賴收集用到兩個東西:activeReactiveEffectStack,targetMap
  • 觸發依賴也用到兩個:targetMap,scheduler的queueJob

可以看出:1.targetMap是用來儲存依賴的

繼續看下scheduler的queueJob任務排程

scheduler

import { callWithErrorHandling } from './errorHandling'

const queue = []
const p = Promise.resolve()
let isFlushing = false

export function queueJob(job) {
    if (!queue.includes(job)) {
        queue.push(job)
        if (!isFlushing) {
            nextTick(flushJobs)
        }
    }
}

export function nextTick(fn) {
    return fn ? p.then(fn) : p
}

function flushJobs(seenJobs) {
    isFlushing = true
    let job
    while ((job = queue.shift())) {
        job()
    }
    isFlushing = false
}
複製程式碼

scheduler queueJob

queueJob主要是利用了Promise來進行一個微任務佇列的依賴更新:其實執行effect例項函式

第一條線內容結束語

到這裡第一線的內容就完了,遺留一個問題:

  • 在get的時候從activeReactiveEffectStack的最後一個取依賴

這說明啥?
說明在呼叫了effect -> 把effect push進activeReactiveEffectStack 之後,需要呼叫proxy[name]來觸發get

明白了這一點之後,繼續來看第二條線,第二條線的compile內容屬於自研,跟原始碼差距比較大

compile

import { effect } from './effect'
import { queueJob } from './scheduler'

export function compile(el, vm){
    let fragment = document.createDocumentFragment();
    let node;
    
    while(node = el.firstChild){
        compileNode(vm, node)
        fragment.append(node)
    }

    return fragment
}

const reg = /\{\{(.*)\}\}/;
function compileNode(vm, node){
    let { nodeType, nodeValue, nodeName } = node;

    node.update = (type, bindName) => {
        return effect(() => {
            node[type] = vm[bindName]
        }, { scheduler: queueJob })
    }


    let bindName;
    switch(nodeType){
        case 1:
            if(nodeName == 'INPUT'){
                let { attributes } = node;
                for(let attr of attributes){
                    if(attr.name === 'v-model'){
                        bindName = attr.value;
                    }
                }
                if(bindName){
                    node.addEventListener('input', e => {
                        vm[bindName] = e.target.value;
                    })
                }
                node.update('value', bindName)()
            }
            break;
        case 3:
            let isModal = reg.test(nodeValue)
            if(isModal){
                bindName = RegExp.$1 && RegExp.$1.trim();
                node.update('nodeValue', bindName)()
            }
            break;
    }
}
複製程式碼

compile的重點

重點在於:當解析出node和bingName之後,其實這時候可以呼叫proxy[name]來直接獲取值了

-> 可是這裡建立了個effect來建立key和當前node之間的關係

  • effect的回撥是用來呼叫node['nodeValue'] = proxy['name']來觸發get收集依賴的
  • 當set name使name改變之後,查詢當前key下面的effect佇列,呼叫各個effect的回撥更新dom

寫在最後

因為時間比較緊,所以寫得很倉促,自己也感覺文章寫的比較亂,單獨看文章的話可能會看不懂。需要先把DEMO過一遍,然後帶著問題來看這篇文章

相關文章