Vue2 Watcher 渲染分析

315 阅读3分钟

本文通过简单的例子分析 renderWatcher、userWatcher、computedWatcher 的初始和更新流程,分解了它们执行函数流程,以及 Dep、Watcher 的关系,实例数据结构。

renderWatcher

var vm = new Vue({
  el: '#app',
  template: `
      <div @click="change">
        {{msg}}
      </div>
      `,
  data() {
    return {
      msg: 1
    }
  },
  methods: {
    change() {
      this.msg = Math.random();
    }
  }
})

初始化:

{
	msg(dep): 1,
	__ob__: Observer (dep)
}
$mount -> mountComponent -> renderWatcher
此时 Watcher 里 lazy = false -> get() -> vm._update(vm._render(), hydrating)
执行 render 函数生成 Vnode 过程中, msg 的 Dep subs[renderWatcher] targetStack[renderWatcher]
生成 DOM 
处理 get() 里的 finally
	popTarget 取栈顶元素, msg Dep.target = undefined
	cleanupDeps 作用是 depIds,deps 保存上一次, newDeps newDepIds 保存当前的

点击更新:

点击:
msg Dep notify 循环处理subs,执行每个 watcher.update()
-> queueWatcher
   -> nextTick(flushSchedulerQueue) 
   将 flushSchedulerQueue 包装成函数放到 callbacks 中,timerFunc 将 flushCallbacks 放到微任务中
-> subs 循环完成
-> flushCallbacks -> flushSchedulerQueue 

image.png

queueWatcher

has 避免相同 Watcher 重复入 queue

flushing 标志在执行 flushSchedulerQueue 时会改为 true,表示正在执行更新,不往 queue 添加观察者

waiting 标志 让重复queueWatcher 时, nextTick(flushSchedulerQueue) 只执行一次

nextTick

pending 标志 nextTick 首次执行为 true , flushCallbacks 执行为 false,也就是同一次事件循环中,所有微任务一起执行。

var vm = new Vue({
  el: '#app',
  template: `
      <div @click="change">
        {{msg}}
      </div>
      `,
  data() {
    return {
      msg: 1
    }
  },
  methods: {
    change() {
      this.msg = Math.random();
    }
  },
  created() {
    this.$nextTick(() => { console.log(1) })
    this.$nextTick(() => { console.log(2) })
    this.$nextTick(() => { console.log(3) })
  }
})

执行 3 次 nextTick ,第一次执行时触发微任务,所有同步代码执行完,执行 flushCallbacks,这时 callbacks [f1,f2,f3],依次执行。

嵌套的 nextTick

var vm = new Vue({
  el: '#app',
  template: `
      <div @click="change">
        {{name}}
      </div>
      `,
  data() {
    return {
      name: 'vvv'
    }
  },
  methods: {
    change() {
      this.name = 'cc'
      this.$nextTick(() => {
        this.name = 'dd'
        this.$nextTick(() => { console.log('第二个 $nextTick') })
      })
    }
  },
})

解析:

初始完成后:
name 的 Dep: subs[renderWatcher], renderWatcher: depIds deps保存 name 的 Dep

点击更新
this.name = 'cc' 
这一步通知 name 的 Dep 中 subs 中的所有 Wathcer 更新,此时只有一个 renderWatcher
将 renderWatcher 放到 queue ,而 queue 中的执行是在 flushSchedulerQueue 触发,
flushSchedulerQueue 的触发是在 flushCallbacks 中执行,其中的 callbacks 的值是由 nextTick 中放入,flushCallbacks 是个微任务。
所以 nextTick(flushSchedulerQueue) 执行完,flushCallbacks 不会执行,执行下一步同步代码

this.$nextTick(cb)
将 cb 处理 放到 callbacks 中,此时不会产生新的微任务 pending = true
调用栈空后,处理 callbacks,先执行 name 的更新,然后是 change 中 的 nextTick 的回调,从这可以看到 nextTick 可以拿到更新后的值。
回调 name 重新赋值和执行 nextTick 和上述一样。又来一个轮回,两次是独立的。因为外层的 name 更新完,会重置原来的参数。

userWatcher

$watch 源码:

Vue.prototype.$watch = function (
expOrFn: string | Function,
 cb: any,
 options?: Object
): Function {
  const vm: Component = this
  // 对象会转出函数 再次调用
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) {
    const info = `callback for immediate watcher "${watcher.expression}"`
    pushTarget()
    // 立即执行回调
    invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
    popTarget()
  }
  return function unwatchFn () {
    // vm._watchers 移除, deps 里 Watcher 移除
    watcher.teardown()
  }
}
var vm = new Vue({
  el: '#app',
  template: `
      <div @click="change">
        {{name}}
      </div>
      `,
  data() {
    return {
      name: 'vvv'
    }
  },
  methods: {
    change() {
      this.name = 'dxx'
    }
  },
})
vm.$watch('name', {
  handler(neV, oldV) {
    console.log({ neV, oldV })
  },
  immediate: true
})

分析:

new Vue 中,name 收集[renderWatcher],执行 vm.$watch 时,将回调函数进行处理,最终执行

// userWatcher
new Watcher(vm, 'name',  handler(neV, oldV) {
	console.log({ neV, oldV })
}, {
immediate: true
user: true
});

实例中
vm._watchers [renderWatcher, userWatcher]
user = true
lazy = false
cb = handler

getter
function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
}

执行 getter
function (vm) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      // vm.name 触发 getter
      obj = obj[segments[i]]
    }
    // 'vvv'
    return obj
}

name 收集变成 [renderWatcher, userWatcher]

immediate=true  回调执行

结束

name 的 Dep 与 renderWatcher、userWatcher 关系

image.png

点击更新执行情况

image.png

$watch 的回调是在 userWatcher 的 run 方法里

watch 中 的 deep 属性用来深度观察

var vm = new Vue({
  el: '#app',
  template: `
      <div @click="change">
        {{name}}
      </div>
      `,
  data() {
    return {
      name: {
        b: 0
      }
    }
  },
  methods: {
    change() {
      this.name.b = Math.random();
    }
  },
  watch: {
    name: {
      handler: function (neV, oldV) {
        console.log({ neV, oldV })
      },
      // deep: true
    }
  },
})

当 b 改变时,watch 回调不会触发,加上 deep:true 就能深度监测。因为监听的是 name 属性,b 的 dep 中没有 userWatcher,所以在 traverse(value) 中进行 getter 收集。

computed Watcher

options.computed

vm._computedWatchers[key] = new Watcher(
  vm,
  getter || noop, //  typeof userDef === 'function' ? userDef : userDef.get
  noop,
  {lazy: true} // 惰性求值
)

// defineComputed 代理 vm 下
// 最终执行的函数 getter
function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
}
var vm = new Vue({
  el: '#app',
  template: `
      <div @click="change">
        {{someComputedProp}}
        <br />
        {{b}}
      </div>
      `,
  data() {
    return {
      a: 0,
      b: 0,
    }
  },
  methods: {
    change() {
      this.b = Math.random();
    }
  },
  computed: {
    someComputedProp() {
      return this.a + 1;
    }
  }
})

分析:

initComputed 创建 computedWatcher 不会进行 get()-> 
$mount -> renderWatcher->
render 函数中 -> 访问 someComputedProp -> computedGetter
watcher.evaluate()
computedWatcher中 getter 执行,即 someComputedProp() { return this.a + 1;} 执行。
someComputedProp 执行时 this.a 触发 a 的 Dep subs[computedWatcher] 收集,newDeps [aDep]
someComputedProp 返回 1
对 computedWatcher cleanupDeps,-> computedWatcher.deps [aDep]
computedWatcher.value = 1

解释下 Dep Watcher 函数干了什么?

targetStack 全局变量栈,存放 Watcher,Dep.target 指向栈顶

Watcher

get

1、将 当前 Watcher 放到 targetStack;

2、value = watcher.getter(); 触发收集

3、deep 为 true 触发深度收集($watch 设置 deep true);

将当前 Watcher 弹出 targetStack;

cleanupDeps:watcher.deps/depIds 存放 Watcher(从 news 中的拿的,其赋值是在 addDep 中)

4、返回 value

addDep

1、将有关 的 Dep 放到 watcher.newDepIds/newDeps

2、Dep.subs 中添加 Watcher

Dep

depend

Dep.target.addDep(this) 调用 Watcher.addDep(Dep)

所以执行到上述阶段(Dep.target 之前):

if (watcher) {
  if (watcher.dirty) {
    watcher.evaluate()
  }
  if (Dep.target) {
    watcher.depend()
  }
  return watcher.value
}

开始 targetStack = [renderWatcher, computedWatcher], Dep.target= computedWatcher;

this.a -> aDep.depend() -> aDep.subs=[computedWatcher] ,computedWatcher.newDeps=[aDep]

finally 执行后

targetStack = [renderWatcher] Dep.target= renderWatcher

computedWatcher.deps=[aDep], computedWatcher.newDeps=[]

computedWatcher.value = 1

继续 watcher.depend 分析

depend () {
  // this computedWatcher
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}
遍历 watcher.deps 数组即 依赖的 Dep,执行 depend

depend () {
  if (Dep.target) {
    // renderWatcher.addDep(aDep)
    Dep.target.addDep(this)
  }
}

addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      // aDep subs[computedWatcher, renderWatcher]
      dep.addSub(this)
    }
  }
}

最终的关系

image.png

更新流程

当 b 改变,通知 renderWatcher 更新和上面 renderWatcher 流程一样,重新渲染,区别是多了 computedWatcher,而 a 没变 -> someComputedProp 也没变,执行 createComputedGetter时,computedWatcher.dirty = false,不走其渲染,剩下和初始化流程一样。

再来看个改变的例子

var vm = new Vue({
  el: '#app',
  template: `
      <div @click="change">
        {{someComputedProp}}
        <br />
        {{b}}
      </div>
      `,
  data() {
    return {
      b: 0,
    }
  },
  methods: {
    change() {
      this.b = Math.random();
    }
  },
  computed: {
    someComputedProp() {
      return this.b + 1;
    }
  }})

image.png

点击时,this.b 改变 notify [computedWatcher, renderWatcher],computedWatcher.update 是将 dirty = true,renderWatcher 走 queueWatcher 流程,最终微任务中是 renderWatcher -> run 重新收集,和初始流程一样,只不过值存在,就不添加了。