Vue 进阶系列丨Object 的变化侦测

  • Post author:
  • Post category:vue


Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!


2013年7月28日,尤雨溪第一次在 GItHub 上为 Vue.js 提交代码;2015年10月26日,Vue.js 1.0.0版本发布;2016年10月1日,Vue.js 2.0发布。

最早的 Vue.js 只做视图层,没有路由, 没有状态管理,也没有官方的构建工具,只有一个库,放到网页里就可以直接用了。

后来,Vue.js 慢慢开始加入了一些官方的辅助工具,比如路由(Router)、状态管理方案(Vuex)和构建工具(Vue-cli)等。此时,Vue.js 的定位是:The Progressive Framework。翻译成中文,就是渐进式框架。

Vue.js2.0 引入了很多特性,比如虚拟 DOM,支持 JSX 和 TypeScript,支持流式服务端渲染,提供了跨平台的能力等。Vue.js 在国内的用户有阿里巴巴、百度、腾讯、新浪、网易、滴滴出行、360、美团等等。

Vue 已是一名前端工程师必备的技能,现在就让我们开始深入学习 Vue.js 内部的核心技术原理吧!



什么是渐进式框架

首先来解答一下,什么是渐进式框架。前言已经讲到,Vue2.0 引入了很多特性,比如 Router,Vuex 和 Vue-cli。所谓渐进式框架,就是把框架分层。

最核心的部分是声明式渲染,然后往外是组件系统(组件机制),客户端路由器(路由机制-Router),大规模的状态管理(Vuex),最外层是构建工具(Vue-cli),如下图所示。


也就是说,你既可以只用最核心的声明式渲染部分来开发项目,也可以在最核心的基础上逐步增加其他特性,如使用声明式的渲染和组件系统来开发项目,当然你也可以使用一整套 Vue.js 全家桶来开发项目。Vue.js 拥有足够的灵活性供用户选择。



什么是变化侦测

从状态生成DOM,再输出到用户界面展示的一整套流程叫做渲染,应用在运行时会不断地进行渲染。

对于响应式框架来说,变化侦测是其重要的组成,可以监听状态的变化,然后通知视图层进行相应的更新,从而达到响应式的效果。

对于 Angular 和 React 来说,状态发生了改变,框架并不知道哪个状态变化了,只知道有状态变了,然后框架会进行暴力比对来找出哪个状态变化,从而进行 DOM 节点重新渲染。

在 Vue.js 中,当一个状态发生了变化,Vue.js 立刻就知道了,并且还知道是哪个状态发生了变化,进而在该状态绑定的依赖中进行比对,最后找到需要渲染的 DOM 节点重新渲染。

所谓状态绑定的依赖,可以这么理解,一个状态的会引起一些 DOM 节点的重新渲染,那么这些 DOM 节点就是这个状态所绑定的依赖。



Object.defineProperty 和 Proxy 的区别

Object.defineProperty 用于监听对象的数据变化,无法监听属性的新增删除;无法监听数组变化;只能劫持对象的属性,当对象的属性值也是对象时,那么需要深度遍历,对属性值继续监听。

Proxy 可以理解为在被劫持的对象之前加了一层拦截,Proxy 返回的是一个新对象, 可以通过操作返回的新的对象达到监听目的,可以直接监听整个对象,而非属性;可以监听数组变化。有关 Proxy 的详细讲解,请移步

ES6 新特性梳理系列丨Proxy 和 Reflect



变化的追踪

由于 Object 和 Array 的变化侦测采用不同的处理方式,所以我们这篇文章将详细介绍Object 的变化侦测。

所谓变化的追踪,其实就是检测一个对象的某个属性的变化,在 JavaScript 中,有两种方式可以监听对象的变化,一种是 Object.defineProperty,另一种是ES6 的 Proxy。由于浏览器兼容性问题以及 ES6 的支持度不够,Vue2.0 采用的是 Object.defineProperty,后来,Vue3.0 采用的是 ES6 的 Proxy。本文旨在讲解 Vue2.0 的变化侦测。

那么 Vue2.0 是如何对状态的变化进行追踪的呢?知道了 Vue2.0 是根据Object.defineProperty 监测状态变化的,那么下面的代码相比你并不陌生。

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

Object.definProperty(obj,prop,desc) 的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性。

  • obj:要定义属性的对象。

  • prop:要定义或修改的属性的名称 。

  • desc:要定义或修改的属性描述符。

desc 描述符有以下几种:

  • confingurable:定义当前属性的描述符能否被改变。

  • enumerable:定义当前属性是否是可枚举的,也就是能否被for…in 所遍历出来。

  • value:当前属性对应的值。

  • writable:当前属性是否可以被改变。

  • getter:当访问该属性时,会调用此函数。该函数的返回值会被作为该属性的值。

  • setter:当前属性值被修改时,会调用此函数。

那么我们就可以从上面代码看出,是在监听 data 对象中的 key 属性的改变,如果外界访问该属性,则进入 getter 方法,返回当前值;如果外界修改当前属性,则进入 setter 方法,判断修改的值是否和之前值相同,不同再修改。



依赖的收集

我们先来解释什么是依赖?所谓状态绑定的依赖,可以这么理解,一个状态的变化会引起一些 DOM 节点的重新渲染,那么这些 DOM 节点就是这个状态所绑定的依赖。

通过 Object.definProperty 可以在 setter 方法中监听对象属性的改变,也就是监听状态的改变,我们要做的就是当状态发生改变时,可以通知那些绑定的依赖进行 DOM 节点的重新渲染。

显而易见,我们是在 getter中收集依赖,在 setter 中触发依赖,进行 DOM 的重新渲染。



依赖收集到了哪里

可以这么理解,每一个 key 都有一个依赖数组,我们可以对上面的代码进行改造。

function defineReactive(data,key,val){
  let arr = []  // 依赖保存的数组
  Object.defineProperty(data,key,{
    enumerable:true,
    configurable:true,
    get:function(){
      arr.push(依赖)
      return val
    },
    set:function(newVal){
      if(newVal == val){
        return
      }
      for(let i = 0;i<Arr.length;i++){
        // 循环依赖数组,触发收集到的依赖
        arr[i](newVal,val)  
      }
      val = newVal
    }
  })
}

我们新增了数组 arr,用来存储被收集的依赖,然后在 set 被触发时,循环依赖数组,触发收集到的依赖。



递归监听所有的属性

我们已经知道在哪里收集依赖和怎么触发依赖了,但是现在我们还只局限于对象的单层监听,如果对象的属性还是一个对象怎么办呢,我们希望把数据中的所有属性(包括子属性)都监听到,所以我们需要递归监听。

我们封装一个 Observer 类,该类的作用是将对象中的所有属性都转换成 getter、setter 的形式,去监听他们的变化。还封装一个 YiLai 类,帮助我们管理依赖。有关 Class 类的相关知识,请移步

ES6 新特性梳理系列丨Class

// Observer 类旨在监听对象的所有属性
class Observer{
  constructor(value){
    this.value = value
    // 判断是否是数组,不是则监听此对象的所有属性
    if(!Array.isArray(value)){
      this.jianting(value)
    }
  }
  jianting(obj){
    // 获取所有的key
    const keys = Object.keys(obj)
    // 循环键(key)的数组,监听当前对象所有属性
    for(let i = 0;i<keys.length;i++){
      defineReactive(obj,keys[i],obj[keys[i]])
    }
  }
}


// YiLai类旨在帮助我们管理依赖
class YiLai{
  constructor(){
    this.yilaiArray = []
  }
  addYiLai(yilai){
    this.yilaiArray.push(yilai)
  }
  jianting(){
    const yilais = this.yilaiArray.slice()
    for(let i = 0;i<yilais.length;i++){
      // 触发依赖,通知DOM节点重新渲染
    }
  }
}


function defineReactive(data,key,val){
  // 判断当前属性是否是对象,是则继续监听
  if(typeof val === 'object'){
    new Observer(val)
  }
  // 创建依赖管理实例
  let yilai = new YiLai()
  Object.defineProperty(data,key,{
    enumerable:true,
    configurable:true,
    get:function(){
      // 收集依赖
      yilai.addYiLai('依赖')
      return val
    },
    set:function(newVal){
      if(newVal == val){
        return
      }
      // 触发依赖
      yilai.jianting()
      val = newVal
    }
  })
}

在上面代码中,我们定义了 Observer 类,用来将一个正常的 object 转换成被监听的 object,然后判断数据类型,只有是 object,才会调用 jianting 方法将每一个属性转换成 getter、setter 的形式。

最后,在 defineReactive 中新增 new  Observer(val) 来递归子属性这样我们就可以把 data 中的所有属性都转换成了 getter、setter 的形式了。

当 data 的属性发生变化时,与这个属性对应的依赖就会被触发,这也就实现了对象的响应式。



关于Object的问题

由于 Object.defineProperty 方法自身原因,Vue2.0 无法监听到一个对象属性的新增和删除,为了解决这个问题,Vue.js 提供了两个API,vm.$set 和 vm.$delete,我们将会在后期为大家详细介绍。




Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!


叶阳辉


HFun 前端攻城狮



版权声明:本文为HFunTeam原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。