終於我們vue3源碼的學習來到尾聲,我們可以來測試看看這幾天寫的東西是否能全部串到一起。不過當我正打算要串到一起時,我發現之前有個小地方沒有寫好,請先跟我一起打開/src/runtime-core/h.ts,在h函數的函數重載找到這一條:
(type: string, children: ChildNode | ChildNode[]): VNode;
原本想說h函數會接收到的type應該是字串,畢竟h函數是要把標籤變成虛擬dom嘛。但今天猛然發現,純文字也是有type為symbol('Text')的虛擬dom的,所以當h函數要生成文本節點時,type需要可以接收symbol型別。所以這條函數重載應該改成這樣:
(type: string | symbol, children: ChildNode | ChildNode[]): VNode;
如此一來我們就能在/src/index.ts寫一些測試代碼,來檢測我們做的vue好不好使了!
import './assets/css/style.less';
import { ref, reactive, watch, computed } from './vue';
import effect from './vue/effect';
import { render } from './runtime-dom';
import { h } from './runtime-core/h';
import { parse } from './ast/parse';
import { VNode, Text } from './runtime-core';
document.body.innerHTML = '<div id="app"></div>';
const app: HTMLElement = document.querySelector('#app')!;
const template = `
<div>{{ face }}</div>
`;
const doH = (target: ReturnType<typeof parse>[0]): string | VNode => {
return typeof target === 'string' ? h(Text, target) : h(target.tag, target.attrs, target.children.map(child => doH(child)));
};
const face = ref(`(*'ω'*)`);
setTimeout(() => {
face.value = `(*´Д`)`;
}, 3000);
effect(() => {
const parsedTemplate = {
tag: 'div',
props: {
id: 'app'
},
children: parse(template)
};
const vnode = doH(parsedTemplate) as VNode;
render(vnode, app);
});
先把該引入的東西引一引,然後給body放一個#app,隨便寫點template,這些都測試代碼標配班底,沒甚麼好講的。
再做一個能遞歸調用h函數把解析過的template變成虛擬dom的doH:
const doH = (target: ReturnType<typeof parse>[0]): string | VNode => {
return typeof target === 'string' ? h(Text, target) : h(target.tag, target.attrs, target.children.map(child => doH(child)));
};
將用parse解析過的template的tag、attrs傳入h函數,如果有children就把children的每一個子節點都遞歸調用doH,如果子節點是字串就用h(Text, target)把它變成文本節點,把template串成這種格式:
h('div', h(Text, "(*'ω'*)"))
然後在effect函數中把響應式變數變成虛擬dom並渲染:
const face = ref(`(*'ω'*)`);
setTimeout(() => {
face.value = `(*´Д`)`;
}, 3000);
effect(() => {
const parsedTemplate = {
tag: 'div',
props: {
id: 'app'
},
children: parse(template)
};
const vnode = doH(parsedTemplate) as VNode;
render(vnode, app);
});
然後,我們就能成功看到——
報錯啦!!!
嗯……看來是因為每個js模塊都有自己的作用域,所以其實在effect的回調作用時,已經取不到main.ts模塊中的face,才會報這個錯。這時我們只要在宣告face後寫上globalThis.face = face
將face掛載到全域——
又報錯啦!!!
沒事沒事,這種程度的小錯誤可難不倒我。target.children.map不是一個方法是因為target.children不是陣列,只要在target.children不是陣列時打印它就能發現我們解析文本時直接把字串放進了children屬性,只要調整/src/ast/parseText.ts如下:
import { isRef } from "../vue";
export const parseTextData = (text: string) => {
const checkData = /{{(.+)}}/;
if (!checkData.test(text)) return [text];
const express = text.match(checkData)[1]?.trim();
const result = eval(express);
return [isRef(result) ? result.value : result];
};
還順便把之前忘了判斷當{{ 響應式變數 }}是ref對象時應該取value屬性的bug給修了,這樣一來一定就能正常運作——
只要給template加上屬性就又報錯啦!!!
……沒事,只要在/src/ast/parseAttr.ts打印接收到的屬性,就能發現在有傳屬性的時候有可能接收到空字串,所以只要在generateAttr方法體最上方加上這行判斷:
if (!attrString) return {};
經過一次又一次的debug,終於!我們的vue終於!成功能夠解析template啦!
接下來只要創建/src/create-app/index.ts文件,把上面的代碼整理一下:
import effect from '../vue/effect';
import { render } from '../runtime-dom';
import { h } from '../runtime-core/h';
import { parse } from '../ast/parse';
import { VNode, Text } from '../runtime-core';
const doH = (target: ReturnType<typeof parse>[0]): string | VNode => {
return typeof target === 'string' ? h(Text, target) : h(target.tag, target.attrs, target.children.map(child => doH(child)));
};
interface Component {
template: string;
setup: () => any;
}
export default (component: Component) => {
const app: HTMLElement = document.querySelector('#app')!;
const variables = component.setup();
effect(() => {
for (const key in variables) (globalThis as any)[key] = variables[key];
const parsedTemplate = {
tag: 'div',
props: {
id: 'app'
},
children: parse(component.template)
};
const vnode = doH(parsedTemplate) as VNode;
render(vnode, app);
for (const value of variables) delete (globalThis as any)[value];
});
};
然後把/src/index.ts改成:
import './assets/css/style.less';
import { ref, reactive, watch, computed } from './vue';
import createApp from './create-app';
document.body.innerHTML = '<div id="app"></div>';
createApp({
template: `
<div class="owo">{{ face }}</div>
`,
setup() {
const face = ref(`(*'ω'*)`);
setTimeout(() => {
face.value = `(*´Д`)`;
}, 3000);
return { face };
}
});
如何,是不是有那麼點我們認識的vue3.0的樣子了?
不過今天修修改改的地方有點多,如果有哪裡找不到該怎麼改,可以參考今天進度的commit「[Day 29]最後收尾!!」。