使用Vue逐步实现Watch属性详解
目录
- watch
- 初始化watch
- deep、immdediate属性
- 结语
watch 对于
watch
的用法,在Vue
文档 中有详细描述,它可以让我们观察data
中属性的变化。并提供了一个回调函数,可以让用户在属性值变化后做一些事情。watch
对象中的value
分别支持函数、数组、字符串、对象,较为常用的是函数的方式,当想要观察一个对象以及对象中的每一个属性的变化时,便会用到对象的方式。下面是官方的一个例子,相信在看完之后就能对
watch
的几种用法有大概的了解:var vm = new Vue({data: {a: 1,b: 2,c: 3,d: 4,e: {f: {g: 5}}},watch: {a: function (val, oldVal) {console.log('new: %s, old: %s', val, oldVal)},// string method nameb: 'someMethod',// the callback will be called whenever any of the watched object properties change regardless of their nested depthc: {handler: function (val, oldVal) { /* ... */ },deep: true},// the callback will be called immediately after the start of the observationd: {handler: 'someMethod',immediate: true},// you can pass array of callbacks, they will be called one-by-onee: ['handle1',function handle2 (val, oldVal) { /* ... */ },{handler: function handle3 (val, oldVal) { /* ... */ },/* ... */}],// watch vm.e.f's value: {g: 5}'e.f': function (val, oldVal) { /* ... */ }}})vm.a = 2 // => new: 2, old: 1
初始化watch 在了解了
watch
的用法之后,我们开始实现watch
。文章图片
在初始化状态
initState
时,会判断用户在实例化Vue
时是否传入了watch
选项,如果用户传入了watch
,就会进行watch
的初始化操作:// src/state.jsfunction initState (vm) {const options = vm.$options; if (options.watch) {initWatch(vm); }}
initWatch
中本质上是为每一个watch
中的属性对应的回调函数都创建了一个watcher
:// src/state.jsfunction initWatch (vm) {const { watch } = vm.$options; for (const key in watch) {if (watch.hasOwnProperty(key)) {const userDefine = watch[key]; if (Array.isArray(userDefine)) { // userDefine是数组,为数组中的每一项分别创建一个watcheruserDefine.forEach(item => {createWatcher(vm, key, item); }); } else {createWatcher(vm, key, userDefine); }}}}
【使用Vue逐步实现Watch属性详解】
createWatcher
中得到的userDefine
可能是函数、对象或者字符串,需要分别进行处理:function createWatcher (vm, key, userDefine) {let handler; if (typeof userDefine === 'string') { // 字符串,从实例上取到对应的methodhandler = vm[userDefine]; userDefine = {}; } else if (typeof userDefine === 'function') { // 函数handler = userDefine; userDefine = {}; } else { // 对象,userDefine中可能会包含用户传入的deep,immediate属性handler = userDefine.handler; delete userDefine.handler; }// 用处理好的参数调用vm.$watchvm.$watch(key, handler, userDefine); }
createWatcher
中对参数进行统一处理,之后调用了vm.$watch
,在vm.$watch
中执行了Watcher
的实例化操作:export function stateMixin (Vue) {// some code ...Vue.prototype.$watch = function (exprOrFn, cb, options) {const vm = this; const watch = new Watcher(vm, exprOrFn, cb, { ...options, user: true }); }; }
此时
new Watcher
时传入的参数如下:vm
: 组件实例exprOrFn
:watch
选项对应的key
cb
:watch
选项中key
对应的value
中提供给用户处理逻辑的回调函数,接收key
在data
中的对应属性的旧值和新值作为参数options
:{user: true, immediate: true, deep: true}
,immediate
和deep
属性当key
对应的value
为对象时,用户可能会传入
Watcher
中会判断options
中有没有user
属性来区分是否是watch
属性对应的watcher
:class Watcher {constructor (vm, exprOrFn, cb, options = {}) {this.user = options.user; if (typeof exprOrFn === 'function') {this.getter = this.exprOrFn; }if (typeof exprOrFn === 'string') { // 如果exprFn传入的是字符串,会从实例vm上进行取值this.getter = function () {const keys = exprOrFn.split('.'); // 后一次拿到前一次的返回值,然后继续进行操作// 在取值时,会收集当前Dep.target对应的`watcher`,这里对应的是`watch`属性对应的`watcher`return keys.reduce((memo, cur) => memo[cur], vm); }; }this.value = https://www.it610.com/article/this.get(); }get () {pushTarget(this); const value = this.getter(); popTarget(); return value; }// some code ...}
这里有俩个重要的逻辑:
- 由于传入的
exprOrFn
是字符串,所以this.getter
的逻辑就是从vm
实例上找到exprOrFn
对应的值并返回 - 在
watcher
实例化时,会执行this.get
,此时会通过this.getter
方法进行取值。取值就会触发对应属性的get
方法,收集当前的watcher
作为依赖 - 将
this.get
的返回值赋值给this.value
,此时拿到的就是旧值
set
方法,进而执行收集的watch
对应的watcher
的update
方法:class Watcher {// some code ...update () {queueWatcher(this); }run () {const value = https://www.it610.com/article/this.get(); if (this.user) {this.cb.call(this.vm, value, this.value); this.value = value; }}}
和渲染
watcher
相同,update
方法中会将对应的watch watcher
去重后放到异步队列中执行,所以当用户多次修改watch
属性观察的值时,并不会不停的触发对应watcher
的更新操作,而只是以它最后一次更新的值作为最终值来执行this.get
进行取值操作。当我们拿到观察属性的最新值之后,执行
watcher
中传入的回调函数,传入新值和旧值。下面画图来梳理下这个过程:
文章图片
deep、immdediate属性 当用户传入
immediate
属性后,会在watch
初始化时便立即执行对应的回调函数。其具体的执行位置是在Watcher
实例化之后:Vue.prototype.$watch = function (exprOrFn, cb, options) {const vm = this; const watcher = new Watcher(vm, exprOrFn, cb, { ...options, user: true }); if (options.immediate) { // 在初始化后立即执行watchcb.call(vm, watcher.value); }};
此时
watcher.value
是被观察的属性当前的值,由于此时属性还没有更新,所以老值为undefined
。如果
watch
观察的属性为对象,那么默认对象内的属性更新,并不会触发对应的回调函数。此时,用户可以传入deep
选项,来让对象内部属性更新也调用对应的回调函数:class Watcher {// some code ...get () {pushTarget(this); const value = https://www.it610.com/article/this.getter(); if (this.deep) { // 继续遍历value中的每一项,触发它的get方法,收集当前的watchertraverse(value); }popTarget(); return value; }}
当用户传入
deep
属性后,get
方法中会执行traverse
方法来遍历value
中的每一个值,这样便可以继续触发value
中属性对应的get
方法,为其收集当前的watcher
作为依赖。这样在value
内部属性更新时,也会通知其收集的watch watcher
进行更新操作。traverse
的逻辑只是递归遍历传入数据的每一个属性,当遇到简单数据类型时便停止递归:// traverse.js// 创建一个Set,遍历之后就会将其放入,当遇到环引用的时候不会行成死循环const seenObjects = new Set(); export function traverse (value) {_traverse(value, seenObjects); // 遍历完成后,清空SetseenObjects.clear(); }function _traverse (value, seen) {const isArr = Array.isArray(value); const ob = value.__ob__; // 不是对象并且没有被观测过的话,终止调用if (!isObject(value) || !ob) {return; }if (ob) {// 每个属性只会有一个在Observer中定义的depconst id = ob.dep.id; if (seen.has(id)) { // 遍历过的对象和数组不再遍历,防止环结构造成死循环return; }seen.add(id); }if (isArr) {value.forEach(item => {// 继续遍历数组中的每一项,如果为对象的话,会继续遍历数组的每一个属性,即对对象属性执行取值操作,收集watch watcher_traverse(item, seen); }); } else {const keys = Object.keys(value); for (let i = 0; i < keys.length; i++) {// 继续执行_traverse,这里会对 对象 中的属性进行取值_traverse(value[keys[i]], seen); }}}
需要注意的是,这里利用
Set
来存储每个属性对应的dep
的id
。这样当出现环时,Set
中已经存储过了其对应dep
的id
,便会终止递归。结语 本文一步步实现了
Vue
的watch
属性,并对内部的实现逻辑提供了笔者相应的理解 。到此这篇关于使用Vue逐步实现Watch属性详解的文章就介绍到这了,更多相关Vue Watch属性内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!推荐阅读
- C++封装静态链接库和使用的详细步骤
- Vue与Axios的传参方式实例详解
- vue长按事件touch示例详解
- 使用Topshelf框架操作Windows服务
- 数据库(mysql命令3及pymsql的使用)---12.20
- 盘点Vue2和Vue3的10种组件通信方式(值得收藏)
- 使用mindspore将pkl文件转为onnx时报错
- GCD-dispatch_group的使用
- VUE实现Studio管理后台(十)(OptionBox,一个综合属性输入界面,可以级联重置)
- LayoutInflater-使用