原文转自 https://const_white.gitee.io/gitee-blog/blog/vue/mini-vue/

Vue响应式原理

图片引自 孟思行 - 图解 Vue 响应式原理


乞丐版 mini-vue

实现mini-vue之前,先看看官网的描述。在Vue官网,深入响应式原理中,是这样说明的:

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。


起步

技术原因,这里不做Virtual DOMrender部分,而选择直接操作DOM

简单来说,mini vue在创建Vue实例时

  1. Vue类负责把data中的属性注入到Vue实例,并调用Observer类和Compiler类。
  2. Observer类负责数据劫持,把每一个data转换成gettersetter。其核心原理是通过Object.defineProperty实现。
  3. Compiler类负责解析指令和插值表达式(更新视图的方法)。
  4. Dep类负责收集依赖、添加观察者模式。通知data对应的所有观察者Watcher来更新视图。在Observer类把每一个data转换成gettersetter时,会创建一个Dep实例,用来负责收集依赖并发送通知。在每一个data中在getter中收集依赖。在setter中通知依赖,既通知所有Watcher实例新视图。
  5. Watcher类负责数据更新后,使关联视图重新渲染。

实现代码都添加了详细的注释,无毒无害,可放心查看

Vue类

class Vue {  constructor(options) {    // 1. 保存 options的数据    this.$options = options || {}    this.$data = options.data || {}    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el    // 2. 为方便调用(vm.msg),把 data中的成员转换成 getter和 setter,并注入到 Vue实例中    this._proxyData(this.$data)    // 3. 调用 Observer类,监听数据的变化    new Observer(this.$data)    // 4. 调用 compiler类,解析指令和插值表达式    new Compiler(this)  }  _proxyData(data) {    Object.keys(data).forEach(key => {      Object.defineProperty(this, key, {        enumerable: true,        configurable: true,        get() {          return data[key]        },        set(newValue) {          if (newValue === data[key]) {            return          }          data[key] = newValue        }      })    })  }}

Observer类

class Observer {  constructor(data) {    this.walk(data)  }  // 遍历 data($data)中的属性,把属性转换成响应式数据  walk(data) {    if (!data || typeof data !== 'object') {      return    }    Object.keys(data).forEach((key) => {      this.defineReactive(data, key, data[key])    })  }  // 定义响应式数据  defineReactive(obj, key, value) {    const that = this    // 负责收集依赖并发送通知    let dep = new Dep()    // 利用递归使深层(内部)属性转换成响应式数据    this.walk(value)    Object.defineProperty(obj, key, {      enumerable: true,      configurable: true,      get() {        // 收集依赖        Dep.target && dep.addSub(Dep.target)        return value      },      set(newValue) {        if (value === newValue) {          return        }        value = newValue        // 如果新设置的值为对象,也转换成响应式数据        that.walk(newValue)        // 发送通知        dep.notify()      }    })  }}

Compiler类

class Compiler {  constructor(vm) {    this.vm = vm    this.el = vm.$el    this.compiler(this.el)  }

  // 编译模板,处理文本节点和元素节点  compiler(el) {    const childNodes = el.childNodes    Array.from(childNodes).forEach(node => {      // 处理文本节点      if (this.isTextNode(node)) {        this.compilerText(node)      } else if (this.isElementNode(node)) {        // 处理元素节点        this.compilerElement(node)      }

      // 判断 node节点是否有子节点。如果有,递归调用 compile      if (node.childNodes.length) {        this.compiler(node)      }    })  }

  // 编译元素节点,处理指令  compilerElement(node) {    // 遍历所有属性节点    Array.from(node.attributes).forEach(attr => {      // 判断是否 v-开头指令      let attrName = attr.name      if (this.isDirective(attrName)) {        // 为了更优雅的处理不同方法,减去指令中的 v-        attrName = attrName.substr(2)        const key = attr.value        this.update(node, key, attrName)      }    })  }

  // 执行对应指令的方法  update(node, key, attrName) {    let updateFn = this[attrName + 'Updater']    // 存在指令才执行对应方法    updateFn && updateFn.call(this, node, this.vm[key], key)  }

  // 处理 v-text指令  textUpdater(node, value, key) {    node.textContent = value

    // 创建 Watcher对象,当数据改变时更新视图    new Watcher(this.vm, key, (newValue) => {      node.textContent = newValue    })  }

  // 处理 v-model指令  modelUpdater(node, value, key) {    node.value = value

    // 创建 Watcher对象,当数据改变时更新视图    new Watcher(this.vm, key, (newValue) => {      node.value = newValue    })

    // 双向绑定    node.addEventListener('input', () => {      this.vm[key] = node.value    })  }

  // 编译文本节点,处理插值表达式  compilerText(node) {    const reg = /\{\{(.+?)\}\}/    let value = node.textContent    if (reg.test(value)) {      // 只考虑一层的对象,如 data.msg = 'hello world',不考虑嵌套的对象。且假设只有一个插值表达式。      const key = RegExp.$1.trim()      node.textContent = value.replace(reg, this.vm[key])

      // 创建 Watcher对象,当数据改变时更新视图      new Watcher(this.vm, key, (newValue) => {        node.textContent = newValue      })    }  }

  // 判断元素属性是否属于指令  isDirective(attrName) {    return attrName.startsWith('v-')  }

  // 判断节点是否属于文本节点  isTextNode(node) {    return node.nodeType === 3  }

  // 判断节点书否属于元素节点  isElementNode(node) {    return node.nodeType === 1  }}

Dep类

class Dep {  constructor() {    this.subs = []  }  // 添加观察者  addSub(sub) {    if (sub && sub.update) {      this.subs.push(sub)    }  }  // 发送通知  notify() {    this.subs.forEach(sub => {      sub.update()    })  }}

Watcher类

class Watcher {  constructor(vm, key, cb) {    this.vm = vm

    // data中的属性名    this.key = key

    // 回调函数负责更新视图    this.cb = cb

    // 把 watcher对象记录到 Dep类的静态属性 target中    Dep.target = this    // 触发 get方法,在 get方法中会调用 addSub    this.oldValue = vm[key]    Dep.target = null  }

  // 当数据发生变化的时候更新视图  update() {    const newValue = this.vm[this.key]    // 数据没有发生变化直接返回    if (this.oldValue === newValue) {      return    }    // 更新视图    this.cb(newValue)  }

}

完整版思维导图


对于数组的监听

这里直接把数组的每一项都添加上了gettersetter,所以vm.items[1] = 'x'也是响应式的。

Vue中为什么没这样做呢?参考 为什么vue没有提供对数组属性的监听


update关联一个视图的时候特别慢_实现一个简单的Vue.js相关推荐

  1. java写一个音乐播放器源码_求一个JAVA音乐播放器的源代码

    展开全部 import javax.media.ControllerEvent; import javax.media.ControllerListener; import javax.media.E ...

  2. 一个软件公司需要多少前端_制作一个小程序商城需要多少钱?开发小程序公司哪家强?...

    搭建小程序商城能更好的为商家打造私域粉丝池,形成私域流量,因此,微信小程序自然成为了企业商家的首选. 随着线上的快速发展,微信小程序能力的升级,使小程序也存在多种类型,例如电商类.资讯类.预约类等等, ...

  3. python另一个程序正在使用此文件_另一个程序正在使用此文件,进程无法访问

    1.Network Error (tcp_error) A communication error occurred: "Operation timed out" 看上去是服务器出 ...

  4. python定义一个类savingaccount表示银行账户_创建一个SavingAccount类

    /*5.创建一个SavingAccount类.使用一个static数据成员 annualInterestRate保存每个存款者的年利率.类的每个 对象都包含一个private数据成员savingsBa ...

  5. 用Vue.js开发一个电影App的前端界面

    我们要构建一个什么样的App? 我们大多数人使用在线流媒体服务(如Netflix)观看我们最喜欢的电影或者节目.这篇文章将重点介绍如何通过使用vue.js 2 建立一个类似风格的电影流媒体WEB交互界 ...

  6. oracle并行parallel update两张表_Oracle并行更新的两种方式(merge/update内联视图)

    对于Oracle的两表联合更新的场景(有A.B两表,以A.id=B.id关联,根据B表中的记录更新A表中的相应字段),一般有update内联视图和merge两种方式,下面举例介绍: 创建用例表: cr ...

  7. 【JetPack】为现有 Android 项目配置视图绑定 ( ViewBinding ) 模块 ( 视图绑定不影响传统布局操作 | 视图绑定类关联 Activity | 视图绑定类本质 )

    文章目录 I . 为现有项目配置 视图绑定 ( ViewBinding ) 应用 II . 视图绑定 ( ViewBinding ) 定制 III . 视图绑定 ( ViewBinding ) 对于正 ...

  8. mysql可以关联视图_Mysql 五: 数据库自关联、视图

    怎么判断两张表中的关系是 一对多 还是 多对一 还是 一对一? 表A 中的 一条数据 对应 表B 中的 一条数据, 则为 一对一 . 表A 中的 一条数据 对应 表B 中的 多条数据, 则为 一对多 ...

  9. Android自定义控件学习(四)------创建一个视图类

    创建一个视图类 精心设计的自定义视图与其他精心设计的类非常相似.它使用易于使用的界面封装了一组特定的功能,它可以高效地使用CPU和内存,等等.不过,作为一个设计良好的设计,自定义视图应该: 符合And ...

最新文章

  1. 找父节点和子节点个数(Poj1634)
  2. “科创30条”鼓励高校开设AI新兴学科
  3. UA MATH564 概率论VI 数理统计基础3 卡方分布上
  4. java中的年轻态,14、Java垃圾回收机制(示例代码)
  5. [Spring5]IOC容器_Bean管理注解方式_注入属性@Autowired_@Qualified_@Resource_@Value
  6. 大数据之-Hadoop3.x_MapReduce_MapTask源码解析---大数据之hadoop3.x工作笔记0126
  7. @IT老司机 6月3日,扫除技术与产品选型难题,CSDN选型智囊团来了!
  8. 【数据分析师自学系列】Kettle下载安装、Kettle环境部署
  9. quartusII编译时出现Error (119013): Current license file does not support the EP4CE6F17C8 device
  10. Log4j 漏洞修复检测 附检测工具
  11. Lotus 新手运维手册
  12. ADP论文关键要点总结
  13. 网上订餐叫外卖的发展优势
  14. 轻松一下:python(turtle模块)绘制分形图
  15. Nginx rewrite路由重写
  16. 位于法国诺曼底旅游核心区的275英亩地产将通过Concierge Auctions无保留地拍卖出售
  17. 芯片验证漫游指南-读书笔记
  18. Javascript DOM 编程艺术读书笔记16/04/01
  19. php高级工程简历技能特长怎么写,简历中如何写好个人特长?
  20. 【流体力学】分享几本流体力学经典教材(持续更新)

热门文章

  1. Java学习笔记004——接口、克隆、回调、内部类
  2. 基于交换机的PC端网络通信
  3. 【剑指offer】面试题15:二进制中1的个数(Java)
  4. mysql 8.0认证失败_Node.js无法对MySQL 8.0进行身份验证
  5. 苹果挂端口方法_调音台变身直播声卡的方法
  6. Java中bytebuffer和string的转换记录
  7. openvino安装小记
  8. 怎样在dos窗口中启动mysql服务器_如何在dos命令中启动mysql或sql server 服务器的一些操作...
  9. mediarecorder添加时间戳_Python脚本实现数据处理(官方实例)和Hive自带时间函数...
  10. C++常用字符串分割方法