vue-computed,watch原理及手动实现

1,290 阅读13分钟

了解computed,watcher之前,建议先理解双向绑定原理

computed原理

  • 需要理解的概念

datavue中的data数据

Dep:全局类,用来收集依赖以及通知订阅者更新,data中的每一个属性都会有一个对应的Dep实例。

watcher:全局类,观察者,订阅者,分为三种类型

  1. user watcher:自定义,既vue实例中开发者自己定义的watch

  2. computed watcher:既vue实例中开发者定义的计算属性,computed初始化的时候,会为每一个计算属性生成一个watcher实例

  3. render watcher:组件实例化,模板编译时,会产生一个watcher,这个watcher最终会回调vue的渲染函数,更新渲染

Dep.target:全局属性,watcher每次执行自己的get时候,都会把target指向自己,targetTask数组中有多个target的时候,Dep.target为上一层的watcher,这个实现是通过targetStack数组以及pushTarget以及popTarget函数实现的

  • computed的原理,是如何和被计算的数据联系起来的?

有如下:

data:{
  firstName:'',
  lastName:''
},
computed:{
  allName:function(){
      return firstName + lastName
  }
}

allName是计算属性,firstNamelastName是被计算属性(data属性)。

1.首先,创建vue实例,进行data数据初始化,循环遍历data属性,为data加上getset设置,同时会给每个data属性实例化一个订阅器Dep,以上为例,称订阅器firstDep,lastDep,在Dep中会定义一个收集数组subs

2.然后对computed初始化,循环遍历每一个computed属性,将计算属性自身实例化成一个订阅者,称作computed watcher,同时,把computed属性挂载到vue组件上,并设置computed属性的getset

3.每一个计算属性都会生成订阅者watcher实例,并在内部实例化一个dep收集器deps,同时,把target指向自己,再将自己添加到每一个被计算属性firstName,lastNamedep中,既firstDeplastDep。这里,会调用firstName,lastName属性进行计算,而调用它们时,就会触发它们的get函数。

4.在firstName属性的get中,进行依赖收集。这一段是关键,也比较绕。以computed watcher(allName)添加到订阅器firstDep中为例。

firstName中定义的get函数,会触发订阅器firstDep的收集depend()函数。

get:function(){
 if(Dep.target){
  	dep.depend()
  }
}

depend()函数中会调用当前target所指向的订阅者watcher,触法订阅者watcheraddDep()函数,并把firstDep当成参数传递。

Dep.prototype.depend=function(){
 if(Dep.target){
     Dep.target.addDep(this) // this 指向firstDep
 }
}

订阅者watcher中的addDep()函数会收集订阅器firstDep,防止重复收集订阅器,会通过id判断去重,然后把订阅器firstDep加入到订阅器收集器deps中,然后再触发订阅器firstDepaddSub()函数,并把当前订阅者watcher当做参数传递。

Watcher.prototype.addDep=function(dep){
   if(!this.newDepsIds.has(dep.id)){
  			this.newDepsIds.add(dep.id)
  			this.newDeps.push(dep);
  			this.deps = this.newDeps
  			if(!this.depIds.has(dep.id)){
  				dep.addSub(this)//this 指向当前watcher
  		}
  } 
}

最后在订阅器firstDepaddSub()函数中,把订阅者watcher加入到订阅器数组中。

Dep.prototype.addSub=function(sub){
    // 加入订阅器
  		this.subs.push(sub)
}

其实很容易理解,allName实例化的订阅者computed watcherfirstName的订阅器firstDep,是两个类的实例,通过相互调用对方的函数,并把自己当成参数传递给对方,让对方收集。最后,订阅者computed watcher 被添加到了firstName的订阅器firstDep中,firstName的订阅器firstDep被添加到了订阅者computed watcherdeps收集器中。

同理,lastName也是如此。

5.更新。当firstName发生改变,订阅器firstDep会通知所有的订阅者,订阅者computed watcher收到通知,调用更新update()函数,重新计算出firstName+lastName的结果,同时把脏值dirty设置为false,如果firstName,lastName不变,那么下次取值调用的时候,就会直接取,而不需要重新计算。

  • computed的原理,是如何和render watcher(视图)联系起来的?

1.在computed初始化的时候,劫持了计算属性的getsetset被设置为noop,所以vue直接设置computed的值是无用的,get则被设置成了自定义函数createComputedGetter的返回值。

2.模板编译时,render watcher进行实例化,把target指向自己,再将自己添加到每一个被计算属性firstName,lastName的订阅器firstDep,lastDep中。这里,会调用allName的值(已经挂载到vue上了),触发allName的取值computedGetter函数。

3.在computedGetter中进行依赖收集

function createComputedGetter(key){
  	return function computedGetter(){
  		var watcher = this._computedWatchers && this._computedWatchers[key]
  		// console.log('-----触发计算watcher'+watcher.id+' 的computed getter----当前watcher',watcher)
  		if(watcher){
  			if(watcher.dirty){
  				watcher.evaluate();
  			}
  			if(Dep.target){
  				// console.log('-----------开始收集')
  				watcher.depend();
  			}
  			// console.log(key,watcher.value)
  			return watcher.value
  		}
  	}
  }

这一段很有趣,之前我一直不明白模板中的{{allName}}是怎么和firstNnme,lastName联系,因为在render watcher中,我只能得到allName这个字段,并不知道它的被计算属性都有什么。

然后仔细阅读源码发现,这里所定义的watcher指的是computed watcher,怎么获取的呢,请看:

var watcher = this._computedWatchers && this._computedWatchers[key]//这里的key = allName

computed watcher在最初实例化的时候,就全局的存储了每个computed watcher,可直接通过key获取。

而代码中Dep.target所指向的watcher,则是第2步实例化的render watcher,所以说,在这一步的时候,是存在两个watcher的。

接下来,调用watcher.depend(),在这里,computed watcher有一个订阅器收集器deps,而收集器deps里面存储的,就是computed watcher实例化时添加进去的订阅器firstDep,lastDep,然后依次调用deps收集器里每一个收集器depdepend()

然后,depend()里,将当前target所指向的订阅者render watcher添加到每一个被计算属性的订阅器里面。跟上一步相似,订阅者render watcher和 订阅器firstDep,相互调用对方的函数,并把自己当成参数传递给对方,让对方收集。

至此,订阅器firstDep里面有两个订阅者['computed watcher','render watcher'],当firstName变化时,通知订阅者,重新计算,render watcher回调重新渲染视图。

自己画的简略图

watch 原理

同computed相似,都是基于watcher,订阅者发布者模式实现的。

1.首先,创建vue实例,进行watch数据初始化,循环遍历watch属性,生成订阅者user watcher实例。

2.user watcher初始化的时候,就会将自己加入到被监听的属性的订阅器中。

3.被监听的属性值改变,通知user watcher,触发回调,执行用户自定义的回调函数。

  • 注销watch

当一个页面跳转到另一个页面时,watch已经没有用了,如果继续使用的话,可能会导致内置溢出,如果watch卸载组件里面,会随着组件的销毁而销毁,如果写在全局中,就需要手动销毁。

源码中的销毁,就是将watch这个订阅者,把它从所有被它加入过的订阅器中删除。

teardown(){
  		// 销毁watch监听
  		var i = this.deps.length;
        while (i--) {
           this.deps[i].removeSub(this);
        }
}
  • 高级用法

immediate属性

初始化watch,不会执行回调,只有当被监听的属性第一次改变时,才会开始执行,如果想要立即执行,需要添加immediate属性为true

deep属性

深度监听,监听对象属性

第一种写法:

watch:{
    'obj.a':{
        handler(newV,oldV){
            console.log('obj.a changed')
        }
    }
}

我们改变obj.a的值,会处理打印。

但如果obj中有很多个属性值需要监听,难道我们要一一写出来么?那样太费事了。

第二种监听对象属性情况:

data:{
    obj:{
        a:123
    }
}
watch:{
    'obj':{
        handler(newN,oldN){
            console.log('obj.a changed')
        }
    }
}

可以看见,当我们改变obj.a的值时,会发现handler并不会处理打印。

为什么?

因为受现代 JavaScript 的限制 (以及废弃 Object.observe),Vue 不能检测到对象属性的添加或删除。

但是当我们对obj进行赋值操作时,handler才会处理打印,默认情况下,handler只监听obj这个对象它所引用的变化,而监听不到它的属性值的变化。

个人理解-> 上面两个例子,watch实例化时,会将obj,obj.a自身实例化成一个订阅者watcher,并将订阅者加入到obj,obj.a的订阅器中,get/set劫持了obj,obj.a,当obj,obj.a的值发生改变时,通知订阅者。

但是,第二例子,obj是一个对象,它的属性值改变时,并不会被definePropertype检测到,所以也就不会触发set函数,但如果是obj本身被改变,则会被检测到,并触发set。

这时候,如果我们需要对一个对象中的多个属性进行监听,而且不想写很多遍watch,那么,就需要用到deep属性。

deep是深入观察的意思,监听器会一层层的往下遍历,给对象的所有属性都加上监听器,当修改obj里面的任何一个值,都会触发handler

但这样做,性能开销非常大,如果对象不是有很多个属性需要监听,一般还是建议使用第一种字符串形式监听。

  • deep原理

从双向绑定,computed,watch监听的原理,可以看出来,依赖收集需要触发被依赖属性的get。也就是definePropertype所劫持的get。

那怎么触发的呢,对,就是通过调用被依赖的值,比如我要 把当前watcher加入到obj的订阅器中,那么就在watcher的get函数中调用obj,继而触发obj的get函数,在get中,收集当前watcher:

value = this.getter.call(vm,vm)//触发data的getter
if(this.deep){
    traverse(value)
}

有了这个思路,深入监听就很简单了。

obj是个对象,我们循环遍历它的所有属性,并调用,然后触发属性的get,从而将当前watcher加入到它们所有属性的订阅器中。只要obj对象其中一个属性变化,就会通知订阅者watcher。

function traverse(obj){
  	var keys,i;
  	if(!isPalinObject(obj)){
  		return;
  	} else {
  		keys = Object.keys(obj)
  		i=keys.length;
  		while(i--){
  		    // obj[keys[i]]就是读取值了
  			traverse(obj[keys[i]])
  		}
  	}
  }

computed,watch完整代码

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <meta name="viewport">
  <title>66666</title>
  <script src='js/jquery.min.js'></script>
</head>
<body>
<div id="app">
 <input id="input" type="text" v-model="firstName.name"/>
 <div id="viewShow">{{firstName.name}}</div>
 <!-- <div>{{allName}}</div> -->
</div>
<script type="text/javascript">

  // 共享数据
  var sharedPropertyDefinition = {
  	enumberable:true,//可枚举
  	configurable:false,//不能再define
  	get:noop,
  	set:noop
  };

  function isNative (Ctor) {
    return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
  }

  var _toString = Object.prototype.toString

  // 判断是否为对象
  function isPalinObject(obj){
  	return _toString.call(obj) === '[object Object]'
  }

 // 重新定义set
 var _Set;
  /* istanbul ignore if */ // $flow-disable-line
  if (typeof Set !== 'undefined' && isNative(Set)) {
    // use native Set when available.
    _Set = Set;
  } else {
    // a non-standard Set polyfill that only works with primitive keys.
    _Set = /*@__PURE__*/(function () {
      function Set () {
        this.set = Object.create(null);
      }
      Set.prototype.has = function has (key) {
        return this.set[key] === true
      };
      Set.prototype.add = function add (key) {
        this.set[key] = true;
      };
      Set.prototype.clear = function clear () {
        this.set = Object.create(null);
      };

      return Set;
    }());
  }

  // 实现一个观察者,递归循环将data数据劫持
  function observe(data){
  	// 判断data是否还有子属性,只支持观测对象的子属性为object
  	if(!data || typeof data !== 'object'){
  		return;
  	}
  	for(let key in data){
  		defineReactive(data,key,data[key])
  	}
  }

  // 劫持数据
  function defineReactive(data,key,val){
  	// console.log('---------实例化一个订阅器--------')
  	// console.log('当前订阅器对应的key---',key)
  	var dep = new Dep();
  	observe(val);//监听子属性
  	Object.defineProperty(data,key,{
  		enumberable:true,//可枚举
  		configurable:false,//不能再define
  		get:function(){
  			// console.log('---------触发劫持get:当前劫持的key---',key)
  			// console.log('当前的订阅者,Dep.target:----',Dep.target)
  			if(Dep.target){
  				// 判断当前是否有订阅者,若有,则加入订阅器
  				// console.log('触发'+key+'的收集当前订阅者')
  				dep.depend()
  			}
  			// console.log(key,val)
  			console.log("you get it")
  			// console.log('---------触发劫持get:当前劫持的key---'+key+'----结束---')
  			return val;
  		},
  		set:function(newVal){
  			if(newVal === val){
  				return;
  			}
  			console.log("you are updating it")
  			val = newVal;
  			// 通知所有订阅者数据更新
  			dep.notify()
  		}
  	})
  }

// 指令解析
  function Compile(el,vm){
  	// 获取vm对象
  	this.$vm = vm
  	// 获取节点
  	this.$el = this.isElementNode(el) ? el : document.querySelector(el);
  	if(this.$el){
  	  // 创建一个虚拟dom
  	  this.$fragment = this.node2Fragment(this.$el)
  	  // 初始化
  	  this.init();
  	  // 将碎片加入真实dom
  	  this.$el.appendChild(this.$fragment)
  	}
  }

  Compile.prototype = {
  	init:function(){
  		// 解析虚拟dom
  		this.compileElement(this.$fragment)
  	},
  	node2Fragment:function(el){
  		// 创建虚拟节点对象
  		var fragment = document.createDocumentFragment()
  		var firstChild;
  		// 先将el.frstChild赋值给firstChild
  		// append方法具有可移动性,执行后,firstChild会置空
  		// 循环把真dom中的节点,一个一个放进去
  		// 直到el.firstChild = null
  		// 退出循环,返回虚拟dom
  		while(firstChild = el.firstChild){
  			fragment.appendChild(firstChild)
  		}
  		return fragment;
  	},
  	compileElement:function(el){
  		// compileElement方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,
        var childNodes = el.childNodes,me = this
        //  [].slice.call 将参数变成伪数组,可使用数组的各种方法
        for(let node of [].slice.call(childNodes)){
        	var text = node.textContent;// 可获取到的内容有,html元素,空行,元素内的文本
        	 var reg = /\{\{\s*(\w+)|((\w+\.)+\w+)\s*\}\}/
        	if(me.isElementNode(node)){
        	    // 元素编译
        		me.compile(node)
        	} else if(me.isTextNode(node) && reg.test(text)){
        		// 文本编译且符合reg
        		// text.match(reg)[0] = "{{name}}"
        		// 这里只考虑"{{name}}" 暂不考虑多种"{{name}} {{name}}"
        		me.compileText(node,text.match(reg)[0]) 
        	}
        	if(node.childNodes && node.childNodes.length){
        		// 子元素继续获取
        		me.compileElement(node)
        	}
        }
  	},
  	compile:function(node){
  		// 获取当前元素的属性,并遍历
  		var nodeAttrs = node.attributes,me = this
  		// v-text="name"
  		for(let attr of [].slice.call(nodeAttrs)){
  			var attrName = attr.name; // 如v-text
  			// 判断当前属性是否为指令
  			if(me.isDirective(attrName)){
  				// 属性的值
  				var exp = attr.value; // name
  				// 属性的后缀
  				var dir = attrName.substring(2) // text
  				if(me.isEventDirective(dir)){
  					// 是否是事件指令 v-on:click
  				} else {
  					if(compileUtil[dir]){
  						if(dir === 'model'){
  							// 为model指令
  							compileUtil[dir](node,me.$vm,exp,attr)
  						} else {
  							compileUtil[dir](node,me.$vm,exp)
  						}
  					}
  				}
  			}
  		}
  	},
  	compileText(node,matchs){
  		compileUtil.compileTextNode(node,this.$vm,matchs);
  	},
  	isElementNode(node){
  		return node.nodeType == 1;
  	},
  	isTextNode(node){
  		return node.nodeType == 3;
  	},
  	isDirective(attr){
  		return attr.indexOf('v-') == 0;
  	},
  	isEventDirective(dir){
  		return dir.indexOf('on') == 0;
  	}
  }

  // 指令处理
  var compileUtil = {
  	reg: /\{\{\s*(\w+)|((\w+\.)+\w+)\s*\}\}/, // 匹配 {{ key }}中的key
  	regLang: /(\w+\.)+\w+/,
  	regshort: /\w+/,
  	compileTextNode:function(node,vm,matchs){
  		// 当前文本内容 "{{name}}"
  		const rawTextContent = node.textContent;
  		// console.log(rawTextContent)
  		var key
  		if(this.regLang.test(rawTextContent)){
  			key = rawTextContent.match(this.regLang)[0] // {{name}} 中的 name
  		} else {
  			key = rawTextContent.match(this.regshort)[0] 
  		}
  		// console.log(rawTextContent,key)
  		// 首次更新
  		this.updateTextNode(vm, node, key,rawTextContent)
  		console.log('----实例化一个文本订阅者----',rawTextContent)
  		// 实例化订阅者
  		new Watcher(vm,key,(newVal,oldVal)=>{
  			// console.log(key,newVal)
  			// 回调更新文本
  			console.log('----------触发文本更新回调------')
  			compileUtil.updateTextNode(vm, node, key, rawTextContent)
  		})
  	},
  	updateTextNode:function(vm,node,key,rawTextContent){
  	   let newTextContent = rawTextContent;
  	   // 获取 name 的值
  	   const val = this.getModelValue(vm, key);
  	   // console.log(val)
  	   // 替换文本内容
       node.textContent = val;
  	},
  	model:function(node,vm,exp,attr){
  		// model
  		const { value: keys, name } = attr;
  		node.value = this.getModelValue(vm,keys);
  		node.removeAttribute(name)
  		// input监听
  		node.addEventListener('input', (e) => {
  			this.setModelValue(vm, keys, e.target.value);
  		});
  		console.log('----实例化一个model订阅者----',keys)
  		// 实例化订阅者
  		new Watcher(vm, keys, (newVal,oldVal) => {
  			// 更新数据
  			node.value = newVal;
        });
  	},
  	getModelValue(vm,keys){
  		var cb = parsePath(keys)
  		var val =  cb.call(vm,vm);
  		return val
  	},
  	setModelValue(vm,keys,val){
  		// 这里的vue源码比较复杂,我简单一点了,哈~
  		var segments = keys.split('.'),obj;
  		if(segments.length === 1){
  			vm[segments[0]] = val
  		}
  		if(segments.length === 2){
  			vm[segments[0]][segments[1]] = val
  		}
  		if(segments.length === 3){
  			vm[segments[0]][segments[1]][segments[2]]= val
  		}
  	}
  }

  function remove (arr, item) {
  	if (arr.length) {
  		var index = arr.indexOf(item);
  		if (index > -1) {
  			return arr.splice(index, 1)
  		}
  	}
  }
  // dep id
  var uid = 0

  function Dep(){
  	this.id = ++uid;
  	// console.log('订阅器的ID---',this.id)
  	this.subs = []
  	// console.log('------------订阅器'+this.id+'实例化完毕')
  }

  Dep.prototype = {
  	addSub:function(sub){
  		// console.log('订阅器'+this.id+'开始收集')
  		// 加入订阅器
  		this.subs.push(sub)
  		// console.log('收集完毕')
  		// console.log('订阅者'+sub.id+'加入订阅器'+this.id)
  		// console.log('订阅器'+this.id+'的收集数组:',this.subs)
  	},
  	removeSub(sub){
  		remove(this.subs, sub);
  	},
  	notify:function(){
  		// console.log('通知订阅器'+this.id+'的所有订阅者subs',this.subs)
  		// 通知订阅者
  		for(let sub of this.subs){
  			// console.log('通知订阅者'+sub.id+'---去更新',sub)
  			sub.update();
  			// console.log('订阅者'+sub.id+'更新完毕')
  		}
  	},
  	depend:function(){
  		if(Dep.target){
  			// console.log('订阅器'+this.id+'收集订阅者Dep.target',Dep.target)
  			// console.log('------收集开始-------')
  			Dep.target.addDep(this)
  			// console.log('------收集完毕-------')
  		}
  	}
  }

  Dep.target = null
  const targetStack = []
  //这里有一个targetStack数组,或者说是栈,这个栈的作用就是当watcher.get执行过程中,如果遇到了别的watcher,就先把当前的watcher入栈,先执行别的watcher。

  function pushTarget(target){
  	// console.log('将当前target指向自己---')
  	targetStack.push(target);
  	Dep.target = target
  }

  function popTarget(){
  	// console.log('取出target,指向前一个')
  	targetStack.pop();
  	Dep.target = targetStack[targetStack.length - 1]
  }

  function parsePath(path){
  	var segments = path.split('.');
  	return function(obj){
  		// console.log('------计算'+exp+'的值')
  		for (var i = 0; i < segments.length; i++) {
  			if (!obj) { return }
  				obj = obj[segments[i]];
  		}
  		return obj
  	}
  }

  var uid_$wachter = 0;// watcher id

  function Watcher(vm,exporFn,cb,options,isRenderWacter){
  	this.cb = cb;//回调函数
  	this.vm = vm;//vue实例
  	this.exporFn = exporFn;//data的属性,或者computed的函数
  	// console.log('----exporFn',exporFn)
  	if(options){
  		this.lazy = !!options.lazy // 懒依赖,首次实例化不执行getter
  		this.user = !!options.user // 是否为用户自定义watch
  		this.deep = !!options.deep // 是否深度监听
  	} else {
  		this.deep = this.user = this.lazy = this.sync = false;
  	}
  	this.dirty = this.lazy // 脏值标志,为true重新计算
  	
    this.id = ++uid_$wachter
    console.log('-----开始实例化订阅者watcher--',this.id)
  	this.deps = [] // 依赖列表
  	this.newDeps = [] // 最新一次添加的依赖列表
  	this.depIds = new _Set() // 依赖ids列表
  	this.newDepsIds = new _Set() // 最近一次添加的依赖ids列表
  	
  	//区分computed watcher和render watcher
  	if(typeof exporFn === 'function'){
  		this.getter = exporFn
  	} else {
  		this.getter = parsePath(exporFn)
  		if(!this.getter){
  			this.getter = noop
  			warn('Failed watching path: \"' + exporFn + '\" ')
  		}
  	}
  	//实例化时,将自己加入dep
  	this.value = this.lazy ? undefined : this.get() 
  	// console.log('--------实例化watcher'+this.id+'---完毕')
  }

  Watcher.prototype = {
  	update:function(){
  		if(this.lazy){
  			this.dirty = true
  		} else {
  			this.run();
  		}
  	},
  	run:function(){
  		var vm = this.vm
  		// console.log(this.getter)
  		var value = this.get();
  		var oldValue = this.value;
  		if(value !== oldValue || this.deep){
  			this.value = value
  			// console.log(value)
  			// compile回调
  			this.cb.call(this.vm,value,oldValue)
  		}
  	},
  	get:function(){
  		// 将target指向自己
  		pushTarget(this)
  		var vm = this.vm
  		value = this.getter.call(vm,vm)//触发data的getter
  		// console.log(value)
  		// console.log('计算完毕')
  		// var value = this.vm[this.exp];
  		// 深度监听
  		if(this.deep){
  			traverse(value)
  		}
  		popTarget();// 用完删除
  		this.cleanupDeps()// 依赖整理,主要用来整理this.deps、this.depIds
  		return value;
  	},
  	addDep(dep){
  		// console.log('当前订阅者:'+this.id+' 当前订阅器:',dep.id)
  		// console.log('当前订阅者'+this.id+'的订阅器器收集器---',this.deps)
  		if(!this.newDepsIds.has(dep.id)){
  			this.newDepsIds.add(dep.id)
  			this.newDeps.push(dep);
  			this.deps = this.newDeps
  			if(!this.depIds.has(dep.id)){
  				dep.addSub(this)
  				// console.log('订阅器'+dep.id+'收集完毕')
  			}
  		}
  	},
  	cleanupDeps(){
  		// var i = this.deps.length;
  		// while (i--) {
  		// 	var dep = this.deps[i];
  		// 	if (!this.newDepIds.has(dep.id)) {
  		// 		dep.removeSub(this);
  		// 	}
  		// }
  		// var tmp = this.depIds;
  		// this.depIds = this.newDepIds;
  		// this.newDepIds = tmp === undefined ? new _Set() : tmp;
  		// // console.log(this.newDepIds)
  		// this.newDepIds.clear();
  		// tmp = this.deps;
  		// this.deps = this.newDeps;
  		// console.log('------订阅者'+this.id+'的收集器deps ',this.deps)
  		// this.newDeps = tmp;
  		// this.newDeps.length = 0;
  	},
  	evaluate(){
  		// console.log('-----重新计算-----')
  		this.value = this.get();
  		this.dirty = false
  	},
  	depend(){
  		let i = this.deps.length;
  		// console.log('订阅者watcher'+this.id+'的收集器deps',this.deps)
  		while(i--){
  			// console.log('----循环触发订阅者watcher'+this.id+'中订阅器的收集')
  			this.deps[i].depend()
  		}
  	},
  	teardown(){
  		// 销毁watch监听
  		var i = this.deps.length;
        while (i--) {
           this.deps[i].removeSub(this);
        }
  	}
  }

  function traverse(obj){
  	var keys,i;
  	if(!isPalinObject(obj)){
  		return;
  	} else {
  		keys = Object.keys(obj)
  		i=keys.length;
  		while(i--){
  			traverse(obj[keys[i]])
  		}
  	}
  }

  function warn(err){
  	console.error(err)
  }

  var noop = ()=>{}


  // 初始化data
  function initData(vm){
  	var data = vm._data = vm.$options.data
  	var me = vm
  	for(let key of Object.keys(data)){
  		// 数据代理
  		me._proxy(key)
  	}
  	observe(data,vm)
  }

  // 初始化computed
  function initComputed(vm,computed){
  	// 定义计算watcher
  	var watcher = vm._computedWatchers = Object.create(null)
  	for(var key in computed){
  		var userDef = computed[key]
  		var getter = typeof userDef === 'function' ? userDef : userDef.get
  		console.log('----实例化一个computed订阅者----',key)
  		watcher[key] = new Watcher(vm, getter || noop, noop)
  		if(!(key in vm)){
  			defineComputed(vm,key,userDef)
  		} else {
  			warn('the computed '+ key +' is already defined in data')
  		}
  	}
  }

  // 把计算属性代理到组件实例上,并且拦截了计算属性的get和set。set被设置为noop,
  function defineComputed(vm,key,userDef){
  	if(typeof userDef === 'function'){
  		sharedPropertyDefinition.get = createComputedGetter(key)
  	}
  	Object.defineProperty(vm,key,sharedPropertyDefinition)
  }

  function createComputedGetter(key){
  	return function computedGetter(){
  		var watcher = this._computedWatchers && this._computedWatchers[key]
  		// console.log('-----触发计算watcher'+watcher.id+' 的computed getter----当前watcher',watcher)
  		if(watcher){
  			if(watcher.dirty){
  				watcher.evaluate();
  			}
  			if(Dep.target){
  				// console.log('-----------开始收集')
  				watcher.depend();
  			}
  			// console.log(key,watcher.value)
  			return watcher.value
  		}
  	}
  }

  // 初始化watch
  function initWatch(vm,watch){
  	for(var key in watch){
  		var handler = watch[key];
  		createWatcher(vm,key,handler)
  	}
  }

  function createWatcher(vm,expOrFn,handler,options){
  	// 你传入的 watch 配置可能是这三种
  	// name:{handler(){}}
  	// name:function(){}
  	// name:"getname"
  	if(isPalinObject(handler)){
  		// 为对象,获取handler函数
  		options = handler;
  		handler = handler.handler
  	}
  	if(typeof handler === 'string'){
  		// 为字符串,获取函数
  		handler = vm[handler]
  	}
  	return vm.$watch(expOrFn,handler,options)
  }

  // 初始化data,prop,computed
  function initState(vm){
  	var opts = vm.$options
  	// 初始化数据
  	if(opts.data){
  		initData(vm)
  	} else {
  		observe(vm._data = {},vm)
  	}
  	// 初始化computed
  	if(opts.computed){
  		initComputed(vm,opts.computed)
  	}
  	// 初始化watch
  	if(opts.watch){
  		initWatch(vm,opts.watch)
  	}

  	vm.$compile = new Compile(vm.$options.el || document.body,vm)
  }

  function MVVM(options){
  	// 初始化
  	this._init(options)
  }

   //从代码中可看出监听的数据对象是options.data,每次需要更新视图,则必须通过var vm = new MVVM({data:{name: 'kindeng'}}); vm._data.name = 'dmq'; 这样的方式来改变数据。
  MVVM.prototype = {
  	_init:function(options){
  		var vm = this
     	// 传入的参数
  	    this.$options = options;
  	    // 初始化data,prop,computed
  	    initState(vm)
  	},
  	_proxy:function(key){
  		var vm = this;
  		Object.defineProperty(vm,key,{
  			configurable:false,
  			enumberable:true,
  			get:function proxyGetter(){
  				return vm._data[key];
  			},
  			set:function proxySetter(newVal){
  				vm._data[key] = newVal
  			}
  		})
  	},
  	$watch:function(expOrFn,cb,options){
  		let vm = this
  		options = options || {}
  		options.user = true
  		var watcher = new Watcher(vm,expOrFn,cb,options)
  		if(options.immediate){
  			cb.call(vm,watcher.value)
  		}
  		return function unwatchFn(){
  			watcher.teardown()
  		}
  	}
  }

  var mvvm = new MVVM({
  	el:'#app',
  	data:{
  		firstName:{
  			name:'yang'
  		},
  		lastName:'ren',
  		secondName:'second',
  		threeName:'three',
  		obj:{
  			a:1
  		}
  	},
  	watch:{
  		'obj':{
  			handler(val,oldVal){
  			  console.log('-obj.a改变了',val,oldVal)
  		    },
  		    deep:true
  		},
  		'obj.a':{
  			handler(val,oldVal){
  			  console.log('ooooo-obj.a改变了',val,oldVal)
  		    }
  		}
  	},
  	computed:{
  		'allName':function(){
  			return this.secondName + this.lastName
  		}
  	}
  })
</script>
</body>
</html>

引申提问:

computed如何控制缓存?

控制缓存最重要的一点是标志位dirtydirtywatcher的一个属性

dirtytrue,读取computed会重新计算

dirtyfalse,读取computed会使用缓存

1.一开始每个计算属性实例化自己的watcher时,会设置dirtytrue

2.当依赖的数据变化,通知computed时,会设置dirtytrue

3.computed计算完成之后,会设置dirtyfalse,当其他地方引用的时候,就会直接读取缓存

computed和watch的区别?

相同点:

  • 都是侦听一个数据,并进行处理的作用

不同点:

  • computed是计算一个新的属性,并将该属性挂载到实例上,而watch是监听一个存在并且已经挂载到实例上的数据,watch也可以监听computed计算属性的变化。

  • computed本质上是一个观察者,具有缓存行,只有当依赖发生变化后,才会计算新的值,而watch不具有缓存性。

  • computed主要用于对同步的处理,而watch可以处理异步操作,

  • 从使用场景上来说,computed适用于一个数据被多个数据影响,而watch适用于一个数据影响多个数据。

vue中data的属性名称与method的方法名称一样时会发生什么问题?

会报错

"Method 'xxx' has already been defined as a data property"

我们可以看下vue的初始化

function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) {
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
}

vue初始化执行顺序,会依次对 props -> methods -> data -> computed -> watch 进行初始化。

如果我们定义了methods方法名和data名相同,那么,先对methods初始化,不会有问题,当vue对data属性进行初始化的时候,会对data属性作出判断,执行如下代码:

{
        if (methods && hasOwn(methods, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a data property."),
            vm
          );
        }
}
if (props && hasOwn(props, key)) {
        warn(
          "The data property \"" + key + "\" is already declared as a prop. " +
          "Use prop default value instead.",
          vm
     );
} 

在使用计算属性的时,函数名和data数据源中的数据可以同名吗?

会报错

"The computed property "xxx" is already defined in data"

我们知道,computed是在data之后进行初始化的,让我们来看看computed初始化的时候,干了什么:


if (!(key in vm)) {
        defineComputed(vm, key, userDef);
} else {
   if (key in vm.$data) {
          warn(("The computed property \"" + key + "\" is already defined in data."), vm);
   } else if (vm.$options.props && key in vm.$options.props) {
          warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
        }
 }
    

props, methods, data, computed不管是哪个,都是挂载到vm上的,在computed初始化的时候,前三个已经挂载完毕,所以可以直接判断属性是否在vm中存在,然后再根据data,props划分。

在使用计算属性的时,函数名和methods的方法名可以同名吗?

可以,也不会报错,但是,调用的时候,methods会覆盖computed。

例如:

computed:{
    someName(){
      return 'com'
    }
},
methods: {
    someName(){
        console.log('haha')
      },
    clickME(){
        console.log(this.someName)
        // function() {console.log('haha')}
    }
},