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

相关推荐
资深前端之路2 分钟前
vue+three.js 五彩烟花效果封装+加载字体
前端·javascript·vue.js
alicema111130 分钟前
matlab+opencv车道线识别
前端·opencv·matlab
火星牛1 小时前
SPA模式下的es6如何加快宿主页的显示速度
前端·ecmascript·es6
疏狂难除1 小时前
【Tauri2】046—— tauri_plugin_clipboard_manager(一)
前端·clipboard·tauri2
污斑兔1 小时前
VMWare清理后,残留服务删除方案详解
前端
gong191723169672 小时前
非受控组件在React中的使用场景有哪些?
前端·javascript·react.js
TE-茶叶蛋2 小时前
React 常见的陷阱之(如异步访问事件对象)
前端·javascript·react.js
zhangpeng4555479402 小时前
C++编程起步项目
开发语言·前端·c++
秋田君3 小时前
构建安全的Vue前后端分离架构:利用长Token与短Token实现单点登录(SSO)策略
前端·javascript·vue.js·elementui·前端框架·mock·sso单点登录客户端
GISer_Jing3 小时前
Canvas &SVG &BpmnJS编辑器中Canvas与SVG职能详解
前端·javascript·编辑器