说一说vue响应式理解?

804 阅读5分钟

烂大街的问题,但却并不是每个人都能回答到位

回答思路:

  1. 啥是响应式?
  2. 为什么vue 需要响应式?
  3. 它能给我们带来什么好处?
  4. vue 的响应式是怎么实现的?有哪些优缺点?
  5. vue3 中的响应式的新变化?

示例:

  1. 所谓数据响应式就是能够使数据变化可以被检测到并对这种变化做出响应机制。
  2. mvvm 框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这一点就需要对数据响应式处理,这样数据一旦发生变化就可以立即做出更新处理。
  3. 以 vue 为例,通过数据响应式加上虚拟 DOM 和 patch 算法,可以使我们只需操作数据,完全不用接触烦琐的 dom 操作,从而大大提升开发效率,减低开发难度。
  4. vue2 中的数据响应式会根据数据类型来做不同处理,如果是对象则采用 Object.defineProperty() 的方式定义数据拦截,当数据被访问或者发生变化时,我们感知并做出响应;如果是数组则通过覆盖数组原型的方法,扩展它的7个变更方法,使这些方法可以额外的做更新通知,从而做出响应。这种机制很好的解决了数据响应化的问题,但实际使用中也存在一些缺点:比如初始化的递归遍历会造成性能损失;新增或者删除属性时需要用户使用 Vue.set/delete 这样特殊的 api 才能生效;对于 es6 中新产生的 Map、Set 这些数据结构不支持等问题。
  5. 为了解决这些问题,vue3 重新编写了这一部分的实现:利用 ES6 的 Proxy 机制代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊 Api,初始化性能和内存消耗都得到大幅改善;另外由于响应化的实现代码抽取为独立的 reactivity 包,使得我们可以更加灵活的使用它,我们甚至不需要引入vue 都可以体验。

  • 响应式实现:
    • object.defineProperty
    • proxy(兼容性不太好)

vue响应式.png

  • observer类
/* observer 类会附加到每一个被侦测的object上 
* 一旦被附加上,observer会被object的所有属性转换为getter/setter的形式 * 当属性发生变化时候及时通知依赖
*/ 

// Observer 实例 
export class Observer {  
  constructor (value) { 
    this.value = value 
    if (!Array.isArray(value)) { 
        // 判断是否是数组
          this.walk(value) // 劫持对象 
    } 
  } 

  walk (obj) { 
      // 将会每一个属性转换为getter/setter 形式来侦测数据变化     
      const keys = Object.keys(obj) 
      for (let i = 0; i < keys.length; i++) { \
        defineReactive(obj, keys[i],obj[key[i]]) // 数据劫持方法 
      } 
  } 
  function defineReactive(data,key,val){ 
    // 递归属性 
    if(typeof val ==='object'){ 
        new Obeserve(val) 
    }
    let dep = new Dep()
    Object.defineProperty(data,key,{         
        enumerable:true, 
        configurable:true,
        get:function(){
            dep.depend()
            return val 
        }, 
        set:function(newVal){ 
            if(val===newVal){ 
                return 
            } 
            val = newVal 
            dep.notify() 
        } 
    }) 
}

定义了 observer类,用来将一个正常的object转换成被侦测的object 然后判断数据类型
只有object类型才会调用walk将每一个属性转换成getter/setter的形式来侦测变化
最后在defineReactive中新增new Observer(val)来递归子属性
当data中的属性变化时,与这个属性对应的依赖就会接收通知

  • dep依赖收集 

    getter中收集依赖,那么这些依赖收集到那?

export default class Dep {
  constructor () {
    this.subs = [] // 观察者集合 
  } 

 // 添加观察者 
  addSub (sub) { 
    this.subs.push(sub) 
  } 

 // 移除观察者 
  removeSub (sub) { 
    remove(this.subs, sub) 
  } 

  depend () { 
     // 核心,如果存在 ,则进行依赖收集操作     
     if (window.target) {
        this.addDep(window.target) 
    } 
  } 
 
  notify () { 
    const subs = this.subs.slice() // 避免污染原来的集合 
    // 如果不是异步执行,先进行排序,保证观察者执行顺序 
    if (process.env.NODE_ENV !== 'production' && !config.async) {       
       subs.sort((a, b) => a.id - b.id) 
    } 
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // 发布执行 
    }
  } 
} 

function remove(arr,item){ 
    if(arr.length){ 
        const index = arr.indexOf(item)         
           if(index > -1){ 
             return arr.splice(index,1)         
           } 
    } 
}

收集的依赖时window.target,他到以是什么? 
当属性变化时候我们通知谁?

  • watcher

    • 是一个中介的角色,数据发生变化时通知它,然后它再去通知其他地方
    export default class Watcher { 
        constructor (vm,expOrFn,cb) { 
            // 组件实例对象  
            // 要观察的表达式,函数,或者字符串,只要能触发取值操作 
            // 被观察者发生变化后的回调 
            this.vm = vm 
            // Watcher有一个 vm 属性,表明它是属于哪个组件的       
            // 执行this.getter()及时读取数据 
            this.getter = parsePath(expOrFn) 
            this.cb = cb 
            this.value = this.get() 
        } 
        get(){ 
            window.target = this 
            let value = this.getter.call(this.vm,this.vmwindow.target = undefined 
            return value 
        } 
    
        update(){ 
            const oldValue = this.value 
            this.value = this.get() 
            this.cb.call(this.vm,this.value,oldValue) 
        } 
    }
    
    • 总结

      • data 通过 Observer 转换成了 getter/setter 的形式来追踪变化
      • 当外界通过 Watcher 读取数据的,会触发 getter 从而 将 watcher 添加到依赖中
      • 当数据变化时,会触发 setter 从而向 Dep 中的依赖 (watcher)发送通知
      • watcher 接收通知后,会向外界发送通知,变化通知到外界可能会触发视图更新,也有可能触发用户的某个回调函数等。
  • 什么是响应式

    我们先来看个例子:

<div id="app">     
    <div>Price :¥{{ price }}</div>    
    <div>Total:¥{{ price * num }}</div>     
    <div>Taxes: ¥{{ totalPrice }}</div>     
    <button @click="changePrice">改变价格</button> 
</div> 
var app = new Vue({   
    el: '#app',   
    data() {     
        return {       
            price: 5.0,       
            num: 2     
        };   
    },  

    computed: {     
        totalPrice() {       
            return this.price * this.num * 1.03;    
        }   
    },   

    methods: {     
        changePrice() {       
            this.price = 10;     
        }   
    } 
})

视图: image.png

  • 上例中当price 发生变化的时候,Vue就知道自己需要做三件事

    • 更新页面上price的值
    • 计算表达式 price * num的值,更新页面
    • 调用totalPrice函数,更新页面
  • 数据发生变化后,会重新对页面渲染,这就是Vue响应式 !

  • 想完成这个过程,我们需要:

    1. 侦测数据的变化
    2. 收集视图依赖了哪些数据
    3. 数据变化时,自动通知需要更新的视图部分,并进行更新
  • 对于专业术语分别是:

    • 数据劫持/数据代理
    • 依赖收集
    • 发布订阅