VUE响应式原理
https://juejin.im/post/6850418111985352711VUE响应式原理
1.什么是VUE响应式?
当数据发生改变时,会重新渲染页面
2.完成这个过程 需要哪些步骤
a.侦测数据的变化 ========================》数据劫持/数据代理
b.收集视图依赖了哪些数据 ========================》依赖收集
c.数据发生变化时 自动通知需要更新的视图部分,并进行更新 =================》发布订阅模式
3.如何侦测数据变化
3.1 Object.Property
vue通过设定对象属性的 setter/getter 的方法拉监听数据的变化,通过 getter 方法进行依赖收集 而每个setter方法就是一个观察者 在数据变更的时候通知订阅者更新视图
function render(){
//set 时候走这里 重新渲染
console.log("模拟视图渲染")
}
let data = {
name:"张三"
loction:{
x:100,y:100}
}
observe(data)
// 定义核心函数
function observe(obj) {
// 判断类型
if( !obj || typeof !== 'object' ){
return
}
Object.keys(obj).forEach( key => {
defineReactive( obj,key,obj[key])
})
function defineReactive(obj,key,value){
// 递归子属性
observe(value)
Object.defineProperty(obj,key,{
enumerable:true, // 可枚举(可以遍历)
configurable:true, // 可配置
get: function reactiveGetter() {
console.log( 'get',value)
return value
}
set: function reactiveSetter( newVal ) {
observe( newVal ) // 如果赋值是一个对象,也要递归子属性
if( newVal !== value){
console.log( 'set',newVal) //
return ()
value = newVal
}
}
})
}
}
改变data的属性 会触发set 然后获取data的属性 会触发get
data.location = {
x:1000,
y:1000
} // 打印 set{ x:1000,y:1000} 模拟视图渲染
data.name // 打印 get 张三
observe函数传入一个需要被追踪变化的对象obj 通过遍历属性方式给对象的每个属性通过defineReactive方法处理 给每个属性添加set和get方法 以此达到实现侦测对象变化,observe会进行递归调用
侦测vue中data的数据:
class Vue {
// vue构造类
constructor(options) {
this._data = options.data
observe(this._data)
}
}
只要 new 一个 vue 对象 就将 data 中的数据进行追踪变化
但是以上代码无法检测对象属性的添加删除 是因为 vue 通过Object.defineProperty 将对象的 key 转换成getter/setter 的形式来追踪变化,但 getter/setter 只能追踪一个数据是否被修改,无法追踪新增属性和删除。 如果删除属性可以用 vm.$delete 实现 如果新增属性 可以用:
a.使用 Vue.set(location, key ,value) 方法向嵌套对象添加响应式属性
b.给这个对象重新赋值 data.location = {…data.location,key:value} Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写
3.2 Proxy的实现
Proxy 是 js2015 的一个新特性。Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代理就能监听同级结构下的所有属性变化,对于深层结构,递归还是必要的。此外Proxy还支持代理数组的变化
function render() {
console.log("模拟视图更新")
}
let obj = {
name:"张三",
age:{
age:100 },
arr:[ 1,2,3 ]
}
let handler = {
// 如果取的值是对象 就再对这个对象进行数据劫持
get(target,key){
if(typeof target[key] == 'object' && target[key] !== null){
return new Proxy(target[key],handler)
}
return Reflect.get(target,key)
}
set(target,key,value){
// key 为 length 时,表示遍历完了最后一个属性
if( key === 'length') return true
render()
return Reflect.set(target,key,value)
}
}
let proxy = new Proxy(obj,handler)
proxy.age.name = "李四"
console.log(proxy.age.name) // 模拟视图更新 李四
proxy.arr[0] = "李四"
console.log(proxy.arr) // 模拟视图更新 李四
proxy.arr.length--
优点:精简、一套代码对对象和数组都适用
缺点:兼容性不好
4.收集依赖
4.1 为什么要收集依赖
之所以要观察数据,是为了在数据属性发生变化时,可以通知那些曾经使用过该数据的地方 依赖收集是如何实现的 核心思想就是“事件发布订阅模式”
4.2 订阅者 Dep
收集依赖需要为依赖找一个存储依赖的地方,因此创建了Dep,用来收集依赖、删除依赖、向依赖发送消息。
先实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操作,主要作用是用来存放 watcher 观察者对象。watcher 可以理解为一个中介角色,当数据发生变化时通知他,然后再通知其他地方。
class Dep {
constructor () {
// 用来存放 watcher 的数组
this.subs = []
}
// 在 subs 中添加一个 watcher 对象
addSub (sub) {
this.subs.push(sub)
}
// 通知所有 watcher 对象更新视图
notify () {
this.subs.forEach( (sub) => {
sub.update()
})
}
}
以上代码主要做两件事:
- 用 addSub 方法可以在目前的 Dep 对象中增加一个 watcher 的订阅操作
- 用 notify 方法通知目前 Dep 对象的 subs 中的所有 watcher 对象触发更新操作。所以当需要收集依赖的时候调用addSub,当需要派发更新的时候调用 notify。
调用:
let dp = new Dep()
dp.addSup( () => {
// 收集依赖时
console.log('emit here')
})
dp.notify() // 派发更新的时候
5.观察者 Watcher
Vue 中定义了一个 Watcher 类来表示观察订阅依赖。当属性发生变化后,需要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户自己写的一个 watch ,这时需要抽象出一个能集中处理这些情况的类。然后我们在收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
收集依赖的目的是:将观察者 Watcher 对象存放到当前闭包的订阅者 Dep 的 subs 中。
class Watcher {
constructor(obj,key,cb){
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update () {
// 获得新值
this.value = this.obj[this.key]
// 定义一个 cb 函数,用来模拟视图更新
this.cb(this.value)
}
}
在执行构造函数时将 Dep.target 指向自身,从而使得收集了对应的 Watcher,在派发更新时候取出对应的 Watcher,然后执行 update 函数。
依赖的本质就是 Watcher,如何收集依赖,总结起来就是一句话:在 getter 中收集依赖,在 setter 中触发依赖。先收集依赖,把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就行。
具体来说,当外界通过 Watcher 读取数据时,便会触发 getter 从而将 Watcher 添加到依赖中,哪个 Watcher 触发可 getter,就把哪个 Watcher 收集到 Dep 中。当数据发生变化时,会循环把所有 Watcher 都通知一遍。
改造 defineReactive 在自定义函数中添加依赖收集和派发更新相关代码,实现简易的数据响应式:
function observe (obj) {
// 判断类型
if( !obj || typeof obj !== 'object'){
return
}
Object.keys(obj).forEach( key => {
defineReactive(obj,key,obj[key])
})
}
function defineReactive(obj,key,value){
observe(value) // 递归子属性
let dp = new Dep() // 新增
Object.defineProperty(obj,key,{
enumerable:true, // 可以枚举(遍历)
configurable:ture // 可以配置
get:function reactiveGetter () {
console.log('get',value) // 监听
// 将 Watcher 添加到订阅
if(Dep.target) {
dp.addSub(Dep.target) // 新增
}
return value
}
set:function reactiveSetter (newVal) {
observe(newVal) // 如果赋值是一个对象,也要递归子属性
if(newVal !== value) {
console.log('set',newVal) // 监听
render()
value = newVal
// 执行 watcher 的新方法
dp.notify() // 新增
}
}
})
}
class Vue{
constructor(options) {
this._data = options.data;
observe(this._data)
// 新建一个 Watcher 对象,这时候 Dep.target 会指向这个 Watcher 对象
new Watcher()
console.log("模拟视图渲染")
}
}
当 render function 被渲染的时候,读取所需对象的值,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中,之后如果去修改对象的值,则会触发 reactiveSetter 方法,通知 Dep 类调用 nofity 来触发所有 Watcher 对象的 update 方法更新对应视图。
完整流程:
- 在 new Vue() 后,Vue 会调用 _init 函数进行初始化,也就是 init 过程,在这个过程 Data 通过 Observe 转换成了 getter/setter 的形式,来对数据追踪变化,当被设置的对象被读取的时候执行 getter 函数,而在被赋值的时候执行 setter 函数。
- 当外界通过 Watcher 读取数据时,会触发 getter 从而将 Watcher 添加到依赖中。
- 在修改对象的值过程中,会触发对应的 setter,setter 通知之前依赖收集到得到的 Dep 中的每一个 Watcher,告诉他们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图。
完整详细代码:
const Observe = function(data) {
console.log(1) // 4 new Vue 开始执行
// 循环修改为每个属性添加get set
for(let key in data){
defineReactive(data,key)
}
}
const defineReactive = function(obj,key){
console.log(2) // 5 new Vue 开始执行
// 局部变量dep,用于get set内部调用
const dep = new Dep()
// 获取当前值
let val = obj[key]
Object.defineProperty(obj,key,{
// 设置当前描述属性为可被循环
enumerable:true,
// 设置当前描述属性可被修改
configurable:true,
get() {
console.log(3) // 10 19
console,log("in get")
// 调用依赖收集器中的 addSub,用于收集当前属性与 Watcher 的中依赖关系
dep.depend()
return val
}
set() {
console.log(4) // 15
if( newVal === val ) {
return
}
val = newVal
// 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
// 这里每个需要更新通过什么断定?dep.subs
dep.notify()
}
})
}
const observe = faunction(data) {
console.log(5) // 3 new Vue 开始执行
return new Observe(data)
}
const Vue = function(options) {
console.log(6) // 1 new Vue 开始执行
const self = this
// 将 data 赋值给 this._data,源码用的是 Proxy,这里用最简单的方式临时实现
if(options && typeof options.data === 'function') {
console.log(7) // 2 new Vue 开始执行
this._data = options.data.apply(this)
}
// 挂载函数
this.mount = function() {
console.log(8) // 7 new Vue以后,执行 vue.mounted()
new Watcher(self,self.render)
}
// 渲染函数
this.render = function() {
console.log(9) // 9 18 render函数执行后走到这里
with(self) {
_data.text // 这里取 data 值的时候,走 get 方法
}
}
// 监听this._data
observe(this._data) // new Vue 时候执行,这里执行完,就表示 new Vue 的过程执行完了
}
const Watcher = function(vm,fn) {
console.log(10) // 8 执行 vue.mounted()后走到这里
const self = this
this.vm = vm
// 将当前 Dep.target 指向自己
Dep.target = this
// 向 Dep 方法添加当前 Watcher
this.addDep = functhion(dep) {
console.log(11) // 13
dep.addSub(self)
}
// 更新方法,用于触发 vm._render
this.update = function() {
console.log(12) // 17
console.log("in watcher update")
fn()
}
// 这里会首次调用 vm._render,从而触发text 的 get
// 从而将当前的Watcher与Dep关联起来
this.value = fn() // 9 fn 是 render 函数,这里 fn()就会赋值的时候执行
// 这里清空了 Dep.target,防止 notify 触发时,不停地绑定 Watcher 与 Dep,造成代码死循环
Dep.target = null
}
const Dep = function() {
console.log(13) // 6 new Vue 时候会执行 new Dep,执行到这里
const self = this
// 收集目标
this.target = null
// 存储收集器中需要通知的 Watcher
this.subs = []
// 当有目标时,绑定 Dep 和 Watcher 的关系
this.depend = function() {
console.log(14) // 11 20 走了 get 获取属性后,就要进行依赖收集
if(Dep.target) {
console.log(15) // 12
// 这里可以直接写 self.addSub(Dep.target)
//
Dep.target.addDep(self)
}
}
// 为当前收集器添加Watcher
this.addSub = function(watcher) {
console.log(16) // 14
self.subs.push(watcher)
}
// 通知收集器中所的所有Wathcer,调用其update方法
this.notify = function() {
console.log(17) // 16
for(let i = 0.i<self.subs.length;i += 1) {
self.subs[i].update()
}
}
}
const vue = new Vue({
data() {
return {
text:"hello world"
}
}
})
vue.mount()
vue._data.text = "123"
解析:
- new Vue,执行 Vue 构造函数,打印6
- Vue 的入参 options 实际上是 { data(){} },是一个包含了 data 函数的对象,所以 options.data 是一个 data 函数,打印7,将 vue 中的 data 函数返回的数据赋值给 _data。
- 67行的observe,执行41行定义的地方
- 43行 new Observe 的时候执行第一行 Observe(关键函数),打印1。Observe 实际上就是给data数据都添加 get 和 set 方法,只不过添加方法被 defineReactive 抽离出去了
- 执行9行,执行 defineReactive,打印2,然后15行给每个属性加上 set 和 get 方法
- 执行12行,new Dep 的时候到95行执行 Dep,打印13。Dep 函数剩下的代码只是定义函数,不会执行,跳出 Dep 函数。回到13行,defineReactive 剩下的代码中的函数也不会执行,所以会回到67行 Observe,new Vue 就执行完了
- 执行135行的 vue.mount(),走到56行,打印8
- 执行 new Watcher 走到70行,打印10,然后Dep.target = this,这一步将 Watch 实例挂载到了 Dep的 target 属性上,关联起来
- 72到88行只是定义,没有执行,89行中 this.value = fn() 中:fn 实际是传进来的 render 函数(57行)然后后面又加了()就会立即执行。然后执行60行的 render 函数,打印9。Watcher 就执行完了,打印完9会继续往下走,读取_data.text。这一步会触发 get 方法(这一步目的就只是触发 get,所以获取值就可以了,并不需要其他操作)
- 执行21行的 get,打印3
- 执行25行的 dep.depend(),到104行,打印14
- 这时候判断 Dep.target,由于第8步将 Watch 挂载到了 Dep.target 上,所以为 true,打印15
- 执行110行,执行77行,打印11
- 执行114行,打印16,完成了依赖收集,然后会回到 Watch ,执行最后一行,Dep.target = null,避免进入死循环,然后 Watch 执行完(Vue.mount也执行完了)
- 136行赋值操作,执行28行 set,打印4
- 向下执行,36行,dep.notify(),执行119行,打印17
- 执行122行,触发 update,执行82行,打印12
- 执行 fn 函数,即 render 函数,执行60行,打印9
- 执行63行,取 data 值,走到 get,21行,打印3
- 执行25行,调到104行,打印14,Dep.target 为 null,15不会打印