现在有这么一道题目:

    1. <!DOCTYPE html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8" />
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    6. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    7. <title>Document</title>
    8. </head>
    9. <body>
    10. <div id="app">
    11. <h1>{{ title }}</h1>
    12. <h2>{{ conten }}</h2>
    13. <h1>{{ title }}</h1>
    14. <h2>{{ content }}</h2>
    15. <button @click="setTitle">设置标题</button>
    16. <button @click="setContent">设置内容</button>
    17. <button @click="reset">重 置</button>
    18. </div>
    19. <script type="module" src="./index.js"></script>
    20. </body>
    21. </html>
    1. import { createApp, ref } from "./vue/index.js";
    2. createApp("#app", {
    3. refs: {
    4. title: ref("This is title."),
    5. content: ref("This is content.")
    6. },
    7. methods: {
    8. setTitle() {
    9. this.title.value = "这是标题。";
    10. },
    11. setContent() {
    12. this.content.value = "这是内容。";
    13. },
    14. reset() {
    15. this.title.$reset();
    16. this.content.$reset();
    17. }
    18. }
    19. });

    image.png
    现在的需求是让我们自行去实现createApp()ref()方法,来实现 HTML 文件 DOM 中数据的解析和对数据的操作。

    首先我们要进行题目的分析:
    1、createApp()中定义的数据类似 Vue 的选项 API 方式。
    2、createApp()ref()又类似 Vue 的组合式 API。
    3、如何使用ref()把数据进行包装,并可以调用方法?
    4、如何进行渲染页面?
    可以先获取到#app节点下所有的 DOM ==> 分析{{ xxx }}的内容 ==> 找到refs种对应的数据 ==> 更新节点内容
    5、当执行方法更改refs里面的数据时如何进行更新呢?
    对数据进行劫持 ==> 触发 setter 机制 ==> 找到对应的 DOM ==> 更新节点内容
    6、如何给元素绑定事件呢?
    找到所有的节点 ==> 分析带有@click的属性 ==> 绑定事件为methods里面的方法

    综上,这是我们对这道题目的一个初始的思路,下面我们先一步步的去实现!

    1、实现createApp()函数
    根据题目我们可以看到createApp()接收两个参数:根节点、数据对象。

    1. import { ref } from "./hooks.js";
    2. // 接收:根节点、初始化 data
    3. export function createApp(el, { refs, methods }) {
    4. const $el = document.querySelector(el);
    5. const allNodes = $el.querySelectorAll("*");
    6. console.log(allNodes);
    7. }
    8. export { ref };

    vue/index.js作为我们程序的入口文件,自然需要暴露出去一个createApp()方法。
    但是我们又不想把 Ref 相关的逻辑放在该文件内部,所以我们需要把 Ref 相关的内容也导入进来然后进行导出。
    createApp()方法执行的时候,我们可以进行跟节点#app然后获取到该节点下所有的 DOM。
    image.png

    2、创建 Ref 对象
    createApp()函数执行的是,定义的refs里数据又会执行ref()函数。
    那么该如何实现ref()函数呢?

    1. ref()函数返回的数据可以被调用$reset()方法,所以ref()函数应该返回一个对象。
    2. 我们如何执行页面中的那些元素使用了该 Ref 对象呢?可以给该对象设置一个deps属性专门依赖的元素。
    3. 因为我们需要调用$reset()方法来还原数据的初始值,需要还需要设置一个_defaultValue来保持默认值。
    4. 当我们对 Ref 对象的值进行更改的时候还需要触发劫持来进行更新,所以我们还需要定义_valuevalue来表示对象的值。value是负责劫持的数据。
    5. 因为refs里面有多个数据,还有调用方法,我们可以使用实例化构造函数的方式来创建 Ref 对象。 ```javascript import Ref from “./Ref.js”;

    export function ref(initialValue) { return new Ref(initialValue); }

    1. 我们可以把 Ref 类抽离出去,然后导入进行实例化,只传递一个初始化的值就可以了。
    2. ```javascript
    3. // Ref 类
    4. export default class Ref {
    5. constructor(initialValue) {
    6. this.deps = new Set(); // 方便我们后面存储 DOM 依赖
    7. this._defaultValue = initialValue; // 该属性不可变的
    8. this._value = initialValue; // 该属性可变的
    9. }
    10. /*
    11. 为什么不能直接使用 _value ?
    12. 因为我要触发劫持,如果直接操作就无法触发劫持了
    13. */
    14. get value() {
    15. return this._value;
    16. }
    17. set value(newValue) {
    18. console.log("劫持被触发!")
    19. this._value = newValue;
    20. }
    21. $reset() {
    22. // 重置的时候直接把 _defaultValue 赋值给 _value 就可以了
    23. this._value = this._defaultValue;
    24. }
    25. }

    然后我们就得到了两个 Ref 对象。
    image.png

    3、收集 Ref 的deps依赖
    我们现在拿到了所有的节点和所有的 Ref 对象,那我们就可以给 Ref 对象设置deps来管理 Ref 和 DOM 的依赖。

    1. import { ref, createRefMap } from "./hooks.js";
    2. export function createApp(el, { refs, methods }) {
    3. const $el = document.querySelector(el);
    4. const allNodes = $el.querySelectorAll("*");
    5. const refsMap = createRefMap(allNodes, refs);
    6. console.log(refsMap)
    7. }
    8. export { ref };
    1. import Ref from "./Ref.js";
    2. // 该正则表达式可以匹配到 {{ xxx }}
    3. const reg_var = /\{\{(.+?)\}\}/;
    4. export function ref(initialValue) {
    5. return new Ref(initialValue);
    6. }
    7. export function createRefMap(allNodes, refs) {
    8. // 遍历 allNodes
    9. allNodes.forEach((element) => {
    10. // 如果 DOM 的文本内容可以被匹配
    11. if (reg_var.test(element.innerText)) {
    12. console.log(element.innerText.match(reg_var));
    13. }
    14. });
    15. }

    这样我们就匹配到了 DOM 中绑定 Ref 的内容。
    image.png
    最后我们只需要取数组的第 1 为并把对应的 Ref 更改即可。

    1. import Ref from "./Ref.js";
    2. // 该正则表达式可以匹配到 {{ xxx }}
    3. const reg_var = /\{\{(.+?)\}\}/;
    4. export function ref(initialValue) {
    5. return new Ref(initialValue);
    6. }
    7. export function createRefMap(allNodes, refs) {
    8. // 遍历 allNodes
    9. allNodes.forEach((element) => {
    10. // 如果 DOM 的文本内容可以被匹配
    11. if (reg_var.test(element.innerText)) {
    12. // 得到 ['{{title}}', 'title']
    13. const refKey = element.innerText.match(reg_var)[1].trim();
    14. refs[refKey].deps.add(element);
    15. }
    16. });
    17. return refs;
    18. }

    image.png
    这样我们就把对应的 DOM 收集了起来。

    4、渲染页面
    现在我们有了 Ref 对象,Ref 对象保存了对应的依赖 DOM,那我们就可以去渲染页面啦。

    1. import { ref, createRefMap } from "./hooks.js";
    2. import { render } from "./render.js";
    3. export function createApp(el, { refs, methods }) {
    4. const $el = document.querySelector(el);
    5. const allNodes = $el.querySelectorAll("*");
    6. const refsMap = createRefMap(allNodes, refs);
    7. // render() 函数只要一个 Ref 对象作为参数即可
    8. render(refsMap);
    9. }
    10. export { ref };
    1. export function render(refs) {
    2. // 遍历 refs
    3. // refs 就是
    4. // {
    5. // title: { deps:... value:xxx }
    6. // content: { deps:... value:xxx }
    7. // }
    8. for (const key in refs) {
    9. _render(refs[key]);
    10. }
    11. }
    12. // 我们把 deps 渲染单独抽离为一个函数,因为 update 的时候也需要
    13. function _render({ deps, value }) {
    14. deps.forEach((element) => {
    15. element.innerText = value;
    16. });
    17. }

    5、绑定事件
    接下来就是给按钮绑定事件了。

    1. import { ref, createRefMap } from "./hooks.js";
    2. import { render } from "./render.js";
    3. import { bindEvent } from "./event.js";
    4. export function createApp(el, { refs, methods }) {
    5. const $el = document.querySelector(el);
    6. const allNodes = $el.querySelectorAll("*");
    7. const refsMap = createRefMap(allNodes, refs);
    8. // render() 函数只要一个 Ref 对象作为参数即可
    9. render(refsMap);
    10. // 通过 apply() 来改变 this 指向 Refs
    11. bindEvent.apply(refsMap, [methods, allNodes]);
    12. }
    13. export { ref };
    1. export function bindEvent(methods, allNodes) {
    2. // 遍历节点
    3. allNodes.forEach((element) => {
    4. // 获取含有 @click 属性的值
    5. const handlerName = element.getAttribute("@click");
    6. if (handlerName) {
    7. // 找到 methods 里对应的方法
    8. element.addEventListener("click", methods[handlerName].bind(this), false);
    9. }
    10. });
    11. }

    到这里,我们点击按钮就会触发对应的事件,然后就会触发 Ref 劫持。
    屏幕录制2023-06-13 17.34.56.gif

    6、更新视图
    已经实现了数据的更改,但是视图咋没变化呢?因为我们还没执行update()

    1. import { update } from "./render.js";
    2. export default class Ref {
    3. constructor(initialValue) {
    4. this.deps = new Set();
    5. this._defaultValue = initialValue; // 不可变的
    6. this._value = initialValue; // 可变的
    7. }
    8. /*
    9. 为什么不能直接使用 _value ?
    10. 因为我要触发劫持,如果直接操作就无法触发劫持了
    11. */
    12. get value() {
    13. return this._value;
    14. }
    15. set value(newValue) {
    16. console.log("劫持被触发!");
    17. this._value = newValue;
    18. // 数据更改后重新渲染数据
    19. update(this);
    20. }
    21. $reset() {
    22. this._value = this._defaultValue;
    23. update(this);
    24. }
    25. }
    1. export function render(refs) {
    2. for (const key in refs) {
    3. _render(refs[key]);
    4. }
    5. }
    6. export function update(ref) {
    7. // 直接调用 _render() 即可
    8. _render(ref);
    9. }
    10. // 我们把 deps 渲染单独抽离为一个函数,因为 update 的时候也需要
    11. function _render({ deps, value }) {
    12. deps.forEach((element) => {
    13. element.innerText = value;
    14. });
    15. }

    这样我们就实现了整个程序的运行:
    屏幕录制2023-06-13 17.40.06.gif

    最后,源码地址献上:
    JSPlusPlus/腾讯课堂/Vue本尊10/10/index.js at main · xiechen1201/JSPlusPlus