Vue源码--解读vue响应式原理
原文链接:https://geniuspeng.github.io/2018/01/05/vue-reactivity/
Vue的官方说明里有深入响应式原理这一节。在此官方也提到过:
当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。
官网只是说了一个大概原理,浏览了一下vue的源码以及其他关于此处的解释,这一块其实可以总结为两点:利用Object.defineProperty进行数据劫持同时结合观察者模式(发布/订阅模式)来实现数据双向绑定,这也是vue响应式原理的核心。首先先把这两个东西简单介绍一下吧~
关于Object.defineProperty
关于Object.defineProperty我觉得还是MDN上说的更明白详细一些,我写在这里无非是一些ctrl C+V,总之我们要知道它是一个定义属性描述符的东西,可以劫持一个对象属性的getter和setter,而Vue正是通过在属性改变时劫持这些来通知watcher(后文会讲)。下面这个例子实现了一个极简的双向绑定:
它的效果就是,input框输出改变的时候,span的内容随之改变,同时如果在控制台直接改变obj.hello属性,input和span的内容同时会一起变化(验证这个得去codepen页面中的console控制台),其原理就是利用Object.defineProperty劫持了属性的set方法。
关于观察者模式
观察者模式(有时又被称为发布(publish )-订阅(Subscribe)模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。
观察者模式根据具体不同的场景可以实现出不同的版本,但其核心思想没有变化,下面是根据vue源码简化出来的一个简单模型
1 |
function Dep() {//主题对象 |
首先有3个主体:发布者,主题对象,订阅者,其中发布者和主题对象是一对一的(某些其他场景可能一对多?),而主题对象和订阅者也是一对多的关系。发布者通过发布某个主题对象,主题对象更新并通知其所有的订阅者,然后每个订阅者执行update进行更新。
对于Vue,我们这里可以认为Vue实例中的data中的每一项属性是一个Dep,而所有用到这个属性的地方都是这个Dep的一个订阅者(sub),当这个属性值变化时,观察者通过监听捕获变化,告诉这个dep去notify每一个订阅者。
Observer ,Dep和Watcher
Observer、Watcher、Dep 是响应式原理中涉及到的 3 个重要的对象,可以说分别上面的发布者,主体对象,订阅者相对应。其关系可以简化为下图所示:
Observer对象
这个东西,字面意思是一个观察者,我个人理解就是上面所说的Object.defineProperty + 发布者的结合体,它的主要功能是做数据劫持,在数据获得更新的时候(拦截set方法),执行主题对象(Dep)的notify方法,通知所有的订阅者(Watcher)。
Observer类定义在src/core/observer/index.js中,先来看一下Observer的构造函数:
1 |
constructor (value: any) { |
value是需要被观察的数据对象,在构造函数中,会给value增加ob属性,作为数据已经被Observer观察的标志。如果value是数组,就使用observeArray遍历value,对value中每一个元素调用observe分别进行观察。如果value是对象,则使用walk遍历value上每个key,对每个key调用defineReactive来获得该key的set/get控制权。
在src/core/observer/index.js中还有几个方法,大致解释如下
- observeArray: 遍历数组,对数组的每个元素调用observe
- observe: 检查对象上是否有ob属性,如果存在,则表明该对象已经处于Observer的观察中,如果不存在,则new Observer来观察对象(其实还有一些判断逻辑,为了便于理解就不赘述了)
- walk: 遍历对象的每个key,对对象上每个key的数据调用defineReactive
- defineReactive: 通过Object.defineProperty设置对象的key属性,使得能够捕获到该属性值的set/get动作。一般是由Watcher的实例对象进行get操作,此时Watcher的实例对象将被自动添加到Dep实例的依赖数组中,在外部操作触发了set时,将通过Dep实例的notify来通知所有依赖的watcher进行更新。(后面详细介绍)
defineReactive方法如下,这是observer类的一个核心方法:
1 |
function defineReactive ( |
defineReactive 是对 Object.defineProperty 方法的包装,结合 observe 方法对数据项进行深入遍历,最终将所有的属性就转化为 getter 和 setter。至此,所有的数据都已经转换为 Observer 对象。即数据的读操作都会触发 getter 函数,写操作都会触发 setter 函数。
那走到这里,会有很多问题,比如Dep在哪?如何往Dep中添加订阅者?
Dep对象
Dep类定义在src/core/observer/dep.js中。
Dep是Observer与Watcher之间的纽带,也可以认为Dep是服务于Observer的订阅系统。Watcher订阅某个Observer的Dep,当Observer观察的数据发生变化时,通过Dep通知各个已经订阅的Watcher。其构造函数和主要方法如下。其中sub就是订阅者Watcher,在后文介绍:
1 |
class Dep { |
Dep提供了如下几个方法:
- addSub: 接收的参数为Watcher实例,并把Watcher实例存入记录依赖的数组中
- removeSub: 与addSub对应,作用是将Watcher实例从记录依赖的数组中移除
- depend: Dep.target上存放这当前需要操作的Watcher实例,调用depend会调用该Watcher实例的addDep方法,addDep的功能可以看下面对Watcher的介绍
- notify: 通知依赖数组中所有的watcher进行更新操作
Watcher对象
Watcher类定义在src/core/observer/watcher.js。其构造函数如下:
1 |
constructor ( |
watcher实例有这些方法:
- get: 将Dep.target设置为当前watcher实例,在内部调用this.getter,如果此时某个被Observer观察的数据对象被取值了,那么当前watcher实例将会自动订阅数据对象的Dep实例
- addDep: 接收参数dep(Dep实例),让当前watcher订阅dep
- cleanupDeps: 清除newDepIds和newDep上记录的对dep的订阅信息
- update: 立刻运行watcher或者将watcher加入队列中等待统一flush
- run: 运行watcher,调用this.get()求值,然后触发回调
- evaluate: 调用this.get()求值
- depend: 遍历this.deps,让当前watcher实例订阅所有dep
- teardown: 去除当前watcher实例所有的订阅
Observer,Dep,Watcher所有的代码都过一遍之后,再来回头看看究竟是如何添加订阅者的。说白了就是Dep中的this.subs = []方法如何添加相应的订阅者watcher的。在这里还应该列出两个重要的方法,watcher的get()方法:
1 |
/** |
我们在Dep中可以看到Dep在一开始定义了一个全局属性Dep.target,在新建watcher是,这个属性为null,而在watcher的构造函数中最后会执行自己的get()方法,进而执行pushTarget(this)方法,可以看到get()方法中做了一件事情:value = this.getter.call(vm, vm),然后popTarget()。Dep.target只是一个标记,存储当前的watcher实例,而执行这句话的意义在于触发Object.defineProperty中的get拦截,而在bject.defineProperty中的get那里,我们可以看到dep.depend(),正是在这里将当前的订阅者watcher绑定当Dep上。
也就是说,每个watcher第一次实例化的时候,都会作为订阅者订阅其相应的Dep。
而关于watcher何时进行实例化的问题,模板渲染初始化相关指令调用data中的属性,或者正常进行数据绑定时都会创建watcher实例。
PS: 在模板渲染过程中,vue1.x与vue2.x完全不同,1.x是利用documentFragment来实现,而2.x向react靠拢,加入了virtual dom,同时通过自己生产的render方法进行渲染,不过无论是那种方法,都会在初始时对所关联的data属性进行watcher实例化并且绑定watcher变化时的更新callback,确保data更新时会重新对相应的地方进行视图更新。
总结一下:
- 模板编译过程中的指令和数据绑定都会生成 Watcher 实例,watch 函数中的对象也会生成 Watcher 实例,在实例化的过程中,会调用 watcher.js 中的 get 函数 touch 这个 Watcher 的表达式或函数涉及的所有属性;
- touch 开始之前,Watcher 会设置 Dep 的静态属性 Dep.target 指向其自身,目的在于标记此watcher实例是第一次创建,需要添加到一个Dep中;
- touch 属性的过程中,属性的 getter 函数会被访问;
- 属性 getter 函数中会判断 Dep.target(target 中保存的是第 2 步中设置的 Watcher 实例)是否存在,若存在则将 getter 函数所在的 Observer 实例的 Dep 实例保存到 Watcher 的列表中,并在此 Dep 实例中添加 Watcher 为订阅者;
- 重复上述过程直至 Watcher 的表达式或函数涉及的所有属性均 touch 结束(即表达式或函数中所有的数据的 getter 函数都已被触发),Dep.target 被置为 null,此时已经将该watcher作为订阅者绑定到Dep中;
文末福利:
福利一:前端,Java,产品经理,微信小程序,Python等10G资源合集大放送:https://www.jianshu.com/p/e8197d4d9880
福利二:微信小程序入门与实战全套详细视频教程。
【领取方法】
关注 【编程微刊】微信公众号:
回复【小程序demo】一键领取130个微信小程序源码demo资源。
回复【领取资源】一键领取前端,Java,产品经理,微信小程序,Python等资源合集10G资源大放送。
Vue源码--解读vue响应式原理的更多相关文章
- 【Vue源码学习】响应式原理探秘
最近准备开启Vue的源码学习,并且每一个Vue的重要知识点都会记录下来.我们知道Vue的核心理念是数据驱动视图,所有操作都只需要在数据层做处理,不必关心视图层的操作.这里先来学习Vue的响应式原理,V ...
- vue源码解析之响应式原理
关于defineReactive等使用细节需要自行了解 一些关键知识点 $mount时 会 new Watcher 把组件的 updateComponent 方法传给watcher 作为getter ...
- 读Vue源码二 (响应式对象)
vue在init的时候会执行observer方法,如果value是对象就直接返回,如果对象上没有定义过_ob_这个属性,就 new Observer实例 export function observe ...
- Vue 源码解读(3)—— 响应式原理
前言 上一篇文章 Vue 源码解读(2)-- Vue 初始化过程 详细讲解了 Vue 的初始化过程,明白了 new Vue(options) 都做了什么,其中关于 数据响应式 的实现用一句话简单的带过 ...
- Vue 源码解读(1)—— 前言
当学习成为了习惯,知识也就变成了常识. 感谢各位的 点赞.收藏和评论. 新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn 文章已收录到 github 仓库 liyongning/blog ...
- Vue 源码解读(2)—— Vue 初始化过程
当学习成为了习惯,知识也就变成了常识. 感谢各位的 点赞.收藏和评论. 新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn 文章已收录到 github 仓库 liyongning/blog ...
- Vue 源码解读(4)—— 异步更新
前言 上一篇的 Vue 源码解读(3)-- 响应式原理 说到通过 Object.defineProperty 为对象的每个 key 设置 getter.setter,从而拦截对数据的访问和设置. 当对 ...
- Vue 源码解读(5)—— 全局 API
目标 深入理解以下全局 API 的实现原理. Vue.use Vue.mixin Vue.component Vue.filter Vue.directive Vue.extend Vue.set V ...
- Vue 源码解读(6)—— 实例方法
前言 上一篇文章 Vue 源码解读(5)-- 全局 API 详细介绍了 Vue 的各个全局 API 的实现原理,本篇文章将会详细介绍各个实例方法的实现原理. 目标 深入理解以下实例方法的实现原理. v ...
随机推荐
- httpurlconnection发送文件到服务端并接收
httpurlconnection发送文件到服务端并接收 客户端 import java.io.DataInputStream; import java.io.File; import java.io ...
- 洛谷 P2504 [HAOI2006]聪明的猴子
洛谷 P2504 [HAOI2006]聪明的猴子 题目描述 在一个热带雨林中生存着一群猴子,它们以树上的果子为生.昨天下了一场大雨,现在雨过天晴,但整个雨林的地表还是被大水淹没着,部分植物的树冠露在水 ...
- Maven学习总结(14)——Maven 多模块项目如何分工?
一.开场白 使用Maven有段时间了,只能感慨真是个好东西,让我从传统模式体会到了严谨.规范.敏捷.方便的特性. 如果你懂Maven或许看过Juven翻译的<Maven权威指南>: 发个牢 ...
- 关于java中String的一点理解
String类是java的最基本类之中的一个,非常好的掌握它的原理非常是必要的! 1.String的Final类型的.是不可继承 的.final类默认的方法都为final类型,保证了方法不能被 ...
- Spark MLlib LDA 源代码解析
1.Spark MLlib LDA源代码解析 http://blog.csdn.net/sunbow0 Spark MLlib LDA 应该算是比較难理解的,当中涉及到大量的概率与统计的相关知识,并且 ...
- Android Studio中怎样引用图片资源
随着不断接触Android Studio,越来越发现和Eclipse有着巨大的差别. 不管是表面的目录结构,还是内在对各种资源的管理. 本篇就来聊聊Android Studio中怎样来显示图片. 以及 ...
- ubuntu-系统卡慢解决(转载)
ubuntu系统狠慢 或者很卡的原因 1. 涉及内存小或者虚拟SWAP分区调整问题 可以通过 系统监视器 进行查看 在UBUNTU系统里面,并不是你的物理内存全部耗尽之后,系统才使用 ...
- 00096_Properties类
1.Properties类介绍 (1)Properties 类表示了一个持久的属性集.Properties 可保存在流中或从流中加载.属性列表中每个键及其对应值都是一个字符串: (2)特点 Hasht ...
- Dcloud课程3 什么是HBuilder和MUI
Dcloud课程3 什么是HBuilder和MUI 一.总结 一句话总结:DCloud(数字天堂)推出一款支持HTML5的Web开发IDE.最大的特点是快.MUI是高性能App的框架,也是目前最接近 ...
- 快速搭建REST API——json server
一:全局安装json-server npm install json-server -g 二:在自己项目跟目录下存放mock/data.json,json内容如下: { "roles&quo ...