[封装04-设计模式] Publish Subscribe 发布订阅模式在前端的应用

1,161 阅读11分钟

导航

[react] Hooks

[封装01-设计模式] 设计原则 和 工厂模式(简单抽象方法) 适配器模式 装饰器模式
[封装02-设计模式] 命令模式 享元模式 组合模式 代理模式
[封装03-设计模式] Decorator 装饰器模式在前端的应用
[封装04-设计模式] Publish Subscribe 发布订阅模式在前端的应用

[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] koa
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick

[源码-react01] ReactDOM.render01
[源码-react02] 手写hook调度-useState实现

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[数据结构和算法01] 二分查找和排序

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 数据结构和算法 - 二分查找和排序
[深入22] js和v8垃圾回收机制
[深入23] JS设计模式 - 代理,策略,单例
[深入24] Fiber
[深入25] Typescript
[深入26] Drag

[前端学java01-SpringBoot实战] 环境配置和HelloWorld服务
[前端学java02-SpringBoot实战] mybatis + mysql 实现歌曲增删改查
[前端学java03-SpringBoot实战] lombok,日志,部署
[前端学java04-SpringBoot实战] 静态资源 + 拦截器 + 前后端文件上传
[前端学java05-SpringBoot实战] 常用注解 + redis实现统计功能
[前端学java06-SpringBoot实战] 注入 + Swagger2 3.0 + 单元测试JUnit5
[前端学java07-SpringBoot实战] IOC扫描器 + 事务 + Jackson
[前端学java08-SpringBoot实战总结1-7] 阶段性总结
[前端学java09-SpringBoot实战] 多模块配置 + Mybatis-plus + 单多模块打包部署
[前端学java10-SpringBoot实战] bean赋值转换 + 参数校验 + 全局异常处理
[前端学java11-SpringSecurity] 配置 + 内存 + 数据库 = 三种方式实现RBAC
[前端学java12-SpringSecurity] JWT
[前端学java13-SpringCloud] Eureka + RestTemplate + Zuul + Ribbon

复习笔记-01
复习笔记-02
复习笔记-03
复习笔记-04

前置知识

(1) 一些单词

broadcast 广播
channel 通道 // new BroadcastChannel(channelName)

(2) function.name

  • 定义:函数的name属性返回 函数的名字
  • 应用:当一个函数作为另一个函数的参数时,获取参数函数的名字
1. 基本情况
function a() {}
const b = function(){}
const c = function myName(){}
a.name // 'a'
b.name // 'b'
c.name // 'myName' 使用变量赋值时,如果函数有具名函数,返回 function后面的函数名 

2. 应用
function test(f) {
  console.log(f.name); // 获取参数函数的名字
}

(3) 观察者模式

  • 定义:对程序中的某个对象进行观察,并在对象发生变化时接收到通知
  • 角色
    • subject 目标对象
    • observer 观察者对象
  • 关系
    • 一个目标对象 -> 可以被多个观察者对象观察
    • (观察者对象) 在目标对象上 (订阅事件),(目标对象) 负责向观察者对象 (广播事件)
  • 观察者模式和发布订阅模式的比较
    • 耦合性
      • 观察者模式中,目标对象中具有观察者对象数组,目标对象维护观察者对象组成的数组
      • 发布订阅模式中,发布者是不需要自己维护订阅者数组的,而是通过中介完成,完全解耦
    • 使用上
      • 观察者模式,多用于单个应用内部
      • 发布订阅模式,则更多的是一种跨应用的模式(cross-application pattern) image.png
Subject 目标对象
  - 维护一个(观察者实例对象)组成的数组,并且具有( 添加,删除,通知 ) 操作该数组的各种方法 
Observer 观察者对象
  - 仅仅只需要维护收到通知后( 更新 )操作的方法
存在问题:
  - 因为 Subject 目标对象中维护了 Observer 组成的数组,所以存在耦合性
解决问题
  - 可以使用发布订阅模式,全权由 EventChannel 来互责维护,订阅,和取消订阅
---
案例  观察者模式

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      function Subject() {
        // 目标对象的构造函数
        this.observers = []; // 观察者实例对象组成的数组
      }
      Subject.prototype = {
        // 目标对象上具有操作 ( 观察者对象数组 ) 的方法,比如 ( 添加,删除,通知 )
        add(...observers) {
          if (observers?.length) {
            this.observers = this.observers.concat(observers);
          }
        },
        delete(...observers) {
          if (this.observers.length) {
            observers.forEach((observer) => {
              const index = this.observers.findIndex(
                (item) => observer === item
              );
              if (index > -1) {
                this.observers.splice(index, 1);
              }
            });
          }
        },
        notify() {
          this.observers.forEach((observer) => observer?.update());
        },
      };
      Subject.prototype.constructor = Subject;

      function Observer(fn, params) {
        this.update = function () {
          return fn.call(this, params);
        };
      }

      function o(p) {
        console.log(p);
      }

      const observer1 = new Observer(o, "observer1");
      const observer2 = new Observer(o, "observer2");

      const subject = new Subject();
      subject.add(observer1, observer2);
      subject.delete(observer1);
      subject.notify();
    </script>
  </body>
</html>

(一) 多页面通信

- 同源多页面通信
  - new BroadcastChannel()
  - storage事件
  
- 非同源多页面通信 - 跨域通信
  - otherWindow.postMessage()

(1) 同源 - 多页面通信 - BroadcastChannel

  • const bc = new BroadcastChannel('channel_name')
    • 参数:表示频道,通信的双方必须是同一个频道才可以
  • 发送消息:bc.postMessage(obj)
  • 接收消息:bc.onmessage = function (e) { e.data }
  • 断开链接:bc.close()
  • 定义
    • 可以让指定 origin 下的任意 browsing context 来订阅它
    • 它允许同源的不同浏览器窗口Tab页frame或者 iframe 下的不同文档之间相互通信
    • 全双工(双向)通信
  • 应用
    • 不同标签页之间的通信
  • 案例源码地址
// 连接到广播频道
// 可以在多个tab中多次实例话,双方通信只需要保证参数一样,即同一个频道
var bc = new BroadcastChannel('test_channel');

// 发送消息
bc.postMessage('This is a test message.');

// 接收消息
// 当消息被发送之后,所有连接到该频道的 BroadcastChannel 对象上都会触发 `message` 事件
// ev对象上的属性
// - currentTarget.name:当前加入的广播频道
// - data:发送的消息
// - origin:当前所在源
bc.onmessage = function (ev) { console.log(ev); }

// 断开频道连接
bc.close()

image.png image.png

(2) 非同源跨域 - 多页面通信 - otherWindow.postMessage()

  • 特点
    • 非同源:表示具有跨域能力
    • 安全性:安全性没有 BroadcastChannel 好
  • otherWindow.postMessage(message, targetOrigin, [transfer])
    • otherWindow
      • 其他窗口的一个引用
      • iframe ------> contentWindow 属性
      • window.open -> 返回的新窗口对象
      • 命名过或数值索引的 window.frames
    • message
      • 将要发送到其他 window 的数据
    • targetOrigin
      • 通过窗口的origin属性来指定哪些窗口能接收到消息事件
      • 其值可以是字符串"*"(表示无限制)
      • 或者一个URI,但必须是启动的server服务的地址,不能是本地静态文件
    • transfer
      • transfer 是一串和 message 同时传递的 Transferable 对象
      • 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
    • 注意
      • 在 targetWindow.open() 后,要等到目标页面加载完成才能进行 postMessage 跨域通信,但是在跨域的情况下,无法对目标窗口进行onload监听
      • 1.所以可以用 setTimeout 延时
      • 2.也可以通过一个按钮去打开新的页面,等新开的页面加载完,然后手动的再发送消息
  • iframe 相关
    • 元素:<iframe />能够将另一个HTML页面嵌入到当前页面
    • 发消息:
      • HTMLIFrameElement.contentWindow.postMessage(message, targetOrigin)
    • 收消息:
      • window.addEventListener("message",()=>{})
      • window.onmessage = () => {}
  • window.open 相关
    • var window = window.open(url, windowName, [windowFeatures])
      • 返回值
        • window.open() 返回值是一个 ( 窗口对象 )
        • 该窗口对象就是 otherWindow.postMessage() 时的 otherWindow
      • 参数
        • url:打开的新页面的url服务器地址
        • windowName:窗口名称
  • 案例源码地址
(一)
otherWindow.postMessage(message, targetOrigin, [transfer]) 跨域通信

(1) otherWindow
- otherWindow指的是其他窗口的一个引用
1. iframe 的 contentWindow 属性
2. window.open() 返回的一个窗口对象
3. 命名过或数值索引的 window.frames
4. 两个窗口之间
  - a -> b,otherWindow是b窗口
  - b -> a,otherWindow是a窗口,即 ( top ) 或者 ( parent )

(2) message
- message指发送给其他窗口的数据
- message会被序列化,所以无需自己序列化

(3) targetOrigin     !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- targetOrigin:设置目标窗口的源(协议域名端口组成的字符串),指定哪些窗口能 ( 接收 ) 到消息事件
- 在 ( 发消息 ) 的时候,如果( 目标窗口 ) 的 ( 协议,域名,端口) 任意一项不满足 targetOrigin 提供的值,消息就不会发送
- 三者要全部匹配才会发送
- targetOrigin的值可以是 (  *  ) 号,表示所有窗口都能接收到消息

(4) transfer
- transfer 是一串和message同时传递的 Transferable 对象
- 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权



发消息:--------------- otherWindow.postMessage(message, targetOrigin, [transfer])
收消息:--------------- window.addEventListener('message', (data) => {console.log(data.data, data.origin)}, false)
收消息:--------------- window.onmessage = (data) => {...}
注意:----------------- 通过 ( targetOrigin - 验证接收方 ) 和 ( data.origin - 验证发送方 ) 来精确通信


- data.origin 和 data.source 和 data.data
- 在接收端的监听函数中,注意 origin 和 source
- origin: ------------- 发送方的协议,域名,端口组成的字符串
- source:------------- 发送方窗口对象的引用
- data:--------------- 接收到的数据
window.addEventListener("message", receiveMessage, false); // 接收消息的tab标签页面监听message事件
function receiveMessage(event) {
  // For Chrome, the origin property is in the event.originalEvent
  // object. 
  // 这里不准确,chrome没有这个属性
  // var origin = event.origin || event.originalEvent.origin; 
  var origin = event.origin
  if (origin !== "http://example.org:8080")
    return;
    // ...
}


(二)
注意事项:
- 使用postMessage将数据发送到其他窗口时,始终要指定精确的目标origin,而不是使用 *
- 使用 origin 和 source 验证 ( 发件人 ) 的身份


(三)
实例:

--------------
a页面
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="a-open">点击,打开b页面</div>
  <div id="a-send">点击,发送消息,a->b</div>
  <script>
    const aOpen = document.getElementById('a-open')
    const aSend = document.getElementById('a-send')
    var a = null
    aOpen.addEventListener('click', () => a = window.open('http://127.0.0.1:5500/b.html'), false)
    aSend.addEventListener('click', () => a.postMessage('this message is a to b', 'http://127.0.0.1:5500'), false)
    // 注意:
    // 1. a.postMessage只有在目标页面(b页面)的页面加载完成时才能发送
    // 2. a.postMessage的第二个参数,表示targetOrigin目标源,即目标窗口的协议域名端口组成的字符串
    // 3. targetOrigin设置过后,只有目标窗口完全符合targetOrigin字符串的值才能接收到消息
  </script>
</body>
</html>


---------------
b页面
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div>b页面</div>
  <script>
    window.addEventListener('message', (data) => { // ----- 监听message事件
      console.log(data)
    }, false)
  </script>
</body>
</html>

(3) 同源 - 多页面通信 - storage事件

  • 原理
    • localStorage 变化时,会触发 storage 事件
  • storage事件的event事件对象
    • e.newValue 获取变化后的最新的值
    • e.oldValue 获取变化前的值

image.png

(二) 发布订阅模式基本实现 - es5

// 发布订阅模式
// 1 模型
// 发布者 -- 事件中心 --- 订阅者
// publisher -- topics/eventChannel --- subscriber
// 2 关系
// 订阅者 -> 可以订阅多个事件
// 3 实现过程的注意点
// 订阅者身份的唯一标记,因为在unSubscribe时需要通过唯一性识别订阅者对象
---

const publishSubscribeDesignPattern = () => {
  // topics对象,存放每个 ( 事件 ) 对应的 ( 订阅者对象 ) 组成的 ( 数组 )
  // key: 事件名
  // value: 订阅者对象组成的数组
  // eventName:[{functionName: fn.name, fn: fn}]
  const topics = {};

  // ID 订阅者为唯一性
  let id = 0;

  // 订阅
  topics.subscribe = (eventName, fn) => {
    // 1
    // 每一个事件可以有多个订阅者,所以 topics[eventName] 是一个数组,并且事件中心用一个对象去维护
    if (!topics[eventName]) topics[eventName] = [];

    // 2
    // topics[eventName].push({ fnName: fn.name, fn });
    // 问题:这里使用了 fn.name 来作为订阅者的唯一识别是有问题的,因为如果订阅者是一个对象时,对象的方法是没有name属性的,只有函数才有
    // 解决:我们使用为一个id来实现,每次自增
    if (fn) {
      fn.id = id++; // 给每个订阅者添加一个唯一的id,id用来做取消订阅时的身份识别
    }
    topics[eventName].push({
      id: fn.id,
      fn,
    });
  };

  // 发布
  topics.publish = (eventName, params) => {
    const subscribers = topics[eventName];
    subscribers?.length && subscribers.forEach(({ fn }) => fn(params));
  };

  // 取消订阅
  topics.unSubscribe = (eventName, fn) => {
    const subscribers = topics[eventName];
    if (subscribers?.length) {
      const index = subscribers.findIndex(({ id }) => id === fn.id); // 在 subscribe 时已经为每个订阅者对象添加了唯一的id
      subscribers.splice(index, 1);
    }
  };

  return topics;
};
const topics = publishSubscribeDesignPattern();
const a = (params) => console.log("a", params);
const b = (params) => console.log("b", params);

topics.subscribe("click", a);
topics.subscribe("click", b);
topics.unSubscribe("click", b);
topics.publish("click", "2021/12/05");

image.png

(三) 发布订阅模式基本实现 - es6

class EventChannel {
  topic = {};
  id = 0;

  subscribe = (eventName, fn) => {
    if (fn) {
      fn.id = this.id++;
    }
    if (!this.topic[eventName]) {
      this.topic[eventName] = [];
    }
    this.topic[eventName].push({
      id: fn.id,
      fn,
    });
  };

  publish = (eventName, params) => {
    if (this?.topic[eventName]?.length) {
      this.topic[eventName].forEach(({ fn }) => fn(params));
    }
  };

  unSubscribe = (eventName, fn) => {
    if (this?.topic[eventName]?.length) {
      const index = this?.topic[eventName].findIndex(
        ({ id }) => id === fn.id
      );
      this.topic[eventName].splice(index, 1);
    }
  };
}

const eventChannel = new EventChannel();

class A {
  a = (params) => console.log("a", params);
}
class B {
  b = (params) => console.log("a", params);
}
const a = new A().a;
const b = new B().b;

eventChannel.subscribe("click", a);
eventChannel.subscribe("click", b);
eventChannel.unSubscribe("click", b);
eventChannel.publish("click", "2021/12/05");

(四) 发布订阅模式 - 手写vue双向数据绑定

前置知识

1. Element.children
- Element.children 返回一个类似数组的对象 ( HTMLCollection ) 集合
- 包含 当前元素的所有子元素
- 如果当前元素没有子元素,则返回的对象包含0个成员

2. Node.childNodes
- 返回一个类似数组的对象( NodeList集合 ),成员包括当前节点的所有子节点 
- NodeList是一个动态集合

3. Node.childNodes 和 Element.children 的区别 
- 类数组的对象
  - 它们都是类似数组的对象
- 静态和动态
  - Element.chilren -> HTMLCollection -> 都是动态集合 ----> 动态时添加减少dom后 ( 实时变化 )
  - Node.childNodes -> NodeList -------> 动态或者静态 -> 静态时添加减少dom后 ( 不会变化 )
    - 动态HTMLCollection:Element.children
    - 动态HTMLCollection:Element.getElementsByClassName
    - 动态HTMLCollection:Element.getElementsByTagName
    -
    - 动态NodeList:Node.childNodes 是动态的NodeList
    - 静态NodeList:Element.querySelectorAll() 方法返回的是一个静态的NodeLis
    - 静态NodeList:Element.querySelector() 方法返回的是一个静态的NodeLis
- 节点类型
  - Element.children ---> 只包含 ( 元素类型的子节点 ),不包含其他类型的子节点 
  - Node.childNodes ----> 包含 ( 元素节点,文本节点,注释节点 )
  
4. 
- Object.keys()
  - 只遍历自身属性 + 可枚举属性,不包括继承的属性
  - Object.keys() 和 Object.values() 和 Object.entries() 三者都具有上面的特征
- Object.getOwnPropertyNames()
  - 遍历自身属性 + 可枚举属性 + 不可枚举属性,不包括继承的属性
- for...in
  - 自身的属性 + 可枚举属性 + 继承的属性
- Object.keys() 和 Object.getOwnPropertyNames()的对比
  - 相同点:
    - 都是遍历自身属性,都不可遍历继承的属性
    - 都可以遍历 ( 对象和数组 ),即参数是数组或对象
  - 不同点
    - `Object.keys() = 自身属性 + 可枚举属性`
    - `Object.getOwnPropertyNames() = 自身属性 + 可枚举属性 + 不可枚举属性`
  - 所以
    - 一般情况都是使用 Object.keys() 去遍历对象
    - 而不是使用 Object.getOwnPropertyNames()
- 复习链接 
  - https://juejin.cn/post/7029703494877577246#heading-22
利用发布订阅模式 - 手写一个简单的vue双向数据绑定
---

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <input type="text" v-model="name" />
      <div v-text="name"></div>
    </div>
    <script>
      // const obj1 = { a: 1 }; // ------------ 1. 原型属性
      // const obj2 = Object.create(obj1);
      // obj2.b = 2;
      // Object.defineProperty(obj2, "c", {
      //   value: 3,
      //   configurable: true,
      //   enumerable: true, // -------------- 2. 可枚举属性
      //   writeable: true,
      // });
      // const keys = Object.keys(obj2);
      // console.log(`keys`, keys);
      // Object.keys() 返回 自身属性 + 可枚举属性 ,不包含继承的属性

      // Watcher
      // 监听类
      // 主要作用是:将改变之后的data中的 ( 最新数据 ) 更新到 ( ui视图 ) 中
      class Watcher {
        constructor(directiveName, el, attr, exp, vm) {
          // this.name = directiveName; // 指令的名字,比如 'v-text','v-model'
          this.el = el; // 每个具体的 DOM 节点
          this.attr = attr; // --- el中的属性,需要需改的属性 - key
          this.exp = exp; // ----- el中属性对应的 ( 属性值 ) - value
          this.vm = vm; // MyVue实例对象

          this.update(); // 注意这里在实例化Watcher时,会执行_update()方法
        }

        update = () => (this.el[this.attr] = this.vm.$data[this.exp]);
        // 将MyVue实例的data属性的最新值更新到ui视图中
      }

      // Vue
      class MyVue {
        constructor(props) {
          const { el, data } = props || {};
          // 在vue中 $ 开头的属性,是vue的保留属性
          this.$el = el;
          this.$data = typeof data === "function" ? data() : data;

          this._subs = {};
          // key:data对象中的 key,递归循环data从而获取每一层的属性作为key
          // value:数组,用来存放Watcher实例

          this._observer(this.$data);
          this._compiler(this.$el);
        }

        _observer = (data) => {
          // 1
          // object.keys() object.values() object.entries() --> 自身属性 + 可枚举属性
          // for...in ----------------------------------------> 自身属性 + 可枚举属性 + 继承的属性
          // 上面的三者包含的成员包括 ( 自身属性 + 可枚举属性 ),但 ( 不包含继承的属性 )
          // 所以 object.entries 不用像 for...in 那样判断是否具有继承的属性,因为 for...in 会遍历继承的属性
          // 详细:https://juejin.cn/post/7029703494877577246#heading-22

          Object.entries(data).forEach(([key, value]) => {
            if (typeof value === "object" && value !== null) {
              this.observer(value); // 还是对象,递归做依赖收集和派发更新
            }
            if (!this._subs[key]) {
              this._subs[key] = []; // 新建
            }
            const that = this;

            Reflect.defineProperty(this.$data, key, {
              enumerable: true, // 可枚举
              configurable: true, // 可修改配置对象,可删除对象的属性
              // writable: true, // value的值可以被修改,但这里使用了get和set,没有使用value
              get() {
                return value;
              },
              set(newValue) {
                if (value !== newValue) {
                  value = newValue;
                }
                // 修改data后去更新ui
                that._subs[key].forEach((watcher) => watcher.update());
              },
            });
          });
        };

        _compiler = (el) => {
          // const app = document.querySelector(el) // 这里不能用 querySelector,因为是静态的NodeList,不能动态变化
          const app = document.getElementById(el);
          const children = app.children; // 返回所有子元素的类似数组的对象
          Object.entries(children).forEach(([key, value]) => {
            if (value.length) {
              this._compiler(value); // 还有子元素,递归
            }
            const vText = value.getAttribute("v-text");
            const vModel = value.getAttribute("v-model");
            // if (value.hasAttribute('v-text')){} 这样判断也是可以的
            if (vText) {
              this._subs[vText].push(
                new Watcher("v-text", value, "innerHTML", vText, this)
              );
            }
            if (
              (vModel && value.tagName === "INPUT") ||
              value.tagName === "TEXTAREA"
            ) {
              this._subs[vModel].push(
                new Watcher("v-model", value, "value", vModel, this)
              ); // watcher 订阅 data中vModel对应的属性的变化
              const that = this;
              value.addEventListener(
                "input",
                (e) => (that.$data[vModel] = e.target.value) // 监听input输入值的变化,修改 data,触发派发更新
              );
            }
          });
        };
      }

      new MyVue({
        el: "app",
        data() {
          return {
            name: "woow_wu7",
          };
        },
      });
    </script>
  </body>
</html>

资料