一文彻底读懂Vue 2.x运行机制

1,209 阅读5分钟

Vue 3.0正式版已经出来一段时间了,全新的语法糖及代码组织方式,特别是TypeScript加持,让你在开发的时候感觉很香。Vue 3.0很多核心功能的实现和2.x其实是一致的,Vue 3.0到来之际,你真的会Vue 2.x吗?

 前言

之前我写过一篇文章,简单介绍了Vue 2.x源码的工程化和用到的技术,没看过的,大家可以去看看《Vue源码工程化及构建流程》

Vue项目初始化

我们通过vue-cli或者自己封装的脚手架初始化一个项目之后,通常在项目根目录下会有src、public等文件夹,src文件夹下有整个项目的入口文件main.ts,main.ts也是构建工具webpack的入口,在main.ts中会有Vue的初始化:

new Vue({
    render: h => h(App)
}).$mount('#app');

所以,整个项目运行是通过new一个Vue的实例开始,初始化Vue实例的时候其实有几个核心功能实现。

Vue源码的几个核心实现

  1. Vue选项的规范化
  2. Vue选项的合并
  3. Vue数据响应式
  4. Vue模版编译器
  5. 模版解析成AST
  6. AST生成Render函数
  7. 虚拟DOM patch

Vue选项规范化

一个单文件组件通常会有几个部分:

  • props
  • data
  • components
  • methods
  • computed
  • watch

比如我们想接收父组件传值,就会在单文件组件对象上定义props属性,但是在写props的时候,你会发现有好几种写法

第一种

export default {
  props: ['name', 'age']
}

第二种

export default {
  props: {
    name: String,
    age: Number
  }
}

第三种

export default {
  props: {
     name: {
        type: String,
        default: ''
     }
  }
}

以上三种写法,我相信用过Vue的同学都很清楚。Vue内部怎样处理这些不同的写法呢?不可能有三种适配器,那么只能规范化,Vue在初始化的时候就会规范props为第三种写法,其他的属性比如computed、methods、watch怎么规范,大家可以去看源码。

Vue选项的合并

一个中大型项目,都会做组件抽象,项目中可能存在上千个.vue组件,而每个组件中都定义了自己的data、props、methods、inject......,多个文件的共同属性是怎么合并在一起的呢?我们这次只讲流程,不讲细节,大家可以下去了解。

Vue数据响应式

Vue如何做响应式的,相信大家并不陌生,不管工作中还是出去面试,经常会遇到这个问题,下面贴一张官方图片

19

关于依赖收集、数据劫持、派发更新的关系,其实远比图片要复杂,因为里面还要考虑很多细节,包括数组的处理,多个属性被重复监听等,不过最重要的一点,Vue在初始化的时候初始化了一个全局的Watcher对象

new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)

Watcher对象在初始化的时候,会往构造函数传一个updateComponent方法,我们看看updateComponent方法的定义

updateComponent = () => {  
    vm._update(vm._render(), hydrating)
}

之后数据有变化,都会通知watcher执行update方法,执行update的时候,就会取出初始化时候的updateComponent来执行,这个方法内部会执行render function和patch,这些后面会讲,记住这里是核心。

Vue模版编译器

单文件组件通常会包含三个部分,template、script、style,template里面会包含html,v-for,v-if,@等指令或符号,vue怎么解析他们?内部有个模版解析器parseHTML

parseHTML会解析出template里面所有标签的属性,指令和属性值,然后转换成AST,内部的核心实现大家可以去看代码。

模版解析成AST

模版编译器解析模版之后会生成AST(抽象语法树),大家可能对AST没什么概念,我们举个例子

HTML片段

<div class="tree" v-if="show">
  <span>{{item}}</span>
</div>

转换成的AST

{
    "type": 1,
    "tag": "div",
    "attrsList": [],
    "attrsMap": {
        "class": "tree",
        "v-if": "show"
    },
    "rawAttrsMap": {},
    "children": [
        {
            "type": 1,
            "tag": "span",
            "attrsList": [],
            "attrsMap": {},
            "rawAttrsMap": {},
            "parent": "[循环引用]",
            "children": [
                {
                    "type": 2,
                    "expression": "_s(item)",
                    "tokens": [
                        {
                            "@binding": "item"
                        }
                    ],
                    "text": "{{item}}",
                    "static": false
                }
            ],
            "plain": true,
            "static": false,
            "staticRoot": false
        }
    ],
    "if": "show",
    "ifConditions": [
        {
            "exp": "show",
            "block": "[循环引用]"
        }
    ],
    "plain": false,
    "staticClass": "\"tree\"",
    "static": false,
    "staticRoot": false,
    "ifProcessed": true
}

看起来是不是很简单?AST就是个js对象,这个对象描述了当前HTML的层级关系,并且通过编译器解析出来的各个标签,标签的属性,Vue指令等。

AST生成Render函数

编译器解析模版之后会生成AST,后面就是把AST转换成Render函数的过程,我们看看核心代码

把ast传到generate方法就能返回Render函数

const code = generate(ast, options)

我们看上面的HTML片段生成AST,然后生成Render函数

with(this){
    return (show)?
        _c('div',{staticClass:"tree"},[_c('span',[_v(_s(item))])])
        :_e()
}

这段代码通过with(this)包裹,这能想象到this指向的就是当前vue实例,而_c,_v,_s是定义在Vue原型链上的一系列方法

_c = createElement

_s = toString

_e = createEmptyElement

通过定义我们可以看出_c是createElement方法,_s是toString方法,_c和_e返回都数据类型是vNode即虚拟DOM,对createElement不熟悉的同学可以看看Vue文档中渲染函数render部分,其中render入参就是createElement。

虚拟DOM patch

通过上面我们知道,数据发生变化时会通知watcher执行update,update执行的时候会执行updateComponent方法

updateComponent = () => { 
    vm._update(vm._render(), hydrating)
}

updateComponent方法内部执行了_update方法,其中入参vm._render方法执行的就是render function,即with(this) { /***/ }代码片段,render function返回当前项目的虚拟DOM,我们看看_update内部实现

_update内部执行了patch方法,入参老虚拟DOM和当前虚拟DOM,patch方法内部执行虚拟DOM的diff算法,细节不谈论,我们只讲流程。

总结

以上七个实现,基本完成了vue的所有核心功能,简单来说,一个vue项目启动的时候会初始化Vue实例,后续过程是这样的

规范 -> 合并 -> 初始化数据响应 -> 编译模版 -> 生成AST -> 生成render函数 -> 执行patch

但是里面的细节还很多,我只是说了整体的实现思路,最后说明一下,我在之前的文章中提过,vue打包后会生成两个版本:完整版和运行时版,其中完整版会把编译器代码打包进去,而运行时版本不带编译器,但是我们的项目引入的是运行时版本,那是怎么做模版解析的呢,这就是webpack loader的职责,大家可以看看vue-loader都做了什么。