本章主要解释Vue中计算属性computed和watch的原理
computed
计算属性,即当他依赖的属性发生变化时才会变化(计算属性的结果会被缓存,除非依赖的响应式变化才会重新计算)
- index.html
<script>
const vm = new Vue({
el:'#app',
data:{
firstname:'张',
lastname: '三',
age:13
},
computed:{
newName(){
return this.age
},
fullname(){
console.log('run')
return this.firstname + this.lastname
}
}
});
setTimeout(()=>{
vm.firstname = 'xxx';
},1000)
</script>
- 在init.js中,进行computed计算属性的初始化
import Watcher from './observe/watcher'
export function initState(vm) {
const opts = vm.$options; // 获取所有的选项
if (opts.data) {
initData(vm);
}
if (opts.computed) {
initComputed(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);
}
}
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
})
}
// 计算属性根本不会收集依赖 ,只会让自己的依赖属性去收集依赖
function createComputedGetter(key) {
// 我们需要检测是否要执行这个getter
return function () {
const watcher = this._computedWatchers[key]; // 获取到对应属性的watcher
if (watcher.dirty) {
// 如果是脏的就去执行 用户传入的函数
watcher.evaluate(); // 求值后 dirty变为了false ,下次就不求值了
}
if (Dep.target) { // 计算属性出栈后 还要渲染watcher, 我应该让计算属性watcher里面的属性 也去收集上一层watcher
watcher.depend();
}
return watcher.value; // 最后返回的是watcher上的值
}
}
- Watcher
class Watcher { // 不同组件有不同的watcher 目前只有一个 渲染根实例的
constructor(vm, fn, options) {
this.id = id++;
this.renderWatcher = options; // 是一个渲染watcher
this.getter = fn; // getter意味着调用这个函数可以发生取值操作
this.deps = []; // 后续我们实现计算属性,和一些清理工作需要用到
this.depsId = new Set();
this.lazy = options.lazy;
this.dirty = this.lazy; // 缓存值
this.vm = vm;
this.value = this.lazy ? undefined : this.get();
}
addDep(dep) { // 一个组件 对应着多个属性 重复的属性也不用记录
let id = dep.id;
if (!this.depsId.has(id)) {
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this); // watcher已经记住了dep了而且去重了,此时让dep也记住watcher
}
}
evaluate(){
this.value = this.get(); // 获取到用户函数的返回值 并且还要标识为脏
this.dirty = false;
}
get() {
pushTarget(this)// 静态属性就是只有一份
let value = this.getter.call(this.vm); // 会去vm上取值 vm._update(vm._render) 取name 和age
popTarget() // 渲染完毕后就清空
return value;
}
depend(){ // watcher的depend 就是让watcher中dep去depend
let i = this.deps.length;
while(i--){
// dep.depend()
this.deps[i].depend(); // 让计算属性watcher 也收集渲染watcher
}
}
update() {
if(this.lazy){
// 如果是计算属性 依赖的值变化了 就标识计算属性是脏值了
this.dirty = true;
}else{
queueWatcher(this); // 把当前的watcher 暂存起来
// this.get(); // 重新渲染
}
}
run() {
let oldValue = this.value;
let newValue = this.get(); // 渲染的时候用的是最新的vm来渲染的
if(this.user){
this.cb.call(this.vm,newValue,oldValue);
}
}
}
computed流程分析
- 当
new Vue({
...,
computed:{
fullname(){ // defineProperty中的get方法
console.log('run')
return this.firstname + this.lastname
}
}
});
首先进行了初始化状态,执行了initState(),这哥函数里会执行判断有没有computed,有的话执行计算属性的初始化initComputed()
-
initComputed会实例vm上维护一个对象_computedWatchers,用来存放接下来创建的watcher,遍历computed,遍历过冲中,创建一个变量记录get函数fn,即计算属性的get,当计算属性是一个函数时,fn就是该函数,当他不是函数,说明是对象类型,取对象的get函数;然后new Watcher()传入实例,刚才的fn,和一个{lazy:true}
-
在new Watcher()的过程中,把传入的fn赋值给watcher的getter,判断传入的options的lazy(lazy为true不执行get()),当执行evaluate时调用get,get又会执行this.getter,当前的watcher(Dep.tag)入栈当前的计算属性watcher(this),然后就会执行传入的fn函数(
function(){return this.firstname + this.lastname})这时执行了data数据中的get方法,讲当前watcher依赖收集到dep中,这时watcher中就有两个dep,dep中也有计算属性的watcher,执行完后出栈,计算属性的watcher去掉,然后将值赋值给当前watcher的value. -
在第二步遍历过程最后一步,会进行一个数据劫持,这样就可以this.computed[key]取值,执行defineComputed,进行数据劫持,setter为用户写的函数或者空函数,getter为当前fn的值,但是直接这样写会有问题,执行多次的问题,但只需要执行一次,封装一个函数给他createComputedGetter
-
createComputedGetter函数返回一个函数,会判断当前这个watcher的dirty是否为true是脏值就执行
watcher.evaluate()(见2),evaluate会执行get方法赋值value,并将dirty置为false,这样下次取值就跳过了,在watcher中的update中判断lazy有的话讲dirty置为true,依赖的属性改变后下次就会执行(dep中存起来了),最后输出watcher.vlaue -
第5步只是输出了value,但是在页面并不会渲染,需要在返回value之前做一步操作,如果watcher栈中有值的话,就执行渲染,把当前渲染watcher添加到dep中,这样属性变化也会执行更新渲染
if (Dep.target) { // 计算属性出栈后 还要渲染watcher, 我应该让计算属性watcher里面的属性 也去收集上一层watcher
watcher.depend();
}
Watch
watch原理也是一样,就是创建一个watcher,存在坚挺属性的dep上,当属性改变时,执行传入的函数就可以了 init.js
mport Watcher from './observe/watcher'
export function initState(vm) {
const opts = vm.$options; // 获取所有的选项
if (opts.data) {
initData(vm);
}
if (opts.computed) {
initComputed(vm);
}
if (opts.watch) {
initWatch(vm);
}
}
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);
}
}
}
function createWatcher(vm,key,handler){
// 字符串 函数
if(typeof handler === 'string'){
handler = vm[handler];
}
return vm.$watch(key,handler)
}
index.js
Vue.prototype.$watch = function (exprOrFn, cb) {
// firstname
// ()=>vm.firstname
// firstname的值变化了 直接执行cb函数即可
new Watcher(this,exprOrFn,{user:true},cb)
}
watcher
class Watcher { // 不同组件有不同的watcher 目前只有一个 渲染根实例的
constructor(vm, exprOrFn, options,cb) {
this.id = id++;
this.renderWatcher = options; // 是一个渲染watcher
if(typeof exprOrFn === 'string'){
this.getter = function(){
return vm[exprOrFn]
}
}else{
this.getter = exprOrFn; // getter意味着调用这个函数可以发生取值操作
}
this.deps = []; // 后续我们实现计算属性,和一些清理工作需要用到
this.depsId = new Set();
this.lazy = options.lazy;
this.cb = cb;
this.dirty = this.lazy; // 缓存值
this.vm = vm;
this.user = options.user; // 标识是否是用户自己的watcher
this.value = this.lazy ? undefined : this.get();
}
addDep(dep) { // 一个组件 对应着多个属性 重复的属性也不用记录
let id = dep.id;
if (!this.depsId.has(id)) {
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this); // watcher已经记住了dep了而且去重了,此时让dep也记住watcher
}
}
evaluate(){
this.value = this.get(); // 获取到用户函数的返回值 并且还要标识为脏
this.dirty = false;
}
get() {
pushTarget(this)// 静态属性就是只有一份
let value = this.getter.call(this.vm); // 会去vm上取值 vm._update(vm._render) 取name 和age
popTarget() // 渲染完毕后就清空
return value;
}
depend(){ // watcher的depend 就是让watcher中dep去depend
let i = this.deps.length;
while(i--){
// dep.depend()
this.deps[i].depend(); // 让计算属性watcher 也收集渲染watcher
}
}
update() {
if(this.lazy){
// 如果是计算属性 依赖的值变化了 就标识计算属性是脏值了
this.dirty = true;
}else{
queueWatcher(this); // 把当前的watcher 暂存起来
// this.get(); // 重新渲染
}
}
run() {
let oldValue = this.value;
let newValue = this.get(); // 渲染的时候用的是最新的vm来渲染的
if(this.user){
this.cb.call(this.vm,newValue,oldValue);
}
}
}
watch流程分析
- 首先和computed一样之心初始化,遍历watch,对应的值可能是一个函数(常见),可能是一个函数数组,value如果是字符串就执行循环执行createWatcher这个方法,
- createWatcher判断是不是字符串,是的话从vm上取到这个函数,将key和回调函数传给$watch
- $watch执行
new Watcher(this,exprOrFn,{user:true},cb),user为watch标识 - watch这时的fn可能为一个字符串,
if(typeof exprOrFn === 'string'){
this.getter = function(){
return vm[exprOrFn]
}
}e
讲cb存起来,this.cb = cd,然后执行this.get,这样就掉用了this.key,将当前的watch存在dep上了,在watcher的run函数中,判断
run() {
let oldValue = this.value;
let newValue = this.get(); // 渲染的时候用的是最新的vm来渲染的
if(this.user){
this.cb.call(this.vm,newValue,oldValue);
}
}
这样当监听的属性发生变化,就会执行run,就会触发cb。