[學習vue3]準備階段[一]

調試環境準備

遷出Vue3源碼: git clone https://github.com/vuejs/vue-next.git
安裝依賴: yarn --ignore-scripts
生成sourcemap文件,package.json

"dev": "node scripts/dev.js --sourcemap"

編譯: yarn dev

生成結果:
packages\vue\dist\vue.global.js
packages\vue\dist\vue.global.js.map

vue3源碼架構

vue3初始化過程

createApp()是如何創建vue實例的;創建的vue實例執行mount()都做了些什么?

//init.html
<div id="app"> 
     <h1>vue3初始化流程</h1>
</div>
<script src="../../dist/vue.global.js"></script>
<script>
const {createApp} = Vue
createApp({}).mount('#app') 
</script>

斷點調試createApp()
ensureRenderer() => renderer => createApp()
createAppAPI() => createApp
app.mount() => render() => patch() => processComponent() => mountComponent() => setupComponent() => setupRenderEffect()

執行流程

createApp() packages/runtime-dom/src/index.ts 創建vue實例、擴展mount方法
createRenderer()/baseCreateRenderer()
packages/runtime-core/src/renderer.ts
創建renderer對象,它對外暴露3個重要方法 render , hydrate , createApp ,其中 render ,和 hydrate 的實際使用者是createApp()返回的vue實例對象。
createAppAPI(render, hydrate)
packages/runtime-core/src/apiCreateApp.ts 返回生產vue實例的createApp函數

render的使用者是vue實例的mount方法 我們發現component()/directive()/use()/mixin()這些方法都變成了實例方法,它們也會返回實例本身,鏈式調用成為可能
filter方法被移除了

createApp({})
.component('comp', { template: '<div>this is comp</div>' }) .directive('focus', { mounted(el) { el.focus() } }) 
.mount('#app')
  • mount(rootContainer: HostElement, isHydrate?: boolean)
    packages/runtime-core/src/apiCreateApp.ts
    將 createApp(rootComponent) 中傳入的根組件轉換為vnode,然后渲染到宿主元素rootContainer 中。
  • render(vnode, container) 將傳入vnode渲染到容器container上。
  • patch(n1, n2, container)
    將傳入的虛擬節點 n1 跟 n2 進行對比,并轉換為dom操作。初始化時 n1 并不存在,因此操作將是一次dom創建。
  • mount(rootContainer)
    packages/runtime-core/src/apiCreateApp.ts 執行根組件掛載,創建其vnode,并將它render()出來
  • render()
    packages/runtime-core/src/renderer.ts 執行補丁函數patch()將vnode轉換為dom。
  • patch(n1, n2, container)
    packages/runtime-core/src/renderer.ts 根據n2的類型執行相對應的處理函數。對于根組件,執行的是processComponent()
  • processComponent()
    packages/runtime-core/src/renderer.ts 執行組件掛載或更新,由于首次執行時n1為空,因此執行組件掛載邏輯mountComponent()
  • mountComponent()
    packages/runtime-core/src/renderer.ts 創建組件實例,執行setupComponent()設置其數據狀態,其中就包括setup()選項的執行

setup()如何生效

在vue3中如果要使用composition-api,就需要寫在setup()中,它是如何生效并和options-api和諧共處的?

<div id="app"> <h1>setup()如何生效</h1> <p>{{foo}}</p>
</div>
<script src="../../dist/vue.global.js"></script> 
<script>
const { createApp, h, ref } = Vue
  createApp({
    setup() {
const foo = ref('hello, vue3!')
      return { foo }
    }
}).mount('#app') 
</script>

執行過程

根組件執行掛載mount()時,執行渲染函數render()獲取組件vnode,然后執行補丁函數patch()將其轉換 為真實dom,對于組件類型會調用processComponent(),這里會實例化組件并處理其setup選項。
setupComponent() packages/runtime-core/src/component.ts 初始化props、slots和data

setupStatefulComponent(instance, isSSR) packages/runtime-core/src/component.ts 代理組件實例上下文,調用setup()

setup()會接收兩個參數,分別是props和setupContext,可用于獲取屬性、插槽內容和派發事件

createApp({
props: ['bar'], // 屬性依然需要聲明 setup(props) {
// 作為setupResult返回
return { bar: props.bar } }
// 傳入rootProps
}, {bar: 'bar'}).mount('#app')

handleSetupResult(instance, setupResult, isSSR) packages/runtime-core/src/component.ts 處理setup返回結果,如果是函數則作為組件的渲染函數,如果是對象則對其做響應化處理。

自定義渲染器

可以自定義渲染器,將獲取到的vnode轉換為特定平臺的特定操作

范例:利用canvas畫圖

第一步:我們創建一個渲染器,需要給它提供節點和屬性的操作

const { createRenderer } = Vue
// 創建一個渲染器,給它提供節點和屬性操作
const nodeOps = {}
const renderer = createRenderer(nodeOps);

第二步:創建畫布,我們通過擴展默認createApp做到這一點

// 保存畫布和其上下文 let ctx;
let canvas;
// 擴展mount,首先創建一個畫布元素
function createCanvasApp(App) {
    const app = renderer.createApp(App);
    const mount = app.mount;
    app.mount = function (selector) {
        canvas = document.createElement('canvas');
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        document.querySelector(selector).appendChild(canvas);
        ctx = canvas.getContext('2d');
        mount(canvas);
    }
    return app;
}
// 創建app實例 createCanvasApp({}).mount('#app')

此時已經可以看到canvas,但是會報一個錯誤,是因為我們上面組件是空的,vue想要創建一個 comment元素導致

第三步:添加模板

<script type="text/x-template" id="chart">
      <bar-chart :data="chartData"></bar-chart>
</script>
<div id="app"></div>
createCanvasApp({
    template: '#chart', data() {
        return {
            chartData: [
                { title: "?銅", count: 200, color: "brown" },
                { title: "磚石", count: 300, color: "skyblue" },
                { title: "星耀", count: 100, color: "purple" },
                { title: "王者", count: 50, color: "gold" }
            ]
        }
    }
}
)

第四步:節點操作實現


// 保存canvas實例和上下文 let ctx, canvas
const nodeOps = {
    createElement: (tag, isSVG, is) => {
        // 創建元素時由于沒有需要創建的dom元素,只需返回當前元素數據對象 return {tag}
    },
    insert: (child, parent, anchor) => {
        // 我們重寫了insert邏輯,因為在我們canvasApp中不存在實際dom插入操作 // 這里面只需要將元素之間的父子關系保存一下即可
        child.parent = parent
        if (!parent.childs) {
            parent.childs = [child]
        } else {
            parent.childs.push(child)
        }
        // 只有canvas有nodeType,這里就是開始繪制內容到canvas 
        if (parent.nodeType === 1) {
            draw(child)
        }
    },
    patchProp(el, key, prevValue, nextValue) {
        el[key] = nextValue;
    }
}

第四步:繪圖邏輯


const draw = (el, noClear) => {
    if (!noClear) {
        ctx.clearRect(0, 0, canvas.width, canvas.height)
    }
    if (el.tag == 'bar-chart') {
        const { data } = el;
        const barWidth = canvas.width / 10,
            gap = 20,
            paddingLeft = (data.length * barWidth + (data.length - 1) * gap) / 2, paddingBottom = 10;
        // x軸
        // 柱狀圖
        data.forEach(({ title, count, color }, index) => {
            const x = paddingLeft + index * (barWidth + gap)
            const y = canvas.height - paddingBottom - count
            ctx.fillStyle = color
            ctx.fillRect(x, y, barWidth, count)
            // text
        });
    }
    // 遞歸繪制子節點
    el.childs && el.childs.forEach(child => draw(child, true));
}

配置自定義組件白名單:
app.config.isCustomElement = tag => tag === 'bar-chart'

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容