美文网首页
学习笔记(十三)模拟 Vue.js 响应式原理

学习笔记(十三)模拟 Vue.js 响应式原理

作者: 彪悍de文艺青年 | 来源:发表于2020-12-02 17:09 被阅读0次

    模拟Vue.js响应式原理

    数据驱动

    • 数据响应式
      • 数据模型是普通的JavaScript对象,当我们修改数据时,视图会进行相应的更新,避免了繁琐的DOM操作,提高开发效率
    • 双向绑定
      • 数据改变,视图发生相应变化,视图变化,数据发生相应的变化
      • 可以使用v-model指令在表单元素上创建双向数据绑定(自定义组件也可以自己实现v-model)
    • 数据驱动
      • 数据驱动是Vue最独特的特性之一,开发过程只需要关心数据,而不需要关心数据如何被渲染到视图

    数据响应式核心原理

    Vue2.x

    • 遍历对象中的属性,并通过Object.defineProperty 方法,将对象中的属性,转换成getter/setter方法
    • Object.defineProperty是ES5中新增的,不支持IE8以下浏览器

    Vue3.x

    • 基于ES6新增的Proxy来实现
    • Proxy代理的是整个对象,而不是对象的属性,因此不需要对对象的属性进行遍历
    • Proxy的性能由浏览器优化,要优于Object.defineProperty
    • 同样不支持IE8以下浏览器

    发布订阅模式和观察者模式

    发布订阅模式

    发布订阅模式包含发布者、订阅者、消息中心,发布者与订阅者相互之间不知道彼此存在,通过消息中心进行消息转发

    • 发布者

    • 订阅者

    • 消息中心broker

    • 模拟实现代码

      class EventEmitter {
          constructor() {
              this.subs = Object.create(null)
          }
      
          // 订阅 - 注册事件
          $on (topic, handler) {
              this.subs[topic] = this.subs[topic] || []
              this.subs[topic].push(handler)
          }
      
          // 发布 - 触发事件
          $emit (topic, ...args) {
              this.subs[topic]?.forEach(handler => {
                  typeof handler === 'function' && handler.apply(null, args)
              });
          }
      }
      

    观察者模式

    观察者模式包含观察者(订阅者)和目标(发布者),不存在消息中心,被观察的目标需要知道观察者的存在

    • 观察者(订阅者)Watcher

      • update方法:处理函数
    • 目标(发布者)

      • notify方法:通知观察者,调用所有观察者的update方法
      • subs数组:储存所有的观察者
      • addSub:添加观察者
    • 模拟实现代码

      export class Target {
          constructor() {
              this.subs = []
          }
      
          addSub(sub) {
              sub && typeof sub.update === 'function' && this.subs.push(sub)
          }
      
          notify(...args) {
              this.subs.forEach(sub => sub.update.apply(null, args))
          }
      }
      
      export class Watcher {
          update(...args) {
              console.log(args)
          }
      }
      

    发布订阅模式 vs 观察者模式

    image-20201109182709237

    模拟Vue.js响应式原理

    Vue类的简单实现

    • 功能

      • 负责接收初始化的参数(选项)
      • 负责把data中的属性转换成getter/setter,并注入到Vue实例中
      • 负责调用observer监听data中所有属性的变化
      • 负责调用compiler解析指令/差值表达式
    • 属性

      • $options
        • 保存传入的选项
      • $data
        • 保存传入的data
      • $el
        • 保存挂载的DOM元素
    • 方法

      • _proxyData
        • 把data中的属性转换成getter/setter,并注入到Vue实例中
    • 实现代码

      export default class Vue {
          // 1. 通过属性保存选项的数据
          constructor(options) {
              this.$options = options || {}
              this.$data = options.data || {}
              this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
              this._proxyData(this.$data)
          }
          // 2. 把data中的属性转换成getter/setter注入到vue实例中
          _proxyData(data) {
              // Vue2.x处理方式
              Object.keys(data).forEach(key => {
                  Object.defineProperty(this, key, {
                      enumerable: true,
                      configurable: true,
                      get() {
                          return data[key]
                      },
                      set(newValue) {
                          if (data[key] === newValue) {
                              return
                          }
                          data[key] = newValue
                      },
                  })
              })
          }
          // 3. 调用observer对象,监听数据变化
          new Observer(this.$data)
          // 4. 调用compiler对象,解析指令和差值表达式
          new Compiler(this)
      }
      

    Observer的简单实现

    • 功能

      • 负责把data中的数据转换成响应式数据
      • data中的某个属性也是对象,把该属性转换成响应式数据
      • 数据变化发送通知
    • 方法

      • walk(data)
        • 遍历data中的所有属性
      • defineReactive(data, key, value)
        • 转换属性为getter/setter
    • 实现代码

      export default class Observer {
          constructor(data) {
              this.walk(data)
          }
          walk(data) {
              // 1. 判断data是否是对象
              // 2. 遍历data的所有属性
              if (!data || typeof data !== 'object') {
                  return
              }
              Object.keys(data).forEach(key => {
                  this.defineReactive(data, key, data[key])
              })
          }
      
          defineReactive(data, key, value) {
              // 为什么需要第三个参数传递value,而不直接使用data[key]?
              // 在get中使用data[key]会循环触发getter,导致栈溢出
          
              // 为什么value属性在defineProperty执行完成后还可以被访问
              // 外部对内部属性存在引用,形成了闭包
      
              // 如果value是对象,则将对象的属性也转换成响应式数据
              const _this = this
      
          // 创建Dep对象收集依赖,发送通知
              let dep = new Dep()
      
              this.walk(value)
              
              Object.defineProperty(data, key, {
                  configurable: true,
                  enumerable: true,
                  get() {
                      // 收集依赖
                      Dep.target && dep.addSub(Dep.target)
      
                      return value
                  },
                  set(newValue) {
                      if (value === newValue) {
                          return
                      }
                      value = newValue
                      // 当属性被重新赋值为一个对象,将对象属性也转换为响应式数据
                      _this.walk(newValue)
                      dep.notify()
                  }
              })
          }
      }
      

    Compiler的简单实现

    • 功能

      • 负责编译模板,解析指令/差值表达式
      • 负责页面的首次渲染
      • 当数据变化后,重新渲染视图
    • 属性

      • el
        • DOM对象
      • vm
        • vue实例
    • 方法

      compiler的方法主要用来进行DOM操作

      • compile(el)
        • 编译解析入口
      • compileElement(node)
        • 解析元素节点的指令
      • compileText(node)
        • 解析文本节点的差值表达式
      • isDirective(attrName)
        • 判断属性是否是指令
      • isElementNode(node)
        • 判断是否是元素节点
      • isTextNode(node)
        • 判断是否文本节点
    • 实现代码

      import Watcher from "./watcher.js"
      
      export default class Compiler {
          constructor(vm) {
              this.el = vm.$el
              this.vm = vm
              this.compile(this.el)
          }
          // 编译模板,处理文本节点和元素节点
          compile (el) {
              [...el.childNodes].forEach(node => {
                  if (this.isTextNode(node)) {
                      this.compileText(node)
                  }
                  if (this.isElementNode(node)) {
                      this.compileElement(node)
                  }
                  // 判断childNodes并递归调用compile
                  if (node.childNodes && node.childNodes.length) {
                      this.compile(node)
                  }
              })
          }
      
          // 编译元素节点,处理指令
          compileElement (node) {
              // 获取并遍历元素的属性节点
              [...node.attributes].forEach(attr => {
                  const { name, value } = attr
                  if (this.isDirective(name)) {
                      this.update(node, name.slice(2), value)
                  }
              })
          }
      
          // 处理指令
          update (node, name, value) {
              const fn = this[`${name}Updater`]
              typeof fn === 'function' && fn.call(this, node, value)
          }
      
          // 处理v-text指令
          textUpdater(node, value) {
              node.textContent = this.vm[value]
              // 创建watcher对象,当数据改变时更新视图
              new Watcher(this.vm, value, newValue => {
                  node.textContent = newValue
              })
          }
      
          // 处理v-model指令
          modelUpdater(node, value) {
              node.value = this.vm[value]
              // 创建watcher对象,当数据改变时更新视图
              new Watcher(this.vm, value, newValue => {
                  node.value = newValue
              })
              // 注册表单input事件实现双向绑定
              node.addEventListener('input', () => {
                  this.vm[value] = node.value
              })
          }    
      
          // 处理其他指令
          // ...
      
          // 编译文本节点,处理差值表达式
          compileText (node) {
              // 使用正则表达式匹配差值表达式 {{ xxx }},并提取获取表达式内容
              let reg = /\{\{(.+?)\}\}/
              const text = node.textContent
              node.textContent = text.replace(reg, (word, key) => {
                  key = key.trim()
                  if (key in this.vm) {
                      // 创建watcher对象,当数据改变时更新视图
                      new Watcher(this.vm, key, newValue => {                    
                          node.textContent = text.replace(reg, (w, k) => this.vm[k.trim()])
                      })
                      return this.vm[key]
                  }
                  return word            
              })        
          }
      
          // 判断属性是否为指令
          isDirective (attrName) {
              return attrName.startsWith('v-')
          }
      
          // 判断是否为元素节点
          isElementNode (node) {
              return node.nodeType === 1
          }
          // 判断是否为文本节点
          isTextNode (node) {
              return node.nodeType === 3
          }
      }
      

    Dep的简单实现

    • 功能

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

      • subs
        • 储存所有的观察者
    • 方法

      • addSubs(sub)
        • 添加观察者
      • notify()
        • 通知观察者,调用所有观察者的update方法
    • 实现代码

      export default class Dep {
          constructor() {
              this.subs = []
          }
       
          addSub(sub) {
              sub && typeof sub.update === 'function' && this.subs.push(sub)
          }
       
          notify(...args) {
              this.subs.forEach(sub => sub.update(...args))
          }
      }
      

    Watcher的简单实现

    • 功能

      • 数据变化触发依赖,接收dep通知更新视图
      • 自身实例化的时候向Dep中添加自己
    • 属性

      • vm
        • vue实例
      • key
        • 观察的属性名称
      • cb
        • 更新时的回调处理函数
      • oldValue
        • 观察的属性数据更新之前的值
    • 方法

      • update()
        • 更新处理函数
    • 实现代码

      import Dep from "./dep.js"
      
      export default class Watcher {
          constructor(vm, key, cb) {
              this.vm = vm
              this.key = key
              this.cb = cb
              // 把watcher对象记录到Dep类的静态属性target
              Dep.target = this
              // 触发get方法,在get中调用addSub
              this.oldValue = vm[key]
              // 清空Dep.target,避免重复添加
              Dep.target = null
          }
          // 数据变化时更新视图
          update(...args) {
              const newValue = this.vm[this.key]
              if (newValue !== this.oldValue) {
                  this.cb(newValue)
              }
          }
      }
      

    总结

    问题

    • 将属性重新赋值成对象,是否是响应式的?
      • 是响应式的,重新赋值成对象时会调用属性的set方法,此时会将新赋值的内容转换为响应式数据
    • 为vue实例添加新的属性时,此属性是否是响应式的?
      • 不是响应式的
      • 可以通过Vue.set()或vm.$set()方法设置新的响应式属性

    整体流程

    image-20201201234840455

    相关文章

      网友评论

          本文标题:学习笔记(十三)模拟 Vue.js 响应式原理

          本文链接:https://www.haomeiwen.com/subject/obduwktx.html