Search K
Appearance
Appearance
最近正在准备面试,复习源码过程中顺便记录Vue2.0
中一些API
的源码实现,看之前需要先学习一下Vue的响应式原理
,详见上文:Vue2.0的响应式原理
转载自这位大佬博客,写的很详细,特此转载记录下来
作者:前端鲨鱼哥 链接:https://juejin.cn/column/6961223264685277192 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
先看下watch
的用法
<script>
// Vue实例化
let vm = new Vue({
el: "#app",
data() {
return {
aa: 1,
bb: 2,
};
},
template: `<div id="a">hello 这是我自己写的Vue{{name}}</div>`,
methods: {
doSomething() {},
},
watch: {
aa(newVal, oldVal) {
console.log(newVal);
},
aa: {
handle(newVal, oldVal) {
console.log(newVal);
},
deep: true
},
aa: 'doSomething',
aa: [{
handle(newVal, oldVal) {
console.log(newVal);
},
deep: true
}]
},
});
setTimeout(() => {
vm.aa = 1111;
}, 1000);
</script>
侦听属性的写法很多 可以写成 字符串 函数 数组 以及对象 对于对象的写法自己可以增加一些 options
用来增强功能 侦听属性的特点是监听的值发生了变化之后可以执行用户传入的自定义方法
initWatch
初始化 Watch
对数组进行处理 createWatcher
处理 Watch
的兼容性写法 包含字符串 函数 数组 以及对象 最后调用 $watch
传入处理好的参数进行创建用户 Watcher
// src/state.js
// 统一初始化数据的方法
export function initState(vm) {
// 获取传入的数据对象
const opts = vm.$options;
if (opts.watch) {
//侦听属性初始化
initWatch(vm);
}
}
// 初始化watch
function initWatch(vm) {
let watch = vm.$options.watch;
for (let k in watch) {
const handler = watch[k]; //用户自定义watch的写法可能是数组 对象 函数 字符串
if (Array.isArray(handler)) {
// 如果是数组就遍历进行创建
handler.forEach((handle) => {
createWatcher(vm, k, handle);
});
} else {
createWatcher(vm, k, handler);
}
}
}
// 创建watcher的核心
function createWatcher(vm, exprOrFn, handler, options = {}) {
if (typeof handler === "object") {
options = handler; //保存用户传入的对象
handler = handler.handler; //这个代表真正用户传入的函数
}
if (typeof handler === "string") {
// 代表传入的是定义好的methods方法
handler = vm[handler];
}
// 调用vm.$watch创建用户watcher
return vm.$watch(exprOrFn, handler, options);
}
原型方法 $watch
就是创建自定义 watch
的核心方法 把用户定义的 options
和 user:true
传给构造函数 Watcher
// src/state.js
import Watcher from "./observer/watcher";
Vue.prototype.$watch = function (exprOrFn, cb, options) {
const vm = this;
// user: true 这里表示是一个用户watcher
let watcher = new Watcher(vm, exprOrFn, cb, { ...options, user: true });
// 如果有immediate属性 代表需要立即执行回调
if (options.immediate) {
cb(); //如果立刻执行
}
};
注:注视的地方代码也是正常的代码,这里主要为了突出关键点,主要关注非注释的地方
// src/observer/watcher.js
import { isObject } from "../util/index";
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; //标识用户watcher
// 如果表达式是一个函数
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;
};
}
// 实例化就进行一次取值操作 进行依赖收集过程
this.value = this.get();
}
// get() {
// pushTarget(this); // 在调用方法之前先把当前watcher实例推到全局Dep.target上
// const res = this.getter.call(this.vm); //如果watcher是渲染watcher 那么就相当于执行 vm._update(vm._render()) 这个方法在render函数执行的时候会取值 从而实现依赖收集
// 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);
// }
// }
// depend(){
// // 计算属性的watcher存储了依赖项的dep
// let 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);
}
}
}
1、实例化的时候为了兼容用户 watch
的写法 会将传入的字符串写法转成 Vue
实例对应的值 并且调用 get
方法获取并保存一次旧值
2、run
方法判断如果是用户 watch
那么执行用户传入的回调函数 cb
并且把新值和旧值作为参数传入进去
总结:
其实vue
中我们使用的watch
API
就是对于订阅者Watcher
的扩展,在此基础上扩展了兼容多种类型、立即执行,回调新旧值等功能。
主要要理解前面文章讲到的 Watcher
,Watcher
作用主要是为this
上的某一个数据创建订阅者,将订阅消息收集到 消息订阅器Dep
中,当数据变化的时候调用Watcher
执行update
方法,以上代码中 update
最终会执行 run
方法,然后 run
方法会执行用户传递进来的自定义回调函数,同时会将新旧两个值传入到回调函数中,这样就实现了 watch
API
同样computed
也是在Watcher
基础上扩展实现的,扩展了缓存(计算属性依赖的值如果没有变化,那么下次调用计算属性不会重新计算而是直接返回函数结果)功能,computed
通过引入了一个boolean
值dirty
在watch
的基础上实现了缓存,当依赖的数据
computed
的使用示例
<script>
// Vue实例化
let vm = new Vue({
el: "#app",
data() {
return {
aa: 1,
bb: 2,
cc: 3,
};
},
// render(h) {
// return h('div',{id:'a'},'hello')
// },
template: `<div id="a">hello 这是我自己写的Vue{{computedName}}{{cc}}</div>`,
computed: {
computedName() {
return this.aa + this.bb;
},
},
});
// 当我们每一次改变数据的时候 渲染watcher都会执行一次 这个是影响性能的
setTimeout(() => {
vm.cc = 4;
}, 2000);
console.log(vm);
</script>
// src/state.js
function initComputed(vm) {
const computed = vm.$options.computed;
const watchers = (vm._computedWatchers = {}); //用来存放计算watcher
for (let k in computed) {
const userDef = computed[k]; //获取用户定义的计算属性
const getter = typeof userDef === "function" ? userDef : userDef.get; //创建计算属性watcher使用
// 创建计算watcher lazy设置为true
watchers[k] = new Watcher(vm, getter, () => {}, { lazy: true });
defineComputed(vm, k, userDef);
}
}
计算属性可以写成一个函数也可以写成一个对象 对象的形式 get
属性就代表的是计算属性依赖的值 set
代表修改计算属性的依赖项的值 我们主要关心 get
属性 然后类似侦听属性 我们把 lazy:true
传给构造函数 Watcher
用来创建计算属性 Watcher
那么 defineComputed
是什么意思呢
关键代码在createComputedGetter
重写了计算属性的get
方法,来判断是否需要进行重新计算,当 dirty
为true
的时候会触发重新计算
// src/state.js
// 定义普通对象用来劫持计算属性
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: () => {},
set: () => {},
};
// 重新定义计算属性 对get和set劫持
function defineComputed(target, key, userDef) {
if (typeof userDef === "function") {
// 如果是一个函数 需要手动赋值到get上
sharedPropertyDefinition.get = createComputedGetter(key);
} else {
sharedPropertyDefinition.get = createComputedGetter(key);
sharedPropertyDefinition.set = userDef.set;
}
// 利用Object.defineProperty来对计算属性的get和set进行劫持
Object.defineProperty(target, key, sharedPropertyDefinition);
}
// 重写计算属性的get方法 来判断是否需要进行重新计算
function createComputedGetter(key) {
return function () {
const watcher = this._computedWatchers[key]; //获取对应的计算属性watcher
if (watcher) {
if (watcher.dirty) {
watcher.evaluate(); //计算属性取值的时候 如果是脏的 需要重新求值
}
return watcher.value;
}
};
}
defineComputed
方法主要是重新定义计算属性 其实最主要的是劫持 get
方法 也就是计算属性依赖的值 为啥要劫持呢 因为我们需要根据依赖值是否发生变化来判断计算属性是否需要重新计算
createComputedGetter
方法就是判断计算属性依赖的值是否变化的核心了 我们在计算属性创建的 Watcher
增加 dirty
标志位 如果标志变为 true
代表需要调用 watcher.evaluate
来进行重新计算了
queueWatcher
方法
// src/observer/scheduler.js
import { nextTick } from "../util/next-tick";
let queue = [];
let has = {};
function flushSchedulerQueue() {
for (let index = 0; index < queue.length; index++) {
// 调用watcher的run方法 执行真正的更新操作
queue[index].run();
}
// 执行完之后清空队列
queue = [];
has = {};
}
// 实现异步队列机制
export function queueWatcher(watcher) {
const id = watcher.id;
// watcher去重
if (has[id] === undefined) {
// 同步代码执行 把全部的watcher都放到队列里面去
queue.push(watcher);
has[id] = true;
// 进行异步调用
nextTick(flushSchedulerQueue);
}
}
// 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; //标识用户watcher
this.lazy = options.lazy; //标识计算属性watcher
this.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方法 进行取值 保留结果 计算属性实例化的时候不会去调用get
this.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置为false
evaluate() {
this.value = this.get();
this.dirty = false;
}
depend() {
// 计算属性的watcher存储了依赖项的dep
let 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);
// }
// }
}
我们主要看没被注释的代码 这里主要改造有四点: 1.实例化的时候如果是计算属性 不会去调用 get
方法访问值进行依赖收集 2.update
方法只是把计算 watcher
的 dirty
标识为 true
只有当下次访问到了计算属性的时候才会重新计算 3.新增 evaluate
方法专门用于计算属性重新计算 4.新增 depend
方法 让计算属性的依赖值收集外层 watcher
这个方法非常重要 我们接下来分析
// src/state.js
function createComputedGetter(key) {
// return function () {
// const watcher = this._computedWatchers[key]; //获取对应的计算属性watcher
// if (watcher) {
// if (watcher.dirty) {
// watcher.evaluate(); //计算属性取值的时候 如果是脏的 需要重新求值
if (Dep.target) {
// 如果Dep还存在target 这个时候一般为渲染watcher 计算属性依赖的数据也需要收集
watcher.depend()
}
// }
// return watcher.value;
// }
// };
// }
这里就体现了 watcher.depend
方法的重要性了 我们试想一下 当我们计算属性依赖的值发生了改变 这时候 watcher
的 dirty
为 true
下次访问计算属性 他确实也重新计算了 但是 我们从头到尾都没有触发视图更新 也就是数据改变了 视图没有重新渲染 这是为什么呢? 因为模板里面只有计算属性 而计算属性的依赖值的 dep
里面只收集了计算 watcher
的依赖 自身变化也只是通知了计算 watcher
调用 update
把 dirty
置为 true
所以我们要想个办法把计算属性的依赖项也添加渲染 watcher
的依赖 让自身变化之后首先通知计算 watcher
进行重新计算 然后通知渲染 watcher
进行视图更新怎么做呢?我们来看看下面的代码就清楚了
// src/observer/dep.js
// 默认Dep.target为null
Dep.target = null;
// 栈结构用来存watcher
const targetStack = [];
export function pushTarget(watcher) {
targetStack.push(watcher);
Dep.target = watcher; // Dep.target指向当前watcher
}
export function popTarget() {
targetStack.pop(); // 当前watcher出栈 拿到上一个watcher
Dep.target = targetStack[targetStack.length - 1];
}
可见最初设计存放 watcher
的容器就是一个栈结构 因为整个 Vue
生命周期的过程中会存在很多的 watcher
比如渲染 watcher
计算 watcher
侦听 watcher
等 而每个 watcher
在调用了自身的 get
方法前后会分别调用 pushTarget
入栈和 popTarget
出栈 这样子当计算属性重新计算之后就立马会出栈 那么外层的 watcher
就会成为新的 Dep.target
我们使用 watcher.depend
方法让计算属性依赖的值收集一遍外层的渲染 watcher
这样子当计算属性依赖的值改变了既可以重新计算又可以刷新视图
总结:
Vue
实例时,我们会给每个计算属性都创建一个对应 watcher
(我们称之为计算属性 watcher
,除此之外还有 渲染 watcher
和 侦听器 watcher
),他有一个 value
属性用于缓存计算属性方法的返回值。默认标识 lazy: true
,懒的,代表计算属性 watcher
,创建时不会立即执行 get方法默认标识 dirty
: true
,脏的,当我们劫持到计算属性访问时,如果是脏的,我们会通过 watcher.evaluate
重新计算 watcher
的 value
值 并将其标识为干净的;如果是干净的,则直接取 watcher
缓存值depend
方法,会让计算属性 watcher
订阅的 dep
去收集上层 watcher
,可能是渲染 watcher
,也可能是计算属性 watcher
(计算属性嵌套的情况),实现洋葱模型的核心方法update
方法,当计算属性依赖的对象发生变化时,会触发 dep.notify
派发更新 并 调用 update
方法,只需更新 dirty
为 true
即可。我们会在后续的渲染 watcher
更新时,劫持到计算属性的访问操作,并通过 watcher.evaluate
重新计算其 value
值大家思考一下 按照之前的逻辑 每次我们改变数据的时候都会触发相应的 watcher
进行更新 如果是渲染 watcher
那是不是意味着 数据变动一次 就会重新渲染一次 这样其实是很浪费性能的 我们有没有更好的方法 让数据变动完毕后统一去更新视图呢
在watcher
更新方法添加异步队列机制
// src/observer/watcher.js
import { queueWatcher } from "./scheduler";
export default class Watcher {
update() {
// 每次watcher进行更新的时候 是否可以让他们先缓存起来 之后再一起调用
// 异步队列机制
queueWatcher(this);
}
run() {
// 真正的触发更新
this.get();
}
}
新建 scheduler.js
文件 表示和调度相关 先同步把 watcher
都放到队列里面去 执行完队列的事件之后再清空队列 主要使用 nextTick
来执行 watcher
队列
// src/observer/scheduler.js
import { nextTick } from "../util/next-tick";
let queue = [];
let has = {};
function flushSchedulerQueue() {
for (let index = 0; index < queue.length; index++) {
// 调用watcher的run方法 执行真正的更新操作
queue[index].run();
}
// 执行完之后清空队列
queue = [];
has = {};
}
// 实现异步队列机制
export function queueWatcher(watcher) {
const id = watcher.id;
// watcher去重
if (has[id] === undefined) {
// 同步代码执行 把全部的watcher都放到队列里面去
queue.push(watcher);
has[id] = true;
// 进行异步调用
nextTick(flushSchedulerQueue);
}
}
// src/util/next-tick.js
let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false; //把标志还原为false
// 依次执行回调
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
let timerFunc; //定义异步方法 采用优雅降级
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是监听dom变化 也是一个异步方法
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降级采用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb) {
// 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组
callbacks.push(cb);
if (!pending) {
// 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false
pending = true;
timerFunc();
}
}
// src/render.js
import { nextTick } from "./util/next-tick";
export function renderMixin(Vue) {
// 挂载在原型的nextTick方法 可供用户手动调用
Vue.prototype.$nextTick = nextTick;
}