前面我们学习了vue的响应式原理,我们知道了vue2底层是通过
Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些数据同样会出发setter导致重新渲染,所以vue在这里做了优化,通过收集依赖来判断哪些数据的变更需要触发视图更新。
如果这篇文章有帮助到你,??关注+点赞??鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新的文章~
我们先来考虑两个问题:
在视图渲染过程中,被使用的数据需要被记录下来,并且只针对这些数据的变化触发视图更新
这就需要做依赖收集,需要为属性创建 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都通知一遍,这里我自己画了一张更清晰的图:
这个类我们上一期已经实现过了,这一期我们主要增加的是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类的角色是一个订阅者,它主要作用是用来存放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类的角色是观察者,它关心的是数据,在数据变更之后获得通知,通过回调函数进行更新。
由上面的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 }}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__()原文首发地址点这里,欢迎大家关注公众号 「前端南玖」。
我是南玖,我们下一期见!!!
-------------------------------------------
个性签名:智者创造机会,强者把握机会,弱者坐等机会。做一个灵魂有趣的人!
如果觉得这篇文章对你有小小的帮助的话,可以关注下方公众号,可以超前阅读笔者的文章,谢谢~
欢迎加入前端技术交流群:928029210
