1.背景
掘金中介绍vue中computed的实现原理的的文章多且精,大家都觉得这个computed的实现比较BT,本文就computed实现过程中的渲染watcher和_computedWatcher如何出入栈的过程详细,深刻理解computed中的属性如何收集到渲染watcher,也是解决自己当时学习源码时的疑问,新人文章不对的老哥们尽管提。
2.简述vue渲染过程
2.1 初始化
众所周知vue初始化就是要new个Vue
const vm = new Vue({
data() {
return {
a: '我', b: '妻',c: '善',d: '逸',
}
},
computed:{
myName(){
return this.a+this.b +this.c+this.d
}
}
})
这个过程发生了什么呢,vue2中使用Object.definePorperty给每个属性添加get和set函数,其中涉及的数据包含data中的a,b,c,d和computed属性的myName,于此同时_computedWatchers诞生了,_computedWatchers用于存放computed属性所创建的观察者watcher,这里的watcher也就是实现vue响应式原理的核心,不理解的小伙伴可以去细看vue响应式原理,会有订阅类和观察者类,有助于理解本文,但是不关键,这里仅贴watcher类里的部分核心方法
class Watcher {
constructor(vm, fn, options,cb) {
this.id = id++
this.vm = vm
this.renderWatcher = options
if(typeof fn === 'string'){
this.getter = function(){ return vm[fn]}
}else{
this.getter = fn
}
this.cb = cb
this.lazy = options?.lazy
this.dirty = this.lazy
//初始化watcher类是lazy为true不执行get方法
this.value = this.lazy ? undefined : this.get()
}
//取值执行回调
get() {
pushTarget(this)
let value = this.getter.call(this.vm)
popTarget()
return value
}
//computed属性
evaluate() {
this.value = this.get()
this.dirty = false
}
}
let stack = [] //用来管理watcher的栈
//入栈
function pushTarget(watcher){
Dep.target = watcher
stack.push(watcher)
}
//出栈
function popTarget(){
stack.pop()
Dep.target = stack[stack.length-1]
}
//初始化computed的属性
function initComputed(vm) {
const computed = vm.$options.computed
const wathcers = vm._computedWatchers = {}
//遍历computed属性
for (let key in computed) {
let userref = computed[key]
let fn = typeof userref == 'function' ? userref : userref.get
//new一个watcher类,属性为lazy并不执行get的回调方法
watchers[key] = new Watcher(vm, fn, { lazy: true })
defineComputed(vm, key, userref)
}
}
function defineComputed(vm, key, userref) {
const getter = typeof userref == 'function' ? userref : userref.get
const setter = userref.set
//给computed属性添加自定义get方法
Object.defineProperty(vm, key, {
get: creatComputedGetter(key),
set: setter
})
}
function creatComputedGetter(key){
return function (){
const watcher = this._computedWatchers[key]
if(watcher.dirty){
//触发计算 即wathcer中的get方法
watcher.evaluate()
}
if(Dep.target){
//当时不理解为什么此时渲染watcher还存在 能被收集到
//让myName的watcher的订阅者收集到渲染watcher
watcher.depend()
}
return watcher.value
}
}
2.2 元素挂载
元素挂在就是我们熟知的vue.$mounted('#app'),
Vue.prototype.$mount = function (el) {
const vm = this;
//省略很多代码 下面这个方法是vue通过模板编译成AST语法树
const render = compileToFunction(template)
options.render = render
mountComponent(vm, el)
}
function mountComponent(vm,el){
vm.$el = el
const updateComponet = ()=>{
vm._update(vm._render())
}
//此时渲染watcher
let watcher = new Watcher(vm,updateComponet)
}
3.渲染watcher和computedWatcher的交融
看完以上代码,思考两个问题:
1.渲染watcher和computedWatcher谁先被创建的?
2.computedWatcher中的myName的收集到渲染watcher(本文立意)
其实很明显,new Vue的过程中数据进行初始化,这个时候computed的属性也被初始化,就每个computed的属性都会创建一个watcher被_computedWatchers统一收集,并绑定自定义的get方法和set方法,所以computedWatcher先被创建了,并追加了lazy为true的属性,回头看代码,lazy为true就不会执行get方法,所以不会出入栈,这个时候不会出入栈,那么什么时候computedWatcher入栈的呢,这是我当时在学习转不过来弯的地方
数据初始化完成之后,到了挂在元素的时候,就会触发新建一个渲染watcher,它为什么叫渲染watcher,因为传入的回调函数,是触发渲染更新的函数vm._update(vm._render()),这也是为什么computedWatcher也要收集这个渲染watcher的原因,因为需要触发视图更新。渲染watcher新建时没有设置lazy参数,所以会执行watcher的get方法,此时渲染watcher入栈了,接下来执行了回调vm._update(vm._render()),vm._render()渲染函数会取视图中需要的变量值,也就是myName,因为在模板编译过程中会解析这个属性,myName的属性在初始化的时候已经自定义了其get方法,所以这个时候会触发watcher.evaluate()这个计算方法,计算方法中会触发当前myName的watcher的出栈以及入栈,那么此时栈中还是存在一个渲染watcher的,myName的watcher的订阅者此时就可以收集到渲染watcher了,之后渲染wactcher出栈,功成身退,从而在实现computed的计算属性。