【手写 Vue2.x 源码】第二十二篇 - dep 和 watcher 关联
一,前言
上篇,主要介绍了 Vue 依赖收集的过程分析;
- 介绍了 Vue 的响应式特性
- 介绍了 Vue 的依赖收集过程
- 介绍了 dep 和 watcher 以及观察者模式;
本篇,Vue 依赖收集的实现
二,Watcher 部分
1,watcher 的本质
根据之前的分析:
vm._render
方法:调用render
方法,生成虚拟节点;vm._update
方法:将虚拟节点更新到页面上;
所以,从本质上来说,通过执行vm._update(vm._render())
就能够触发视图的更新:
断点测试vm._update(vm._render())
调用前后的视图更新情况:
// dist/index.html<div id="app"><li>{{name}}</li><li>{{age}}</li>
</div>let vm = new Vue({el: '#app',data() {return { name: "Brave" , age: 123 }}
}); // 主动调用测试视图更新
vm.name = "Brave Wang"; // 数据改变
debugger; // 点断:查看更新前后的页面数据变化
vm._update(vm._render()); // 视图更新
断点查看,更新前:
断点查看,更新后:
在Vue
中,数据更新的原理如下:
- 每个数据有一个
dep
属性:记录使用该数据的组件或页面的视图渲染函数watcher
; - 当数据发生变化时,
dep
属性中存放的多个watcher
将会被通知(观察者模式)
这里的
watcher
就相当于vm._update(vm._render())
因此,需要将视图渲染逻辑vm._update(vm._render())
,抽取为一个可单独被调用的函数;
2,抽取视图更新逻辑 watcher
将视图渲染逻辑抽取成为可调用函数,包装为function
:
export function mountComponent(vm) {// 抽取成为一个可被调用的函数let updateComponent = ()=>{vm._update(vm._render()); }// 调用视图渲染逻辑updateComponent();
}
接下来,只要能够通过watcher
来调用执行updateComponent
方法,就能够触发视图更新了;
3,创建 Watcher 类
“数据改变,视图更新”,所以
Watcher
类应从属于响应式模块;
创建watcher
类:src/observe/watcher.js
// src/observe/watcher.jsclass Watcher {constructor(vm, fn, cb, options){this.vm = vm;this.fn = fn;this.cb = cb;this.options = options;this.getter = fn; // fn 为页面渲染逻辑this.get(); // Watcher初始化时调用页面渲染逻辑}get(){this.getter();}
}export default Watcher;
Watcher
为什么使用类的方式来实现而不使用prototype
实现:
- 如果是一个整体的功能,那么优先采用类来实现;
- 如果希望将功能拆分到不同文件中,使用
prototype
来实现;
将页面的更新逻辑updateComponent
注入到Watcher
类中,
再考虑如何通过watcher
调用页面更新方法updateComponent
;
// src/lifecycle.jsexport function mountComponent(vm) {// 包装更新方法let updateComponent = ()=>{vm._update(vm._render()); }// 注入更新方法-渲染 watcher(每个组件都有一个渲染 watcher)new Watcher(vm, updateComponent, ()=>{console.log('Watcher-update')}, true)
}
4,依赖收集的必要性
提出问题:
- 由数据响应式原理可知,当响应式数据发生变化时,就会进入
Object.defineProperty
中的set
方法,- 那么,此时在
set
方法中调用视图更新逻辑vm._update(vm._render())
就能触发视图的更新操作;这样做,就实现了“数据变化,视图更新”;那么,是否还存在其他问题呢?
代码示例:
// src/observe/index.js#defineReactiveObject.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// dep 对象的唯一 id
let id = 0;class Dep {constructor(){this.id = id++;this.subs = [];}// 保存数据的渲染 watcherdepend(){this.subs.push(Dep.target)}
}// 静态属性,用于记录当前 watcher
Dep.target = null; export default Dep
2,为 data 中的属性添加 dep
在数据初始化过程中,通过Object.defineProperty
为每个数据添加属性时,为当前属性key
创建一个dep
实例:
// src/observe/index.jsfunction defineReactive(obj, key, value) {observe(value);let dep = new Dep(); // 为每个属性添加一个 depObject.defineProperty(obj, key, {get() {return value;},set(newValue) {if (newValue === value) returnobserve(newValue);value = newValue;}})
}
当视图渲染时,就会执行Watcher
中的get
方法,即执行了vm._update(vm._render())
;
这里,利用了JS
的单线程特性,在即将执行页面的渲染逻辑前,先将当前watcher
保存到Dep
类静态属性中,即Dep.target = this
:
// src/observe/watcher.jsclass 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
:
在数据渲染时,如果当前数据被视图所使用,当进入
Object.defineProperty
的get
方法时,Dep.target
有值且为当前watcher
对象,使用当前数据的dep
对象记住此渲染watcher
;
// src/observe/index.jsfunction defineReactive(obj, key, value) {observe(value);let dep = new Dep();Object.defineProperty(obj, key, {get() {// 如果 Dep.target 有值,将当前 watcher 保存到 depif(Dep.target){dep.depend(); }return value;},set(newValue) {if (newValue === value) returnobserve(newValue);value = newValue;}})
}
这样,dep
就“记住”了“自己”参与渲染的全部watcher
;当未参与视图渲染的数据更新时,由于dep
中并没有记录该watcher
,所以不会触发多余的视图更新操作;
四,结尾
本篇, dep 和 watcher 关联
- 介绍了依赖收集的必要性;
- 介绍了 Watcher 和 Dep 的作用;
- 实现了 Watcher 类和 Dep 类;
- Watcher 和 Dep 如何产生关联;
下一篇,视图更新部分
维护日志:
- 20210801:修改目录结构,将 Watcher 和 Dep 部分分离;更新文章摘要;
- 20230203:将必要过程进行拆解并添加图片说明;添加内容中的代码高亮;
- 20230207:添加部分注释,调整部分内容描述,使表达更加清晰、易懂;调整图片显示;
【手写 Vue2.x 源码】第二十二篇 - dep 和 watcher 关联相关推荐
- 前端进阶-手写Vue2.0源码(三)|技术点评
前言 今天是个特别的日子 祝各位女神女神节快乐哈 封面我就放一张杀殿的帅照表达我的祝福 哈哈 此篇主要手写 Vue2.0 源码-初始渲染原理 上一篇咱们主要介绍了 Vue 模板编译原理 它是 Vue ...
- 【手写 Vue2.x 源码】第二十八篇 - diff 算法-问题分析与 patch 优化
一,前言 首先,对 6 月的更文内容做一下简单回顾: Vue2.x 源码环境的搭建 Vue2.x 初始化流程介绍 对象的单层.深层劫持 数组的单层.深层劫持 数据代理的实现 对象.数组数据变化的观测 ...
- 【手写 Vue2.x 源码】第十八篇 - 根据 render 函数,生成 vnode
一,前言 上篇,介绍了 render 函数的生成,主要涉及以下两点: 使用 with 对生成的 code 进行一次包装 将包装后的完整 code 字符串,通过 new Function 输出为 ren ...
- 【手写 Vue2.x 源码】第十九篇 - 根据 vnode 创建真实节点
一,前言 上篇,根据 render 函数,生成 vnode,主要涉及以下几点: 封装 vm._render 返回虚拟节点 _s,_v,_c的实现 本篇,根据 vnode 虚拟节点渲染真实节点 二,根据 ...
- 【手写 Vue2.x 源码】第三十一篇 - diff 算法 - 比对优化(下)
一,前言 上篇,diff 算法-比对优化(上),主要涉及以下几个点: 介绍了如何对儿子节点进行比对: 新老儿子节点可能存在的 3 种情况及代码实现: 新老节点都有儿子时,diff 的方案介绍与处理逻辑 ...
- 【卷积神经网络CNN 实战案例 GoogleNet 实现手写数字识别 源码详解 深度学习 Pytorch笔记 B站刘二大人 (9.5/10)】
卷积神经网络CNN 实战案例 GoogleNet 实现手写数字识别 源码详解 深度学习 Pytorch笔记 B站刘二大人 (9.5/10) 在上一章已经完成了卷积神经网络的结构分析,并通过各个模块理解 ...
- android米聊手写和涂鸦源码,Android访米聊手写和涂鸦源码
Android访米聊手写和涂鸦源码 \请下载源代码,只上传Android访米聊手写和涂鸦源码源程序列表内容,如果需要此程序,请点击-下载,下载需要资料源代码. Android访米聊手写和涂鸦源码.ra ...
- 价值4500的国际版多语言点赞抖音分享点赞任务平台源码(十二种语言)
介绍: 平台会员分享给我的,他自己搭建成功了,测试可用!我就不测试了,需要的拿! 九种语言 :西班牙语,泰语.日语,印度尼西亚语言.越南语言.英文.繁体中文,简体中文,印度语 前台支持更换5种颜色风格 ...
- 【vue-router源码】十二、useRoute、useRouter、useLink源码分析
[vue-rouer源码]系列文章 [vue-router源码]一.router.install解析 [vue-router源码]二.createWebHistory.createWebHashHis ...
最新文章
- 命令行查看电脑WIFI密码
- 【每日一算法】相交链表
- NS_ASSUME_NONNULL_BEGIN 延伸
- 两个有序链表排序C语言,K个有序链表的归并排序(C语言)
- matlab中textread 函数
- 从零开始学习jQuery (八) 插播:jQuery实施方案
- mysql带参数的sql_MySql存储过程是带参数的存储过程(动态执行SQL语句)
- 使用Express和MongoDB构建CRUD应用程序-第2部分
- Linux 修改SSH端口 和 禁止Root远程登陆
- json转为tfrecord格式文件怎么转_word怎么转换成pdf格式?这样转很方便
- 使用RN开发App,引入图标失效问题的解决
- Genaro Network厚积薄发,开创区块链3.0新时代
- 中兴c语言 面试题,华为,英飞凌,中兴硬件工程师面试题
- JAVA提取纯文本_从常见文档中提取纯文本内容 | IT人生录
- jitsi-meet react 框架改造
- Jetpack-MVVM-高频提问和解答,附带学习经验
- 用树莓派DIY波士顿机器狗,帮你省下50万:教程开源,人人皆可上手
- 赛迪顾问看好中国信息安全市场稳步发展
- 南京网预赛 11 BY bly
- 智能小区java_java毕业设计_springboot框架的模式下的智能小区规划系统