Vue响应式原理解析

Vue的响应式由ObserverDepWatcher三个核心部分组成。

这里我把Vue的响应式分为三个阶段 数据劫持 依赖收集 派发更新

我们先准备好需要用的工具函数

js 复制代码
export function def( obj, key, value, enumerable){
    Object.defineProperty( obj, key,{
        value,
        enumerable,
        writable:true,
        configurable:true
    })
}

数据劫持

通俗的来说就为属性转为响应式 我们需要一个observe函数,注意与Observer类区别

js 复制代码
//observe函数 判断一个值是否需要转为响应式,并触发 Observer 的创建。
export default function observe(value){ 
    //如果value不是对象,则什么都不做
    if(typeof value !== 'object') return 
    //如果value是对象,则new一个Observer对象
    var ob
             if(typeof value.__ob__ !== 'undefined'){  //存在__ob__代表他已经被响应式处理过了
       ob = value.__ob__
     }else{
      
       ob = new Observer(value)
     }
     return ob
  }

除此之外我们还需要一个方法用来真正调用Object.defineproperty 这个方法就是defineReactive

js 复制代码
import observe from './observe'

export default function defineReactive(data, key, value){
    
    if(arguments.length == 2) {
        value = data[key]
    }
    // 子元素要进行observe  
    let childOb = observe(value)
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get(){
            console.log("🚀 您正在访问" + key)   
            return value
        },
        set(newValue){
            console.log("🚀 您正在设置" + key + '=' + newValue)
            if(newValue === value) return
            value = newValue
            // 新值也要观察
            childOb = observe(newValue)
        }
    })
}
js 复制代码
let obj = {
  a:4,
  b:{
    m:1,
    d:{
      n:2
    }
  }
}

observe(obj)
console.log("🚀 ~ obj:", obj)
obj.b.d.n = 3

也就是说我们已经成功的为obj的属性包括子对象都完成了响应式处理 每个__ob__都是Observer类的实例

这里给出Observer类的代码

文章开头给出的工具函数def此时就派上用场了,我们通过def 将__ob__设置为不可枚举

js 复制代码
export default class Observer{
    constructor(data){
        // 保存数据引用
        this.data = data
        // 给数据添加__ob__属性,值为this,表示数据已经被观察,并且这个属性不可枚举 所以我们通过工具函数def来做
        // this 代表创建的实例
        def(data,'__ob__',this, false)
            this.walk(data)
    }

    // 添加walk方法
    walk(data) {
        Object.keys(data).forEach(key => {
            defineReactive(data, key, data[key])
        })
    }

}

对数组的处理

由于Object.defineProperty 对数组不能够很好的监听比如说 a[1] = 10 这种方式不能够监听到,所以我们需要对数组特殊处理 我们将对数组的处理放在另一个js文件里

js 复制代码
array.js
//数组检测不到 需要改写方法
import { def } from "./utils"
//保存一下原型
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
const methodsNeedChange = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]
methodsNeedChange.forEach(method => {

   const original = arrayPrototype[method]
   def( arrayMethods, method, function(...args){
    const result = original.apply(this, args) //先保存一下执行结果
    const ob = this.__ob__
     let inserted = []
     //push,unshfit splice func  can  add or delete element of array ,need new element to observe
     //实际上这里我们只针对 push splice unshfit进行特殊处理
     switch(method){
        case 'push':
        case 'unshift':
            inserted = args
            break
        case 'splice':
           // splice(下标,删除的数量,插入的项)
           //所以 从2 开始 获取插入的项
          inserted = args.slice(2)
          break;
     }
     //判断有没有要插入的项 ,有的话也要变成响应式
     if(inserted.length) {
        ob.observeArray(inserted)
     }
    //obj.b.push(4)
    //这里this取决于 也就是数组
    console.log('啦啦啦')
     return result
   },false)
})

除此之外Observer类也要做出改变

js 复制代码
export default class Observer{
    constructor(data){
        //每个Observer实例都有一个dep
        this.dep = new Dep()
        // 保存数据引用
        this.data = data
        // 给数据添加__ob__属性,值为this,表示数据已经被观察,并且这个属性不可枚举 所以我们通过工具函数def来做
        // this 代表创建的实例
        def(data,'__ob__',this, false)

        // 遍历对象的所有属性,使其变为响应式
        if(Array.isArray(data)){
            //如果是数组,则重写数组方法
            Object.setPrototypeOf(data,arrayMethods) 
            this.observeArray(data)  //代理数组
        }else {
            this.walk(data)
        }
        
    }

    // 建议添加walk方法
    walk(data) {
        Object.keys(data).forEach(key => {
            defineReactive(data, key, data[key])
        })
    }
    observeArray(data){
        data.forEach(item => {
            observe(item)
        })
    }
}

总结

通过observe判断数据是否需要转为响应式,并触发 Observer 类的创建 ,Observer类 的walk方法遍历对象属性,调用defineReactive转为响应式。defineReactive就是利用Object.defineProperty 定义属性的 getter/setter , 为了处理递归子对象 defineReactive也会调用一次observe 这样就形成了 三个函数互相调用完成 数据劫持。

针对数组的特殊处理,重写 push unshift splice。在重写的数组方法中,先调用原始方法如果有新元素插入,使用 observe() 将其转换为响应式调用 dep.notify() 通知视图更新

依赖收集

每个响应式属性对应一个 Dep 实例,负责管理依赖(即 Watcher)。只有Watcher 触发的 getter 才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher 收集到Dep

这里给出Dep类代码

我们用一个数组来存储订阅者watcher 另外这个Dep.target实际上是一个全局变量,当然你也可以用window.target 这里不影响

js 复制代码
var uid = 0
export default class Dep {
    constructor(){
       console.log('Dep构造器');
       //存储自己的订阅者也就是watcher实例
       this.id = uid++
       this.subs = []
    }
    //添加订阅者
    addSub(watcher){
        this.subs.push(watcher)
    }
    depend(){
        // Dep.target 是一个全局变量,用于存储当前正在执行的watcher实例
        if(Dep.target){
            console.log("🚀 ~ Dep ~ depend ~ Dep.target:", Dep.target,this)
            // this.addSub(Dep.target)
            Dep.target.addDep(this)
        }
    }
    //通知所有订阅者更新
    notify(){
        console.log('通知所有订阅者更新');
        const subs = this.subs.slice()
        for(let i = 0; i < subs.length; i++) {
            subs[i].update()
        }
    }
}

// 添加targetStack来处理嵌套的Watcher
Dep.target = null
const targetStack = []

export function pushTarget(target) {
    targetStack.push(target)
    Dep.target = target
}

export function popTarget() {
    targetStack.pop()
    Dep.target = targetStack[targetStack.length - 1]
}

Watcher类代码 这里我们需要一个工具函数

js 复制代码
//得到对象的某个属性的值
function parsePath(str) {
    const segments = str.split('.')
    return (obj) => {
        for(let i = 0; i < segments.length; i++) {
            if(!obj) return
            obj = obj[segments[i]]
        }
        return obj
    }
}
//比如一个对象 { a: { b: { c: 1 } } }
//parsePath('a.b.c') 返回一个函数,这个函数接收一个对象,返回对象的c属性
//parsePath('a.b') 返回一个函数,这个函数接收一个对象,返回对象的b属性
//parsePath('a') 返回一个函数,这个函数接收一个对象,返回对象的a属性

我们可以把watcher想象成监听器watch

js 复制代码
let obj = {
            a:1,
            b:{
              c:5
            }
          }
 new Watcher( obj, 'a.b.c', function(){
   console.log('当c的值发生变化的时候,触发该回调函数')
 })
js 复制代码
import Dep, { pushTarget, popTarget } from './Dep'

var uid = 0
export default class Watcher {
    constructor(target, expression, callback){
        console.log('Watcher构造器');
        //通过target 配合expression 就可以获取watcher依赖的数据
        this.id = uid++
        this.target = target
        this.getter = parsePath(expression)
        this.callback = callback
        this.value = this.get() //watcher初始化就会读取值
    }

    addDep(dep) {
        dep.addSub(this)
    }

    update(){
        this.run()
    }

    get(){
        pushTarget(this)   // 将当前watcher实例压入栈
        const obj = this.target
        var value 
        try {
            value = this.getter(obj)  //这里watcher读取值 触发 defineReactive中的getter 也就是我们在defineReative中就可以收集依赖了
        } finally {
            popTarget()    // 恢复上一个watcher
        }
        return value
    }

    run() {
        this.getAndInvoke(this.callback)
    }

    getAndInvoke(cb){
        const value = this.get()
        if(value !== this.value || typeof value === 'object'){
           //新的值 与旧值不一致 就会触发回调
            const oldValue = this.value
            this.value = value
            cb.call(this.target, value, oldValue) 
        }
    }
}

  

这里给出defineReactive完整代码

js 复制代码
import observe from './observe'
import Dep from './Dep'

export default function defineReactive(data, key, value){
    const dep = new Dep()  //这里new一个Dep 我认为是为了方便下面调用方法
    
    if(arguments.length == 2) {
        value = data[key]
    }

    // 子元素要进行observe
    let childOb = observe(value)

    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get(){
            console.log("🚀 您正在访问" + key)   
            if(Dep.target){    //当watcher初始化就会触发getter,这里Dep.target就存储了当前的watcher实例
                dep.depend()
                if(childOb){
                    childOb.dep.depend()
                    // 如果是数组,需要对每个元素进行依赖收集
                    if(Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            return value
        },
        set(newValue){
            console.log("🚀 您正在设置" + key + '=' + newValue)
            if(newValue === value) return
            value = newValue
            // 新值也要观察
            childOb = observe(newValue)
            // 发布订阅模式
            dep.notify()
        }
    })
}

// 处理数组的依赖收集
function dependArray(array) {
    for(let e of array) {
        e && e.__ob__ && e.__ob__.dep.depend()
        if(Array.isArray(e)) {
            dependArray(e)
        }
    }
}

派发更新

修改属性值时,setter 会调用 dep.notify(),通知所有订阅的 Watcher 执行 update()

相关推荐
百万蹄蹄向前冲18 分钟前
组建百万前端梦之队-计算机大学生竞赛发展蓝图
前端·vue.js·掘金社区
云隙阳光i31 分钟前
实现手机手势签字功能
前端·javascript·vue.js
imkaifan1 小时前
vue2升级Vue3--native、对inheritAttrs作用做以解释、声明的prop属性和未声明prop的属性
前端·vue.js·native修饰符·inheritattrs作用·声明的prop属性·未声明prop的属性
小程序设计1 小时前
【2025】基于springboot+vue的宠物领养管理系统(源码、万字文档、图文修改、调试答疑)
vue.js·spring boot·宠物
小程序设计1 小时前
【2025】基于springboot+vue的体育场馆预约管理系统(源码、万字文档、图文修改、调试答疑)
vue.js·spring boot·后端
柠檬树^-^2 小时前
app.config.globalProperties
前端·javascript·vue.js
1024小神2 小时前
vue/react/vite前端项目打包的时候加上时间最简单版本,防止后端扯皮
前端·vue.js·react.js
轻口味3 小时前
Vue.js 与 RESTful API 集成之使用 Axios 请求数据
前端·vue.js·restful
程序员SKY4 小时前
Vue 系列之:ref、reactive、toRef、toRefs
vue.js
奶糖 肥晨4 小时前
基于 Vue 和 Element Plus 的时间范围控制与数据展示
前端·vue.js·elementui