vue2源码学习--03实现依赖收集

前边我们已经实现数据的响应式和模板解析,并在模板解析的时候读取data的数据触发了get,本篇的目的是实现基本数据的依赖收集(老样子先不写数组),并在数据更新后自动更新视图。

Dep类和Watcher类

说过好多次在读取数据的时候会触发get方法,我们只要在get的时候把哪个方法读取了这个数据給记录下来然后在触发set的时候把记录的方法循环调用即可。

这个记录是给每个属性绑定一个dep,每个应用数据或者说将数据展示到页面上的操作都是通过watcher实现的,也就是dep去记住wahtcher。同时为了后续computed的实现,也需要watcher记住dep,也就是dep和watcher是多对多的互相记住的。

新建src/observe/dep.js和 src/observe/watcher.js 分别写Dep类和Watcher类

get的时候调用dep的depend方法,更新的时候调用notify方法。这里可以理解为一种闭包,每个属性都实例化一次Dep,set的时候调用的相同的一个Dep

js 复制代码
export function defineReactive(target, key, value) { 
  // 递归判断
  observe(value)
  let dep = new Dep()
  Object.defineProperty(target, key, {
      get() {
          // 可以进行调试
          console.log('被读取了')
          dep.depend() // 依赖收集
          return value
      },
      set(newValue) { 
          if(newValue === value) return 
          console.log('被修改了')
          //设置的新值也要进行劫持
          observe(newValue)
          value = newValue 
          dep.notify() // 数据更新
      }
  })
}
我们具体来实现Dep
js 复制代码
// 每个dep有自己的id 以此来作为唯一标识
let id = 0
class Dep {
    constructor() {
        this.id = id ++
        // 数组记录用到的watcher
        this.subs = [] // 对应所有的watcher
    }
    depend() {
        // 不希望放重复的watcher 
        // this.subs.push(Dep.target)
        Dep.target.addDep(this) // 让watcher记住dep 
    }
    // addSub是在watcher里调用的 目的是去重watcher和dep互相都不重复记录
    addSub(watcher) {
        this.subs.push(watcher )
    }
    // 通知更新
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}
// 静态属性这里在读取到get的时候将当前watcher存放到Dep的静态属性上 这样Dep可以调用 watcher的方法 
Dep.target = null
export default Dep

实现watcher之前,这里还要说明一下我们从模板解析到读取数据生成真实dom的操作,实际调用的是vm._update(vm._render())但是之前说的是用的watcher,所以在初次渲染的时候我们需要小做修改mountComponent

js 复制代码
export function mountComponent(vm, el) {
  vm.$el = el
  // 1.调用render产生虚拟节点 虚拟dom
  //vm.$options.render()  虚拟节点
  const updateComponent = () => {
      vm._update(vm._render())
  } 
  // 将初次渲染的的方法传给watcher,在watcher里调用
  new Watcher(vm, updateComponent,true) // true表示渲染watcher 第三个参数作为一个渲染watcher的标识

  // 2.根据虚拟dom产生真实dom

  // 3.插入到el元素中
}
watcher.js 先简单实现 不考虑watch和computed
js 复制代码
import Dep, { popTarget, pushTarget } from './dep'
let id = 0

// 1) 创建渲染watcher的时候 把当前watcher放到Dep.target
// 2) 调用_render() 的取值会走到 get上
class Watcher {
    constructor(vm, fn, options) {
        this.id = id++
        this.renderWatcher = options
        this.getter = fn
         
        this.deps = [] // 后续实现计算属性 和清理工作
        this.depsId = new Set()
        this.vm = vm
        if(options) this.get()
    }
    get() {
        // 打个log看效果
        console.log('更新了')
        Dep.target = this // 调用的时候把当前watcher赋值给Dep.target
        this.getter.call(this.vm) // 会去vm上取值
        Dep.target = null // 渲染完成置空
    } 
    addDep(dep) { //一个组件对应多个属性 ,重复属性不用记录 
        let id = dep.id
        if(!this.depsId.has(id)) {
            this.deps.push(dep)
            this.depsId.add(id)
            dep.addSub(this) // watcher已经记住dep了而且去重 此时让dep也记住watcher
        }
    }
    update() { 
        this.get()
    }
}
export default Watcher

检验成果

优化

最开始的目标已经达到,2秒后视图上的数据自动更新了

但是问题其实还是很明显的,当出现了多个变量都进行修改操作,我们调用了多次相同的更新方法,其原因是两个变量是在一个watcher上使用的,我们读两个变量的时候都进行了depend,更新的时候循两个变量都执行了notify,所以会调用两次相同的watcher,接下来我们来优化这里,同一次更新相同的watcher只执行一次。

js 复制代码
class Watcher{
    ...
    update() { 
        queueWatcher(this)
    }
    run() {
      this.get()
    }
}
// 这块也很好理解,就是传进来的watcher通过id去重,传进来一个如果不重复就放进队列里
// 然后就是用一个变量锁pending,一进来就关闭,然后中间是异步代码,期间多次执行update都只会往队列里放不重复的watcher,而不会执行中间的异步代码,执行完异步代码放开锁
let has = {}
let queue = []
let pending = false
function flushSchedulerQueue () {
  let flushQueue = queue.slice(0)
  queue = []
  has = {}
  pending = false
  flushQueue.forEach(q => q.run())
}
function queueWatcher(watcher) {
  const id = watcher.id
  if(!has[id]) {
      queue.push(watcher)
      has[id] = true
      // 不管watcher执行多少次 但是只执行一次刷新操作
      if(!pending) {
          setTimeout(flushSchedulerQueue,0) 
          pending = true
      }
  }
}

再看效果

ok,目的达成,效果实现。

遗留问题

但是还有个问题,就是这个异步更新用的setTimeout实现的,当我们更新了数据同时去获取dom,由于异步原因我们获取dom的操作是先于更新数据的,所以拿不到最新的dom,这就要我们再开一个异步去获取dom,如果我们用setTimeout还好两个宏任务按顺序执行也能拿到最新的dom,但是一旦用promise.then()去获取dom,这个微任务是早于宏任务执行的,这又会出现问题,所以下一篇写统一异步更新方法即 nextTick

相关推荐
jay神14 分钟前
基于 YOLOv8 的轴承缺陷检测(Web 版)
前端·人工智能·yolo
jay神22 分钟前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat26 分钟前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss1 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan2 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真2 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment2 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan2 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么2 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技3 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue