Vue3源碼解析,實現mini版Vue3

在看這篇文章以前但願你們仍是有必定的基礎,可以瞭解Vue2的一些基礎原理,瞭解Proxy代碼的基礎應用以及WeakMap,Map,Set這些數據結構的特性。若是對這些還不瞭解的同窗建議先去補一下基礎知識。javascript

若是你對vue3還不瞭解建議先去學習下vue3。html

vue3基礎特性vue

vue3+Ts實戰java

聲明: 本文中採用的方法名均爲源碼中的方法名,不少代碼結構按照源碼的結構來寫的,目的就是在於但願可以對想看源碼的同窗作一些引導。node

若是代碼中有些地方你不能一會兒想明白的,先嚐試去接受它,而後回過頭來再去理解它。react

重中之重:對於本文的學習方式上但願你們必定要更着去手寫代碼,多作一些思考,看完以後我相信你必定會有所收穫。程序員

若是有什麼不對的地方也歡迎你們留言指正。數組

響應式

咱們都知道vue2中是經過defineProperty實現對數據的攔截監聽,而vue3中採用的Proxy,相較於defineProperty,既能更方便的監聽數組,不須要進行嵌套,不用遍歷每一個屬性,針對整個對象更加輕便。markdown

vue3經過reactive建立一個響應式對象,當對象被修改的時候,視圖也會隨着更新,對應的反作用函數也會執行(這個咱們後面實現watchEffect的時候再分析)。這其中主要有兩個過程:數據結構

  • 對數據進行監聽並收集依賴
  • 更新視圖(後話)

reactive內部就是經過Proxy實現對數據的攔截監聽。

// v3.js

// reactive 的實現
function reactive(target){
    return createReactiveObject(
        target,
        mutableHandlers
    );
}

/** * 真正建立響應式對象的函數 * @param {*} target 對象 * @param {*} handler 處理程序 */
function createReactiveObject(target,handler){
    const proxy = new Proxy(target,handler);
    return proxy;
}
複製代碼

Proxy對數據的監聽主要在handler中進行處理, handler是一個定義一個或多個陷阱的對象。接着咱們實現一下mutableHandlers對象

// v3.js

const get = createGetter();
function createGetter(){
    return function get(target,key,receiver){
        const res = Reflect.get(target, key, receiver);
        console.log('get執行了',key);
        return res;
    }
}

const set = createSetter();
function createSetter(){
    return function set(target,key,value,receiver){
        const res = Reflect.set(target,key,value,receiver);
        console.log('set執行了',key);
        return res;
    }
}

// proxy的處理程序,是一個定義一個或多個陷阱的對象
// 只處理簡單的 get 和 set
const mutableHandlers = {
    get,
    set
}

// reactive 的實現
function reactive(target){
  return createReactiveObject(
    target,
    mutableHandlers
  );
}

/** * 真正建立響應式對象的函數 * @param {*} target 對象 * @param {*} handler 處理程序 */
function createReactiveObject(target,handler){
  const proxy = new Proxy(target,handler);
  return proxy;
}
複製代碼

這樣咱們就實現了一個簡單的reactvie,可以對數據進行監聽了。能夠經過如下代碼測試一下:

// v3.js

const testObj = reactive({ count:10 }})
testObj.count
testObj.count = 20

// 經過node v3.js執行當前代碼
// get執行了 count
// set執行了 count
複製代碼

這只是實現了對簡單對象的監聽,若是對象複雜一點,就會存在問題,例如:

// v3.js

const testObj = reactive({ count:10, info: { name:'lucas' }})
testObj.info.name
testObj.info.name = 'viky'

// 執行結果以下
// get執行了 count
// set執行了 count
複製代碼

這樣監聽就失效了,沒有監聽到name屬性,優化一下get方法,若是當值爲對象的時候,咱們應該遞歸監聽:

// v3.js

const isObject = (target)=>{ return target !== null && typeof target === 'object'}
function createGetter(){
    return function get(target,key,receiver){
        const res = Reflect.get(target, key, receiver);
        console.log('get執行了',key);
        if(isObject(res)){
            return reactive(res);
        }
        return res;
    }
}
複製代碼

到這裏咱們的reactive已經實現了對數據的監聽。

訂閱

接下來咱們開始收集依賴,依舊是在get中進行處理,經過track函數進行依賴收集,在這以前先大體介紹一下vue3中用於存儲依賴的數據結構。

20210326151449 (1)_gaitubao_375x265.jpg

緊緊記住這個數據結構(計時一分鐘...)

咱們開始擼代碼,實現track函數:

// v3.js

let targetMap = new WeakMap(); // 存儲對象 
let activeEffect; // 當前要添加的effect

/** * 收集依賴 * @param {*} target 目標對象 * @param {*} key 鍵值 */
function track(target,key){
    if(activeEffect === undefined){
        return;
    }
    let desMap = targetMap.get(target);
    if(!desMap){
        targetMap.set(target,(desMap=new Map()))
    }
    let deps = desMap.get(key);
    if(!deps){
        desMap.set(key,(deps = new Set()))
    }
    if(!deps.has(activeEffect)){
        deps.add(activeEffect)
        // 用於清除依賴(後面分析)
        activeEffect.deps.push(deps)
    }
}
複製代碼

咱們在get陷阱中進行調用:

// v3.js

function createGetter(){
    return function get(target,key,receiver){
        const res = Reflect.get(target, key, receiver);
        // 依賴收集
        track(target, key)
        if(isObject(res)){
            return reactive(res);
        }
        return res;
    }
}
複製代碼

到這裏你可能會產生一個疑問,activeEffect這究竟是個什麼東西,在哪裏賦值的?

activeEffect是一個被封裝過的反作用函數,裏面的執行函數多是咱們本身傳進去的函數,也多是更新視圖的函數。至於具體在何時被賦值的,咱們在這裏留一個問題一,後面在一一解答,先接着往下看。

發佈

咱們的收集工做大體完成了,接下來想一下當監聽的對象被修改時,咱們如何執行收集的對應的反作用函數。v3中經過trigger來實現。

//v3.js

/** * 觸發對應的反作用函數 * @param {*} target 目標對象 * @param {*} key 鍵值 */
function trigger(target,key){
    // 獲取咱們存儲的target對應的Map
    const depsMap = targetMap.get(target);
    if(!depsMap){
        return;
    }
    const effects = new Set(); // 反作用函數隊列
    // 定義向隊列中添加的add方法
    const add = (effectsToAdd)=>{
        effectsToAdd.forEach(effect => {
            effects.add(effect)
        });
    }
    // 將對應key的反作用函數加入隊列
    add(depsMap.get(key));
    // 定義執行函數
    const run = (effect)=>{
        effect();
    }
    // 執行
    effects.forEach(run);
}
複製代碼

做者在保留源碼痕跡的前提下簡化了trigger函數,去除了一些其餘狀況的處理。咱們繼續優化下set陷阱:

// v3.js

function createSetter(){
    return function set(target,key,value,receiver){
        const res = Reflect.set(target,key,value,receiver);
        // 執行對應的反作用函數
        trigger(target,key)
        return res;
    }
}

複製代碼

到這邊咱們基本上告一個段落,經過實現了reactive建立一個響應式對象,並在getset陷阱中進行了訂閱以及發佈。但咱們還留了一個關於activeEffect的問題,爲了更好的回答這個問題以及理解以上的代碼,咱們經過實現一個官方的watchEffect函數讓以上代碼能跑起來,驗證咱們的發佈訂閱是否生效。若是有不太瞭解watchEffect的同窗,建議先去看看它的基礎用法,而後不妨先結合咱們上面的代碼思考一下它的實現方式。

子曰:弗思何以得

watchEffect的實現

watchEffect是根據響應式對象自動應用和從新應用反作用的函數,接收一個反作用函數並當即執行一次。 返回一個能夠中止偵聽的函數。 開始一步步實現這個函數,

// v3.js

/** * 監測響應式對象並執行對應的反作用函數 * @param {*} effect 反作用函數 */
function watchEffect(effect){
    return doWatch(effect);
}

function doWatch(fn){
    // 包裝處理
    const getter = () => {
        // 永遠不要相信程序員的代碼不會報錯
        return callWithErrorHandling(fn);
    }
    // 執行器
    const runner = effect(getter);
    // 當即執行一次以收集依賴
    runner();
    // 返回一個函數以清除反作用函數
    return ()=>{
        stop(runner)
    }
}

// 執行函數並對報錯進行處理
function callWithErrorHandling(fn){
    var res;
    try {
        res = fn();
    } catch (error) {
        // 報錯處理
        throw new Error(error);
    }
    return res;
}

複製代碼

首先將咱們傳進來的反作用函數經過callWithErrorHandling方法包裝一下,以便作函數執行時的報錯處理(這裏就簡單處理了),獲得咱們的getter,再用getter去建立一個effect函數,而後該函數當即執行一次, 最後返回一個能夠中止偵聽的函數。大體結構就是這樣。

咱們接着來看看effect函數長啥樣呢?

// v3.js

/** * 建立effect * @param {*} fn 函數 */
function effect(fn){
    const effect = createReactiveEffect(fn);
    return effect;
}

// effect標識id
let uid = 0;

function createReactiveEffect(fn){
    const effect = function reactiveEffect(){
        try {
            activeEffect = effect;
            return fn();
        } finally {
            // 及時釋放,避免形成污染
            activeEffect = undefined;
        }
    }
    // 標識
    effect.id = uid++;
    // 被收集在set的數組,以便清除
    effect.deps = []
    return effect;
}
複製代碼

effect是一個函數,該函數會被賦給activeEffect,當該函數被執行的時候,會執行咱們最開始傳進watchEffect反作用函數,若是當反作用函數中有讀取到咱們的響應式對象的時候就會觸發track進行依賴收集,收集的就是這個effect函數(即activeEffect),這也是爲何上面說runner要當即執行一次的緣由,用於收集依賴。當響應式對象改變的時候,會觸發trigger再執行這個effect(activeEffect)函數。effect函數上還添加了惟一標識id和用於存放被收集(我本身取得名稱)的數列deps

這裏可能會有點繞,若是還有點沒明白的同窗建議多看兩遍,「書讀百遍,其義自現」,代碼也同樣。後面在會再總的分析下這些數據之間的聯繫。

接着咱們先繼續來看下中止偵聽的stop方法。

// v3.js

function stop(effect){
    cleanup(effect)
}

// 清除反作用函數
function cleanup(effect){
    const { deps } = effect;
    if(deps.length){
        for(var i =0; i<deps.length; i++){
            deps[i].delete(effect)
        }
    }
}
複製代碼

上面說到effect函數上有一個存放被收集的數列,這個數列裏面存放的都是收集了這個effectSet集合。刪除這些集合中對該effect的收集便可。

我用一張圖再幫你們梳理一下這其中數據結構之間的一個關係。

20210401222347 (1)_gaitubao_1000x455.jpg

結合這張圖,多看兩遍代碼,這其中的關係天然銘記在心了。

這就是watchEffect的實現了,咱們來看看跑起來看一下,新建一個html頁面。

// html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue3源碼解析</title>
</head>
<body>
    <button onclick="add()">點擊+1</button>
    <button onclick="clearFn()">中止反作用</button>
    <script src="./v3.js"></script>
    <script>
        const obj = reactive({
            count:1
        })
        const add = () => {
            obj.count++;
        }
        const clearFn = watchEffect(()=>{
            console.log('watchEffect執行',obj.count);
        })
    </script>
</body>
</html>
複製代碼

效果以下如圖:

rjx20-vt47e.gif

乘熱打鐵,接着咱們來看下初始化流程。

初始化流程

咱們先來看vue3是怎麼建立一個應用程序的:

createApp({
    ...
}).mount("#app");
複製代碼

先大膽想一下,這個調用模式是怎麼實現的?

經過createApp方法返回了一個對象,而後對象裏面有個mount方法?

咱們先來看下大體結構:

// v3.js

// 建立應用程序入口
const createApp = (...args) => {
    const app = ensureRenderer().createApp(...args);
    return app;
}

const ensureRenderer = () => {
    // 源碼返回 renderder || (renderer = createRenderder()) 
    // 保存了renderer 
    // 初始化renderer確定爲空,因此直接建立,省略中間函數
    return baseCreateRenderer();
}

// 實際建立renderer的函數
const baseCreateRenderer = () => {
    // 這裏面定義了不少渲染函數
    // ...
    const render = () => {} ;
    return {
        render,
        createApp:createAppAPI(render)
    }
}

function createAppAPI(render){
    /**建立app實例的方法 * rootComponent:createApp傳進來的初始化Oject對象 */
    return function createApp(rootComponent){
        const app = {
            // 這裏面是咱們熟悉的方法,字段:_uid,_props,_context等等
            _component:rootComponent,
            // use(){ 
            // return app 
            // },
            // component(){
            // return app
            // },
            // directive(){
            // return app
            // },

            // 裝載節點
            mount(rootContainer){
                // return app 用於鏈式調用
                return app;
            }
        }
        return app;
    }
}
複製代碼

createApp方法返回一個app實例,再經過調用實例上的mount方法將數據渲染成頁面(這個跨度有點大),這是它的一個大體骨架,咱們來一點點填充。

首先咱們從mount入手,咱們經過傳進來的節點id來找到要裝載的dom。源碼中在createApp方法中對mount方法進行了擴展,在擴展中獲取dom節點和模版信息。 而後經過render函數進行渲染。

// v3.js

const createApp = (...args) => {
    const app = ensureRenderer().createApp(...args);
    const { mount } = app ; 
    app.mount = (containerOrSelector) => {
        // 獲取真實dom節點
        const container = document.querySelector(containerOrSelector);
        if(!container){
            return;
        }

        // 獲取並保存template
        const component = app._component
        component.template = container.innerHTML;

        const proxy = mount(container);
        return proxy;
    }
    return app;
}

function createAppAPI(render){
    /**建立app實例的方法 * rootComponent:createApp傳進來的初始化Oject對象 */
    return function createApp(rootComponent){
        const app = {
            // 這裏面是咱們熟悉的方法,字段:_uid,_props,_context等等
            _component:rootComponent,
            mount(rootContainer){
                // 建立組件的vnode
                const vnode = createVNode(rootComponent);
                render(vnode, rootContainer);
                // return app 用於鏈式調用
                return app;
            }
        }
        return app;
    }
}

// 建立虛擬dom
function createVNode(type){
    return {
        type,
        props:null,
        children:null 
    }
}
複製代碼

注意: 咱們在建立app實例的時候將rootComponent參數直接賦值給了app._component,因爲rootComponent是個Object,複製的實際上是內存地址,隨後又在擴展的mount方法內設置了template字段,其實就是給rootComponent添加了template字段。(基礎知識,提醒一下)

虛擬dom(vnode)想必你們都很熟悉了,它其實就是個Object對象,經過對象字面量的方式描述了一個dom元素的相關信息。這邊做者就寫了等會要用到的三個字段:type, props, children

源碼中的vnode長這樣:

{
    __v_isVNode: true,
    [ReactiveFlags.SKIP]: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    children: null,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
}
複製代碼

繼續寫render函數,做者這邊寫的比較簡單,初始化有些流程就直接跳過了,直接在render中執行mountComponent方法了,咱們來更新下baseCreateRenderer方法:

// v3.js

const baseCreateRenderer = () => {
    // ...
    // 這裏面定義了不少渲染函數
    
    const mountComponent = (vnode,container) =>{
        // 初始化一個組件實例子
        // const instance = createComponentInstance(vnode);
        
        const instance = { type: vnode.type }; 	

        // 初始化setup
        setupComponent(instance)

        // 初始化渲染反作用函數
        setupRenderEffect(instance,container)
    }

    // ...
    const render = (vnode,container) => {
        mountComponent(vnode,container);
    };
   
    return {
        render,
        createApp:createAppAPI(render)
    }
}
複製代碼

mountComponent中首先會建立一個組件實例(做者比較懶,直接寫了一個對象),會執行兩個主要的函數setupComponentsetupRenderEffect

setupComponent方法主要作幾件事。

  • 初始化執行setup,執行結果爲result
  • result經過reactive進行響應式監聽並綁定在組件實例instance上,
  • template轉換爲ast
  • ast轉換成render函數並綁定在組件實例instance上。
  • 兼容vue2.0的options。(後面再說)

因爲編譯模塊相對複雜,咱們先實現前兩個點,編譯模塊後面在單獨分析。

setup的結果在進行響應式監聽是爲了在進行讀取的時候可以觸發依賴收集。

setupRenderEffect方法設置頁面初始化渲染和更新的反作用函數(上面有提到)並執行一次反作用函數。

開始擼代碼,提醒你們注意看寫代碼的位置:

// v3.js

const baseCreateRenderer = () => {
    // ...
    // 這裏面定義了不少渲染函數
    
    // 渲染反作用函數
    const setupRenderEffect = (instance,container) => {
        instance.update = effect(function componentEffect(){
            // 直接渲染
            if(!instance.isMounted){
                // 獲取渲染的樹vnode
                const subTree = (instance.subTree = renderComponentRoot(instance))
                patch(
                    null,
                    subTree,
                    container
                )
                instance.isMounted = true;
            }else{
                // 要獲取新舊vnode進行對比

                // 獲取以前的vnode
                const prevTree = instance.subTree;
                // 下一次即將更新的樹
                const nextTree = renderComponentRoot(instance);
                patch(
                    prevTree,
                    nextTree,
                    container
                )
            }
        })

        // 手動初始化執行一次
        instance.update();
    }
    
    // ...
    
    return {
        render,
        createApp:createAppAPI(render)
    }
}

// 初始化setup
function setupComponent(instance){
    const { setup } = instance.type;
    if(!setup){
        return;
    }
    // 永遠不要相信程序員的代碼不會報錯
    const setupResult = callWithErrorHandling(setup);
    instance.setupState = reactive(setupResult);
}

// 構造的渲染樹
function renderComponentRoot(instance){
    const { setupState }  = instance;
    // 因爲沒有模版編譯,咱們直接建立renderNode
    return {
        type:'div',
        props:{
            id:'demo'
        },
        children:[
            {
                type:'h1',
                props:{
                    onclick:()=>{
                        console.log('點擊事件觸發了~');
                    }
                },
                children: setupState.count
            }
        ]
    }
}

複製代碼

componentEffect在源碼中是經過effect函數的lazy字段進行控制自執行一次。函數中在第一次執行的時候會生成咱們渲染節點樹vnode,並保存,標記isMounted爲true,當下次觸發器的時候會建立新的渲染節點樹vnode,而後會將二者傳進patch進行比較後渲染爲真實的dom

注意: 程序第一次運行的時候咱們建立的vnode存放在了instance上,用於後面更新的時候取爲舊節點。

咱們這邊在renderComponentRoot函數裏面索性就直接返回了一個vnode

注意: 這邊的渲染節點樹(vnode)和最開始在mount建立的vnode是不同的,這裏的vnode是真正的要渲染的樹結構。

又出現一個核心函數patch,相信你們應該不陌生。

// v3.js

const baseCreateRenderer = () => {
    // ...
    // 這裏面定義了不少渲染函數
    
    // vnode => dom
    const mountElement = () => {} 
    
    // 更新 dom
    const patchElement = () => {}
    
    const patch = (n1,n2, container) => {
        // 初始化渲染
        if(!n1){
            mountElement(n2,container)
        }else{
            patchElement(n1,n2,container)
        }
    }
    
   
    return {
        render,
        createApp:createAppAPI(render)
    }
}
複製代碼

這個patch函數相比於源碼作了簡化,當n1爲空的時候直接渲染,不然進行對比以後渲染。

渲染Vnode

mountElement纔是真正的將vnode轉爲真實的dom,咱們將其中會涉及到的dom操做和一些對比過程封裝爲一個renderOptions對象,並傳給baseCreateRenderer,咱們先實現mountElement:

// v3.js

// 渲染操做
let rendererOptions = {
    // 插入節點
    insert: (child, parent) => {
        parent.insertBefore(child,  null)
    },
    // 建立節點
    createElement: (tag) => document.createElement(tag),
    // 建立文本
    createText: text => document.createTextNode(text),
    // 對比屬性
    patchProp: (container, key, value) => {
        if (value) { 
            // 當值爲方法的時候,作包裹處理
            if(typeof value === 'function'){
                container.setAttribute(key,`(${value})()`)
            }else{
                container.setAttribute(key,value)
            }
        } else {
            container.removeAttribute(key)
        }
    },
    // 設置節點文本
    setElementText: (el, text) => {
        el.textContent = text
    },
    // 獲取父節點
    parentNode: node => node.parentNode,
}

const ensureRenderer = () => {
    return baseCreateRenderer(rendererOptions);
}

const baseCreateRenderer = (options) => {
    const {
        insert: hostInsert,
        patchProp: hostPatchProp,
        createElement: hostCreateElement,
        setElementText:hostSetElementText,
        parentNode:hostGetParentNode
    } = options
    
    // ...
        
    const patch = (n1,n2, container) => {
        // 初始化渲染
        if(!n1){
            mountElement(n2,container)
        }else{
            patchElement(n1,n2,container)
        }
    }

    // vnode => dom
    const mountElement = (n2,container) => {
        const { type , props, children } = n2;
        // type 建立元素
        const el = n2.el = hostCreateElement(type)
        // props 屬性
        if(props){
            Object.keys(props).forEach((key)=>{
                // 直接設置
                hostPatchProp(el,key,props[key])
            })
        }
        // children 內容
        if(typeof children === 'string' || typeof children === number){
            hostSetElementText(el,children)
        }else if(Array.isArray(children)){
            children.forEach((vnode)=>mountElement(vnode,el))
        }
        
        // 添加節點
        hostInsert(el,container);
    }

    // 對比更新
    const patchElement = (n1,n2) => {
    }

    // ...
    const render = (vnode,container) => {
        mountComponent(vnode,container);
    };

    return {
        render,
        createApp:createAppAPI(render)
    }
}

複製代碼

封裝的基礎方法就不說了,來看mountElement方法作了哪些事情:

首先根據type建立元素,建立的元素咱們要保存在vnode上,用於下次更新的時候直接進行patchElement,而後在設置元素的props,判斷children類型,若是是字符串或數字直接渲染,不然遞歸調用mountElement渲染子節點。

注意: 咱們在建立節點的時候將這個節點掛載到了vnodeel字段上,用於後面作更新操做。

更新視圖

patchElement函數用於對比新舊vnode,將須要更新的地方進行更新,這邊須要注意的是,咱們舊的vnode裏面存了一個咱們的真實的dom, 那咱們就不必根據新的vnode進行從新建立了,而是在舊的el上進行一個差別更新就行了。

實現patchElement函數:

// v3.js

// 對比更新
const patchElement = (n1, n2) => {
    const { type: oldType, props: oldProps, children: oldChildren,el } = n1;
    const { type: nextType, props: nextProps, children: nextChildren } = n2;
    // type
    if (oldType !== nextType) {
        // 獲取父節點
        const container = hostGetParentNode(n1.el);
        // 直接從新渲染
        mountElement(n2, container);
        return;
    } else { 
        if (nextProps) { 
            Object.keys(nextProps).forEach(key => { 
                // 當舊元素的屬性不等於新元素的屬性直接設置新元素的元素
                if (oldProps[key] !== nextProps[key]) { 
                    hostPatchProp(el,key,nextProps[key])
                }
            })
        }

        if (oldProps) { 
            Object.keys(oldProps).forEach(key => {
                // 新元素中沒有這個屬性了,則移除
                if (!(key in nextProps)) { 
                    hostPatchProp(el,key,null)
                }
            });
        }

        const isStringOrNumber = (str) => {
            return str === '' || typeof str === 'string' || typeof str === 'number';
        }

        // children
        if (isStringOrNumber(nextChildren)) {
            if (isStringOrNumber(oldChildren)) {
                // 都是字符串且值不同
                if (nextChildren !== oldChildren) {
                    hostSetElementText(el, nextChildren)
                }
            } else if (Array.isArray(oldChildren)) {
                // 新值爲字符串,舊元素爲數組
                hostSetElementText(el, nextChildren)
            }
        } else { 
            // 當新舊元素的children都是數組
            patchChildren(n1, n2);
        }
    }
}

// 對比children
const patchChildren = (n1, n2) => {
    const { children: oldChildren, el } = n1;
    const { children: nextChildren } = n2;
    for (var i = 0; i < nextChildren.length; i++) { 
        // 若是舊元素有值舊進行對比
        if (oldChildren[i]) {
            patchElement(oldChildren[i], nextChildren[i]);
        } else { 
            // 沒有直接建立
            mountElement(nextChildren[i],el)
        }
    }
}
	
複製代碼

判斷節點類型,若是不同,直接按照新的vnode從新渲染。

判斷屬性,遍歷新vnode中的屬性,若是有不同的,直接替換爲新的,再循環舊屬性,保留以前舊節點的本來就有的屬性。

判斷子節點,這邊要考慮節點的類型,有多是數組,有多是字符串:

  • 新舊子節點都是字符串類型

若是類型不同直接替換。

  • 舊子節點爲數組,新子節點爲字符串

直接替換

  • 新舊子節點都是數組

暫且將新的children稱爲n2,將舊的children稱爲n1。既然是更新,咱們確定是以n2爲主,因此循環n2,若是對應的n1也有數據的話就經過patchElement遞歸進行節點對比。若是n1沒有的話就經過mountElement建立。

以上就是patch的大體思路了,這邊的patch過程其實很不嚴謹,有不少狀況沒有考慮到,好比元素的先後順序等。這邊掌握大體思路就能夠了。

激動人心的時候終於要來了,讓咱們來看看代碼運行起來的結果,新建一個html文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue3源碼解析</title>
</head>
<body>
    <div id="app"></div>
    <script src="./v3.js"></script>
    <script> createApp({ setup(){ const obj = reactive({ count:0 }) return { obj } } }).mount("#app") </script>
</body>
</html>
複製代碼

再來看下renderComponentRoot返回了渲染節點:

{
    type:'div',
    props:{
        id:'demo'
    },
    children:[
        {
            type:'h1',
            props:{
                onclick:()=>{
                    console.log('點擊事件觸發了~');
                }
            },
            children: setupState.obj.count
        }
    ]
}
複製代碼

根據vnode咱們推測最後渲染出來的頁面結構應該是這樣:

<div id="app">
    <div id="demo">
        <h1 onclick="(()=>{console.log('點擊事件觸發了~');})()">0</h1>
    </div>
</div>
複製代碼

執行結果以下:

5eta7-89mr3.gif

咱們再來看一下響應式更新:

// html


<script> createApp({ setup(){ const obj = reactive({ count:0 }) const changeCount = ()=>{ obj.count++; } return { obj, changeCount } } }).mount("#app") </script>

複製代碼

這邊因爲咱們在設置元素屬性的時候簡單的經過setAttribute進行了設置,因此點擊事件中的對象都仍是調用的window的對象,咱們還須要稍微改一下setupComponent的代碼,將instance.setupState的執行結果在綁定在window上。順便修改下vnode的點擊事件:

// v3.js

// 初始化setup
function setupComponent(instance){
    ...
    Object.keys(instance.setupState).forEach(key=>{
        // 掛載到window上
        window[key] = instance.setupState[key]
    })
}

// 構造的渲染樹
function renderComponentRoot(instance){
    const { setupState }  = instance;
    return {
        ...
                type:'h1',
                props:{
                    onclick: setupState.changeCount
                },
                children: setupState.obj.count
        ...
    }
}
複製代碼

效果以下:

hw49y-800jz.gif

結尾

咱們實現了響應式原理,瞭解了Vue3初始化流程,並實現了響應式更新頁面。附帶着實現了watchEffect

學而不思則罔,思而不學則殆。

打工人,今天的你收穫了嗎?

相關文章
相關標籤/搜索