vue 源码分析之new Vue() 的时候都发生了什么事

疫情期间学习成果继续输出,若有不对的地方请指出,感激不尽!

在做vue项目的时候都会运行以下这段代码,我只知道这是new了一个vue实例,然后初始化,编译,挂载,卸载,如下图:

 

 

 

 

但是vue内部都具体怎么操作的一概不知,今天学习源码的过程中发现了终于知道了其中的奥秘。我们来一步步的解析这个过程都做了哪些事。先找到项目的入口文件。

 1.首先找到项目的入口文件

我们会在启动vue项目的时候都会执行npm run dev,那这个指令都做了哪些操作,我们可以打开packge.json文件找到这行指令

 可以看到有个scripts/config.js文件,这就是这个指令执行的配置文件,我们打开config.js继续往下找

 这个是当前项目的入口地址。

web/entry-runtime-with-compiler.js 运行时编译

 

 

 可以看到这个文件主要做的是扩展$mount方法,处理template和el选项,不管传过来的options是template还是el选项,最终都会编译成render函数

同时导出了一个vue

 可以看到这个vue是从runtime/index文件引入的,打开这个文件

 这个文件定义了$mount方法,并且由mountComponent()这个方法执行挂载,将根组件挂载到宿主元素

这个文件还不是真正定义vue的地方,这个文件导出的vue是从core/index文件引入的

 打开core/index文件

 这个文件定义了全局API,set、delete、nextTick等方法,同时从instance/index引入了Vue,打开这个文件

 终于找到了定义vue的地方,可以看到这个这个里面有几个核心方法及其作用,分别是

initMixin(Vue)  // 实现init函数 
stateMixin(Vue) // 状态相关api $data,$props,$set,$delete,$watch 
eventsMixin(Vue)// 事件相关api $on,$once,$off,$emit 
lifecycleMixin(Vue) // 生命周期api _update,$forceUpdate,$destroy 
renderMixin(Vue)// 渲染api _render,$nextTick

当执行new Vue的时候就是执行this._init()这个方法,进行生命周期的初始化,而init方法是由initMixin(Vue)这个方法实现的

2.项目初始化init()方法的调用

 这个方法首先整合options选项,将用户传递的options选项与当前构造函数的options选项及其父级构造函数的options选项合并生成一个新的options并赋值给$options。

然后再判断如果用户传递了el选项,就自动开启模板编译阶段与挂载阶段,如果没有el选项就需要用户手动执行vm.$mount方法进行模板编译和挂载阶段。其中ininState()会初始化事件、 props、 methods、 data、 computed 与 watch,我们着重来看一看data的初始化过程。其实就是数据响应化和依赖收集的过程。

先判断data是否是函数,如果是函数的话就getData函数并将返回值赋给data和vm._data,daita中的数据会被保存在vm._data中。

这个方法中有一个observe方法,对data中的属性进行遍历,执行相应的操作,具体看一下observe方法都做了什么

 这个方法返回了一个Observe实例

 判断当前数据类型

 如果是object类型就就执行defineReactive()方法

 

 

 

 

 defineReactive定义对象属性的getter/setter,getter负责添加依赖,setter负责通知更新 ,这是数据响应化的处理。再来看一下依赖收集,这个方法中有一个Dep,看一下它的作用

 Dep就是订阅者,负责管理一组Watcher(观察者),包括watcher实例的增删及通知更新 ,用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作; 用 notify 方法通知 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作 ,下面再看Watcher的作用

watcher一创建就会执行get方法进行依赖收集

  

Watcher解析一个表达式并收集依赖,当数值变化时触发回调函数,常用于$watch API和指令中。
每个组件也会有对应的Watcher,数值变化会触发其update函数导致重新渲染

到此,初始化过程就完成了。

3. $mount执行挂载

$mount方法是在runtime/index这个文件里面定义的

我们可以看到$mount这个方法是由mountComponent()这个函数执行的

可以看到,这个函数会new一个Watcher,调用更新函数更新组件

其中updateComponent方法会做两件事,第一是通过_render()方法将之前编译的render function 渲染成VNode,第二是通过_updata()方法将虚拟dom转换成真实dom显示在页面上。

接下来我们来分别看这两个步骤

4.渲染函数render

打开render.js

 这里面有个createElement()函数,它的核心作用是将用户传进来的render function转换成虚拟dom。

 

 

 其实render函数的执行结果就是createElement()的执行结果,在这里就不详细讲解其过程。

接下来执行_updata()的方法,这个过程会先执行patch方法对新旧虚拟dom进行差异对比

5.patch对比

 如果没有新的虚拟dom,表示是第一次初始化程序,就直接将这个虚拟dom作为真实dom,否则就表示要更新,这个时候需要新旧虚拟dom对比,由__patch__()这个方法实现

打开vdom/patch.js,

 

patch算法通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当 高效的算法。

同层级只做三件事:增删改。具体规则是:new VNode不存在就删;old VNode不存在就增;都存在就 比较类型,类型不同直接替换、类型相同执行更新;

两个VNode类型相同,就执行更新操作,包括三种类型操作:属性更新PROPS、文本更新TEXT、子节点更新REORDER patchVnode具体规则如下:
1. 如果新旧VNode都是静态的,那么只需要替换elm以及componentInstance即可。

2. 新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren

3. 如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节 点加入子节点。

4. 当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。

5. 当新老节点都无子节点的时候,只是文本的替换。

至此,vue的渲染过程执行完毕

总结:new Vue() 之后的关键步骤

1. 执行init方法进行初始化

   。进行初始化生命周期、事件、 props、 methods、 data、 computed 与 watch

   。initState()方法会进行data初始化,其实就是数据响应化 和 依赖收集 的过程

在图中表现为这一部分

2.编译

如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要调用第一个$mount() 方法进行「 编译」步骤,并最终生成render函数

编译过程如下:

  。解析 - parse 

     解析器将模板解析为抽象语法树AST,只有将模板解析成AST后,才能基于它做优化或者生成代码字符串。

  。优化 - optimize 

     优化器的作用是在AST中找出静态子树并打上标记。静态子树是在AST中永远不变的节点,如纯文本节 点。

  。代码生成 - generate 

    将AST转换成渲染函数中的内容,即代码字符串。 

在图中表现为下面部分

 

 

 3、生成VNode,再将VNode转换成真实dom

 调用第二个$mount()方法,通过_render() 方法将 render funtion 生成虚拟dom,再通过_patch() 方法,进行diff算法,把虚拟dom转换成真实dom,显示在页面。在这个过程中会进行响应化处理,具体过程如下:

   。再将render function转换成VNode的过程中会读取data里面设置的属性,所以会触发getter方法,进行依赖收集,并将观察者 Watcher 对象存放到订阅者 Dep 的 subs 中如果数据发生变化就        会触发setter方法,通知更新视图,进行updata方法,这个方法会执行patch进行对比差异,然后更新。

在图中表现为这部分:

下面是真实开发调用栈的执行结果(当时截图软件和某一个热键冲突了截不了就直接拍照了)

      

 下面是整个代码执行思维导图

 

posted @ 2020-11-18 00:34  leahtao  阅读(1788)  评论(0编辑  收藏  举报