Vue.js 源码剖析-响应式原理、虚拟 DOM中 Key 的作用以及Vue 中模板编译的过程

Vue.js 源码剖析-Vue 首次渲染过程、响应式原理、虚拟 DOM、模板编译和组件化


本文主要从 Vue.js 源码进行分析,剖析 Vue 首次渲染的过程、Vue 响应式原理、渲染虚拟Dom中k的作用及好处、以及模板的编译过程。让你更深刻的了解 Vue.js 从初始化到渲染到页面的整个工作过程!

Vue源码地址:https://github.com/vuejs/vue

首先需要准备一份Vue.js的源码,这里主要看src目录下的源码,怎么从gitHub上把项目Clone到本地就不详细讲解了,不会的自行百度。下面我们从Vue 首次渲染的过程开始,探索Vue.js的整个运行过程;

Vue 首次渲染的过程

  1. 在首次渲染之前,首先进行Vue的初始化,初始化实例成员和静态成员;
  2. 当初始化结束之后,要调用 Vue 的构造函数 new Vue(),在构造函数中调用了_init() 方法,这个方法相当于我们整个Vue 的入口;
  3. 在 _init 方法中,最终调用了 this.$mount(),一共有两个$mount:第一个定义在 entry-runtime-with-compiler.js 文件中,也就是我们的入口文件 $mount,这个 $mount 方法的核心作用是帮我们把模板编译成 render 函数。此处的 $mount 方法首先会判断options中是否传入了 render 选项,如果没有传入,它会去获取我们的 template 选项,如果 template 选项也没有的话, 会把 el 中的内容作为我们的模板通过 compileToFunctions 函数编译成 render 函数,当把 render 函数编译好之后,会把 render 函数存储在我们的options.render 中。
if (!options.render) {
  let template = options.template
    if (template) {
      ...
    }
}

Vue.compile = compileToFunctions
export default Vue

  1. 接着会调用 src/platforms/web/runtime/index.js 文件中的 $mount 方法,在这里首先重新获取 el ,因为如果是运行时版本的 Vue ,是不会在 entry-runtime-with-compiler.js 这个文件中获取 el 的,所以 这里为了更加的严谨会在 runtime/index.js 中的 $mount 中重新获取一下 el 。
  2. 判断完 el 之后接着会调用 mountComponent(this, el, hydrating) 这个方法。这个方法在src/core/instance/lifecycle.js 文件中定义。 在 mountComponent() 中首先会判断 vm.$options.render 。如果没有 render 选项,但是我们传入了模版,并且当前是开发环境的话会给我们发送一条警告,告诉我们运行时版本不支持编译器。接下来会出发 beforeMount 这个生命周期中的钩子函数,也就是开始挂在之前。
  3. 接下来定义 updateComponent(), 在这个方法中定义了_render 和 _update, _render 的作用是生成vnode, _update 的作用是将虚拟Dom 转换成真实Dom,并挂载到页面中来。
  4. 在接下来就是创建 Watcher 对象,在创建Watcher 时,传递了 updateComponent 这个函数,这个函数最终是在 Watcher 内部调用的。在 Watcher 创建完之后还调用了 get 方法,在 get 方法中,会调用 updateComponent() 这个方法。
  5. 最后触发 mounted 生命周期钩子函数,挂载结束, 最终返回 Vue 实例。
    这就是 Vue 首次渲染的整个过程,以上过程可通过 Vue 源码进行调试加深印象。

Vue 响应式原理

响应式原理
官网用了一张图来表示Vue 响应式整个过程,现在咱们分析一下这张图:
Vue 响应式过程分为三个步骤:

  1. init 阶段: Vue 的 data 属性注册到Vue 实例上,所有属性都会变成响应式的,也就是加上了 getter/ setter函数。
    在这里插入图片描述
  2. 发布通知阶段:当调用 defineReactive () 这个方法的 set方法时,最终会调用dep.notify()方法(派发更新(发布更改通知)),在 notify 这个方法中 会调用每个订阅者的update方法实现更新
// 将观察对象和 watcher 建立依赖
  depend () {
    if (Dep.target) {
      // 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
      Dep.target.addDep(this)
    }
  }

  // 发布通知
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    // 调用每个订阅者的update方法实现更新
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
  1. 更新阶段:在调用 watcher 对象的 update() 方法 时会调用 queueWatcher(this) 这个方法,并触发 beforeUpdate 钩子函数,最后调用 watcher.run() 这个方法,接着会调用 watcher 里面的 getter() 方法, 之后调用updateComponent,触发updated 钩子函数。

下面我梳理了一份响应式原理的导图,方便理解 Vue 响应式原理的整个过程,它的作用是判断 watcher 是否被处理, 如果没有的话添加到 queue 队列中,并调用 flushSchedulerQueue(), 在flushSchedulerQueue 中
在这里插入图片描述
我们整个响应式是从 src/core/instance/init.js 的 initState 方法中开始的,调用 initState() 初始化 vue 实例的状态,在 initState 中调用 initData(vm),将 data 属性注册到 vue 实例上,并且调用 observe() 将data 对象转换成响应式对象。observe就是我们响应式的入口。
最后总结一下:
1、第一步:组件初始化的时候,先给每一个Data属性都注册getter,setter,也就是reactive化。然后再new 一个自己的Watcher对象,此时watcher会立即调用组件的render函数去生成虚拟DOM。在调用render的时候,就会需要用到data的属性值,此时会触发getter函数,将当前的Watcher函数注册进sub里。
2、第二步:当data属性发生改变之后,就会遍历sub里所有的watcher对象,通知它们去重新渲染组件。

虚拟 DOM 中 使用 Key 的作用和好处

在 patchVNode 比较新旧节点的差异时,通过源码调试,我们可以总结出:
当没有设置 Key 时,会多次更新 DOM 操作 和插入操作。但,当我们设置 Key 的时候, 在比较 updateChildren 中比较子节点的时候,因为 key 相同,没有更新 Dom操作, 只做了一次插入操作。所以设置Key 可以减少 dom 的操作,减少 diff 和渲染所需要的时间,提升了性能,大大的优化了 Dom 操作。

Vue 中模板编译的过程

先上一张图大致看一下整个流程
在这里插入图片描述
从图中可以看到 compile 是从 mount 后开始进行,整体逻辑大致分为三个部分

  1. 解析器(parse) - 将 模板字符串 转换成 AST 抽象语法树
  2. 优化器(optimize) - 对 AST 进行静态节点标记, 主要用来做虚拟 DOM 的渲染优化
  3. 代码生成器(generate) - 将 AST 抽象语法书转换为 render 函数的 js 字符串

这里我先解释一下什么是 AST 即 抽象语法树

AST(abstract syntax tree 抽象语法树), 是源代码的抽象语法结构的树状表现形式;
从代码上简单理解一下

<div class="name">AST</div>
//转成AST后会得到如下格式
[
  {
    "type": "tag",
    "name": "div",
    "attribs": {
      "class": "name"
    },
    "children": [
      {
        "data": "AST",
        "type": "text",
        "next": null,
        "startIndex": 18,
        "prev": null,
        "parent": "[Circular ~.0]",
        "endIndex": 20
      }
    ],
    "next": null,
    "startIndex": 0,
    "prev": null,
    "parent": null,
    "endIndex": 26
  }
]

AST会经过generate得到render函数,render的返回值是VNode;

解析器(parse)
parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的;
优化器(optimize)
通过 optimize 把整个 AST 树中的每一个 AST 元素节点标记了 static 和 staticRoot, optimize 的过程,就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点则在每次重新渲染时不需要再重新生成节点
代码生成器(generate)
把抽象语法树生成字符串形式的 js 代码;

之后将 render 函数 通过 createFunction 函数 转换为 一个可以执行的函数,将 最后的 render 函数 挂载到 option 中,最后执行公共的 mount 函数。

总结: 首先通过parse将template解析成AST,其次optimize对解析出来的AST进行标记,最后generate将优化后的AST转换成可执行的代码;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值