详解vue原理之观察模式Dep->Watcher

素颜马尾好姑娘i 2023-01-16 11:18 332阅读 0赞

根据前几节课,相信大家都明白的vue的基本原理 能够实现vue响应及渲染 这如果还不清楚的 请看上几篇文章

这节课 我们讲解vue中数据的响应实现 即vue中的观察模式 如果还不明白观察模式的 也请看我的文章详解js中观察模式和订阅发布模式的区别

Dep(Dependency)

在这里插入图片描述

功能

  • 收集依赖,添加观察者(watcher)
  • 通知所有观察者

结构
在这里插入图片描述
下面是代码的基本实现

  1. // 要实现数据的响应机制 即数据变化 视图变化
  2. // 在vue的响应机制中 我们要使用观察模式来监听数据的变化
  3. // 因此 在vue中我们要实现Dep和watcher Dep的主要作用是收集依赖 在vue中的每一个响应属性 都会创建一个dep对象 负责手机依赖于该属性的所有依赖 即订阅者 并在数据更新时候发布通知 调用watcher对象中的update方法去更新视图 简单说明就是在数据劫持监听中的get去添加依赖 在set中去发布通知
  4. class Dep {
  5. // 存储所有观察者
  6. constructor() {
  7. this.subs = []
  8. }
  9. // 添加观察者
  10. addSub(sub) {
  11. if (sub && sub.update) {
  12. this.subs.push(sub)
  13. }
  14. }
  15. // 发布通知
  16. notify() {
  17. this.subs.forEach(sub => {
  18. sub.update()
  19. })
  20. }
  21. }

在 compiler类 中收集依赖,发送通知

  1. // defineReactive 中 // 创建 dep 对象收集依赖
  2. const dep = new Dep()
  3. // getter 中 // get 的过程中收集依赖
  4. Dep.target && dep.addSub(Dep.target)
  5. // setter 中 // 当数据变化之后,发送通知
  6. dep.notify()

Watcher

在这里插入图片描述
功能

  • 当数据变化触发依赖, dep 通知所有的 Watcher 实例更新视图
  • 自身实例化的时候往 dep 对象中添加自己

结构

在这里插入图片描述

  1. ```cpp
  2. //具体代码实现如下
  3. class Watcher {
  4. constructor(vm, key, cb) {
  5. this.vm = vm;
  6. // data中的属性名称
  7. this.key = key;
  8. // 回调函数 负责更新视图
  9. this.cb = cb;
  10. // 把watcher对象记录到Dep类的静态属性target中
  11. Dep.target = this;
  12. // 触发get方法 在get方法中调用addSub
  13. this.oldValue = vm[key]
  14. Dep.target = null;
  15. }
  16. // 当数据发生变化的时候 更新视图
  17. update() {
  18. let newValue = this.vm[this.key];
  19. if (newValue === this.oldValue) {
  20. return
  21. }
  22. this.cb(newValue)
  23. }
  24. }

在 compiler类 中为每一个指令/插值表达式创建 watcher 对象,监视数据的变化

  1. // 因为在 textUpdater等中要使用
  2. this updaterFn && updaterFn.call(this, node, this.vm[key], key)
  3. // v-text 指令的更新方法
  4. textUpdater (node, value, key) { node.textContent = value }
  5. // 每一个指令中创建一个 watcher,观察数据的变化
  6. new Watcher(this.vm, key, value => { node.textContent = value }) }

附上完整的代码

  1. // 具体实现步骤
  2. // 1: 通过属性 保存选项的数据
  3. // 2: 把data中的成员 转换为getter和setter 注入到vue实例中 方便使用
  4. // 3:调用observer对象 监听数据变化
  5. // 4:调用compiler 解析指令和插值表达式
  6. class Vue {
  7. constructor(options) {
  8. // 通过属性 保存选项的数据
  9. this.$options = options || { };//如果我们在调用vue构造函数的时候 没有传入参数 我们初始化一个空对象
  10. this.$data = options.data || { };
  11. this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;//如果我们是传入的选择器 则将选择器转换为dom对象
  12. // 把data中的成员 转换为getter和setter 注入到vue实例中 方便使用
  13. this._proxyData(this.$data)
  14. // 调用observer对象 监听数据变化
  15. new Observer(this.$data)
  16. // 调用compiler 解析指令和插值表达式
  17. new Compiler(this)
  18. }
  19. _proxyData(data) { //vue传过来的参数 转换为getter和setter
  20. // 遍历data的所有属性
  21. Object.keys(data).forEach(key => {
  22. Object.defineProperty(this, key, {
  23. // 可遍历 可枚举
  24. enumerable: true,
  25. configurable: true,
  26. get() {
  27. return data[key]
  28. },
  29. set(newValue) {
  30. if (newValue === data[key]) {
  31. return
  32. } else {
  33. data[key] = newValue;
  34. }
  35. }
  36. })
  37. })
  38. // 把data中的属性 注入到vue实例中
  39. }
  40. }
  41. class Observer {
  42. constructor(data) {
  43. this.walk(data)
  44. }
  45. // 1. 判断数据是否是对象,如果不是对象返回
  46. // 2. 如果是对象,遍历对象的所有属性,设置为 getter/setter
  47. walk(data) {
  48. if (!data || typeof data != 'object') {
  49. return
  50. }
  51. Object.keys(data).forEach(key => {
  52. this.defineReactive(data, key, data[key])
  53. })
  54. }
  55. // 定义响应式成员 即对data总的数据实现setter和getter
  56. defineReactive(data, key, val) {
  57. //负责收集依赖 并发布通知
  58. let dep = new Dep()
  59. const that = this
  60. // 如果 val 是对象,继续设置它下面的成员为响应式数据
  61. this.walk(val)
  62. Object.defineProperty(data, key, {
  63. enumerable: true,
  64. configurable: true,
  65. get() {
  66. // 收集依赖
  67. Dep.target && dep.addSub(Dep.target)
  68. return val;
  69. },
  70. set(newValue) {
  71. if (val === newValue) {
  72. return
  73. }
  74. // 如果 newValue 是对象,设置 newValue 的成员为响应式
  75. that.walk(newValue)//这里不用this 因为在set方法中 在function的内部 会开启新的作用域 此时的this执行data对象
  76. val = newValue;
  77. // 发布通知
  78. dep.notify()
  79. }
  80. })
  81. }
  82. }
  83. class Compiler {
  84. constructor(vm) {
  85. this.el = vm.$el;
  86. this.vm = vm;
  87. this.compile(this.el);
  88. }
  89. // 编译模板 处理文本节点和元素节点
  90. compile(el) {
  91. const nodes = el.childNodes;
  92. Array.from(nodes).forEach(node => {
  93. if (this.isElementNode(node)) {
  94. this.compileElement(node);
  95. } else if (this.isTextNode(node)) {
  96. this.compileText(node)
  97. }
  98. // 如果当前节点中还有子节点,递归编译
  99. if (node.childNodes && node.childNodes.length) {
  100. this.compile(node)
  101. }
  102. })
  103. }
  104. // 编译元素节点 处理指令
  105. compileElement(node) {
  106. // 暂时只涉及v-text和v-model
  107. // 逻辑思路 首先我们获取node节点所有的属性 查找到我们需要的指令 并用值替换
  108. // 下面我们打印一下node的所有属性 具体可切换google浏览器查看
  109. // console.log(node.attributes)
  110. // 遍历所有的属性节点
  111. Array.from(node.attributes).forEach(attr => { //Array.from 将伪数组转换为数组
  112. let attrName = attr.name;
  113. if (this.isDirective(attrName)) {
  114. // 在指令中 有v-text v-model等各种各样的指令 我们不能用if语句判断 如果用if 在后期 如果增加了别的指令 则不便于维护 需要手动增加if判断
  115. // 因此 我们讲指令的v-去掉 只保留后面部分 即:v-text -> text v-model ->model
  116. attrName = attrName.substr(2)
  117. let key = attr.value
  118. this.update(node, key, attrName)
  119. }
  120. })
  121. // 判断是否是指令
  122. }
  123. // 通过update方法拼接处对应指令对应的方法 方便后序或者 如:v-text指令 则为textUpdater方法 v-model指令 则为modelUpdater方法 Updater字符串固定 因此 在后续 如果追加的不同的指令 执行增加指令名+Updater凭借的方法
  124. // 通过update方法拼接处对应指令对应的方法 方便后序或者 如:v-text指令 则为textUpdater方法 v-model指令 则为modelUpdater方法 Updater字符串固定 因此 在后续 如果追加的不同的指令 执行增加指令名+Updater凭借的方法
  125. update(node, key, attrName) {
  126. let updateFun = this[attrName + 'Updater']
  127. // updateFun && updateFun(node, this.vm[key])
  128. //将这里使用call改变this 引入这里的updateFun是直接调用 this对象不是compiler 为了在modelUpdater和textUpdater方法中的this指向是compiler而不是window 所以使用call将updateFun的this指向改为compiler
  129. // 增加key参数 是为了在textUpdater和modelUpdater方法中能有key使用
  130. updateFun && updateFun.call(this,node, this.vm[key], key)
  131. }
  132. textUpdater(node, value, key) {
  133. node.textContent = value
  134. new Watcher(this.vm, key, (newValue) => {
  135. node.textContent = newValue;
  136. })
  137. }
  138. modelUpdater(node, value, key) {
  139. node.value = value
  140. new Watcher(this.vm, key, (newValue) => {
  141. node.value = newValue;
  142. })
  143. }
  144. // 编译文本节点 处理插值表达式
  145. compileText(node) {
  146. // console.dir(node)
  147. // { { msg }} 解析: 插值表达式 是双括号中奖有一个变量 变量的前后可能有空格 可能无空格 可能有多个空格 我们需要将变量提出来 因此用正则表达式
  148. // 详解{ { msg }}案例正则使用规则
  149. // 1:正则的使用规范 首先用//表示正则的开始和介绍
  150. // 2:插值表达式中有{ {}},即前后两个大括号 因此这里的正则是 /{ {}}/
  151. // 3: 犹豫{是特殊符号 因此需要转义符\ 故正则是 /\{\{\}\}/
  152. // 4: 匹配花括号 即{ {}}中间的名字 而变量的名字前面都可能有空格 故:我们需要匹配任意的字符 使用点表示 即 '.' 点表示匹配任意的单个字符 ,不包括换行 后面跟上'+', 加号表示以前修饰的内容出现一次或者多次 因此 .+就可以匹配变量的名字 故 正则为: /\{\{.+\}\}/
  153. // 5: 我们在.+后面跟上问好'?' 表示今早的来结束匹配 故 正则为:/\{\{.+?\}\}/
  154. // 6: 通过/\{\{.+?\}\}/ 我们可以匹配到变量的名字 现在我们需要将匹配的变量名字提取出来 也就是把".+?"的这个位置的内容提取出来 在正则表达式中 这个非常简单 我们只需要需要提取内容的位置加上小括号(),小括号在正则中有分组的含义 我们可以获取到分组中的结果 最终的正则为/\{\{(.+?)\}\}/
  155. let reg = /\{ \{ (.+?)\}\}/
  156. let value = node.textContent;//获取文本的内容
  157. if (reg.test(value)) {
  158. let key = RegExp.$1.trim();//这里使用正则对象的构造函数 来获取第一个分组的内容 这里$1表示第一个分组的内容 如果要获取第二个 则用$2 获取之后 可能有空格 因此使trim去掉空格
  159. node.textContent = value.replace(reg, this.vm[key]);//replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
  160. new Watcher(this.vm, key, (newValue) => {
  161. node.textContent = newValue;
  162. })
  163. }
  164. }
  165. // 判断元素是否是指令
  166. isDirective(attrName) {
  167. return attrName.startsWith('v-')
  168. }
  169. // 判断元素是否是元素节点
  170. isElementNode(node) {
  171. return node.nodeType === 1;
  172. }
  173. // 判断元素是否是文本节点
  174. isTextNode(node) {
  175. return node.nodeType === 3;
  176. }
  177. }
  178. // 要实现数据的响应机制 即数据变化 视图变化
  179. // 在vue的响应机制中 我们要使用观察模式来监听数据的变化
  180. // 因此 在vue中我们要实现Dep和watcher Dep的主要作用是收集依赖 在vue中的每一个响应属性 都会创建一个dep对象 负责手机依赖于该属性的所有依赖 即订阅者 并在数据更新时候发布通知 调用watcher对象中的update方法去更新视图 简单说明就是在数据劫持监听中的get去添加依赖 在set中去发布通知
  181. class Dep {
  182. // 存储所有观察者
  183. constructor() {
  184. this.subs = []
  185. }
  186. // 添加观察者
  187. addSub(sub) {
  188. if (sub && sub.update) {
  189. this.subs.push(sub)
  190. }
  191. }
  192. // 发布通知
  193. notify() {
  194. this.subs.forEach(sub => {
  195. sub.update()
  196. })
  197. }
  198. }
  199. //数据变化 watcher去更新视图
  200. // 当我们去创建一个watcher对象时 需要把自己添加到自己的主题对象中去
  201. class Watcher {
  202. constructor(vm, key, cb) {
  203. this.vm = vm;
  204. // data中的属性名称
  205. this.key = key;
  206. // 回调函数 负责更新视图
  207. this.cb = cb;
  208. // 把watcher对象记录到Dep类的静态属性target中
  209. Dep.target = this;
  210. // 触发get方法 在get方法中调用addSub
  211. this.oldValue = vm[key]
  212. Dep.target = null;
  213. }
  214. // 当数据发生变化的时候 更新视图
  215. update() {
  216. let newValue = this.vm[this.key];
  217. if (newValue === this.oldValue) {
  218. return
  219. }
  220. this.cb(newValue)
  221. }
  222. }
  223. let vm = new Vue({
  224. el: '#app',
  225. data: {
  226. msg: 'hello',
  227. count: 123,
  228. person: {
  229. name: 'zs'
  230. }
  231. }
  232. })
  233. // vm.msg = { 'sex': 'ada' }

上一篇:详解vue原理中的编译compiler
下一篇:

发表评论

表情:
评论列表 (有 0 条评论,332人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java 观察模式 详解

    观察者模式是一种常见的设计模式,也称作发布-订阅模式。它主要解决了对象之间的通知依赖关系问题。在这种模式中,一个对象(称作Subject)维护着一个对象列表,这些对象(称作Ob

    相关 设计模式观察模式

    前言 使用场景: 1、拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价。 2、西游记里面悟空请求菩萨降服红孩儿,菩萨洒了一地水招来一个老乌龟,这个乌龟

    相关 设计模式观察模式

    [上一篇:设计模式之策略模式][Link 1] 故事要从气象站说起,气象站有个WeatherData对象,这个对象负责拿到所有的气象数据(温度、湿度、气压),而气象站同时也

    相关 设计模式——观察模式

    > 设计模式: > > 前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定。而是一套用来提高代码可复用性、可维护性、可读性、稳健性、以及安全性的解决方案