前言
之前我写过一篇文章,简单介绍了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源码的几个核心实现
- Vue选项的规范化
- Vue选项的合并
- Vue数据响应式
- Vue模版编译器
- 模版解析成AST
- AST生成Render函数
- 虚拟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如何做响应式的,相信大家并不陌生,不管工作中还是出去面试,经常会遇到这个问题,下面贴一张官方图片
关于依赖收集、数据劫持、派发更新的关系,其实远比图片要复杂,因为里面还要考虑很多细节,包括数组的处理,多个属性被重复监听等,不过最重要的一点,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都做了什么。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!