前言
随着前面的学习,我对整个vue框架有了更深的理解,接下来学习的是计算属性computed的原理和侦听器watch的原理,这两基本也是在工作用的比较多的了。
侦听属性watch的原理
主要实现方式:当定义一个侦听属性时,Vue会在内部创建一个Watcher实例来监视指定的数据。这个Watcher实例会在初始化时获取初始值,并将自身添加到数据的依赖列表中。当被侦听的数据发生变化时,Watcher实例会被通知,并触发相应的回调函数。
1.侦听属性的初始化
在Vue实例化(initMixin(vue)的_init)时,会对侦听器进行初始化在initState中调用initWatch。在initWatch中Watch会对数组进行遍历处理,然后才调用createWatcher,通过原型方法$watch传入处理参数创建一个观察者收集依赖变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
export function initState(vm) { const opts = vm.$options; if (opts.watch) { initWatch(vm); } }
function initWatch(vm) { let watch = vm.$options.watch; for (let k in watch) { const handler = watch[k]; if (Array.isArray(handler)) { handler.forEach((handle) => { createWatcher(vm, k, handle); }); } else { createWatcher(vm, k, handler); } } }
function createWatcher(vm, exprOrFn, handler, options = {}) { if (typeof handler === "object") { options = handler; handler = handler.handler; } if (typeof handler === "string") { handler = vm[handler]; } return vm.$watch(exprOrFn, cb, options); }
|
2.$watch
1 2 3 4 5 6 7 8 9 10 11
| import Watcher from "./observer/watcher"; Vue.prototype.$watch = function (exprOrFn, cb, options) { const vm = this; let watcher = new Watcher(vm, exprOrFn, cb, { ...options, user: true }); if (options.immediate) { handler(); } };
|
3.Watcher
在之前写的Watcher中,通过调用 get 方法获取并保存一次旧值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
|
import { pushTarget, popTarget } from "./dep";
let id = 0;
export default class Watcher { constructor(vm, exprOrFn, cb, options) { this.vm = vm; this.exprOrFn = exprOrFn; this.cb = cb; this.options = options; this.id = id++; this.deps = []; this.depsId = new Set(); this.user = options.user; if (typeof exprOrFn === "function") { this.getter = exprOrFn; } this.get(); } get() { pushTarget(this); this.getter(); popTarget(); }, set(newValue) { if (newValue === value) return; observe(newValue); value = newValue; dep.notify(); } addDep(dep) { let id = dep.id; if (!this.depsId.has(id)) { this.depsId.add(id); this.deps.push(dep); dep.addSub(this); } } update() { this.get(); } run() { const newVal = this.get(); const oldVal = this.value; this.value = newVal; if (this.user) { if (newVal !== oldVal || isObject(newVal)) { this.cb.call(this.vm, newVal, oldVal); } } else { this.cb.call(this.vm); } } }
|
计算属性computed的原理
具体原理如下:
- 在Vue实例化时,会对计算属性进行初始化。计算属性的定义包括一个计算函数和一个缓存属性。
- 当计算属性被访问时,会触发计算函数的执行。在计算函数中,可以访问其他响应式数据。
- 计算函数会根据依赖的响应式数据进行计算,并返回计算结果。
- 计算属性会将计算结果缓存起来,下次访问时直接返回缓存的值。
- 当依赖的响应式数据发生变化时,计算属性会被标记为”dirty”(脏),下次访问时会重新计算并更新缓存的值。
通过计算属性,我们可以将复杂的逻辑封装成一个属性,并在模板中直接使用。计算属性会自动追踪依赖的数据,并在需要时进行更新,提供了一种简洁和高效的方式来处理衍生数据。
注意: 计算属性适用于那些依赖其他响应式数据的场景,而不适用于需要进行异步操作或有副作用的场景。对于这些情况,可以使用侦听器(watcher)或使用methods来处理。
书接上文,在Vue实例化(initMixin(vue)的*_init*)时,不仅会对侦听器进行初始化同时也会对计算属性进行初始化。
1.计算属性初始化
在initComputed函数中,遍历计算属性对象,为每个计算属性创建一个Watcher实例,并将其存储在vm._computedWatchers中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| export function initState(vm) { const opts = vm.$options; if (opts.watch) { initWatch(vm); } if(opts.computed) { initComputed(vm) } } function initComputed(vm) { const computed = vm.$options.computed; const watchers = vm._computedWatchers = Object.create(null); for (const key in computed) { const userDef = computed[key]; const getter = typeof userDef === 'function' ? userDef : userDef.get; watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true }); if (!(key in vm)) { defineComputed(vm, key, userDef); } } }
|
2.对计算属性进行属性劫持
defineComputed 方法主要是重新定义计算属性,其实最主要的是劫持get方法也就是计算属性依赖的值。
为啥要劫持呢? 因为我们需要根据依赖值是否发生变化来判断计算属性是否需要重新计算
createComputedGetter方法就是判断计算属性依赖的值是否变化的核心了 我们在计算属性创建的Watcher增加dirty标志位,如果标志变为true代表需要调用watcher.evaluate来进行重新计算了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| function defineComputed(target, key, userDef) {
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }; if (typeof userDef === 'function') { sharedPropertyDefinition.get = createComputedGetter(key); } else { sharedPropertyDefinition.get = createComputedGetter(key); sharedPropertyDefinition.set = userDef.set; } Object.defineProperty(target, key, sharedPropertyDefinition); }
function createComputedGetter(key) { return function computedGetter() { const watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value; } }; }
|
3.watcher的改造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
|
export default class Watcher { constructor(vm, exprOrFn, cb, options) { this.lazy = options.lazy; this.dirty = this.lazy;
this.value = this.lazy ? undefined : this.get(); } get() { pushTarget(this); const res = this.getter.call(this.vm); popTarget(); return res; } update() { if (this.lazy) { this.dirty = true; } else { queueWatcher(this); } } evaluate() { this.value = this.get(); this.dirty = false; } depend() { let i = this.deps.length; while (i--) { this.deps[i].depend(); } } }
|
小结
计算属性computed和侦听属性watch还是有很大区别的计算属性一般用在需要对依赖项进行计算并且可以缓存下来,当依赖项变化会自动执行计算属性的逻辑,一般用在模板里面较多而侦听属性用法是对某个响应式的值进行观察(也可以观察计算属性的值)一旦变化之后就可以执行自己定义的方法
个人博客
耀耀切克闹 (yaoyaoqiekenao.com)
参考文章
手写Vue2.0源码(十)-计算属性原理 - 掘金 (juejin.cn)