【Vue2源码】响应式原理

【Vue2源码】响应式原理


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__ = newArrayProto
            this.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 () {
    
     //取值的时候会执行get
            console.log(key,"key");
            return value
        },
        set (newValue) {
    
      //修改的时候会执行set
            if (newValue === value) return
            observe(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 inserted
        let ob = this.__ob__
        switch (method) {
    
    
            case 'push':
            case 'unshift':
                inserted = args
                break;
            case 'splice' : //arr.splice(0,1,{a:1},{b:2})
                inserted = args
                break

        }
        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__ = newArrayProto

            this.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 方法

猜你喜欢

转载自blog.csdn.net/m0_63779088/article/details/130273962