前面我们学习了vue的响应式原理,我们知道了vue2底层是通过
Object.defineProperty
来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些数据同样会出发setter导致重新渲染,所以vue在这里做了优化,通过收集依赖来判断哪些数据的变更需要触发视图更新。
前言
如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~
我们先来考虑两个问题:
1.我们如何知道哪里用了data里面的数据?
2.数据变更了,如何通知render更新视图?
在视图渲染过程中,被使用的数据需要被记录下来,并且只针对这些数据的变化触发视图更新
这就需要做依赖收集,需要为属性创建 dep 用来收集渲染 watcher
我们可以来看下官方介绍图,这里的collect as Dependency
就是源码中的dep.depend()
依赖收集,Notify
就是源码中的dep.notify()
通知订阅者
依赖收集中的各个类
Vue源码中负责依赖收集的类有三个:
Observer:可观测类
,将数组/对象转成可观测数据,每个Observer
的实例成员中都有一个Dep
的实例(上一篇文章实现过这个类)
Dep:观察目标类
,每一个数据都会有一个Dep
类实例,它内部有个subs队列,subs就是subscribers的意思,保存着依赖本数据的观察者
,当本数据变更时,调用dep.notify()
通知观察者
Watcher:观察者类
,进行观察者函数
的包装处理。如render()
函数,会被进行包装成一个Watcher
实例
依赖就是Watcher
,只有Watcher
触发的getter
才会收集依赖,哪个Watcher
触发了getter
,就把哪个watcher
收集到Dep
中。Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的watcher
都通知一遍,这里我自己画了一张更清晰的图:
Observer类
这个类我们上一期已经实现过了,这一期我们主要增加的是defineReactive
在劫持数据gētter
时进行依赖收集,劫持数据setter
时进行通知依赖更新,这里就是Vue收集依赖的入口
class Observer { constructor(v){
// 每一个Observer实例身上都有一个Dep实例
this.dep = new Dep()
// 如果数据层次过多,需要递归去解析对象中的属性,依次增加set和get方法
def(v,'__ob__',this) //给数据挂上__ob__属性,表明已观测
if(Array.isArray(v)) {
// 把重写的数组方法重新挂在数组原型上
v.__proto__ = arrayMethods
// 如果数组里放的是对象,再进行监测
this.observerArray(v)
}else{
// 非数组就直接调用defineReactive将数据定义成响应式对象
this.walk(v)
}
}
observerArray(value) {
for(let i=0; i<value.length;i++) {
observe(value[i])
}
}
walk(data) {
let keys = Object.keys(data); //获取对象key
keys.forEach(key => {
defineReactive(data,key,data[key]) // 定义响应式对象
})
}
}
function defineReactive(data,key,value){
const dep = new Dep() //实例化dep,用于收集依赖,通知订阅者更新
observe(value) // 递归实现深度监测,注意性能
Object.defineProperty(data,key,{
configurable:true,
enumerable:true,
get(){
//获取值
// 如果现在处于依赖的手机阶段
if(Dep.target) {
dep.depend()
}
// 依赖收集
return value
},
set(newV) {
//设置值
if(newV === value) return
observe(newV) //继续劫持newV,用户有可能设置的新值还是一个对象
value = newV
console.log('值变化了:',value)
// 发布订阅模式,通知
dep.notify()
// cb() //订阅者收到消息回调
}
})
}
将Observer
类的实例挂在__ob__
属性上,提供后期数据观察时使用,实例化Dep
类实例,并且将对象/数组
作为value属性保存下来 - 如果value是个对象,就执行walk()
过程,遍历对象把每一项数据都变为可观测数据(调用defineReactive
方法处理) - 如果value是个数组,就执行observeArray()
过程,递归地对数组元素调用observe()
。
Dep类(订阅者)
Dep
类的角色是一个订阅者
,它主要作用是用来存放Watcher
观察者对象,每一个数据都有一个Dep
类实例,在一个项目中会有多个观察者,但由于JavaScript是单线程的,所以在同一时刻,只能有一个观察者
在执行,此刻正在执行的那个观察者
所对应的Watcher
实例就会赋值给Dep.target
这个变量,从而只要访问Dep.target
就能知道当前的观察者是谁。
var uid = 0export default class Dep {
constructor() {
this.id = uid++
this.subs = [] // subscribes订阅者,存储订阅者,这里放的是Watcher的实例
}
//收集观察者
addSub(watcher) {
this.subs.push(watcher)
}
// 添加依赖
depend() {
// 自己指定的全局位置,全局唯一
//自己指定的全局位置,全局唯一,实例化Watcher时会赋值Dep.target = Watcher实例
if(Dep.target) {
this.addSub(Dep.target)
}
}
//通知观察者去更新
notify() {
console.log('通知观察者更新~')
const subs = this.subs.slice() // 复制一份
subs.forEach(w=>w.update())
}
}
Dep
实际上就是对Watcher
的管理,Dep
脱离Watcher
单独存在是没有意义的。
Dep
是一个发布者,可以订阅多个观察者,依赖收集之后Dep
中会有一个subs
存放一个或多个观察者,在数据变更的时候通知所有的watcher
。
Dep
和Observer
的关系就是Observer
监听整个data,遍历data的每个属性给每个属性绑定defineReactive
方法劫持getter
和setter
, 在getter
的时候往Dep
类里塞依赖(dep.depend)
,在setter
的时候通知所有watcher
进行update(dep.notify)
。
Watcher类(观察者)
Watcher
类的角色是观察者
,它关心的是数据,在数据变更之后获得通知,通过回调函数进行更新。
由上面的Dep
可知,Watcher
需要实现以下两个功能:
dep.depend()
的时候往subs里面添加自己
dep.notify()
的时候调用watcher.update()
,进行更新视图
同时要注意的是,watcher有三种:render watcher、 computed watcher、user watcher(就是vue方法中的那个watch)
var uid = 0import {parsePath} from "../util/index"
import Dep from "./dep"
export default class Watcher{
constructor(vm,expr,cb,options){
this.vm = vm // 组件实例
this.expr = expr // 需要观察的表达式
this.cb = cb // 当被观察的表达式发生变化时的回调函数
this.id = uid++ // 观察者实例对象的唯一标识
this.options = options // 观察者选项
this.getter = parsePath(expr)
this.value = this.get()
}
get(){
// 依赖收集,把全局的Dep.target设置为Watcher本身
Dep.target = this
const obj = this.vm
let val
// 只要能找就一直找
try{
val = this.getter(obj)
} finally{
// 依赖收集完需要将Dep.target设为null,防止后面重复添加依赖。
Dep.target = null
}
return val
}
// 当依赖发生变化时,触发更新
update() {
this.run()
}
run() {
this.getAndInvoke(this.cb)
}
getAndInvoke(cb) {
let val = this.get()
if(val !== this.value || typeof val == 'object') {
const oldVal = this.value
this.value = val
cb.call(this.target,val, oldVal)
}
}
}
要注意的是,watcher
中有个sync
属性,绝大多数情况下,watcher
并不是同步更新的,而是采用异步更新的方式,也就是调用queueWatcher(this)
推送到观察者队列当中,待nextTick
的时候进行调用。
这里的parsePath
函数比较有意思,它是一个高阶函数,用于把表达式解析成getter,也就是取值,我们可以试着写写看:
export function parsePath (str) { const segments = str.split('.') // 先将表达式以.切割成一个数据
// 它会返回一个函数
return obj = > {
for(let i=0; i< segments.length; i++) {
if(!obj) return
// 遍历表达式取出最终值
obj = obj[segments[i]]
}
return obj
}
}
Dep与Watcher的关系
watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者, dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。
总结
依赖收集
initState
时,对 computed
属性初始化时,触发 computed watcher
依赖收集
initState
时,对侦听属性初始化时,触发 user watcher
依赖收集(这里就是我们常写的那个watch)
render()
时,触发 render watcher
依赖收集
re-render
时,render()
再次执行,会移除所有 subs
中的 watcer
的订阅,重新赋值。
observe->walk->defineReactive->get->dep.depend()->watcher.addDep(new Dep()) ->
watcher.newDeps.push(dep) ->
dep.addSub(new Watcher()) ->
dep.subs.push(watcher)
派发更新
组件中对响应的数据进行了修改,触发defineReactive
中的 setter
的逻辑
然后调用 dep.notify()
最后遍历所有的 subs(Watcher 实例)
,调用每一个 watcher
的 update
方法。
set -> dep.notify() ->
subs[i].update() ->
watcher.run() || queueWatcher(this) ->
watcher.get() || watcher.cb ->
watcher.getter() ->
vm._update() ->
vm.__patch__()
推荐阅读
【Vue源码学习】响应式原理探秘
JS定时器执行不可靠的原因及解决方案
从如何使用到如何实现一个Promise
超详细讲解页面加载过程
原文首发地址点这里,欢迎大家关注公众号 「前端南玖」。
我是南玖,我们下一期见!!!
还没有评论,来说两句吧...