我总算搞懂了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的响应式的整个流程是有一个较为清晰的。可能没法很好的表达出来,大家看一乐就好,希望能有所帮助。

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

相关推荐
M_emory_11 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito14 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
Dread_lxy10 小时前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
龙猫蓝图11 小时前
vue el-date-picker 日期选择器禁用失效问题
前端·javascript·vue.js
peachSoda712 小时前
随手记:简单实现纯前端文件导出(XLSX)
前端·javascript·vue.js
Tttian62212 小时前
Vue全栈开发旅游网项目(11)-用户管理前端接口联调
前端·vue.js·django
龙猫蓝图13 小时前
vue el-date-picker 日期选择 回显后成功后无法改变的解决办法
前端·javascript·vue.js
刘志辉14 小时前
Pure Adminrelease(水滴框架配置)
vue.js
工业互联网专业14 小时前
Python毕业设计选题:基于Django+uniapp的公司订餐系统小程序
vue.js·python·小程序·django·uni-app·源码·课程设计
黄景圣15 小时前
CURD低代码程序设计
前端·vue.js·后端