1.渲染組件
如果是組件則:vnode .type的值是一個對象。
如下:
const vnode = {type: MyComponent,}
為了讓渲染器
能處理組件類型的虛擬節點,我們還需要在patch函數中對組件類型的虛擬節點進行處理,如下:
function patch(n1, n2, container, anchor) {if(!n1 && n1.type !== n2.type) {unmount(n1)n1 = nill}const { type } = n2if (typeof type === 'string') {} else if (typeof type === 'object') {// 組件if (!n1) {// 掛載組件
+ mountComponent(n2, container,anchor )} else {// 更新組件
+ patchComponent(n1, n2, anchor)}}
}
一個組件必須包含一個渲染函數,即render函數,并且渲染函數的返回值應該是虛擬dom。如下:
const MyComponent = {name: 'MyComponent',render() {return {type: 'div',children: '我是文本'}}
}
有了基本的結構,渲染器就能完成組件的渲染。渲染器中真正完成組件的渲染的是mountComponent函數。實現如下:
function mountComponent(vnode, container, anchor) {// 通過vnode獲取組件的選項對象,即vnode.typeconst componentOptions = vnode.type// 獲取組件的渲染函數const { render } = componentOptions// 執行渲染函數,獲取組件的渲染函數內容,即render返回的虛擬domconst subTree = render()// 最后調用 patch 函數來掛載組件所描述的內容,即 subTreepatch(null, subTree, container, anchor)
}
2.組件狀態與自更新
為組件設計自身的狀態:data
我們用data函數來定義組件自身的狀態。
const MyComponent = {name: 'MyComponent',data() {return {foo: 'dd'}},render() {return {type: 'div',children: `foo 的值是: ${this.foo}` // 在渲染函數內使用組件狀態}}
}
我們約定用戶必須使用data函數來定義組件自身的狀態,同時可以在渲染函數中通過this訪問data函數返回的狀態數據:
function mountComponent(vnode, container, anchor) {// 通過vnode獲取組件的選項對象,即vnode.typeconst componentOptions = vnode.type// 獲取組件的渲染函數
+ const { render, data } = componentOptions+ //調用 data 函數得到原始數據,并調用 reactive 函數將其包裝為響應式數據
+ const state = reactive(data())// 執行渲染函數,獲取組件的渲染函數內容,即render返回的虛擬dom
+ // 調用 render 函數時,將其 this 設置為 state,從而 render 函數內部可以通過 this 訪問組件自身狀態數據
+ const subTree = render.call(state,state)// 最后調用 patch 函數來掛載組件所描述的內容,即 subTreepatch(null, subTree, container, anchor)
}
實現組件自身狀態的初始化需要兩個步驟:
-
- 通過組件的選項對象取得 data 函數并執行,然后調用 reactive 函數將 data 函數返回的狀態包裝為響應式數據;
-
- 在調用 render 函數時,將其 this 的指向設置為響應式數據 state,同時將 state 作為 render 函數的第一個參數傳遞。
當組件自身狀態發生變化時,我們需要有能力觸發組件更新,即 組件的自更新。
為此,我們需要將整個渲染任務包裝到一個effect中,如下:
function mountComponent(vnode, container, anchor) {// 通過vnode獲取組件的選項對象,即vnode.typeconst componentOptions = vnode.type// 獲取組件的渲染函數const { render, data } = componentOptions//調用 data 函數得到原始數據,并調用 reactive 函數將其包裝為響應式數據const state = reactive(data())+ // 將組件的 render 函數調用包裝到 effect 內
+ effect(() => {// 執行渲染函數,獲取組件的渲染函數內容,即render返回的虛擬dom// 調用 render 函數時,將其 this 設置為 state,從而 render 函數內部可以通過 this 訪問組件自身狀態數據const subTree = render.call(state,state)// 最后調用 patch 函數來掛載組件所描述的內容,即 subTreepatch(null, subTree, container, anchor)
+ })}
將組件的 render 函數調用包裝到 effect 內,這樣一旦組件自身響應式數據發生變化,組件就會自動重新 執行渲染函數,從而完成更新。但是,由于effect的執行是同步的,因此放響應式數據發生變化時,與之關聯的副作用函數會同步執 行。
換句話說,如果多次修改響應式數據的值,將會導致渲染函數執 行多次,這實際上是沒有必要的。因此,我們需要設計一個機制,以 使得無論對響應式數據進行多少次修改,副作用函數都只會重新執行 一次。為此,我們需要實現一個調度器,當副作用函數需要重新執行 時,我們不會立即執行它,而是將它緩沖到一個微任務隊列中,等到 執行棧清空后,再將它從微任務隊列中取出并執行。有了緩存機制,我們就有機會對任務進行去重,從而避免多次執行副作用函數帶來的性能開銷。
具體實現如下:
// 任務緩存隊列,用一個 Set 數據結構來表示,這樣就可以自動對任務進行去重
const queue = new Set()// 一個標志,代表是否正在刷新任務隊列
let isFlushing = false// 創建一個立即 resolve 的 Promise 實例
const p = Promiser.resolve()// 調度器的主要函數,用來將一個任務添加到緩沖隊列中,并開始刷新隊列
function queueJob(job) {queue.add(job)// 如果還沒有開始刷新隊列,則刷新之if (!isFlushing) {isFlushing = truep.then(() => {try {// 執行任務隊列中的任務queue.forEach((job) => job())} finally{// 重置狀態isFlushing = falsequeue.clear = 0}})}
}
上面是調度器的最小實現,本質上利用了微任務的異步執行機 制,實現對副作用函數的緩沖。其中 queueJob 函數是調度器最主要 的函數,用來將一個任務或副作用函數添加到緩沖隊列中,并開始刷 新隊列
。有了 queueJob 函數之后,我們可以在創建渲染副作用時使 用它,
function mountComponent(vnode, container, anchor) {// 通過vnode獲取組件的選項對象,即vnode.typeconst componentOptions = vnode.type// 獲取組件的渲染函數const { render, data } = componentOptions//調用 data 函數得到原始數據,并調用 reactive 函數將其包裝為響應式數據const state = reactive(data())// 將組件的 render 函數調用包裝到 effect 內effect(() => {// 執行渲染函數,獲取組件的渲染函數內容,即render返回的虛擬dom// 調用 render 函數時,將其 this 設置為 state,從而 render 函數內部可以通過 this 訪問組件自身狀態數據const subTree = render.call(state,state)// 最后調用 patch 函數來掛載組件所描述的內容,即 subTreepatch(null, subTree, container, anchor)}, {// 指定該副作用函數的調度器為 queueJob 即可scheduler: queueJob})}
這樣,當響應式數據發生變化時,副作用函數不會立即同步執行,而是會被 queueJob 函數調度,最后在一個微任務中執行。
不過,上面這段代碼存在缺陷。可以看到,我們在 effect 函數內調用 patch 函數完成渲染時,第一個參數總是 null。這意味著,每次更新發生時都會進行全新的掛載,而不會打補丁,這是不正確的。正確的做法是:每次更新時,都拿新的 subTree 與上一次組件所渲染的 subTree 進行打補丁。為此,我們需要實現組件實例,用它來維護組件整個生命周期的狀態,這樣渲染器才能夠在正確的時機執行合適的操作。
3.組件實例與組件的生命周期
組件實例本質上是一個狀態集合(對象)。
引入組件實例
function mountComponent(vnode, container, anchor) {// 通過vnode獲取組件的選項對象,即vnode.typeconst componentOptions = vnode.type// 獲取組件的渲染函數const { render, data } = componentOptions//調用 data 函數得到原始數據,并調用 reactive 函數將其包裝為響應式數據const state = reactive(data())+ const instance = {
+ state, // 組件自身的狀態數據,即 data
+ isMounted: false, // 一個布爾值,用來表示組件是否已經被掛載,初始值為 false
+ subTree: null // 組件所渲染的內容,即子樹(subTree)
+ }// 將組件實例設置到 vnode 上,用于后續更新
+ vnode.component = instance// 將組件的 render 函數調用包裝到 effect 內effect(() => {// 執行渲染函數,獲取組件的渲染函數內容,即render返回的虛擬dom// 調用 render 函數時,將其 this 設置為 state,從而 render 函數內部可以通過 this 訪問組件自身狀態數據const subTree = render.call(state,state)// // 檢查組件是否已經被掛載
+ if (!isMounted) {// 初次掛載,調用 patch 函數第一個參數傳遞 null
+ patch(null, subTree, container, anchor)+ // 將組件實例的isMounted設置為true,這樣當更新發生時就不會再次進行掛載操作。而是執行更新
+ instance.isMounted = true
+ } else {
+ // 當isMounted為true時,說明組件已經掛載了,只需要完成自更新即可
+ patch(instance.subTree,subTree, conatiner, anchor)
+ }// 最后調用 patch 函數來掛載組件所描述的內容,即 subTree
+ patch(null, subTree, container, anchor)
+ // 更新組件實例的子樹
+ instance.subTree = subTree}, {// 指定該副作用函數的調度器為 queueJob 即可scheduler: queueJob})}
在上面這段代碼中,我們使用一個對象來表示組件實例,該對象有三個屬性。
- state:組件自身的狀態數據,即 data。
- isMounted:一個布爾值,用來表示組件是否被掛載。
- subTree:存儲組件的渲染函數返回的虛擬 DOM,即組件的子樹 (subTree)。
在上面的實現中,組件實例的 instance.isMounted 屬性可以 用來區分組件的掛載和更新。
function mountComponent(vnode, container, anchor) {// 通過vnode獲取組件的選項對象,即vnode.typeconst componentOptions = vnode.type// 從組件選項對象中取得組件的生命周期函數
+ const { render, data, beforeCreate, created, beforeMount,
mounted, beforeUpdate, updated } = componentOptions// 在這里調用beforeCreate鉤子beforeMount && beforeMount()const state = reactive(data())const instance = {state,isMounted: false,subTree: null} vnode.component = instance// 在這里調用 created 鉤子created && created(state)effect(() => {const subTree = render.call(state, state)if (!instance.isMounted) {beforeMount && beforeMount.call(state)}}) }