Vue进阶之响应式原理

11 篇文章 0 订阅

Vue进阶之响应式原理

数据驱动
  • 数据响应式、双向绑定、数据驱动
  • 数据响应式
    • 数据模型仅仅是普通的 JavaScript 对象,而当我们修改数据时,视图会进行更新,避免了繁琐的 DOM 操作,提高开发效率
  • 双向绑定
    • 数据改变,视图改变;视图改变,数据也随之改变
    • 我们可以使用 v-model 在表单元素上创建双向数据绑定
  • 数据驱动是 Vue 最独特的特性之一
    • 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图
响应式原理的简单演示

Vue2.x 利用Object.defineProperty
数据劫持:当访问或者设置 vm 中的成员的时候,做一些干预操作

<!DOCTYPE html>
<html lang="cn">
<head>
  <title>defineProperty</title>
</head>
<body>
  <div id="app">
    hello
  </div>
  <script>
    // 模拟 Vue 中的 data 选项
    let data = {
      msg: 'hello'
    }

    // 模拟 Vue 的实例
    let vm = {}

    // 数据劫持:当访问或者设置 vm 中的成员的时候,做一些干预操作,多个成员时利用循环注册干预;
    Object.defineProperty(vm, 'msg', {
      // 可枚举(可遍历)
      enumerable: true,
      // 可配置(可以使用 delete 删除,可以通过 defineProperty 重新定义)
      configurable: true,
      // 当获取值的时候执行
      get () {
        console.log('get: ', data.msg)
        return data.msg
      },
      // 当设置值的时候执行
      set (newValue) {
        console.log('set: ', newValue)
        if (newValue === data.msg) {
          return
        }
        data.msg = newValue
        // 数据更改,更新 DOM 的值
        document.querySelector('#app').textContent = data.msg
      }
    })

    setTimeout(() => {
      // 测试
      vm.msg = 'Hello World'
    }, 3000)
    
    console.log(vm.msg)
  </script>
</body>
</html>

Vue3.x 利用new Proxy(对象代理器)

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Proxy</title>
</head>
<body>
  <div id="app">
    hello
  </div>
  <script>
    // 模拟 Vue 中的 data 选项
    let data = {
      msg: 'hello',
      count: 0
    }

    // 模拟 Vue 实例
    let vm = new Proxy(data, {
      // 执行代理行为的函数
      // 当访问 vm 的成员会执行
      get (target, key) {
        console.log('get, key: ', key, target[key])
        return target[key]
      },
      // 当设置 vm 的成员会执行
      set (target, key, newValue) {
        console.log('set, key: ', key, newValue)
        if (target[key] === newValue) {
          return
        }
        target[key] = newValue
        document.querySelector('#app').textContent = target[key]
      }
    })
    
    setTimeout(()=>{
      // 测试
      vm.msg = 'Hello World'
    },3000)
    
    console.log(vm.msg)
  </script>
</body>
</html>
模拟发布订阅模式

发布/订阅模式

  • 订阅者 (在事件中心注册事件)
  • 发布者 (发布事件)
  • 信号中心或者事件中心

vue 的发布订阅模式

// Vue 自定义事件
// Vue 自定义事件
let vm = new Vue()

// 注册事件(订阅消息)
vm.$on('dataChange', () => {
  console.log('dataChange')
})

vm.$on('dataChange', () => {
  console.log('dataChange1')
})
// 触发事件(发布消息)
vm.$emit('dataChange')

模拟发布订阅

<!DOCTYPE html>
<html lang="en">
<head>
  <title>发布订阅模式</title>
</head>
<body>
  <script>
    class EventCenter {
      constructor() {
        // {'click': [fn1,fn2], 'click2': [fn] }
        this.subs = {} // 数据中心
      }

      $on(eventType, handler) { // 订阅者入口
        this.subs[eventType] = this.subs[eventType] || [];
        this.subs[eventType].push(handler);
      }

      $emit(eventType) { // 发布者入口
        this.subs[eventType] && this.subs[eventType].forEach(handler => { handler()});
      }
    }

    let vm = new EventCenter();
    // 测试
    vm.$on('dataChange', () => {
      console.log('dataChange')
    })

    vm.$on('dataChange', () => {
      console.log('dataChange1')
    })
    // 触发事件(发布消息)
    vm.$emit('dataChange')
  </script>
</body>
</html>
观察者模式
  • 观察者(订阅者) – Watcher update():当事件发生时,具体要做的事情
  • 目标(发布者) – Dep
    • subs 数组:存储所有的观察者
    • addSub():添加观察者
    • notify():当事件发生,调用所有观察者的 update() 方法
  • 没有事件中心

简单的观察者模式

// 发布者 Dep
class Dep {
  constructor() {
    // 观察者列表
    this.subs = []
  }
  // 添加观察者方法
  addsubs(sub) {
    sub && sub.update && this.subs.push(sub)
  }
  // 事件触发调用观察者
  notify() {
    this.subs.forEach(sub => {
      sub.update();
    })
  }
}
// 观察者(订阅者)
class Watcher {
  update() {
    console.log('watcher_update')
  }
}

let dep = new Dep();
let wat = new Watcher();

dep.addsubs(wat);

setTimeout(() => {
  dep.notify();
},3000)

总结

  • 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在,发布/订阅模式隔离了发布者和订阅者,使用更加灵活;

实现简单Vue

实现的几个模块儿
  1. Vue — 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
  2. Observer — 能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep
  3. Compiler — 解析每个元素中的指令/插值表达式,并替换成相应的数据
  4. Dep — 添加观察者(watcher),当数据变化通知所有观察者
  5. Watcher — 数据变化更新视图
首先Vue模块,功能
  • 负责接收初始化的参数(选项);
  • 负责把data中的属性注入到Vue实例中,转换成getter/setter;
  • 负责调用 observer 监听 data 中所有属性的变化;
  • 负责调用 compiler 解析指令/差值表达式
/* 类图
  + Vue
  ----------
  + $options
  + $el
  + $data
  ----------
  - _proxyData() //代理data中的属性,转化为getter和setter,注入到vue实例中
*/ 

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中的成员转换为getter和setter注入到vue实例中
    this._proxyData(this.$data)
    // 3、调用observer对象,监听数据的变化
    // 4、调用compiler对象,解析指令和差值表达式
  }

  _proxyData(data) {
    // 遍历获取属性,利用Vue实例做数据劫持
    Object.keys(data).forEach(key => {
      // 这里this就是Vue实例
      Object.defineProperty(this, key, {
        enumerable: true, //可遍历
        configurable: true, //可枚举
        get() {
          return data[key]
        },
        set(newval) {
          if(newval === data[key])return;
          data[key] = newval
        }
      })
    })
  }
}
Observer模块 (数据劫持)

功能

  • 负责把data选型中的属性转换成响应式数据,getter/setter
  • data中的某个属性也是对象的话,也要把该属性转换成响应式数据;
  • 数据变比后发送通知(观察者模式)
/*
  Observer
  --------------
  + walk(data)  //遍历data中的值,并筒处理
  + defineReactive  //给属性添加getter和setter
*/ 
class Observer{
  constructor(data){
    this.walk(data)
  }

  walk(data) {
    // 1.判断数据类型兼容错误类型
    if(!data || typeof data !== 'object') return;
    // 2.遍历data的所有属性
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }

  defineReactive(obj, key, val) {
    const that = this;
    this.walk(val); //这里walk会判断是否对象,对象的话继续递归的往下遍历,这个设计很好。
    Object.defineProperty(obj, key, {
      enumerable: true,  //可遍历
      configurable: true,  //可枚举
      get() { // 注意这里不可使用obj[key], 使用obj[key]就是调用get方法会陷入死循环
        // 收集依赖添加观察者
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set(newval) {
        if(newval === val) return;
        val = newval;
        that.walk(newval) //当属性新赋的值对对象时,也要添加getter和setter方法
        // 发送通知
      }
    })
  }
}

Compiler模块 (DOM渲染)

功能

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

这里先简化不使用虚拟DOM

/*
  Compiler
  -------------
  + el
  + vm vue实例
  -------------
  + compile(el) 变了所有节点
  + compileElement(node) 标签 - 解析指令
  + compileText(node) 文本 - 解析差值表达式
  + isDirective(attrName) 判断此属性是否为指令
  + isTextNode(node)  是否文本
  + isElementNode(node)  是否标签
*/ 

class Compiler{
  constructor(vm) {
    this.el = vm.$el
    this.vm = vm
    this.compile(this.el)
  }
  // 编译模板,处理文本节点和元素节点
  compile(el) {
    let childNodes = el.childNodes;
    childNodes.forEach(node => {
      if(this.isTextNode(node)){ //文本
        this.compileText(node)
      }else if(this.isElementNode(node)){ //元素
        this.compileElement(node)
      }
      // 判断node节点,是否有子节点,有就递归
      if(node.childNodes && node.childNodes.length) this.compile(node);
    });
  }
  // 编译元素节点,处理指令
  compileElement(node) {
    // console.dir(node.attributes);
    // 遍历所有属性,判断是否是指令 node.attributes是伪数组需要使用Array.from转换一下
    Array.from(node.attributes).forEach(attr => {
      let attrName = attr.name
      if(this.isDirective(attrName)){
        attrName = attrName.substr(2)
        let key = attr.value
        this.updata(node, key, attrName)
      }
    })
  }
  updata(node, key, attrName) {
    let updatefn = this[attrName+'Updater']
    updatefn && updatefn(node, this.vm[key])
  }

  //处理各种指令,各自去处理各个指令 v-text
  textUpdater(node, val) {
    node.textContent = val
  }
  //处理各种指令 v-model
  modelUpdater(node, val) {
    node.value = val
  }

  // 编译文本,处理差值表达式
  compileText(node) {
    // console.dir(node);
    // {{ msg }}
    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-')
  }
  // 判断是否文本
  isTextNode(node) {
    return node.nodeType === 3
  }
  // 判断是否标签
  isElementNode(node) {
    return node.nodeType === 1
  }

}
Dep模块 (发布者)

功能

  • 收集依赖,收集观察者(watcher)
  • 通知所有观察者
/*
  Dep
  ----------
  + subs
  ----------
  + addSub(sub)
  + notify()
*/

class Dep{
  constructor() {
    this.subs = []
  }
  // 添加观察者
  addSub(sub) {
    sub && sub.updata && this.subs.push(sub)
  }
  // 发布通知观察者
  notify() {
    this.subs.forEach(sub => {
      sub.updata()
    })
  }
}
Watcher模块 (观察者)

功能

  • 当时数据变化是触发依赖,dep通知所有watcher实例更新视图
  • 自身实例化的时候添加到dep对象的watcher列表中
/*
  Watcher
  -----------
  + vm
  + key
  + cb
  + oldValue
  -----------
  + upData
 */ 

class Watcher{
  constructor(vm, key, cb) {
    this.vm = vm  // vue实例
    this.key = key // 监听的data属性
    this.cb = cb //回调函数负责跟新视图
    // 将Watcher对象记录到Dep类的tatget静态属性中
    Dep.target = this
    // 触发get方法,在get方法中调用addsub
    this.oldValue = vm[key] //触发get
    // 设置完成后将target至为空
    Dep.target = null
  }

  upData() { //数据变化跟新
    let newValue = this.vm[this.key]
    if(newValue === this.oldValue) return;
    this.oldValue = newValue //替换oldValue
    this.cb && this.cb(newValue)
  }

}

Watcher模块的调用注意this指向

// Compiler.js
...
  // 编译元素节点,处理指令
  compileElement(node) {
    // console.dir(node.attributes);
    // 遍历所有属性,判断是否是指令 node.attributes是伪数组需要使用Array.from转换一下
    Array.from(node.attributes).forEach(attr => {
      let attrName = attr.name
      if(this.isDirective(attrName)){
        attrName = attrName.substr(2)
        let key = attr.value
        this.updata(node, key, attrName)
      }
    })
  }
  updata(node, key, attrName) {
    let updatefn = this[attrName+'Updater']
    updatefn && updatefn.call(this, node, this.vm[key], key)
  }

  //处理各种指令,各自去处理各个指令 v-text
  textUpdater(node, val, key) { 
    // 这里注意this,textUpdater是在updata中调用的,这里的this指向了undefined,所以调用时,要通过call传递this
    node.textContent = val
    // 创建Watcher对象,监听数据变化,跟新视图
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue
    })
  }
  //处理各种指令 v-model
  modelUpdater(node, val, key) {
    // 这里注意this,modelUpdater是在updata中调用的,这里的this指向了undefined,所以调用时,要通过call传递this
    node.value = val
    // 创建Watcher对象,监听数据变化,跟新视图
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })
  }

  // 编译文本,处理差值表达式
  compileText(node) {
    // console.dir(node);
    // {{ msg }}
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if(reg.test(value)){
      let key = RegExp.$1.trim() //匹配到变量后去除空格
      node.textContent = value.replace(reg, this.vm[key])
      // 创建Watcher对象,监听数据变化,跟新视图
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue
      })
    }
  }
...
双向数据绑定

监听v-model的数据变化,变化后赋值给data数据
在之前的基础之上

// Compiler.js
...
  //处理各种指令 v-model
  modelUpdater(node, val, key) {
    // 这里注意this,modelUpdater是在updata中调用的,这里的this指向了undefined,所以调用时,要通过call传递this
    node.value = val
    // 创建Watcher对象,监听数据变化,跟新视图
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue //这里不会触发input事件所以不会死循环
    })

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

  }
...
技巧:去除所有断点,sources-> breakpoints->右键移除所有断点
总结

问题
给 Vue 实例新增一个成员是否是响应式的? 不是
给属性重新赋值成对象,是否是响应式的? 是的vue的流程题

  • Vue
    • 记录传入的选项,设置 $data/$el
    • 把 data 的成员注入到 Vue 实例
    • 负责调用 Observer 实现数据响应式处理(数据劫持)
    • 负责调用 Compiler 编译指令/插值表达式等
  • Observer 数据劫持
    • 负责把 data 中的成员转换成 getter/setter
    • 负责把多层属性转换成 getter/setter
    • 如果给属性赋值为新对象,把新对象的成员设置为 getter/setter
    • 添加 Dep 和 Watcher 的依赖关系
    • 数据变化发送通知
  • Compiler
    • 负责编译模板,解析指令/插值表达式
    • 负责页面的首次渲染过程
    • 当数据变化后重新渲染
  • Dep
    • 收集依赖,添加订阅者(watcher)
    • 通知所有订阅者
  • Watcher
    • 自身实例化的时候往dep对象中添加自己
    • 当数据变化dep通知所有的 Watcher 实例更新视图
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值