学习笔记:Vue响应式模拟

177 阅读6分钟

内容为自学的笔记如有疏漏及错误的地方欢迎指出相互学习,谢谢

Vue响应式模拟

1. 前置知识

  • 数据驱动
  • 响应式核心原理
  • 发布订阅模式和观察者模式

1.1 数据驱动

  • 数据响应式

    • 数据模型仅仅式普通的javaScript对象,而我们修改数据式,试图会进行更新,避免繁琐的DOM操作,提高开发效率
  • 双向绑定

    • 数据改变,试图改变;试图改变,数据也随之改变(直观表单)
    • 我们可以使用v-model在表单元素上创建双向数据绑定
  • 数据驱动:Vue最独特的特性之一

    • 开发过程中仅需要关注数据本身,不需要关心数据式如何渲染视图,开发过程中我们仅需关注数据的操作,不需要关心渲染过程

1.2 数据响应原理

  • vue 2.0就是Object.defineProperty(),通过get()set()实现,因为式新的属性所有有兼容性的问题
  • vue 3.0是es6的proxy,好处是可以直接监听对象,而非属性,而且更多拦截操作,这部分内容也可以网上查阅。

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

  • 定义 :

    • 发布订阅模式是一种订阅模式,消息的放送者(发送者)不会直接发送消息特定的订阅者,而是将发布的消息消息发送给调度中心,由调度中心做信息过滤之后,根据之前依赖关系(订阅者和发布者的关系)当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。

3552020801-5d2322c1d88e7_fix732.png

演示代码:

 <script>
      class Sub {
         constructor() {
             // list用来收集依赖(调度中心)
             this.list = {}
          }
          //订阅
          on(name,user,fn) {
              // 当用户调用订阅法昂发的时候 step1 需要再调度中心添加一跳记录 =》建立依赖
              if(!(this.list[name] instanceof Array)){
                  this.list[name] = []  //判断是否为数组,因为一个数据存在多一个依赖
              }
              this.list[name].push({user,fn})
           }
          //发布
          emit(name,content) {
              // 先找到这个发布者有多少个订阅者 并且把content的内容发布到每个订阅者中
              this.list[name].forEach(ele =>{
                  // ele.就是每个订阅者和它的fn()
                  ele.fn(content)
              })
           }
          //取消订阅
          cancel(name,user) { 
              this.list[name].forEach((ele,index) => {
                  if(ele.user  === user){
                      this.list[name].splice(index,1)
                  }
              })
          }
      }
      let exSub = new Sub()
      // 用户a关注了bluej,调用on的方法
      // on,应该又三参数 1,发布者 2,用户名(watcher) 3.回调函数(收到数据改变后得方法)
      exSub.on('bluej', 'A', function (content) {
          console.log('A用户接收到了bluej发送过来的推文' + content);
      })
      exSub.on('bluej', 'B', function (content) {
          console.log('B用户接收到了bluej发送过来的推文' + content);
      })
      exSub.on('bluej', 'C', function (content) {
          console.log('C用户接收到了bluej发送过来的推文' + content);
      })
      exSub.on('haha', 'B', function (content) {
          console.log('B用户接收' + content);
      })
      exSub.on('haha', 'C', function (content) {
          console.log('C用户接' + content);
      })
      console.log(exSub);
      //触发emit之后会执行订阅了该发布者(bluej)的waicher('A')的回调函数
      exSub.emit('bluej','前端的坑点')  
      exSub.emit('haha','这里haha的发布的推文')
      exSub.cancel('bluej','A')
      exSub.emit('bluej','今天下大雨')
</script>

1.4 vue基本实现原理

vue实现基本原理.png

2. Vue响应式原理模拟

2.1 基本流程

首先要对数据(data)进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器(调度中新)Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。

20210202221630232.png

2.2 Vue类

  • 基本功能

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

v2-b67396d73d0affc376035875fd36e3f1_r.jpg

  • $options:用来记录传入的数据
  • $el: 记录一个dom对象
  • $data: 记录传入数据
  • _proxtData():是私有成员,该方法是私有方法,把data中的属性注入到Vue实例,转换成getter/setter
class Vue {
    constructor(options) {
        // 1 负责接收初始化的参数(选项) 
        this.$options = options || {}
        this.$data = options.data || {}
        this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
        // 2 负责把data中的属性注入到Vue实例,转换成getter/setter
        this._proxyData(this.$data)
        // 3 负责调用observer监听data中所有属性的变化
        // 4 负责调用compiler 解析指令/差值表达式
​
    }
    _proxyData(data) {
        //遍历 data中的说有数据,把他注入到vue实例中
        // Object.keys()方法会返回一个由一个给定对象的自身可枚举属性组成的数组
        Object.keys(data).forEach(key => {    //注意使用箭头函数,指向vue实例。否则指向window
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get(){
                    return data[key]
                },
                set(newVal){
                    if(newVal === data[key]) return;
                    data[key] = newVal
                }
            })
        })
    }
​
}
  • 在index页面中引入并调用,在控制台中打印vm可以看到对应的结果
<script src="./js/vue.js"></script>
<script>
    let vm = new Vue({
        el: '#app',
        data:{
            msg: 'hello Vue',
            count: 100
        }
    })
</script>

2.3 Observer

  • 功能

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

    • + walk(data) ------遍历data中所有属性
    • + defineReactive(data, key, value)------- 定义响应式数据
  • 基本代码

    • 在Vue类constructor中调new Observer(this.$data) 使用
class Observer {
    constructor(data) {
        this.walk(data)
    }
    walk(data) {
        if (!data || typeof data !== 'object') return
        // 遍历data对象的所有属性
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
        })
    }
    defineReactive(obj, key, val) {
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get(){
                //如果这里式return一个data[key]的话,会触发一个死递归
                // 这里当我们创建一个新的vue实例的时候,会创建一个Observer类
                // 外部$data就会引用到这个get方法形成闭包
                return val
            },
            set(newValue){
                if (newValue === val ) return
                val = newValue
            }
            //发送通知
        })
    }
}
  • 调用
<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>
<script>
    let vm = new Vue({
        el: '#app',
        data:{
            msg: 'hello Vue',
            count: 100,
            person: { name:"小明"}
        }
    })
    console.log(vm.msg);
    vm.msg = {test:"this is test"}
</script>
  • 问题
    • 实例中data无法深度监听
    • 当data的键值改为对象时无法监听
  • 解决后代码
class Observer {
    constructor(data) {
        this.walk(data)
    }
    walk(data) {
        if (!data || typeof data !== 'object') return
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
        })
    }
    defineReactive(obj, key, val) {
        // 优化1:实例中data无法深度监听.调用walk经行处理
        this.walk(val)
        const that = this
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get(){
                return val
            },
            set(newValue){
                if (newValue === val ) return
                val = newValue
                // 优化2:实例中data改变成对象无法监听.调用walk经行处理
                that.walk(newValue)
            }
        })
    }
}

2.4 Compiler

  • 功能

    • 负责编译模板,解析指令中的表达式
    • 负责页面的首次渲染图
    • 当数据变化后重新渲染试图
    • 一句话概括dom操作
  • 结构

    名称作用
    elvue中传入的$el
    vmVue实例
    compile(el)遍历对象的所有节点,并判断节点内容
    compileElement(node)解析元素节点,处理指令
    compileText(node)处理文件节点,解析插值表达式
    isDirectivr(attrName)解析指令
    isTextNode(node)判断是否文本节点
    IsElementNode(node)判断是否元素节点
class Compiler {
      constructor(vm) {
          this.el = vm.$el
          this.vm = vm
          this.compiler(this.el)
      }
      // 编译模板,处理文本节点和元素节点
      compiler(el) {
          let childNode = el.childNodes // 获取节点中子节点结合
          Array.from(childNode).forEach(node => { // 判断节点类型并给相关函数处理
              if (this.isTextNode(node)) {
                  this.compilerText(node)
              } else if (this.isElementNode(node)) {
                  this.compilerElement(node)
              }
              // 解决无法处理内层元素节点问题
              if (node.childNodes && node.childNodes.length) {
                  this.compiler(node)
              }
          })
      }
      // 编译元素节点,处理指令
      compilerElement(node) {
          // console.log(node.attributes);
          // 遍历所有的属性节点
          Array.from(node.attributes).forEach(attr => {
              // 判断是否是指令
              let attrName = attr.name
              if (this.isDirective(attrName)) {
                  attrName = attrName.slice(2)
                  let key = attr.value   // 获取属性值 => 对应的data
                  this.updata(node, key, attrName)
              }
          })
      }
  
      // 执行对应的updata指令 ,这样不用通过判断直接执行对应的函数
      updata(node, key, attrName) { // attrName--指令的后缀 =》的应的updata方法
          let updataFn = this[`${attrName}Updata`]
          updataFn && updataFn(node, this.vm[key])
      }
      // 处理v-text指令
      textUpdata(node, value) {
          node.textContent = value
      }
      // 处理v-modle指令
      modleUpdata(node, value) {
          node.value = value  // 更新表单的时候用的
      }
      // 编译文件节点,处理插值表达式
      compilerText(node) {
          // {{ value }} 使用正则去匹配该内容
          // 将括号内的内容取出
          let reg = /\{\{(.+?)\}\}/
          let value = node.textContent
          if (reg.test(value)) {
              let key = RegExp.$1.trim() // 匹配第一个原子组,获取属性名
              node.textContent = value.replace(reg, this.vm[key])
          }
      }
      // 判断元素属性是否指令
      isDirective(attrName) {
          return attrName.startsWith('v-') // 判断是否式v-开头
      }
      // 判断节点是都文件节点
      isTextNode(node) {
          return node.nodeType === 3
      }
      // 判断节点是否元素节点
      isElementNode(node) {
          return node.nodeType === 1
      }
  }

2.5 Dep(dependency)

  • 功能

    • 收集依赖,添加观察者模式
    • 通知所有观察者
  • 结构

    功能作用
    subs存储所有的观察者
    addSub(sub)添加观察者
    notify()派发消息
  • 调用

    • 我们需要对每一个响应式数据创建一个dep对象,收集依赖
    • 当数据发生变法的时候通知观察者
    • 调用观察这者中的updata方法去更新试图
    • 在Observer中条用
      • defineReactive去创建对象 let dep = new Dep()
      • set的去执行更新的方法 dep.nocify()
      • 在get中收集依赖 Dep.target && dep.addSub(Dep.target) (watcher时候回来看)

2.6 Watcher

watcher.jpg

  • 功能

    • 当数据变化触发依赖,dep同时所有watcher实例更新视图
    • 自身实例化的时候往dep对象添加自己
  • 结构

功能作用
vmvue实例
key数据名
cb回调函数,当更新不同类型时候所触发的会低调
oldValue旧值
update()更新数据

2.7 流程分析

我们回到最后最开始流程图,通过浏览器断点模拟整个过程

20210202221630232.png

  • 首次渲染 (断点设置在new Vue)

    =》浏览器渲染基本html
    =》创建一个 Vue实例
    =》记录参数,生成app节点将,所有数据注入到vue实例中
    =》调用Obsever,为每个data数据创建一个Dep(调度中心),同时劫持数据(设置getter和setter)。
        - 因为dep实例子啊get中有引用所以会被保留下来
    =》调用compiler编译模板
        - 当到对应的模板语法的时候,触发对应的updata指令,将数据渲染到页面上(首次)
        - 同时渲染后,创建有一个Watcher对象(订阅者)
        - wathcher对象会记录对应data的数据,会触发getter方法,将这个wather对象push到Dep的队列中
    
  • 数据变化(断点设置在set)

    =》当数据变化的触发setter,向调用中心发送消息(调用该数据的dep对象中的nocify方法)
    =》调度中心会遍历队列中的watcher,并向他们发送消息,触发他们的update的方法。
    

2.8 代码地址

gitee.com/wycna11/Min…