iT邦幫忙

2023 iThome 鐵人賽

DAY 19
0
Vue.js

淺談vue3源碼,很淺的那種系列 第 30

[Day 29]最後收尾!!

  • 分享至 

  • xImage
  •  

終於我們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]最後收尾!!」。


上一篇
[Day 28]ast抽象語法樹 - 5——v-bind與style
下一篇
[Last Day]後記
系列文
淺談vue3源碼,很淺的那種31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言