Vue源码之响应式原理(个人向)

浅谈响应式原理

  • 关于响应式原理,其实对于vue这样一个大项目来说肯定有很多细节和优化的地方,在下水平精力有限,不能一一尝试探索,本文仅以将响应式的大致流程个人向的梳理完毕为目的。
  • 对于响应式主要分为三大部分来分析,1.响应式对象;2.依赖收集;3.派发更新。
    最后将是个人的分析。

1、响应式对象 (Object.defineProperty)

我们先从初始化数据开始,再介绍几个比较核心的方法。

1.1、initState

文件位置:src/core/instance/state.js

在Vue的初始化阶段,_init?法执?的时候,会执?initState(vm)?法,。是对props、methods 、data、computed和wathcer等属性做了初始化操作。

export?function?initState?(vm:?Component)?{
??vm._watchers?=?[]
??const?opts?=?vm.$options
??if?(opts.props)?initProps(vm,?opts.props)
??if?(opts.methods)?initMethods(vm,?opts.methods)
??if?(opts.data)?{
????initData(vm)
??}?else?{
????observe(vm._data?=?{},?true?/*?asRootData?*/)
??}
??if?(opts.computed)?initComputed(vm,?opts.computed)
??if?(opts.watch?&&?opts.watch?!==?nativeWatch)?{
????initWatch(vm,?opts.watch)
??}
}

此处我们仅大致看一下对于props和data的初始化。

1.1.1 initProps

源码略

作用:遍历定义的props配置。遍历的过程主要做两件事情:?个是调用defineReactive ?法把每个prop对应的值变成响应式;另?个是通过proxy 把vm._props.xxx 的访问代理到 vm.xxx上.

1.1.2 initData

源码略

作用:两件事,?个是对定义data函数返回对象的遍历,通过proxy 把每?个值 vm._data.xxx都代理到vm.xxx上;另?个是调?observe?法观测整个data的变化,把data 也变成响应式,

  • 小结:这里可以看到再初始化props和data的时候都调用了proxy(代理)和将其变成响应试的过程,虽然props时用的defineReactive,但追究其中还是调用了observer这个方法将数据变成响应式。接下来我们看一下proxy和observer.

1.2、proxy

源码位置:src/core/instance/state.js

作?是把props和data上的属性代理到vm实例上,,通过Object.defineProperty把 target[sourceKey][key]的读写 变成了对target[key]的读写。

const?sharedPropertyDefinition?=?{
??enumerable:?true,
??configurable:?true,
??get:?noop,
??set:?noop
}

export?function?proxy?(target:?Object,?sourceKey:?string,?key:?string)?{
??sharedPropertyDefinition.get?=?function?proxyGetter?()?{
????return?this[sourceKey][key]
??}
??sharedPropertyDefinition.set?=?function?proxySetter?(val)?{
????this[sourceKey][key]?=?val
??}
??Object.defineProperty(target,?key,?sharedPropertyDefinition)
}

经过这种代理的操作。所以对于props??,对vm._props.xxx的读写变成了vm.xxx 的读写,?对于vm._props.xxx我们可以访问到定义在 props中的属性,所以我们就可 以通过 vm.xxx访问到定义在props中的xxx属性了。同理,对于data??,对vm._data.xxxx的读写变成了对vm.xxxx的读写,?对于vm._data.xxxx我们可以访问到定义在data函数返回对象中的属性,所以我们就可以通过vm.xxxx访问到定义在data函数返回对 象中的xxxx属性了。

1.3、oberver

源码位置:src/core/observer/index.js

observe的功能就是?来监测数据的变化

/**
?*?Attempt?to?create?an?observer?instance?for?a?value,
?*?returns?the?new?observer?if?successfully?observed,
?*?or?the?existing?observer?if?the?value?already?has?one.
?*/
export?function?observe?(value:?any,?asRootData:??boolean):?Observer?|?void?{
??if?(!isObject(value)?||?value?instanceof?VNode)?{
????return
??}
??let?ob:?Observer?|?void
??if?(hasOwn(value,?'__ob__')?&&?value.__ob__?instanceof?Observer)?{
????ob?=?value.__ob__
??}?else?if?(
????shouldObserve?&&
????!isServerRendering()?&&
????(Array.isArray(value)?||?isPlainObject(value))?&&
????Object.isExtensible(value)?&&
????!value._isVue
??)?{
????ob?=?new?Observer(value)
??}
??if?(asRootData?&&?ob)?{
????ob.vmCount++
??}
??return?ob
}

在这里vue源码的注释已经说的很明白。通过这个函数会给?VNode的对象类型数据添加?个Observer ,如果已经添加过则直接返回,否则在满??定条件下去实例化?个Observer对象实例。下面看一下这个Observer类的具体内容

1.3.1 Observer

作?是给对象的属性添加 getter 和 setter,?于依赖收集和派发更新

 /**
  ?*?Observer?class?that?is?attached?to?each?observed  //这个类是附加再每个被observed重
  ?*?object.?Once?attached,?the?observer?converts?the?target//对象一旦被驱动,观察者就转换(观察到这个)目标
  ?*?objects?property?keys?into?getter/setters?that //将对象属性键放入getter/setter中
  ?*?collect?dependencies?and?dispatch?updates. //收集依赖项并分发更新。!!!!!重要
 **/
  export?class?Observer?{
  ??value:?any;
  ??dep:?Dep;
  ??vmCount:?number;?//?number?of?vms?that?have?this?object?as?root?$data

  ??constructor?(value:?any)?{
  ????this.value?=?value
  ????this.dep?=?new?Dep()
  ????this.vmCount?=?0
  ????def(value,?'__ob__',?this)
  ????if?(Array.isArray(value))?{
  ??????if?(hasProto)?{
  ????????protoAugment(value,?arrayMethods)
  ??????}?else?{
  ????????copyAugment(value,?arrayMethods,?arrayKeys)
  ??????}
  ??????this.observeArray(value)
  ????}?else?{
  ??????this.walk(value)
  ????}
  ??}

  ??/**
  ???*?Walk?through?all?properties?and?convert?them?into
  ???*?getter/setters.?This?method?should?only?be?called?when
  ???*?value?type?is?Object.
  ???*/
  ??walk?(obj:?Object)?{
  ????const?keys?=?Object.keys(obj)
  ????for?(let?i?=?0;?i?<?keys.length;?i++)?{
  ??????defineReactive(obj,?keys[i])
  ????}
  ??}

  ??/**
  ???*?Observe?a?list?of?Array?items.
  ???*/
  ??observeArray?(items:?Array<any>)?{
  ????for?(let?i?=?0,?l?=?items.length;?i?<?l;?i++)?{
  ??????observe(items[i])
  ????}
  ??}
  }

Observer的构造函数逻辑很简单,?先实例化Dep对象,接着通过执?def函数把??实例添加到数据对象 value的 ob 属性上,

源码位置:src/core/util/lang.js

 /**
 * Define a property.
 */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

def函数是?个?常简单的Object.defineProperty的封装,这就是为什么我在开发中输出data 上对象类型的数据,会发现该对象多了?个__ob__的属性。

继续看Observer 的构造函数,不得不说Vue的源码注释还是相当好的,大致翻译我写在上面了,请关注一下,然后看这个类的具体操作包括

它会对value做判断,对于数组会调?observeArray?法,否则对纯对象调?walk?法。可以看到observeArray是遍历数组再次调?observe?法,?walk?法是遍历对象的key调?defineReactive?法,

1.4、defineReactive

源码位置:src/core/observer/index.js

defineReactive的功能就是定义?个响应式对象,给对象动态添加 getter 和 setter.
源码略了,太长了,有兴趣的可自行查看,下面贴一个自己学着写的简易版,大致是这么个意思

//定义响应式
????definReactive(obj,key,value){
????????let?that?=?this
????????let?dep?=?new?Dep()?//每个变化的数据,都会对应一个数组,这个数组是存放所有更新的操作
????????Object.defineProperty(obj,key,{
????????????enumerable:true,
????????????configurable:true,
????????????get(){
????????????????Dep.target?&&?dep.addSub(Dep.target)
????????????????return?value;
????????????},
????????????set(newValue){?//当给data属性中设置值。更改获取属性的值
????????????????if(newValue!=value){
????????????????????//这里的this不是实例
????????????????????that.observer(newValue)?//如果是对象继续劫持
????????????????????value=newValue
????????????????????dep.notify()?//通知所有人?数据更新了
????????????????}
????????????},
????????})
????}

函数最开始初始化Dep对象的实例,接着拿到obj的属性描述符,然后对?对 象递归调?observe ?法,这样就保证了?论obj的结构多复杂,它的所有?属性也能变成响应式的对象,

  • 小结:所谓响应式对象的总体就是利?Object.defineProperty给数据添加了getter和setter,?的就是为了在我们访问数据以及写数据的时候能?动执??些逻辑,究其核心就是使用defineReactive这个方法进行处理,同时也将dep的添加subs方法存放在了get 中,将dep的notify方法放在set中。从而为后面的依赖收集和派发更新提供了入口?(不知道怎么形容好,,,,,)。
  • 前置:所谓依赖收集其实和后面的派发更新完全是以watcher和dep为核心的关系。
    接下来我们根绝不同的目的来大致看一下这两个方法,

    2、依赖收集(getter)

    我们去回看 defineReactive方法,对于依赖收集我们关注两点,?个是const dep = newDep()实例化?个Dep的实例,另?个是在get函数中通过dep.depend做依赖收集。

    2.1、Dep

    源码位置:src/core/observer/dep.js

Dep是整个 getter 依赖收集的核? 对于依赖收集主要是(dep.depend()),派发更新主要是(dep.notify())

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

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

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs arent sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

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

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

Dep是?个Class,它定义了?些属性和?法,这?需要特别注意的是它有?个静态属性target , 这是?个全局唯?Watcher ,这是?个?常巧妙的设计,因为在同?时间只能有?个全局的Watcher 被计算,另外它的??属性subs也是Watcher的数组。Dep实际上就是对Watcher的?种管理,Dep脱离Watcher单独存在是没有意义的,为了完整地讲清楚依赖收集过程,我们有必要看?下Watcher的?些内容

2.2、watcher

源码位置: src/core/observer/watcher.js
源码太长略
下面贴一个自己学着写的简易版
注释已经和很详细了,

//观察者 的目的就是给需要观察的元素天机一个观察这,当数据变化后执行相对应的方法
class Watcher{
    constructor(vm,expr,cb){
        this.vm=vm
        this.expr=expr
        this.cb=cb;
        //先获取一下老的值
        this.value  = this.get();
    }

    getVal(vm,expr){ //获取实例上对应的数据
        expr= expr.split('.');
        return expr.reduce((prev,next)=>{ //vm.$data.a
            return prev[next]
        },vm.$data)
    }
    get (){

        //
        Dep.target=this //这个地方就是上面在1.3.1 Observer中注释中说到的大致意思,要将所有被观察者的数据都要放在dep的target中后面,dep会将这个target存起来,等到notify时一个个分发出去。然后走这里的update方法,去更新视图

        //
        let value= this.getVal(this.vm,this.expr);

        Dep.tatget=null

        return value
    }
    //对外暴露的方法
    update(){
        let newValue = this.getVal(this.vm,this.expr);
        let oldValue = this.value;
        if(newValue!=oldValue){
            this.cb(newValue); //调用watch的callback
        }
    }
}

//用新值和老值对比,如果变化就调用更新方法
  • 小结:收集依赖的?的是为了当这些响应式数据发送变化,触发它们的setter的时候,我们把这个过程叫派发更新,其实Watcher和Dep就是?个?常经典的观察者设计模式的实现。

3、派发更新(setter)

我们继续回去看 defineReactive方法,对于派发更新集我们关注两点,?个是childOb =!shallow &&observe(newVal),如果shallow为false的情况,会对新设置的值变成?个响应式对象;另?个是 dep.notify(),通知所有的订阅者,

  1. 这里其实对越整个响应式的大致流程已经走完,但是这里Vue对于派发更新引?了?个队列的概念,这也是Vue 在做派发更新的时候的?个优化的点,它并不会每次数据改 变都触发watcher的回调,?是把这些watcher先添加到?个队列?,然后在nextTick后执?flushSchedulerQueue(源码地址:src/core/observer/scheduler.js)。
  2. 对于这个队列优化部分,和flushSchedulerQueue这个很关键的函数,这篇文章不去细讲,因为关系到我们常用的$nextTick之类的,如果有精力的化,计划再写个文章细说吧。
  • 小结:我们对Vue 数据修改派发更新的过程也有了认识,实际上就是当数据发?变化的 时候,触发 setter逻辑,把在依赖过程中订阅的的所有观察者,也就是watcher,都触发它们的update过程,这个过程?利?了队列做了进?步优化(略)


好了,到这里我们总算是把整个响应式的大致流程走完了,下面我先放一个我自己做的关系图,根据图我再做个总结

总结

  • 首先,我们可以看出针对所谓响应式最基础的核心还是我们入门记得理解Object.defineProperty,Vue依靠它实现了数据变成响应式对象。依次为基础我们才能再通过数据劫持和发布订阅者去真正实现数据的响应式。
  • 其次,就是Vue通过observe与defineReactive的功能进行遍历循环去实现将数据都使用Object.defineProperty变成响应式。注意的是,在defineReactive过程中的我们就将Dep实例化,并且将depend和notify分别放在了setter和getter中。以实现数据的劫持(不敢断定,貌似是)?
  • 而后就是Dep和Watcher上演观察订阅模式的实践了。具体就是,
    1. 我们在getter中去获取dep中的target并通过depen方法存起来,而这个target其实就是来自与Watcher在出事数据中就赋予的,也可以说在这个过程中,把当前的watcher订阅到这个数据持有的dep的subs中,这个?的是为后续数据变化时候能通知到哪些subs准备。
    2. 我们在setter中会走Dep的notify,此时我们就会遍历所有的subs然后执行watcher中的update的过程,继而更新数据和视图,完成整个流程的过程,当然在notify过程中Vue做的各种优化和操作,又是一个大内容暂不讨论,
  • 最后,我们完成了整个流程。(MM呀 终于完事了。。。)

后记

  1. 关于源码的学习,说实话,很早之前就看过vue源码的资料,内容和关系脑图一直是手写在A4纸上,我阅读学习的资料是Vue源码和一个好久之前忘了来源的一个PDF讲Vue源码的。这里说明一下,文章引用部分来自那个PDF,人家写的很干练了就直接引用了。当时学习就是一边看PDF一遍看源码学习,讲道理看了好多遍,算是有些理解.其实看起来也真的头疼。
  2. 为什么写这篇博客(电子知识梳理),是年后网上看到了某培训机构两年前的MVVM源码公开课内容,用两个小时看完,继而又花了三个小时跟着撸了一遍,在撸完的那一刹那,我感觉最起码这响应式基础的流程在我心中真的是豁然开朗。我就想着赶紧记录下来,这也是总结了一句话。纸上得来终觉浅,绝知此事要躬行。。哈哈,,,,如果有人有兴趣看看简易版MMVM的代码,也就是文章中我贴的,可以直接下载看看,我就厚脸皮的贴上地址了"https://github.com/jia-03/self-study-mvvm.git"
  3. 讲道理,这文章我吧我之前的Xmind文档整理书写到现在花了四个小时,好累啊,,,但是算是又过了一遍流程,也算是收获吧,也希望看到的你有收获。
  4. 如果哪里写的不对不好的,需要改进的希望能友善交流。
  5. 最后我放上,PDF版的原理图梳理,大佬梳理的更好,但是很多东西,在下能力有限,不能一次获取,还有我真的时不知道,PDF的出处,如果有任何知识自主的侵犯,希望作者能告知,我加上引用或是删除内容。。。。

原文地址:https://www.cnblogs.com/jiayiyi/p/12443976.html

时间: 2024-10-15 22:01:25

Vue源码之响应式原理(个人向)的相关文章

vue源码之响应式数据

分析vue是如何实现数据响应的. 前记 现在回顾一下看数据响应的原因. 之前看了vuex和vue-i18n的源码, 他们都有自己内部的vm, 也就是vue实例. 使用的都是vue的响应式数据特性及$watchapi. 所以决定看一下vue的源码, 了解vue是如何实现响应式数据. 本文叙事方式为树藤摸瓜, 顺着看源码的逻辑走一遍, 查看的vue的版本为2.5.2. 目的 明确调查方向才能直至目标, 先说一下目标行为: vue中的数据改变, 视图层面就能获得到通知并进行渲染. $watchapi监

[Vue源码分析] v-model实现原理

最近小组有个关于vue源码分析的分享会,提前准备一下- 前言:我们都知道使用v-model可以实现数据的双向绑定,及实现数据的变化驱动dom的更新,dom的更新影响数据的变化.那么v-model是怎么实现这一原理的呢?接下来探索一下这部分的源码. 前期准备①:vue2.5.2源码(用于阅读.查看关联等)②:建立vue demo,创建包含v-model指令的实例(用于debugger)以下为demo: genDirectives在模板的编译阶段, v-model跟其他指令一样,会被解析到 el.d

Ace 1.3.1 网站管理后台源码 Bootstrap响应式模板主题

Ace 响应式管理后台模板最新版本v1.3.1同步升级,欢迎下载使用,注意:本站提供为官网购买压缩源码版,在官网上用18$买来,现只售20元,可以帮亲省3/4的额外开销哦,需要的亲可以到本博的淘宝小店购买,包升级(免费!). Ace简介: Ace (v1.3.1)是一个轻量.功能丰富.HTML5.响应式.支持手机及平板电脑上浏览的管理后台模板,基于CSS框架Bootstrap制作,Bootstrap版本更新至 3.0,Ace – Responsive Admin Template当前最新版! 淘

Matrix Admin html5网站管理后台源码 Bootstrap响应式模板主题

Matrix Admin 响应式管理后台模板最新版本,欢迎下载使用,注意:本站提供为官网购买未压缩源码版,在官网上用10$买来,现只售15元,可以帮亲省额外开销哦,需要的亲可以到本博的淘宝小店购买. Matrix Admin 简介: Matrix Admin 是一个轻量.功能丰富.HTML5.响应式.支持手机及平板电脑上浏览的管理后台模板,基于CSS框架Bootstrap制作,Bootstrap版本更新至 3.0,Matrix Admin 当前最新版! 淘宝购买地址(新地址,更新至最新的1.3.

java 企业站源码 自适应响应式 兼容手机平板PC SSM freemaker 静态引擎主流框架

前台: 支持四套模版, 可以在后台切换 系统介绍: 1.网站后台采用主流的 SSM 框架 jsp JSTL,网站后台采用freemaker静态化模版引擎生成html 2.因为是生成的html,所以访问速度快,轻便,对服务器负担小 3.网站前端采用主流的响应式布局,同一页面同时支持PC.平板.手机(三合一)浏览器访问 4.springmvc +spring4.3.7+ mybaits3.3  SSM 普通java web(非maven, 赠送pom.xml)  数据库:mysql --------

java 企业门户网站 源码 自适应响应式 freemarker 静态引擎 html5 SSM

1.网站后台采用主流的 SSM 框架 jsp JSTL,网站后台采用freemaker静态化模版引擎生成html 2.因为是生成的html,所以访问速度快,轻便,对服务器负担小 3.网站前端采用主流的响应式布局,同一页面同时支持PC.平板.手机(三合一)浏览器访问 4.springmvc +spring4.3.7+ mybaits3.3  SSM 普通java web(非maven, 赠送pom.xml)  数据库:mysql 1.   网站信息:维护网站基本信息,比如标题.描述.关键词.联系方

vue源码分析:响应式之依赖收集

今天记录下vue源码的响应式的源码学习,第一步,我们想下,为什么为们写一个data.然后里边的数据变化就能映射到dom的变化上.这里用到了Object.defineProperty(obj,key, {}),如果你还不了解建议去jsmdn上仔细了解下,这个函数的目的就是检测你得赋值和取值的过程用来定义自己的行为,比如obj[key]会拿到配置里边的get函数定义,obj[key] = 'xxx'会调用配置中的set.那么vue也是一样的,在get中做了依赖收集(也就是说哪些dom用到了我定义的这

vue源码解读(一)Observer/Dep/Watcher是如何实现数据绑定的

欢迎star我的github仓库,共同学习~目前vue源码学习系列已经更新了5篇啦~ https://github.com/yisha0307/... 快速跳转: Vue的双向绑定原理(已完成) 说说vue中的Virtual DOM(已完成) React diff和Vue diff实现差别 Vue中的异步更新策略(已完成) Vuex的实现理解 Typescript学习笔记(持续更新ing) Vue源码中闭包的使用(已完成) 介绍 最近在学习vue和vuex的源码,记录自己的一些学习心得.主要借鉴

vue系列---响应式原理实现及Observer源码解析(一)

_ 阅读目录 一. 什么是响应式? 二:如何侦测数据的变化? 2.1 Object.defineProperty() 侦测对象属性值变化 2.2 如何侦测数组的索引值的变化 2.3 如何监听数组内容的增加或减少? 2.4 使用Proxy来实现数据监听 三. Observer源码解析 回到顶部 一. 什么是响应式? 我们可以这样理解,当一个数据状态发生改变的时候,那么与这个数据状态相关的事务也会发生改变.用我们的前端专业术语来讲,当我们JS中的对象数据发生改变的时候,与JS中对象数据相关联的DOM