首页 > 编程语言 >vue2源码-八、依赖收集的过程

vue2源码-八、依赖收集的过程

时间:2023-04-18 19:46:29浏览次数:34  
标签:收集 渲染 dep vm 视图 watcher Dep 源码 vue2

依赖收集的过程

  • 前言

    使用真实节点替换原始节点,主要涉及以下步骤:

    1.新老节点的更新方案。

    2.虚拟节点与真实节点映射。

    3.实现新老节点的替换。

  • 依赖收集

    已经完成了Vue的两大核心部分:响应式数据和数据渲染,即完成了整个Vue的初始化流程:

    new Vue()时,执行_init初始化,通过mountComponent做组件的挂载:

    1.vm._render:调用render方法,创建新节点。

    2.vm._render:更新逻辑,将虚拟节点渲染成真实DOM

    patch:根据虚拟节点生成真实节点,新节点替换老节点

    Vue特性:当响应式数据发生变化时,会触发对应视图的更新。

    举个例子:同一数据可能被放到多个视图中(页面或者组件)所共享,比如Vuex中的数据:

    A组件,使用了数据name;

    B组件,使用了数据name

    这样,A,B两个组件就都依赖了数据name,当数据发生变化时,两个组件都会触发对应视图更新操作。

    这就需要知道数据和视图间的对应关系,从而准确触发该数据对应的视图更新操作,从而设计模式上看就是观察者模式。

    重点:depwatcher

    Vue中,依赖收集的实现使用了观察者模式:

    • watcher函数:每个组件或者页面所对应的渲染函数
    • dep属性:每个数据都具有一个dep属性,用于记录使用该数据的组件或页面的视图渲染函数watcher

    当数据发生变化时,dep属性中存放的多个watcher将会被通知,watcher通过调用自身对应的更新方法update,完成页面的重新渲染:

    • name添加属性dep:用于收集组件A和组件B的渲染逻辑watcherAwatcherB
    • watcherAwatcherB添加各自的更新方法update
    • 当数据发生变化时,通知dep中存放的watcherAwatcherB触发各自的更新方法update

    之前的内容;

    • 由于vm._update(vm._render)执行了数据渲染和更新操作
    • 所以watcher中的update方法,便触发vm._update(vm._render())重新进行数据渲染和视图更新。
    • 所以,需要将vm._update(vm._render())改造为可以通过watcher调用的方法。

    最后:

    • 数据响应式过程中,为每个属性扩展dep,用于收集watcher,在数据渲染时记录watcher
    • 当同一数据在同一视图中被多次使用时,在dep中需要对watcher进行查重,确保watcher进行查重,确保相同watcher仅记录一次。
    • 防止只要数据变化就会渲染视图的情况:当数据在视图中没有被使用时,数据的变化不应触发watcher渲染,需要在视图渲染时进行依赖收集,知道哪些数据被“真正”使用了;

dep和watcher关联

  • watcher部分

    根据上篇

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

    所以就是通过执行vm._update(vm._render())就能触发视图的更新。

    vue中,数据更新的原理如下:

    • 每个数据都有一个dep属性:记录使用改数据的组件或页面的视图渲染函数watcher
    • 当数据发生变化时:dep属性中存放的多个watcher将会被通知(观察者模式)。

    这里的watcher就相当于vm._update(vm._render())

    因此,需要将视图渲染逻辑vm._update(_render()),抽取为一个可单独调用的函数。

    抽取视图更新逻辑watcher

    将视图渲染逻辑抽取成为可调用函数,包装为function:

    export function mountComponent(vm, el) {
      // 1.调用render方法产生虚拟节点虚拟DOM
      vm.$el = el;
    
      const updateComponent = () => {
        vm._update(vm._render());
      };
    
      const watcher = new Watcher(vm, updateComponent, true);
      // vm._update(vm._render()); // vm.$options.render
      // 2.根据虚拟DOM产生真实DOM
      // 3.插入el元素中
    }
    

    接下来,只要能够通过watcher来调用执行updateComponent方法,就可以触发视图更新。

    创建watcher

    “数据改变,视图更新”,所以Watcher类应从属响应式模块;

    class Watcher {
    
      constructor(vm, fn, options){
        this.vm = vm;
        this.fn = fn;
        this.options = options;
    
        this.getter = fn; // fn 为页面渲染逻辑
        this.get();       // Watcher初始化时调用页面渲染逻辑
      }
    
      get(){
        this.getter();
      }
    }
    
    export default Watcher;
    

    收集依赖的必要性

    做法:

    • 有数据响应式原理可知,当响应式数据发生变化时,就会进入Object.definePropertyset方法。
    • 那么,此时在set方法中调用视图更新逻辑vm._update(vm.render())就能触发图的更新操作。

    问题:

    • 由于所有的响应式数据被修改时都会进入到set方法,这就将会导致未被视图使用的数据发生变化时也会触发页面的更新。
    • 这种做法会触发不必要的视图更新,造成多余的性能开销。

    针对上面,就需要进行依赖收集操作,为数据创建dep用来收集渲染watcher

  • Dep部分

    创建Dep类

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

    所以:

    • dep类中,需要具有一个添加watcher的方法;
    • watcher类中,也需要有一个添加dep的方法。

    dep

    // src/observe/dep.js
    
    // dep 对象的唯一 id
    let id = 0;
    
    class Dep {
    
      constructor(){
        this.id = id++;
        this.subs = [];
      }
    
      // 保存数据的渲染 watcher
      depend(){
        this.subs.push(Dep.target)
      }
    }
    
    // 静态属性,用于记录当前 watcher
    Dep.target = null;  
    
    export default Dep
    

    为data中的属性添加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

    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
    

    在数据渲染时,如果当前数据被视图所使用,当进入Object.definePropertyget方法时,Dep.target有值且为当前watcher对象,使用当前数据的dep对象记住此渲染watcher

    function defineReactive(obj, key, value) {
    
      observe(value);
      let dep = new Dep();
    
      Object.defineProperty(obj, key, {
        get() {
          // 如果 Dep.target 有值,将当前 watcher 保存到 dep
          if(Dep.target){
            dep.depend(); 
          }
          return value;
        },
    
        set(newValue) {
          if (newValue === value) return
          observe(newValue);
          value = newValue;
        }
      })
    }
    

视图更新部分

  • 前言

    上篇,主要介绍了依赖收集过程中depwatcher关联:

    利用js单线程特性,在watcher类中get方法,即将触发视图更新前,利用全局的类静态树丛Dep.target记录Watcher实例
    并且,在试图渲染的取值过程中,在Object.defineProperty的get方法中,让数据dep记住渲染watcher,从而,实现了dep与watcher相关联,只有参与视图渲染的数据发生变化才会触发视图更新。
    
  • 实现视图更新逻辑

    查重watcher

    问题:同一数据在视图中多次使用会怎么样?

    按照当前逻辑,同一数据在一个视图中被多次使用时,相同watcher会在dep中被重复保存多次:

    <div id="app">
      <li>{{name}}</li>
      <li>{{name}}</li>
      <li>{{name}}</li>
    </div>
    

    name属性的dep中,将会保存三个相同的渲染watcher,所以需要对watcher进行查重。

    因此需要设置一个id作为标识符,每次new Watcherid自增,因此作为标识对watcher实例进行查重。

      constructor(vm, fn, options) {
        this.id = id++; // 创建时递增
        this.renderWatcher = options;
        this.getter = fn; // getter意味着调用这个函数可以发生取值操作
        this.deps = []; // 后续实现计算属性和一些青理工作需要用
        this.depsId = new Set();
        this.get();
      }
    

    让watcher也记住dep

    前面,让数据dep记住了渲染watcher,同样的,watcher也有必要记住dep

    let id = 0;
    class Dep {
      constructor() {
        this.id = id++; // 属性dep要收集watcher
        this.subs = []; // 这里存放着当前属性对应的watcher有哪些
      }
      depend() {
        // 这里我们不希望放重复的watcher,而且刚才只是单向的关系 dep->watcher
        // watcher记录dep
        // this.subs.push(Dep.target);
        Dep.target.addDep(this); // 让watcher记住dep
      }
      // 让dep记住watcher-在watcher中被调用
      addSub(watcher) {
        this.subs.push(watcher);
      }
    
      notify() {
        this.subs.forEach((watcher) => {
          watcher.update(); // 告诉watcher去更新
        });
      }
    }
    
    Dep.target = null;
    
    export default Dep;
    

    这里,如果互相记住,watcher中要对dep查重,dep中也要对watcher查重;

    用这个方法,使depwatcher关联起来,只需要判断一次就可以了。

    import Dep from "./dep";
    
    let id = 0;
    
    // 1)当我们创建渲染watcher的时候我们会把当前的渲染watcher放到Dep.target中
    // 2)调用_render()会取值走到get上
    
    // 不同的组件有不同的watcher,目前只有一个渲染根实例的
    class Watcher {
      constructor(vm, fn, options) {
        this.id = id++;
        this.renderWatcher = options;
        this.getter = fn; // getter意味着调用这个函数可以发生取值操作
        this.deps = []; // 后续实现计算属性和一些青理工作需要用
        this.depsId = new Set();
        this.get();
      }
    
      addDep(dep) {
        let id = dep.id;
        // dep查重
        if (!this.depsId.has(id)) {
          // 让watcher记住dep
          this.deps.push(dep);
          this.depsId.add(id);
          // 让dep也记住watcher
          dep.addSub(this); // watcher已经记住了dep并且去重了,此时让dep也记住了watcher
        }
      }
    
      get() {
        Dep.target = this; // 静态属性就只有一份
        this.getter(); // 会去vm上取值
        Dep.target = null; // 渲染完之后就清空
      }
      update() {
        // this.get(); // 重新渲染
        queueWatcher(this); // 先把当前的watcher暂存起来
      }
      run() {
        this.get();
      }
    }
    

    这样实现,会让depwatcher保持一种共存关系。

    如果watcher中存在dep,那么dep中一定存在watcher,反之,亦然。

    所以,只需要判断一次,就能够完成depwatcher查重。

    数据改变触发视图更新

    当视图改变的时候,会进入Object.definePropertyset方法。

    因此,需要在set方法中,通知dep中所有收集的wathcer执行视图更新方法:

    function defineReactive(obj, key, value) {
    
      observe(value);
      let dep = new Dep();  // 为每个属性添加一个 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.notify(); 
        }
      })
    }
    

    Dep中添加notify方法:

      notify() {
        this.subs.forEach((watcher) => {
          watcher.update(); // 告诉watcher去更新
        });
      }
    

    Watcher中添加update方法

      get(){
        Dep.target = this;
        this.getter();
        Dep.target = null;
      }
      // 执行视图渲染逻辑
      update(){
        this.get();
      }
    

    结尾

    Vue依赖收集的视图更新部分,主要涉及以下几点:

    视图初始化:

    • render方法中会进行取值操作,进入Object.definepropertyget方法。
    • get方法中为数据添加dep,并记录当前的渲染的watcher
    • 记录方式:watcher查重并记住depdep再记住watcher

    数据更新时:

    • 当数据发生改变,会进入Object.definePropertyset方法。
    • set方法中,使dep中收集的全部watcher执行视图渲染操作watcher.get()
    • 在视图渲染前(this.getter方法执行前),通过dep.target记录当前渲染的watcher
    • 重复视图初始化流程

标签:收集,渲染,dep,vm,视图,watcher,Dep,源码,vue2
From: https://www.cnblogs.com/dgqp/p/17330837.html

相关文章

  • OpenHarmony源码解析之系统服务管理子系统
    1预备知识Linux中主要的IPC机制有:管道(pipe)、信号(signal)、信号量(semophore)、消息队列(Message)、共享内存(ShareMemory)、套接字(Socket)等。OpenHarmony基于binder驱动封装了一套ipc机制(foundation\communication\ipc)用于实现设备内的跨进程通信。Binder机制通常采用客户端-服务器(Cli......
  • pandas读取Excel核心源码剖析,面向过程仿openpyxl源码实现Excel数据加载
    今天我们将研究pandas如何使用openpyxl引擎读取xlsx格式的Excel的数据,并考虑以面向过程的形式简单的自己实现一下。截止目前本人所使用的pandas和openpyxl版本为:pandas:1.5.2openpyxl:3.0.10今天所有的测试全部基于以下文件:pandas的read_excel核心代码这里我使用pycharm工具对以下代......
  • 客服系统vue源码聊天界面,ajax上传图片功能实现
    在线客服系统的聊天界面上,有上传图片按钮功能,使用js实现ajax上传图片功能html部分,有一个点击事件<divclass="iconExtendBtn"@click="uploadImg"><divclass="elIconel-icon-picture"></div>......
  • 【Vue2.x源码系列06】计算属性computed原理
    上一章Vue2异步更新和nextTick原理,我们介绍了JavaScript执行机制是什么?nextTick源码是如何实现的?以及Vue是如何异步更新渲染的?本章目标计算属性是如何实现的?计算属性缓存原理-带有dirty属性的watcher洋葱模型的应用初始化在Vue初始化实例的过程中,如果用户options选......
  • Spring源码分析——BeanFactory体系之接口详细分析
    Spring的BeanFactory的继承体系堪称经典。这是众所周知的!作为Java程序员,不能错过!前面的博文分析了Spring的Resource资源类Resouce。今天开始分析Spring的IOC部分。众所周知,IOC是Spring框架最迷人的地方。它最重要的接口,就是BeanFactory了。BeanFactory有着庞大的继承、实现......
  • 【内附源码和文档】基于C++14异步蒙特卡洛工具函数
    Simple-Monte-Carlo-Tool-Function这是一个使用C++实现的简单的异步蒙特卡洛算法工具函数C++标准:C++14使用autores=MonteCarlo(sample_nums,check_sample_funtion,generate_sample_funtion,…args);doublep=res.get();std::cout<<p<<std::endl;sample_nums:需要生成的样......
  • 如何运行编译.NetCore的源码?
    作为.net的开发人员,为了能更好的code,我们要知其然并知其所以然,了解.netcore的源码是我们的基本素养✊源码地址.NETPlatform(github.com)这个是.net在github上开源的源码地址aspnetcore这个是.netcore的源码地址构建方法构建有几点需要注意一下:构建比较费时间,可以......
  • 直播app源码,使用vue-awesome-swiper创建轮播图幻灯片
    直播app源码,使用vue-awesome-swiper创建轮播图幻灯片1.引入引入方式可以参考官方文档,两种方式选一种即可:vue-awesome-swiperatv3.1.3 (1)第一种方式:在main.js入口文件中全局引入 ///src/main.js //swiper全局引入importVueAwesomeSwiperfrom'vue-awesome-swiper'im......
  • app直播源码,Node.js实现密码散列加密
    app直播源码,Node.js实现密码散列加密1.安装所需的包: npmibcryptjs--save​2.修改MongoDB中的模型: ///models/AdminUser.js constmongoose=require('mongoose')//定义模型的字段constschema=newmongoose.Schema({  username:{//用户名    ty......
  • 从源码角度深入解析Callable接口
    摘要:从源码角度深入解析Callable接口,希望大家踏下心来,打开你的IDE,跟着文章看源码,相信你一定收获不小。本文分享自华为云社区《一个Callable接口能有多少知识点?》,作者:冰河。并发编程一直是程序员们比较头疼的,如何编写正确的并发程序相比其他程序来说,是一件比较困难的事情,并发编......