浅析VUE的变化侦测原理

背景介绍

最近部门的前端框架开始从老旧的Angular JS向VUE迁移,为了工作起来能够更加顺手,特意花了些时间,学习了一番vue。本文对vue的变化侦测原理,做了一些简单的介绍。由于实践经验有限,本文若有错漏,还希望大家多多指正。

MVVM模式

在介绍vue之前,先简单介绍一下MVVM模式:
MVVM 源自于经典的 Model–View–Controller(MVC)模式。MVVM 的出现促进了 GUI 前端开发与后端业务逻辑的分离,极大地提高了前端开发效率。MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用。如下图所示:
在这里插入图片描述
vue就是一款基于MVVM设计模式的渐进式框架。笔者认为,MVVM模式相对于传统的前端,带来最大的改变,就是双向数据绑定了,也就是说,只要只要数据model或者前面页面view,任意一方改变,另一方也会跟着改变。VUE最独特的特性之一就是这看起来并不显眼的响应式系统。
从状态生成DOM,再输出到用户界面展示的一整套流程叫渲染,应用在运行时会不断的重新渲染,而如何确定哪些部分需要重新渲染,这就是变化侦测的作用了。

对比react与angular

变化侦测分为两种类型:一种是“推”,另一种是“拉”。
Angular和React中的变化侦测都属于“拉”,这就是说,状态变化时,它不知道哪些状态变了,只知道状态可能已经改变,然后发送信号给框架,然后框架会进行一个暴力对比找出哪些DOM节点需要重新渲染。这在Angular中是脏检查的流程,在React中使用的是虚拟DOM。
Vue的是属于“推”。在VUE中,每个状态绑定多个依赖,每个依赖表示一个具体的DOM节点,当状态改变时,向此状态的所有依赖发送通知,更新DOM。所以当状态改变时,框架立刻就知道了,而且一定程度上知道是哪些状态改变了,从而vue相对react和angular可以进行更细粒度的更新。
但它也有一定的代价,粒度越细,依赖追踪所花开销越大。因此,vue从2.0开始也引入了虚拟DOM,将粒度与开销做了一个折中处理。

变化侦测的实现

Vue是采用数据劫持结合发布者-订阅者模式的方式来实现的变化侦测。

下面,我们看下vue变化侦测的实现思路:

  1. 首先实现一个Observer数据监听器,能够对数据对象(data)的所有属性进行监听
  2. 在修改时,将依赖存放在一个全局的dep管理器中,
  3. 依赖修改后,由dep管理器通知中介watcher,由watcher去通知外界。
    在这里插入图片描述

observer数据监听实现

在js中,侦测一个对象的变化有两种方式:object.defineProperty和ES6的Proxy,在vue2.6之前都是采用的object.defineProperty。vue2.6之前都是使用的object.defineProperty。下面我们用object.defineProperty来实现数据监听。

下面的这段代码中,首先定义一个 defineReactive 函数,在用 Object.definePorperty 遍历对象的对象属性时,设置 get 和 set方法;当对象的属性被读取时触发 get,对象的属性被设置时触发 set。这样就完成了对 data 的数据劫持:

function defineReactive(data,key,val){
  Object.definePorperty(data,key,{
    enumerable: true,
    configurable: true,
    get:function(){
      return val;
    }
    set :function (newVal){
      if(val === newVal){return}
      val = newVal;
    }
  })
}

但是我们发现defineReactive 这个函数只能将object的某一个属性转换为 get/set 的形式,所以我们需要一个Observer 观察者用来帮助递归的侦测所有的 key。下面代码中,walk方法就是负责将object的每个属性转为get/set的形式。

class Observer{
  constructor(value){
    this.value = value
  }
  if(!Array.isArry(value)){
    this.walk(value)
  }
  walk(obj){
    const keys = Object.keys(obj)
    for(let i = 0; i < keys.length ;i++){
      defineReactive(data,keys[i],obj[keys[i])
    }
  }
}

dep依赖收集

我们创建一个 Dep 依赖管理类,在 get 中收集依赖,在 set 中新增依赖

class Dep{
  constructor(){
    this.arr = []
  }
  addSub(sub){
    this.arr.push(sub)
  }
   removeSub(sub){
    remove(this.arr,sub)
  }
  depend(){
    if(window.target){
      this.addSub(window.target)
    }
  }
  notify(){
    const arrs = this.arr.slice();
    for(let i = 0; i< arrs.lenth ;i ++){
      arrs[i].update();
    }
  }
 
}
 
function defineReactive(data,key,val){
 
  let dep = new Dep()
  Object.definePorperty(data,key,{
    enumerable: true,
    configurable: true,
    get:function(){
    // 收集依赖
      dep.depend(); 
      return val;
    }
    set :function (newVal){
      if(val === newVal){return}
      val = newVal;
      // 新增依赖
      dep.notify(); 
    }
  })
}

Watcher

依赖收集完成之后,我们定义一个中介订阅者 Watcher,每次收集完成都通知它,它再与外界交互。Watcher每次触发 get 的时候都将 dep 指向自己,这样就可以收集到依赖;每次 set 的时候都循环调用 Watcher 的 update 方法。

class Watcher{
  constructor(vm,exp,cb){
    this.vm = vm;
    this.cb = cb;
    this.exp = exp;
    this.value = this.get();
  }
  get(){
    Dep.target = this;  // 将当前订阅者指向自己
    var value = this.vm[exp];  // 触发getter,添加自己到属性订阅器中
    Dep.target = null;  // 添加完毕,重置
    return value;
  }
  update(){
    const oldVal = this.value;
    this.value = this.get();
    this.cb.call(this.vm,this.value,oldVal)
  }
}

当 Vue 实例挂载好之后,模板都会绑定一个 Watcher,谁的属性发生变化了就会通知响应的 Watcher,Watcher 再去通知编译器 Compile 进行视图更新
Vue的变化侦测原理,大致就是这样。

结语

前面了解到了,数据变化是通过get/set来追踪的,也正是由于这种方式,有些语法中,即便数据变化,vue也追踪不到,比如,向object添加属性:

methods:{
action(){
this.obj.name = '李四'
  }
}

上面的这种写法,是不会导致页面重新渲染的。为了解决这个问题,vue提供了vm.$setvm. $delete,千万注意不要踩坑QAQ

参考书籍:
《深入浅出Vue.js》

猜你喜欢

转载自blog.csdn.net/vipshop_fin_dev/article/details/106457414