vue进行页面渲染首先经过属性的初始化,在创建render函数最后进行挂载阶段。
在进行属性初始化的时候,会对计算属性computed进行初始化,主要代码如下:
1.计算属性可以有两种写法一种是函数形式一种是对象形式,对象格式是有get和set属性。
2.对于每一个属性创建一个对应的watcher,并在vue实例对象上面创建_computedWatchers属性用来存放对应计算属性的watcher,并设置参数lazy:true;这个参数用于告诉watcher我是个计算属性watcher以及赋值给watcher实例dirty属性用来控制计算属性的缓存功能。
由于我们每次调用计算属性都会返回一个值,所以计算属性也是需要使用Object.defineProperty来进行改写,主要代码如下
接下来就是改写计算属性的get方法。
每次获取计算属性的值的时候,会调用get方法,在get方法中我们获取到对应的计算属性watcher,如果当前计算属性的watcher存在则判断watcher的dirty是都是脏的,如果是脏的那么可以对当前的watcher进行求值。我们来看计算属性的watcher初始化以及evaluate方法。
class Watcher {
constructor(vm,exprOrFn,cb,options){
this.vm= vm;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.options = options
this.deps = []
this.depsId = new Set()
this.user = options.user || false; // 这是一个用户watch
this.lazy = options.lazy; // lazy不可变 如果watch上有lazy属性说明是计算属性
this.dirty = this.lazy; // dirty代表取值时,是否执行用户提供的方法exprOrFn
this.id = id++; // watcher的唯一标识
if(typeof exprOrFn == 'function'){
this.getter = exprOrFn;
}else{
// 自定义watcher exprOrFn是个key
/**
* exprOrFn 可能传递过来的是一个字符串a
*/
this.getter = function(){
// 当去当前实例取值的时候 才会触发依赖手机
let path = exprOrFn.split('.');
let obj = vm;
for(let i = 0;i < path.length;i++){
obj = obj[path[i]]
}
return obj;
}
}
// if(this.lazy)
// 默认先调用一次get方法,进行取值 将结果保留下来
this.value = this.lazy ? void 0 : this.get() // 默认会调用get方法
// console.log(' this.value', this.value)
}
addDep(dep){
// 一个组件watcher或者其他watcher会存放很多dep
let id = dep.id;
if(!this.depsId.has(id)){
// 对于一个watcher 没有重复的dep 就相当于dep中没有重复的watcher
this.deps.push(dep)
this.depsId.add(id)
dep.addSub(this)
}
}
get(){
// 当前watcher的实例
// Dep.target增加了当前的watcher实例 然后渲染页面 然后调用get方法
//computed: 在页面进行初始化的时候,会创建渲染watcher,这时候会将渲染 watcher存放到Dep的target上面接着获取模板上面相关的属性值,这时候比如会调用fullName的Object.defineProperty里面的get方法,第一次调用计算属性的get方法会调用相关的计算属性的watcher这时候dirty为true会接着调用计算属性相关联的属性,这时候会将当前的计算属性watcher存放到Dep.target中,这时候stack栈中会有两个watcher
pushTarget(this)
// 对于计算属性来说 这个this.getter对应的是watcher实例调用this.getter的时候函数内部的this永远指向的是watcher实例招不到对应的依赖属性 如 this.firstName 和 this.lastName
// console.log('方法',this.getter)
let result = this.getter.call(this.vm) // 默认会调用 exprOrFn 这里是渲染页面 需要取值 render 方法(with(vm))
// console.log('result',result)
console.log('存放的watcher',this)
popTarget()
console.log('删除对应的watcher')
return result;
}
update(){
if(this.lazy){ // 是计算属性 如果计算属性相关联的属性发生了变化那么将dirty设置为true下次获取计算属性的时候可以调用对应的evaluate求值方法
this.dirty = true;
}else{
// 这里不要么看次都调用get方法 get方法会重新渲染页面
// 先把当前watcher的get缓存起来
queueWatcher(this) // 暂存的概念
// this.get()
}
}
// 求值 调用get
evaluate(){
// dirty 当脏值为true 的时候获取computed的值,会调用对应的watcher的evaluate方法,这个方法会调用get方法,get方法会调用computed里面的函数(get)函数,改函数会调用对应依赖的属性,此时会将依赖的属性上面增加对应的computedwatcher
this.value = this.get();
this.dirty = false // 取过一次值
}
run(){
let newValue = this.get();
let oldValue = this.value; // 老值
// 当新的值和老的值一样 在设置的时候已经进行了拦截
if(this.user){
this.cb.call(this.vm,newValue,oldValue)
// 更新一下老的值
this.value = newValue
}
}
depend(){
// 计算属性watcher 会存储 dep dep会存储watcher
let i = this.deps.length; // 计算属性的dep都取出来 让依次的dep存放当前的渲染watcher
while(i--){
this.deps[i].depend(); // 让计算属性相关的依赖属性dep存放渲染watcher这时候每个依赖的属性的dep当中都有一个computed计算属性watcher和渲染watcher,当属性发生变化的时候先调用计算属性的watcher的update将dirty设置为true,然后调用渲染watcher的update重新渲染页面
}
}
}
计算属性watcher初始化的时候会有个默认的options,options就是刚才说的lazy:true,然后将lazy赋值给dirty:true,lazy只是标志这是个计算属性的watcher,而dirty是真正控制是否调用watcher的get方法的。在初始化的时候判断是否是计算属性watcher如果是 this.value = this.lazy ? void 0 : this.get() 那就默认不调用get方法。当获取计算属性的时候默认dirty为true,那么调用计算属性的evaluate方法
evaluate(){
// dirty 当脏值为true 的时候获取computed的值,会调用对应的watcher的evaluate方法,这个方法会调用get方法,get方法会调用computed里面的函数(get)函数,改函数会调用对应依赖的属性,此时会将依赖的属性上面增加对应的computedwatcher
this.value = this.get();
this.dirty = false // 取过一次值
}
get(){
// 当前watcher的实例
// Dep.target增加了当前的watcher实例 然后渲染页面 然后调用get方法
//computed: 在页面进行初始化的时候,会创建渲染watcher,这时候会将渲染 watcher存放到Dep的target上面接着获取模板上面相关的属性值,这时候比如会调用fullName的Object.defineProperty里面的get方法,第一次调用计算属性的get方法会调用相关的计算属性的watcher这时候dirty为true会接着调用计算属性相关联的属性,这时候会将当前的计算属性watcher存放到Dep.target中,这时候stack栈中会有两个watcher
pushTarget(this)
// 对于计算属性来说 这个this.getter对应的是watcher实例调用this.getter的时候函数内部的this永远指向的是watcher实例招不到对应的依赖属性 如 this.firstName 和 this.lastName
// console.log('方法',this.getter)
let result = this.getter.call(this.vm) // 默认会调用 exprOrFn 这里是渲染页面 需要取值 render 方法(with(vm))
// console.log('result',result)
console.log('存放的watcher',this)
popTarget()
console.log('删除对应的watcher')
return result;
}
Dep.target = null; // 类的静态属性
let stack = []; // 存放watcher的栈
export function pushTarget(watcher){
// Dep.target就是一个承载 承载当前的watcher实例
Dep.target = watcher
stack.push(watcher) // 有渲染watcher
console.log('stack',stack)
}
/**
* 分析:第一次存放的是渲染watcher 然后调用get方法,然后接下来又开始调用计算属性的get方法,evaluate方法中调用get方法,这个方法也会讲对应的计算属性watcher存放进去然后,继续进行下一行popTarget方法就是删除当前栈顶的watcher,即当前的计算属性watcher
*/
export function popTarget(){
// Dep.target就是一个承载 承载当前的watcher实例
// 删除当前的watcher实例
// Dep.target = null
stack.pop();
Dep.target = stack[stack.length - 1]; // 设置当前的Dep的静态属性target,为现在的渲染watcher,如果发现栈中还有watcher那么就将当前的渲染watcher放入的相应的dep中
}
evaluate方法会调用watcher的get方法,get方法第一行是将Dep的静态属性存放但当前的watcher,第二行调用计算属性的get方法默认会获取计算属性相关依赖属性比如
fullName(){
console.log('回执行吗')
// this.firstName , this.lastName求值时候,会记住计算属性的watcher,当这些属性发生改变的时候,会调用对应的computed的watcher去重新获取computed的值
return this.firstName + this.lastName
},
1.这时候会将firstName和lastName属性的dep中存放当前的计算属性watcher。
2.get调用完成以后会将当前的computed属性值给当前的watcher的属性value,并返回给get方法。这时候初始化就算完成了。
3.当相关的属性发生改变的时候如firstName和lastName改变的时候,会调用dep的notify方法,调用相关依赖的watcher的update方法
update(){
if(this.lazy){ // 是计算属性 如果计算属性相关联的属性发生了变化那么将dirty设置为true下次获取计算属性的时候可以调用对应的evaluate求值方法
this.dirty = true;
}else{
// 这里不要么看次都调用get方法 get方法会重新渲染页面
// 先把当前watcher的get缓存起来
queueWatcher(this) // 暂存的概念
// this.get()
}
}
判断如果是计算属性的话,那么重新将dirty设置为true,表示脏值为true,下次再获取计算属性的时候,会重新调用计算属性的evalute方法,重新获取计算属性的值。
我们把computed初始化讲完以后我们开始讲解当依赖的属性发生变化,页面是怎么重新渲染的。
这要从vue是怎么进行渲染页面的开始说起,vue挂载的时候会生成一个渲染watcher
export function mountComponent(vm,el){
// 调用render方法去渲染 el属性
// 先调用render方法创建虚拟节点,再讲虚拟节点渲染到页面上
//vm._render() 创建虚拟节点 vm._update将虚拟节点创建成真实的节点
callHook(vm,'beforeMount')
let updateComponent = ()=>{
vm._update(vm._render())
}
// 这个watcher是用于渲染的 目前没有任何功能 updateComponent()
// 初始化渲染阶段 将Dep.target 赋值为当前的watcher 渲染结束以后Dep.target设置为空
new Watcher(vm,updateComponent,()=>{
callHook(vm,'updated')
},true) // 渲染watcher标志
// 要把属性和watcher绑定在一起
callHook(vm,'mounted')
}
get(){
pushTarget(this)
let result = this.getter.call(this.vm)
popTarget()
return result;
}
depend(){
// 计算属性watcher 会存储 dep dep会存储watcher
let i = this.deps.length; // 计算属性的dep都取出来 让依次的dep存放当前的渲染watcher
while(i--){
this.deps[i].depend(); // 让计算属性相关的依赖属性dep存放渲染watcher这时候每个依赖的属性的dep当中都有一个computed计算属性watcher和渲染watcher,当属性发生变化的时候先调用计算属性的watcher的update将dirty设置为true,然后调用渲染watcher的update重新渲染页面
}
}
这个方法会给计算属性相关依赖属性如firstName和lastName增加当前的渲染watcher,所以当我们改变firstName和lastName 的时候,会执行两种watcher一个是计算属性watcher一个是渲染watcher。这时候页面重新渲染。