> 文章列表 > 【Vue2源码】响应式原理

【Vue2源码】响应式原理

【Vue2源码】响应式原理

【Vue2源码】响应式原理

文章目录

  • 【Vue2源码】响应式原理
    • `Vue响应式`的核心设计思路
    • 整体流程
    • 响应式中的关键角色
    • 检测变化注意事项
    • 响应式原理
        • 数据观测
        • 重写数组7个变异方法
        • 增加__ob__属性
          • __ob__有两大用处:

Vue.js 基本上遵循 MVVM(Model–View–ViewModel)架构模式,数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。 本文讲解一下 Vue 响应式系统的底层细节。

Vue响应式的核心设计思路

当创建Vue实例时,vue会遍历data选项的属性,利用Object.defineProperty为属性添加gettersetter对数据的读取进行劫持(getter用来依赖收集,setter用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。

每个组件实例会有相应的watcher实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有computed watcher,user watcher实例),之后依赖项被改动时,setter方法会通知依赖与此datawatcher实例重新计算(派发更新),从而使它关联的组件重新渲染。

整体流程

作为一个前端的MVVM框架,Vue的基本思路和AngularReact并无二致,其核心就在于: 当数据变化时,自动去刷新页面DOM,这使得我们能从繁琐的DOM操作中解放出来,从而专心地去处理业务逻辑。

这就是Vue的数据双向绑定(又称响应式原理)。数据双向绑定是Vue最独特的特性之一。此处我们用官方的一张流程图来简要地说明一下Vue响应式系统的整个流程:

在这里插入图片描述

Vue中,每个组件实例都有相应的watcher实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。

这是一个典型的观察者模式。

响应式中的关键角色

在 Vue 数据双向绑定的实现逻辑里,有这样三个关键角色:

  • Observer: 它的作用是给对象的属性添加gettersetter,用于依赖收集和派发更新
  • Dep: 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个Dep实例(里面subsWatcher实例数组),当数据有变更时,会通过dep.notify()通知各个watcher
  • Watcher: 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

检测变化注意事项

Vue 2.0中,是基于·Object.defineProperty 实现的响应式系统 (这个方法是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因) vue3 中,是基于Proxy/Reflect来实现的

1、由于 JavaScript 的限制,这个 Object.defineProperty() api 没办法监听数组长度的变化,也不能检测数组和对象的新增变化。

2、Vue 无法检测通过数组索引直接改变数组项的操作,这不是 Object.defineProperty() api 的原因,而是尤大认为性能消耗与带来的用户体验不成正比。对数组进行响应式检测会带来很大的性能消耗,因为数组项可能会大,比如1000条、10000条。

响应式原理

响应式基本原理就是,在 Vue 的构造函数中,对 options 的 data 进行处理。即在初始化vue实例的时候,对data、props等对象的每一个属性都通过 Object.defineProperty 定义一次,在数据被set的时候,做一些操作,改变相应的视图。

数据观测

基于 Object.defineProperty 来实现一下对数组和对象的劫持。

\\src\\observe\\index.js

import { newArrayProto } from "./array"class Observe {constructor (data) {//Object.defineReactive只能劫持已经存在的属性(vue俩民回为此单独写一些api)//data.__ob__ = this  //这里的this指的是Observe,把这个实例附到了ob上,还可以用于检测是否被劫持过Object.defineProperty(data,'__ob__',{value:this,enumerable:false //将__ob__编程不可枚举,这样循环的时候就无法获取到,不会进入死循环})if(Array.isArray(data)) {data.__proto__ = newArrayProtothis.observeArray(data)  //如果数组中放置的是对象,也可以被监控到//这里我们可以重写数组中的方法,7个变异方法,是可以修改到数组本身的}else {this.walk(data)}}walk (data) { //循环对象,对属性依次劫持//“重新定义”属性  性能差Object.keys(data).forEach(key => defineReactive(data, key, data[key]))}observeArray(data) {data.forEach(item=> observe(item))}
}export function defineReactive (target, key, value) {  //闭包observe(value) //对所有的对象都进行属性劫持  深度劫持Object.defineProperty(target, key, {get () { //取值的时候会执行getconsole.log(key,"key");return value},set (newValue) {  //修改的时候会执行setif (newValue === value) returnobserve(newValue)value = newValue}})
}export function observe (data) {if (typeof data !== 'object' || data == null) {//只对对象进行劫持return}if(data.__ob__ instanceof Observe) { //如果存在data.__ob__就说明这个被代理过了return data.__ob__}//如果一个对象被劫持过了,那就不需要再被劫持了//要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过return new Observe(data)}

重写数组7个变异方法

7个方法是指:push、pop、shift、unshift、sort、reverse、splice。(这七个都是会改变原数组的) 实现思路:面向切片编程!!!

不是直接粗暴重写 Array.prototype 上的方法,而是通过原型链继承与函数劫持进行的移花接木。

利用 Object.create(Array.prototype) 生成一个新的对象 newArrayProto,该对象的 proto 指向 Array.prototype,然后将我们数组的 proto 指向拥有重写方法的新对象 newArrayProto,这样就保证了 newArrayProto 和 Array.prototype 都在数组的原型链上。

arr.proto === newArrayProto;newArrayProto.proto === Array.prototype

然后在重写方法的内部使用 Array.prototype.push.call 调用原来的方法,并对新增数据进行劫持观测。
\\src\\observe\\array.js


let oldArrayProto = Array.prototype  //获取数组的原型export let newArrayProto = Object.create(oldArrayProto)let methods = [ //通过遍历寻找到所有变异方法'push','pop','shift','reverse','sout','splice'
] //concat slice不会改变原数组methods.forEach(method => {newArrayProto[method] = function (...args) { //这里重写了数组的方法const result = oldArrayProto[method].call(this,...args)  //再内部调用原来的方法,函数的劫持,切片编程//我们需要对新增的数据进行劫持let insertedlet ob = this.__ob__switch (method) {case 'push':case 'unshift':inserted = argsbreak;case 'splice' : //arr.splice(0,1,{a:1},{b:2})inserted = argsbreak}console.log("xinzeng ");if(inserted) {//对新增的内容再次进行观测ob.observeArray(inserted)}return result}
})

增加__ob__属性

这是一个恶心又巧妙的属性,我们在 Observer 类内部,把 this 实例添加到了响应式数据上。相当于给所有响应式数据增加了一个标识,并且可以在响应式数据上获取 Observer 实例上的方法

class Observe {constructor (data) {//Object.defineReactive只能劫持已经存在的属性(vue俩民回为此单独写一些api)//data.__ob__ = this  //这里的this指的是Observe,把这个实例附到了ob上,还可以用于检测是否被劫持过Object.defineProperty(data,'__ob__',{value:this,enumerable:false //将__ob__编程不可枚举,这样循环的时候就无法获取到,不会进入死循环})if(Array.isArray(data)) {data.__proto__ = newArrayProtothis.observeArray(data)  //如果数组中放置的是对象,也可以被监控到//这里我们可以重写数组中的方法,7个变异方法,是可以修改到数组本身的}else {this.walk(data)}}walk (data) { //循环对象,对属性依次劫持//“重新定义”属性  性能差Object.keys(data).forEach(key => defineReactive(data, key, data[key]))}observeArray(data) {data.forEach(item=> observe(item))}
}
__ob__有两大用处:

1、如果一个对象被劫持过了,那就不需要再被劫持了,要判断一个对象是否被劫持过,可以通过 ob 来判断


export function observe (data) {if (typeof data !== 'object' || data == null) {//只对对象进行劫持return}if (data.__ob__ instanceof Observe) { //如果存在data.__ob__就说明这个被代理过了return data.__ob__}//如果一个对象被劫持过了,那就不需要再被劫持了//要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过return new Observe(data)}

2、我们重写了数组的7个变异方法,其中 push、unshift、splice 这三个方法会给数组新增成员。此时需要对新增的成员再次进行观测,可以通过 ob 调用 Observer 实例上的 observeArray 方法