【手写 Vue2.x 源码】第二十二篇 - dep 和 watcher 关联

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

一前言

上篇主要介绍了 Vue 依赖收集的过程分析

本篇Vue 依赖收集的实现


二Watcher 部分

1watcher 的本质

之前分析可知

  • vm._render调用 render 方法
  • vm._update将虚拟节点更新到页面上

本质上vm._update(vm._render())就可以触发视图的更新

    let vm = new Vue({
      el: '#app',
      data() {
        return { name: "Brave" , age: 123}
      }
    }); 

    vm.name = "Brave Wang";   // 数据改变
    vm._update(vm._render()); // 视图更新

在 Vue 中数据更新

  • 每个数据有一个 dep 属性记录使用该数据的组件或页面的视图渲染函数 watcher
  • 当数据发生变化时dep 属性中存放的多个 watcher 将会被通知这里是观察者模式
  • 这里的 watcher 就相当于vm._update(vm._render())

2抽取视图更新逻辑 watcher

将视图渲染逻辑抽取成为可调用函数

export function mountComponent(vm) {
  // 抽取成为一个可被调用的函数
  let updateComponent = ()=>{
    vm._update(vm._render());  
  }
  updateComponent();
}

最终的目标是让updateComponent方法通过 watcher 被调用

3创建 Watcher 类

数据改变视图更新所以 watcher 应所属于响应式模块

创建 watcher 类

// src/observe/watcher.js

class Watcher {
  constructor(vm, fn, cb, options){
    this.vm = vm;
    this.fn = fn;
    this.cb = cb;
    this.options = options;

    this.getter = fn; // fn 为页面渲染逻辑
    this.get();
  }

  get(){
    this.getter();  // 调用页面渲染逻辑
  }
}
export default Watcher;

将页面更新逻辑 updateComponent 注入到 Watcher 类中
再考虑如何通过 watcher 调用页面更新方法 updateComponent

export function mountComponent(vm) {
  let updateComponent = ()=>{
    vm._update(vm._render());  
  }

  // 渲染 watcher 每个组件都有一个 watcher
  new Watcher(vm, updateComponent, ()=>{
    console.log('Watcher-update')
  },true)
}

4依赖收集的必要性

数据改变时会被劫持进入 Object.defineProperty 的 set 方法

那么如果在此时调用了视图更新逻辑是不是就可以做到“数据变化视图更新”了?

// src/observe/index.js#defineReactive
Object.defineProperty(obj, key, {
    get() {
      return value;
    },
    set(newValue) {
      if (newValue === value) return
      vm._update(vm._render());  // 当数据变化时触发视图更新
      observe(newValue);
      value = newValue;
    }
})

这样做虽然可以实现视图的更新

但是有一个严重的问题

  • 此时由于所有的响应式数据被修改时都会进入 set 方法
  • 导致未被视图使用的数据变化也会触发页面的更新
  • 即这种做法会触发不必要的视图更新

所以在视图渲染过程中被使用的数据需要被记录下来并且只针对这些数据的变化触发视图更新

这就需要做依赖收集需要创建为属性创建 dep 用来收集渲染 watcher


三Dep 部分

1创建 Dep 类

前面说过

  • 每一个数据都有一个 dep 属性用于存放对应的 watcher
  • 每一个 watcher 中也可能有多个 dep

所以Dep 类中要有一个添加 watcher 的方法Watcher 类中 也要有一个添加 dep 的方法

当数据变化时通知数据 dep 属性中的所有 watcher 执行视图更新应用了观察者模式

为了标识 Dep 的唯一性每次 new Dep时添加一个唯一 id

// src/observe/dep.js

let id = 0;

class Dep {
  constructor(){
    this.id = id++;
    this.subs = [];
  }
  // 保存数据的渲染 watcher
  depend(){
    this.subs.push(Dep.target)
  }
}

Dep.target = null;  // 静态属性

export default Dep

2为属性添加 dep 属性

Object.defineProperty时会为每个数据添加属性,在此时为属性添加 dep

function defineReactive(obj, key, value) {
  observe(value);
  let dep = new Dep();  // 为每个属性添加一个 dep
  Object.defineProperty(obj, key, {
    get() {
      return value;
    },
    set(newValue) {
      if (newValue === value) return
      observe(newValue);
      value = newValue;
    }
  })
}

当视图渲染时会走 Watcher 中的 get 方法即vm._update(vm._render())

此时利用 JS 的单线程特性在即将进行渲染前记录当前渲染 watcher

class Watcher {
  constructor(vm, fn, cb, options){
    this.vm = vm;
    this.fn = fn;
    this.cb = cb;
    this.options = options;

    this.getter = fn;
    this.get();
  }
  get(){
    Dep.target = this;  // 在触发视图渲染前将 watcher 记录到 Dep.target 上
    this.getter();      // 调用页面渲染逻辑
    Dep.target = null;  // 渲染完成后清除 Watcher 记录
  }
}
export default Watcher

在视图渲染的过程中将会触发数据的取值如vm.name

此时进入 Object.defineProperty 中 get 方法

所以如果 get 方法中 Dep.target 有值即当前 watcher就让数据的 dep 记住渲染 watcher

function defineReactive(obj, key, value) {
  observe(value);
  let dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
      if(Dep.target){
        dep.depend();
      }
      return value;
    },
    set(newValue) {
      if (newValue === value) return
      observe(newValue);
      value = newValue;
    }
  })
}

这样dep 会记住所有渲染 watcher未参与视图渲染的数据更新时不会触发视图更新


四结尾

本篇 dep 和 watcher 关联

  • 介绍了依赖收集的必要性
  • 介绍了 Watcher 和 Dep 的作用
  • 实现了 Watcher 类和 Dep 类
  • Watcher 和 Dep 如何产生关联

下一篇视图更新部分


维护日志

20210801修改目录结构将 Watcher 和 Dep 部分分离更新文章摘要

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: vue