vue2源码解析---watch和computed
监听属性watch
监听属性介绍
我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数wach 可以用于异步任务
监听属性的初始化
watch和computed都先走initSate判断传入选项
export function initState(vm) {const opts = vm.$options; // 获取所有的选项if (opts.data) {initData(vm);}if (opts.computed) {initComputed(vm);}if (opts.watch) {initWatch(vm);}
}
接下来initWatch进入此函数
function initWatch(vm){let watch = vm.$options.watch;for(let key in watch){const handler = watch[key]; // 字符串 数组 函数if(Array.isArray(handler)){for(let i = 0; i < handler.length;i++){createWatcher(vm,key,handler[i]);}}else{createWatcher(vm,key,handler);}}}
通过原型方法$watch传入处理参数创建一个观察者收集依赖变化。
function createWatcher(vm,key,handler){// 字符串 函数if(typeof handler === 'string'){handler = vm[handler];}return vm.$watch(key,handler)
}
原型上的$watch函数
export function initStateMixin(Vue) {Vue.prototype.$nextTick = nextTick;// 最终调用的都是这个方法Vue.prototype.$watch = function (exprOrFn, cb) {// firstname// ()=>vm.firstname// firstname的值变化了 直接执行cb函数即可new Watcher(this, exprOrFn, { user: true }, cb)}
}
计算属性computed
计算属性介绍
计算属性是在 Vue 实例的computed选项中定义的,可以是一个函数或具有get和set方法的对象。函数形式的计算属性会在调用时被执行,而对象形式的计算属性则可以提供自定义的get和set方法
计算属性适用于那些依赖其他响应式数据的场景,而不适用于需要进行异步操作或有副作用的场景。对于这些情况,可以使用侦听器(watcher)或使用methods来处理。
计算属性实现过程
计算属性的初始化
1 在initComputed函数中,遍历计算属性对象,为每个计算属性创建一个Watcher实例,并将其存储在vm._computedWatchers中。
export function initState(vm) {const opts = vm.$options; // 获取所有的选项if (opts.data) {initData(vm);}if (opts.computed) {initComputed(vm);}if (opts.watch) {initWatch(vm);}
}
function initComputed(vm) {const computed = vm.$options.computed;const watchers = vm._computedWatchers = {}; // 将计算属性watcher保存到vm上for (let key in computed) {//获取用户定义的计算属性let userDef = computed[key];// 我们需要监控 计算属性中get的变化let fn = typeof userDef === 'function' ? userDef : userDef.get// 如果直接new Watcher 默认就会执行fn, 将属性和watcher对应起来 watchers[key] = new Watcher(vm, fn, { lazy: true })defineComputed(vm, key, userDef);}
}
属性劫持
2 defineComputed 方法主要是重新定义计算属性,其实最主要的是劫持get方法。
为啥要劫持呢? 因为我们需要根据依赖值是否发生变化来判断计算属性是否需要重新计算
function defineComputed(target, key, userDef) {// const getter = typeof userDef === 'function' ? userDef : userDef.get;const setter = userDef.set || (() => { })// 可以通过实例拿到对应的属性Object.defineProperty(target, key, {get: createComputedGetter(key),set: setter})
}
3 createComputedGetter判断计算属性的值是否变化 增加dirty
如果是true执行更新
// 计算属性根本不会收集依赖 ,只会让自己的依赖属性去收集依赖
function createComputedGetter(key) {// 我们需要检测是否要执行这个getterreturn function () {const watcher = this._computedWatchers[key]; // 获取到对应属性的watcherif (watcher.dirty) {// 如果是脏的就去执行 用户传入的函数watcher.evaluate(); // 求值后 dirty变为了false ,下次就不求值了}if (Dep.target) { // 计算属性出栈后 还要渲染watcher, 我应该让计算属性watcher里面的属性 也去收集上一层watcherwatcher.depend();//计算属性watcher收集渲染watcher}return watcher.value; // 最后返回的是watcher上的值}
}
watcher
新增了dirty属性 标识是否需要更新视图
增加了evaluate方法 重新渲染 并且将dirty变成true
// src/observer/watcher.js// import { pushTarget, popTarget } from "./dep";
// import { queueWatcher } from "./scheduler";
// import {isObject} from '../util/index'
// // 全局变量id 每次new Watcher都会自增
// let id = 0;export default class Watcher {constructor(vm, exprOrFn, cb, options) {// this.vm = vm;// this.exprOrFn = exprOrFn;// this.cb = cb; //回调函数 比如在watcher更新之前可以执行beforeUpdate方法// this.options = options; //额外的选项 true代表渲染watcher// this.id = id++; // watcher的唯一标识// this.deps = []; //存放dep的容器// this.depsId = new Set(); //用来去重dep// this.user = options.user; //标识用户watcherthis.lazy = options.lazy; //标识计算属性watcherthis.dirty = this.lazy; //dirty可变 表示计算watcher是否需要重新计算 默认值是true// 如果表达式是一个函数// if (typeof exprOrFn === "function") {// this.getter = exprOrFn;// } else {// this.getter = function () {// //用户watcher传过来的可能是一个字符串 类似a.a.a.a.b// let path = exprOrFn.split(".");// let obj = vm;// for (let i = 0; i < path.length; i++) {// obj = obj[path[i]]; //vm.a.a.a.a.b// }// return obj;// };// }// 非计算属性实例化就会默认调用get方法 进行取值 保留结果 计算属性实例化的时候不会去调用getthis.value = this.lazy ? undefined : this.get();}get() {pushTarget(this); // 在调用方法之前先把当前watcher实例推到全局Dep.target上const res = this.getter.call(this.vm); //计算属性在这里执行用户定义的get函数 访问计算属性的依赖项 从而把自身计算Watcher添加到依赖项dep里面收集起来popTarget(); // 在调用方法之后把当前watcher实例从全局Dep.target移除return res;}// 把dep放到deps里面 同时保证同一个dep只被保存到watcher一次 同样的 同一个watcher也只会保存在dep一次// addDep(dep) {// let id = dep.id;// if (!this.depsId.has(id)) {// this.depsId.add(id);// this.deps.push(dep);// // 直接调用dep的addSub方法 把自己--watcher实例添加到dep的subs容器里面// dep.addSub(this);// }// }// 这里简单的就执行以下get方法 之后涉及到计算属性就不一样了update() {// 计算属性依赖的值发生变化 只需要把dirty置为true 下次访问到了重新计算if (this.lazy) {this.dirty = true;} else {// 每次watcher进行更新的时候 可以让他们先缓存起来 之后再一起调用// 异步队列机制queueWatcher(this);}}// 计算属性重新进行计算 并且计算完成把dirty置为falseevaluate() {this.value = this.get();this.dirty = false;}depend() {// 计算属性的watcher存储了依赖项的deplet i = this.deps.length;while (i--) {this.deps[i].depend(); //调用依赖项的dep去收集渲染watcher}}// 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 {// // 渲染watcher// this.cb.call(this.vm);// }// }
}
computed和watch的区别
**相同点:**底层都会创建一个watcher computed定义的属性可以在模板中使用 watch不能在视图中国使用
不同点: computed不会默认执行 只有取值会执行 内部会以一个dirty属性控制依赖的值是否变化
watch默认用户会提供一个回调函数 数据变化就使用用户传入的回调
本周总结
vue2手写部分学习完了 其实感觉收集依赖那一部分还是有点绕 后续应该会多看点别人总结的内容对着自己代码复习复习也学习了基础的webpack
下周主要还是学习一下源码 复习一下js高级啥的