了解computed,watcher
之前,建议先理解双向绑定原理
computed原理
- 需要理解的概念
data
:vue
中的data
数据
Dep
:全局类,用来收集依赖以及通知订阅者更新,data
中的每一个属性都会有一个对应的Dep
实例。
watcher
:全局类,观察者,订阅者,分为三种类型
-
user watcher
:自定义,既vue
实例中开发者自己定义的watch
。 -
computed watcher
:既vue
实例中开发者定义的计算属性,computed
初始化的时候,会为每一个计算属性生成一个watcher
实例 -
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
是计算属性,firstName
,lastName
是被计算属性(data
属性)。
1.首先,创建vue
实例,进行data
数据初始化,循环遍历data
属性,为data
加上get
和set
设置,同时会给每个data
属性实例化一个订阅器Dep
,以上为例,称订阅器firstDep
,lastDep
,在Dep
中会定义一个收集数组subs
。
2.然后对computed
初始化,循环遍历每一个computed
属性,将计算属性自身实例化成一个订阅者,称作computed watcher
,同时,把computed
属性挂载到vue
组件上,并设置computed
属性的get
和set
。
3.每一个计算属性都会生成订阅者watcher
实例,并在内部实例化一个dep
收集器deps
,同时,把target
指向自己,再将自己添加到每一个被计算属性firstName
,lastName
的dep
中,既firstDep
,lastDep
。这里,会调用firstName
,lastName
属性进行计算,而调用它们时,就会触发它们的get
函数。
4.在firstName
属性的get
中,进行依赖收集。这一段是关键,也比较绕。以computed watcher
(allName)添加到订阅器firstDep
中为例。
firstName
中定义的get
函数,会触发订阅器firstDep
的收集depend()
函数。
get:function(){
if(Dep.target){
dep.depend()
}
}
depend()
函数中会调用当前target
所指向的订阅者watcher
,触法订阅者watcher
的addDep()
函数,并把firstDep
当成参数传递。
Dep.prototype.depend=function(){
if(Dep.target){
Dep.target.addDep(this) // this 指向firstDep
}
}
订阅者watcher
中的addDep()
函数会收集订阅器firstDep
,防止重复收集订阅器,会通过id
判断去重,然后把订阅器firstDep
加入到订阅器收集器deps
中,然后再触发订阅器firstDep
的addSub()
函数,并把当前订阅者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
}
}
}
最后在订阅器firstDep
的addSub()
函数中,把订阅者watcher
加入到订阅器数组中。
Dep.prototype.addSub=function(sub){
// 加入订阅器
this.subs.push(sub)
}
其实很容易理解,allName
实例化的订阅者computed watcher
和 firstName
的订阅器firstDep
,是两个类的实例,通过相互调用对方的函数,并把自己当成参数传递给对方,让对方收集。最后,订阅者computed watcher
被添加到了firstName
的订阅器firstDep
中,firstName
的订阅器firstDep
被添加到了订阅者computed watcher
的deps
收集器中。
同理,lastName
也是如此。
5.更新。当firstName
发生改变,订阅器firstDep
会通知所有的订阅者,订阅者computed watcher
收到通知,调用更新update()
函数,重新计算出firstName+lastName
的结果,同时把脏值dirty
设置为false
,如果firstName,lastName
不变,那么下次取值调用的时候,就会直接取,而不需要重新计算。
- computed的原理,是如何和render watcher(视图)联系起来的?
1.在computed
初始化的时候,劫持了计算属性的get
和set
,se
t被设置为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
收集器里每一个收集器dep
的depend()
。
然后,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如何控制缓存?
控制缓存最重要的一点是标志位dirty
,dirty
是watcher
的一个属性
dirty
为true
,读取computed
会重新计算
dirty
为false
,读取computed
会使用缓存
1.一开始每个计算属性实例化自己的watcher
时,会设置dirty
为true
,
2.当依赖的数据变化,通知computed
时,会设置dirty
为true
3.computed计算完成之后,会设置dirty
为false
,当其他地方引用的时候,就会直接读取缓存
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')}
}
},