我总算搞懂了vue2响应式

前言

关于响应式原理的资料可谓是五花八门,我或多或少知道了几个单词,例如dep、watcher、observe等,同时我对这些概念比较的模糊,所以我结合源码和各种资料彻底的学习了一下响应式,这篇文章就是我对响应式的理解,明白是一回事,但是能不能写出来又是另一回事,我不保证我能把响应式的原理给写明白,但是我会把我的理解写出来,希望大家看完会有所收获。

本文实现一个简单的vue2响应式,变量命名与vue2源码保持统一,方便大家后续阅读源码。

我理解vue2的响应式一共分两步

  1. 遍历监听
  2. 收集依赖 派发更新

只要明白这两步在什么时候做的,做了什么响应式也就基本搞清楚了。

Object.defineProperty

在讲述这两步之前,我们要先了解一个api。

在data中的数据发生修改的时候,页面的内容也会随之修改,这个就是所谓的响应式。想要实现这个效果,我们首先需要对data对象里面的内容进行观察,一旦data里面的数据发生了改变我们第一时间发现,从而做出改变。

在vue2中是通过Object.defineProperty()这个api实现的。

Object.defineProperty()我们可以设置指定对象属性特性,如是否可以遍历,是否可修改,是否可删除等。它还提供了对象属性的get和set方法,当我们修改了对象的某个属性时会触发set方法,当读取某个属性时会触发get方法。

js 复制代码
const obj = {
  a:1
}

let _temp
Object.defineProperty(obj,'a',{
  get(){
    console.log('发出了get方法')
    return _temp
  },
  set(newValue){
    console.log('触发了set方法')
    _temp = newValue
  }
})

obj.a = 2 // 触发了set方法

console.log(obj.a) // 触发了get方法

注意这里需要一个中间变量_temp来承载属性值的变化,如果没有这个值直接对a本身进行修改,会导致死循环。

当我们对obj.a进行修改,触发set方法,然后set方法内部又触发set方法。。。。。

js 复制代码
const obj = {
  a:1
}

Object.defineProperty(obj,'a',{
  get(){
    console.log('发出了get方法')
    return obj.a
  },
  set(newValue){
    console.log('触发了set方法')
    obj.a = newValue
  }
})

obj.a = 2

这也就是vue2为什么会有defineReactive这个方法的val参数,其实就是上面_temp的作用。后面再详细说。

关于Object.defineProperty这个api就先介绍到这里,更加详细的介绍大家可以去mdn了解。

地址在这⬇

Object.defineProperty() mdn地址

遍历监视data的一举一动

vue里面会有一个data对象,完成响应式的第一步就是对data中的每一项都进行监视,也就是说需要对data对象中每个属性都设置Object.defineProperty

js 复制代码
var app = new Vue({
  el: '#demo',
  data: {
    name: '明明',
    sex: '男',
    age:18,
    obj:{
      c:'adfadsf'
    }
  }
})

在源码中有这些个方法和类,我现在列举一下。

  1. observe方法,观察对象有没有__ob__属性,如果有就返回__ob__属性,如果没有就调用Observer 类生成__ob__属性。(这个属性的作用是标识一个数据对象是否已经被观察过,并存储对应的观察者对象的引用)
  2. Observer 类用于为对象添加__ob__属性,并遍历对象对的每个属性,将其作为defineReactive方法的参数调用。
  3. defineReactive方法是Object.defineProperty的封装,对具体的属性进行监听。但data中的对象可能是多层嵌套的,这种情况可能在defineReactive方法中,还会调用observe对子对象进行逐个监听。

这个三个方法的调用为 observe中调用Observer类,Observer类中调用defineReactive方法,当data数据对象嵌套时defineReactive方法中还会调用observe,进行循环调用。

observe方法

先写一个入口,index.js 定义个obj对象,内有嵌套。然后把整个对象作为参数传入observe中

js 复制代码
import observe from './src/observe'
const obj = {
  name: '明明',
  sex: '男',
  age: 18,
  obj: {
    c: 'adfadsf'
  }
}

observe(obj)

observe做的事情非常的单纯,就是当对象身上没有__ob__的时候创建Observer实例。typeof data !== 'object'这句话是后面循环调用时的退出条件,如果传入的data是简单类型的时候直接退出,这里可以暂时忽略,因为刚开始data肯定是一个对象类型。

js 复制代码
import Observer from './Observer'
export default function observe(data) {
    if (typeof data !== 'object') {
        return
    }
    if (data.__ob__) {
        return data.__ob__
    } else {
        return new Observer(data)
    }

}

Observer类

当创建Observer类时,触发构造函数,直接通过def函数对传入的对象添加__ob__属性值为Observer的实例,作为标识,标志该对象已经被观察过了。

walk函数为Observer类的核心方法,对对象中的属性逐个进行defineReactive的调用。对属性进行监视,添加get和set方法。

js 复制代码
import { def } from "./utils";
import defineReactive from "./defineReactive";
class Observer {
    constructor(value) {
        def(value, '__ob__', this, false)
        if (Array.isArray(value)) {
           // 本例中忽略数组 
        } else {
            this.walk(value)
        }
    }

    walk(value) {
        for (let i in value) {
            defineReactive(value, i)
        }
    }
}

export default Observer

def方法,是对Object.defineProperty的一个简单封装,方便我们在一个对象上添加某个属性,并对其特性进行设置。

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

defineReactive

这个方法是响应式中比较核心的方法,上文中提过,Object.defineProperty需要一个中间变量进行值的中转,否则会造成死循环,这里的val本质上起到了一个中间变量的作用,运用闭包为对象中的每个属性都生成一个中间值。

js 复制代码
import observe from './observe'
export default function defineReactive(obj, key, val) {
    val = val || obj[key]
    observe(val) // 如果对象中的属性为对象则继续调用observer为子对象中的属性添加get、set方法
    Object.defineProperty(obj, key, {
        set(value) {
            val = value
        },
        get() {
            return val
        }
    })
}

到这里就完成了监视data的一举一动,data中所有的属性都被添加上了get和set方法,一旦读取或赋值,我们都可以对其进行操作。

到这里为止应该是比较好理解的,就是给data中的所有属性添加get和set方法,如果涉及到对象嵌套,那就对子对象再调用一次observe,继续为子对象的属性添加get和set方法。比较难理解的可能就是循环调用这部分了。

依赖收集和派发更新

在get和set中具体做了什么我们现在还没有写,这块应该也是比较核心的代码。这块就涉及到依赖收集和派发跟更新了,也是比较难理解的一部分。

下面我将用比较长的一段文字描述一下响应式做了什么。

Vue在页面初始化的时候通过observe,对每个data中的每个属性都添加个get和set方法。随后vue会读取用户的计算属性,监听器方法,页面上的data属性。分别生成计算属性watcher用户监听器watcher渲染watcher

当这些watcher在实例化的时候会触发对应属性的get函数,这时候会有一个数组把watcher实例记录下来,我们管这个数组叫dep

举个例子 data中有name属性,这个name属性在页面中用到了,在计算属性中也用到了,页面初始化的时候就会生成两个关于name属性的watcher实例,分别是计算属性watcher和渲染watcher。这两个watcher是分别实例化的,在初始化计算属性时会实例化一个计算属性watcher,实例化的过程中会触发name的get方法,会把计算属性watcher的实例化后的对象添加到dep数组中,组件渲染的时候会实例化渲染watcher,这时候又会触发name的get函数,此时又会把watcher的实例添加到dep数组中,此时dep数组中就有了两个watcher实例。这个过程我们称作依赖收集

随后我们可能在代码中对name属性的值进行了修改,触发name 的set方法。这时候会把dep数组中的元素都遍历一遍执行watcher的update()方法,触发watcher的回调函数,执行相应的逻辑,这就是派发更新。例如渲染watcher的回调函数就是更新页面,监听器watcher的回调函数就是执行函数内的逻辑。

执行watcher的update()的时候是有顺序的会先触发计算属性然后是监听器的逻辑,最后才是页面渲染。为的可能是先更新数据,最后才渲染页面。

知道了vue在get和set中具体做了什么我们就先补全一下get和set中的逻辑

js 复制代码
import Dep from './Dep'
import observe from './observe'
export default function defineReactive(obj, key, val) {
    val = val || obj[key]
    const dep = new Dep()
    observe(val)
    Object.defineProperty(obj, key, {
        set(value) {
            if (val === value) {
                return;
            }
            val = value
            dep.noitfy()

        },
        get() {
            dep.depend()
            return val
        }
    })
}

Dep类的作用就是管理和存储watcher实例,它内部有一个数组,调用depend方法就是往数组里push一个watcher实例,即收集依赖。noitfy就是遍历数组挨个调用watcher的update方法,即派发更新

Watcher

在写Watcher的代码之前先明确一下,它应该是怎么被调用的。根据调用的形式进行编写更容易明白入参到底是是什么,在vue2中的Watcher类的调用方法,其实和watch监听器类似,监听某个对象的某个属性,回调函数。对象是字符串类型可以是'c.d.e.f'这种类型。

js 复制代码
const w1 = new Watcher(obj, 'a', (newValue, oldValue) => {
  console.log(newValue, oldValue, 'w1')
})

const w2 = new Watcher(obj, 'c.d.e.f', (newValue, oldValue) => {
  console.log(newValue, oldValue, 'w2')
})

为什么要把watcher的实例放到Dep.target上?因为watcher和dep在两个类中,无法共享数据,watcher在实例的过程中会触发被监听属性的get方法,get方法中回到用dep.depend()进行依赖收集,收集的对象就是watcher实例,也就是Dep.target上的this对象,Dep.target在这里是作为一个全局变量存在的,可以把Dep.target看作window.target,这样在Dep类中进行依赖收集时就可以根据Dep.target获取到watcher实例了。

js 复制代码
import Dep from './Dep'
class Watcher {
    constructor(target, expression, callback) {
        // 就是保存监听器的回调函数
        this.callback = callback 
        // 保存监听的对象
        this.target = target  
        // parsePath用于解析类似 c.d.e.f的值 getter方法用于获取newVal
        this.getter = parsePath(expression) 
         // 存储监听属性的初始值也就是oldValue,并且在内部访问被监听属性触发get方法
        this.value = this.get()
    }
    get() {
        // 将watcher的实例挂在到Dep上,这里的Dep是一个全局变量,和挂在到window上效果相同
        // this就是被监听属性的watcher实例,它将作为依赖被Dep类收集。
        Dep.target = this 
        const obj = this.target 
        let value
        try {
        // getter内部会触发监听属性的get方法,get中会进行依赖收集,调用Dep.depend()
        // depend方法会把Dep.target上的watcher实例推到内部的数组中完成依赖收集
            value = this.getter(obj) 
        } finally {
            // 执行到这里时,已经完成了依赖收集Dep.target的值已经被添加到Dep的数组中
            // 这时Dep.target已无意义故清除掉。
            Dep.target = null
        }
        return value
    }

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

    update() {
        this.run()
    }

    run() {
        const value = this.getter(this.target)
        // 如果新旧值不相同,就调用传入的回调函数。
        if (this.value !== value || typeof value === 'object') {
            this.callback.call(this.target, value, this.value)
        }
    }
}

function parsePath(str) {
    var segments = str.split(".");

    return (obj) => {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return;
            obj = obj[segments[i]];
        }
        return obj;
    };
}

export default Watcher    

Dep

每个属性在进行defineProperty时都会创建一个dep实例,这时创建的这个实例只会为当前的属性服务,当触发get函数时,它会把所有关于这个对象的watcher实例都存入subs数组中,触发set时会挨个调用watcher中的update()方法触发回调函数。

js 复制代码
class Dep {
    constructor() {
        this.subs = []
    }

    addSub(w) {
        this.subs.push(w)
    }
    noitfy() {
        for (let i of this.subs) {
            i.update()
        }
    }

    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }
}

export default Dep

总结

js 复制代码
import observe from './src/observe'
import Watcher from './src/Watcher'
const obj = {
  a:1,
  b:2,
  c:{
    d:{
      e:{
        f:1
      }
    }
  },
  g:{
    z:'zzz'
  }
}

observe(obj)
// 到此为止,obj的所有属性都执行了defineReactive被添加上了get和set方法。
// 并且每个属性都有一个属于自己的dep实例用来存储依赖,只是此时依赖数组还没有值

console.log(obj, 'sdfsdfsdf')

//在实例的过程中,会触发a属性的get方法,调用a属性的dep实例上的depend方法,
//把这个w1存入dep的依赖数组中 完成依赖收集
const w1 = new Watcher(obj, 'a', (newValue, oldValue) => {
  console.log(newValue, oldValue, 'w1')
})

//在实例的过程中,会触发f属性的get方法,调用f属性的dep实例上的depend方法,
//把这个w2存入dep的依赖数组中 完成依赖收集
const w2 = new Watcher(obj, 'c.d.e.f', (newValue, oldValue) => {
  console.log(newValue, oldValue, 'w2')
})

// 此时触发a属性的set方法,触发dep的noitfy方法,此时会循环遍历数组 执行watcher的update方法
// 因为只收集了一个实例w1,只会执行w1的update方法,最终调用watcher的回调函数
// (newValue, oldValue) => {
//  console.log(newValue, oldValue, 'w1')
// }
obj.a = '5' //打印 5 1 w1

结尾

看了好久的资料和源码,总算对vue2的响应式的整个流程是有一个较为清晰的。可能没法很好的表达出来,大家看一乐就好,希望能有所帮助。

如果有什么错误欢迎评论区指正。

相关推荐
一枚小小程序员哈2 小时前
基于Vue的个人博客网站的设计与实现/基于node.js的博客系统的设计与实现#express框架、vscode
vue.js·node.js·express
定栓2 小时前
vue3入门-v-model、ref和reactive讲解
前端·javascript·vue.js
LIUENG3 小时前
Vue3 响应式原理
前端·vue.js
wycode4 小时前
Vue2实践(3)之用component做一个动态表单(二)
前端·javascript·vue.js
wycode5 小时前
Vue2实践(2)之用component做一个动态表单(一)
前端·javascript·vue.js
第七种黄昏5 小时前
Vue3 中的 ref、模板引用和 defineExpose 详解
前端·javascript·vue.js
pepedd8646 小时前
还在开发vue2老项目吗?本文带你梳理vue版本区别
前端·vue.js·trae
前端缘梦6 小时前
深入理解 Vue 中的虚拟 DOM:原理与实战价值
前端·vue.js·面试
HWL56796 小时前
pnpm(Performant npm)的安装
前端·vue.js·npm·node.js
柯南95277 小时前
Vue 3 reactive.ts 源码理解
vue.js