Vue源码学习6-响应式原理-$mount

  • 内容
  • 评论
  • 相关

前言

--以下所有代码来自Vue 2.6.11 版本

--以下的vm都代指vue实例

--文章中讲的 依赖收集派发更新 未涉及到 组件化 的内容,仅限于 根Vue

--这篇文章需配合前面几篇文章食用,请多开几个窗口看源码

贴源码

前面简单梳理了Dep 和 Watcher 之间的关系,这篇文章讲一讲 $mount 挂载,篇幅可能略长。

第一篇文章中简单的梳理过 new Vue 的一个大致流程,也说过了 $mount 方法定义的位置,以及它为什么会定义在两个地方(如果忘了回头看看

下面再次贴出定义 $mount 的源码,它们在浏览器中实际的运行顺序也是这样的,第一段代码正常定义 $mount ,第二段代码先缓存已经定义好的 $mount,然后再定义一个自己的 mount 方法,也就是比原本的 mount 方法多了一段编译的逻辑(编译的代码有点多,讲的话还得单独拎出来),编译的目的就是生成 render (渲染函数),这个渲染函数在Vue文档里有说明,在平时开发中,我们基本数是使用vue-cli搭建的项目进行开发,vue-loader 已经把编译的事情给做了,所以生产环境的代码中都不会默认是不会有这段编译代码。

定义于 src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
定义于 src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
...................
................代码省略 模板编译流程

  return mount.call(this, el, hydrating)
}

总之,最后还是会走到原本的 mount 方法,然后就是 mountComponent 方法,它是定义在这里面的

src/core/instance/lifecycle.js(代码在下方)

省掉一些不太重要的代码和注释,剩下的逻辑发生在 beforeMount 和 mounted 这两个钩子函数之间,熟悉Vue的人都知道,在Vue的整个生命周期中,想要访问到真实的 dom 节点,必须在 mounted 执行时和执行后。

依赖收集

接下来认真了,要紧跟着源码走

1:callHook --- beforeMount

2:updateComponent 函数赋值,那个vm._update也是定义在 src/core/instance/lifecycle.js 中的,它就是 patch 发生的地方,也就是 vnode(没错,就是那个被大家传的很牛皮的虚拟dom) 映射成真实 dom 的地方,具体的细节就先略过了,patch的整个流程挺复杂的.....

3:接下来就是重点了,这里面实例化了一个Watcher,这个watcher就是这个 vm 中唯一的一个 render-watcher(这里就不贴Watcher的源码了,多开个窗口看源码)

4:进入构造函数的逻辑,省略......走到 this.cb = cb,这里的 cb 是个空函数

5:继续省略.....,走到第二个 if 逻辑,this.getter 会被赋值一个 function ,这个function就是外面传进来的 updateComponent 方法

5:继续省略.....,走到最后 this.value = this.lazy ? undefined : this.get(),这里的 this.lazy 是false,除了computed(计算属性)会设置为 true,其他的watcher都是false

6:进入 get 方法,划重点,pushTarget(this),前面介绍 Dep 的时候简单提过这个方法,它和 Dep 定义在同一个文件中,执行方法当前的这个 render-watcher会压栈,然后 Dep.target 这个静态变量,会赋值为当前的 render-watcher。包括后面会讲的 computed 和 watch,它们都会通过这种压栈的方式来达到 依赖收集 的目的

7:然后走到  value = this.getter.call(vm, vm) 这里,(call方法的作用就不说了,不懂就去查)这里会执行 this.getter方法,前面说过,this.getter就是外部传进来的那个 updateComponent 方法

8:此时回到 mountComponent  代码中,看到 updateComponent 被赋值那个地方,它在watcher中被执行了

9:vm._render (src/core/instance/render.js)是一个实例方法(这里不细说),它执行的过程中会生成 vnode,而生成vnode的过程中就会就会发生 依赖收集 ,想要获取 data/props 中的值,就会触发 defintReactive 中的 get 逻辑

10:回过头打开 defineReactive 的源码查看,看到 Object.defineProperty 的get逻辑,第一步求值 value ,注意此时的 Dep.target 就是上面赋值的 render-watcher

11:进入 if 逻辑,执行dep.depend,打开 Dep 源码进到方法中去,走到 addDep(this),addDep方法中可以先忽略掉 newDeps那些操作,(那是Vue为了优化所做的一些事情,下面讲到派发更新的时候再回顾这里)。然后走完 dep.addSub(this) 的逻辑,至此,Dep 和 Watcher正式建立联系,也代表 vm 中 data/props 的数据和render-watcher(视图)建立了依赖关系

12:最后,假设代码没报错,get 方法正常走到 finally,this.deep 默认是false,这里的render-watcher也不需要 deep 为 true(后面讲watch的时候应该能用上),然后就是 popTarget 出栈,它和上面所讲的 pushTarget 是成对操作,有压栈就得有出栈,不然依赖收集就会乱套

13:接着 cleanupDeps ,它是干嘛的?清除依赖,这里是初次 依赖收集 ,暂时没用,下面说完 派发更新 后再单独说说这个方法。

14:get 逻辑走完,new Watcher 完毕,接着执行 mounted 钩子(此处的mounted仅仅只是 root-vm 的钩子,至于组件的mounted在其他的地方,已经先于 root-vm 的mounted执行了),整个 new Vue 的工作正式走完。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  .....代码省略
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      ......  埋点操作
      vm._update(vnode, hydrating)
      ....... 埋点操作 
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

派发更新

上面已经梳理完整个 依赖收集 的过程,接下来就是 派发更新 的过程。

我们回头看着 defineReactive 代码中 Object.defineProperty的set逻辑,假设 我们的 $mount 过程已经走完了,视图已经生成完毕,页面已经展示出来,并且代码逻辑如下所示:

1:点击按钮触发事件,改变 this.name

<template>
  <div>
     <button @click='onClick'>change</button>
     <span>{{name}}</span>
  </div>
</template>
<script>
  export default {
    data(){
      return {
        name:'123'
      }
    },
    methods:{
      onClick(){
        this.name = new Date().getTime()+'';
      }
    }
  }
</script>

2:打开 defineReactive 的代码,看到 Object.defineProperty的set逻辑,前面的 依赖收集已经建立好了 view 和 data 的依赖,这时候修改this.name就会触发 set 逻辑

3:首先,求值得到 value,然后 newVal(新值) 和 value(旧值)比较,如果两个值相等就不往下走了,也就不会触发视图改变

4:如果新旧值不一样,就接着走,customSetter 方法,这个东西为了在开发环境下为了防止用户修改 props 中的值,传入的警告方法,就只是警告而已,生产环境就直接忽略掉

5:然后,val(旧值)被正式修改掉,接着 observe 方法,最后触发 notify (通知),通知谁呢?通知依赖当前 value 的 watcher

6:查看 Dep 的源码,notify中会遍历 watcher 数组,挨个执行 update 方法

7:查看 Watcher 的update方法,有三段逻辑,第一个是给 computed(计算属性)用的,此处略过;第二个是同步更新视图,this.sync一般都为 false ,仔细想想,如果 data 数据更新过于频繁,视图也会频繁的更新,影响性能就不好了

8: queueWatcher 这里就不深入查看了,简单了解一下,这个方法是在搜集render-watcher,放入到一个队列中,再将队列中watcher的视图更新操作全部放到下一个事件循环中进行(它和Vue常常使用的 $nextTick 有关,涉及到 EventLoop 知识点,不懂就去查阅),这个过程中做了一些优化处理,就是去重(通过每个watcher都有 且唯一的 watcher.id),每个render-watcher可以同时监听很多个 Dep ,当多个 Dep 触发 notify 通知了同一个 render-watcher,如果不做去重,就会搜集到很多重复的watcher,到时候视图渲染也会出现无用的重复渲染工作

9:总之,正常情况下最后会去执行render-watcher的 run 方法。打开Watcher源码,进入 run 方法

this.active 默认是为 true,虽然它也会有为false的时候,不过这里咱就不讨论了。然后顺利执行 get 逻辑,这里是新的一轮 依赖收集 ,里面就是我们已经熟悉过,render过程以及压栈出栈操作。接着下面一段 if 逻辑这里是进不去的,那和 watch 的深度依赖有关

10:至此,派发更新 的操作走到这里就完了,get 逻辑完成后视图就已经渲染完毕了

cleanupDeps

上面的 依赖收集 的过程中我们注意到这个方法,它的代码虽然不多,但却是Vue的在细节上的优化。平时开发中经常会遇到这样的情况,代码如下:

<template>
    <div class="hello">
        <button @click="isShow=!isShow">change</button>
        <div v-if="isShow">
            {{person1}}
        </div>
        <div v-else>
            {{person2}}
        </div>
    </div>
</template>
<script>
new Vue({
    el:'#app',
    data:{
        person1: 'Tifa',
        person2: 'Cloud',
        isShow: true
    }
})
</script>

通过 v-if 渲染不同的视图,初始化的情况下,person1 会和 render-watcher 建立依赖,person2 不会,因为 isShow 为true,在执行 render 函数的时候,v-else 那块儿逻辑是不会进去的,也就无法触发 defineReactive 的 get 逻辑,由于是第一次收集依赖,此时的 cleanupDeps 是没啥作用的,render-watcher中的那些newDeps newDepIds depIds deps 都是空的,清不清除依赖都一样。

但是,当点击按钮的时候,isShow被修改然后触发 set 逻辑,也就是 派发更新,最后会走到 render-watcher 的 get 方法,开始新的一轮 依赖收集,这一轮在收集的时候,person2 就可以顺利的和 render-watcher 建立依赖;此时,新的一轮依赖收集完毕(进入Watcher源码查看 addDep中是如何收集的),newDeps 和 newDepIds 都有 person2 的dep记录,deps 和 depIds中还保留着 person1 的dep记录。

进入 cleanupDeps 方法中,while 逻辑首先会清除掉当前 render-watcher 中所有的旧依赖关系,也就是和 person1 之间建立的依赖,在这里被清除掉了;当然,也不是无脑清除,会先判断 newDepIds 中有没有,有就不清除了,毕竟这玩意儿(newDepIds)是新一轮 依赖收集建立的最新的依赖,不能删除。

接着下面的那段代码就是一个互换的操作了,deps/depIds 和 newDeps/newDepIds 互换,然后清空 newDeps/newDepIds 的数据,这样deps/depIds就保存着最新的依赖关系,这个时候 person1 就已经和 render-watcher没有关系了,现在不管我们在代码中如何花式修改 this.person1 ,都不会触发 render-watcher 的视图更新。这是一个很棒的优化,避免了很多无效的 视图更新 操作。

总结

以上,再结合前面几篇文章,就完整的走通了响应式原理的流程,后面会讲的 组件化 computed watch  它们的整个流程其实和这里是差不多的。

现在回过头看看这里的最后一张图片,整个流程就比较清晰了。

 

评论

0条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注