Vue源码解析-响应式原理
以下内容来自 拉勾教育大前端训练营 笔者在学习过程中对笔记进行的一个整理
心得体会
嘿嘿嘿~~~ 首先说说拉勾教育大前端训练营的课程视频吧,课程的质量是真的很好哦,并且已经收到了非常多的好评,在课程规划知识体系上,非常详细,对于每一个知识点都讲的非常透彻,可以说是 手摸手系列了,并且视频是学习完一章才能进入下一章学习,给我的感觉就是在打怪升级一样,学习完解锁下一章节,并且也非常期待下一章会Get
到什么的技能,从而不断的强化自己 。
以在Vue
框架源码与进阶为例,再看源码之前了,我学习了手写 Vue Router
、手写Vue
响应式实现、虚拟 DOM
和 Diff
算法,以前只是停留在会用的阶段,到目前已经深入学习了Vue的响应式原理,和如何去手写实现一个Vue Router
, Get
到了非常多的技能以及 黑魔法 。
首先回顾 Vue Router
的基本使用,以及 Hash
模式和 History
模式的区别,然后自己手写一个实现基 History
模式的前端路由,了解路由内部实现的原理;接下来在数据响应式实现原理分析中,自己动手一个简易版本的 Vue
;最后掌握虚拟 DOM
的作用,通过一个虚拟 DOM
库 Snabbdom
真正了解什么是虚拟 DOM
,以及 Diff
算法的实现和key
的作用。
除此之外呢,在学习完一个大章节都会进行一次互动直播答疑总结, 嘿嘿嘿~~~
我其实是非常期待每一次的直播的,因为每一次都是干货满满,收获很多,还有好几位助教老师在群里进行答疑,只要我们有不懂得问题,老师都会以最快的速度帮助我们解决,如果是实在解决不了的问题,熊熊老师会亲自git 你的代码,然后运行代码进行问题的定位,找到问题并解决之后也会告诉你是如果解决的,真的真的很贴心 ~ emmm 还有其实我们每章都有一个大作业,班主任老师呢会每天督促大家去完成作业,让大家都紧跟脚步,有问题,班主任老师也会及时记录下来。
再说一点吧,学习群每天都很活跃,每天大家都会遇到非常多的问题,大家只要把问题丢进去,很快就会得到其他同学的解答,包括我自己也是非常开心的帮助其他同学解决问题,在拉勾大前端训练营和大家一起学习,一起进步。
笔记将会对以下三点进行总结
- Vue.js 的静态成员和实例成员初始过程
- 首次渲染的过程
- 数据响应式原理
一. 准备工作
Vue源码的获取
- 项目地址 Vue源码获取
- Fork 一份到自己的仓库,克隆到本地,可以自己写注释提交到github
为什么分析Vue2.6
- 到目前为止
Vue3.0
的正式版本还没有发布 - 新版本发布后,现有的项目不会升级到
3.0, 2.x
还有很长的一段过渡期 - Vue3.0项目地址
源码目录结构
我们获取Vue
源码后,重点看src
下面的目录结构
|--src
|--compiler // 编译相关
|--core // Vue 核心库
|--platforms // 平台相关代码
|-- server // SSR,服务端渲染
|--sfc // .vue 文件编译为 js对象
|--shared // 公共的代码
compiler
编译器把模板转换成render
函数,render
函数会帮我们创建虚拟DOM
core components
中定义了keep-alive
组件,接下来是global-api
, 它定义了Vue
的静态方法,assets
extend
,mixin
,use
等方法instance
是创建Vue实例的位置,这里定义了Vue
的构造函数,以及Vue
的初始化,还有Vue
生命周期函数observer Vue
响应式核心util
公共成员vdom
虚拟DOM
platforms
平台相关代码web weex
server vue2.0
支持SSR
,服务端渲染sfc
将.vue
文件编译为 js对象shared
公共的代码
了解Flow
- Flow官网
JavaScript
的静态资源类型检测器Flow
的静态文件类型检查错误是通过静态类型推断实现的- 文件开头通过
// @flow
或者 、/* @flow */
声明,如下
/* @flow */
function square(n: number): number {
return n * n;
}
square("2"); // Error
二.调试设置
如何对Vue源码进行打包和调试
打包
- 打包工具
Rollup
Vue.js
源码的打包工具使用的是Rollup
,相比Webpack
更轻量Webpack
把所有文件当做模块,Rollup
只处理js
文件更适合在Vue.js
这样的库中使用Rollup
打包不会生成冗余的代码
安装依赖
npm i
设置SourceMap
package.json
文件中的dev
脚本中添加参数 --sourcemap
方便我们调试
"dev": "rollup -w -c script/config.js --sourcemap --environment TARGET:web-full-dev"
执行dev
npm run dev
执行打包, 用的是rollup, -w
参数是监听文件的变化,文件变化自动重新打包- 结果:
package.json 文件中的dev脚本中添加参数 --sourcemap,方便我们调试,出现错误可以看到具体的位置
"dev": "rollup -w -c script/config.js --sourcemap --environment TARGET:web-full-dev"
-w
是watch-c
设置配置文件scripts/config.js
--environment
环境变量,用来打包生成不同版本Vue
执行打包命令 npm run dev
- 打包过程会先找到入口文件,然后会编译到dist 目录vue.js中
- 此时dist中会生成两个文件vue.js 和vue.js.map
三.Vue 的不同构建版本
npm run build
重新打包所有文件- 官方文档- 对不同构建版本的解释
dist\REMADME.md
术语
- 完整版: 同时包含编译器和运行时的版本
- 编译器: 用来将模板字符串编译为
JavaScript
渲染函数的代码,体积大,效率低 - 运行时: 用来创建
Vue
实例、渲染并处理虚拟DOM
等代码,体积小、效率高,基本就是出去编译器的代码 - UMD: UMD版本通用的模块版本,支持多种模块方式,
vue. js
默认文件就是运行时+编译器的UMD
版本 - CommonJS(cjs):
CommonJS
版本用来配合老的打包工具比如Browserify
或webpack 1.
- ES Module:从
2.6
开始Vue
会提供两个ES Modules (ESM)
构建文件,为现代打包工具提供的版本。 ESM
格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行tree shaking
并将用不到的代码排除出最终的包。ES6
模块与CommonJS
模块的差异
vue inspect > output.js 输出文件 查看webpack配置
- 我们在创建
Vue-cli
项目中使用的Vue
版本就是vue.runtime.esm.js
运行时的版本 - 推荐使用运行时的版本
四.寻找入口文件
- 查看
dist/vue.js
的构建过程
执行构建
npm run dev
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
// environment TARGET:web-full-dev 设置环境变量TARGET
script/config.js 的执行过程
- 作用: 生成
rolllup
构建的配置文件 - 使用环境变量
TARGET = web-full-dev
// 判断环境变量是否有TARGET
// 如果有的话使用genConfig() 生成rollup 配置文件
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else{
// 否则获取全部配置
exports.getBuild = genConfig
exports.getAllBuilds = () => object.keys(builds).map(genConfig)
}
在package.json文件中
"script": {
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
"build": "node scripts/build.js",
}
- dev打包就是dist文件中的一个版本
- build是所有的版本
- TARGET:web-full-dev需要打包的版本
五.从入口开始
- src/platform/web/entry-runtime-with-compiler.js
通过查看源码解决下面问题
- 观察以下代码,通过阅读源码,回答在页面上输出的结果
const vm = new Vue({
el: '#app',
template: '<h3> Hello template</h3>',
render (h) {
return h('h4', 'Hello render')
}
})
如果传入了render函数 不处理template,直接调用mount方法
阅读源码记录
el
不能是body
或者html
标签- 如果没有render,把
template
转换成render
函数 - 如果有
render
方法,直接调用mount
挂载DOM
// el不能是body 或者html
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
// 把template/el 转换成render函数
if (!options.render) {
...
// 2把template/el转换成render 函数
}
// 3调用mount 方法,挂载DOM
调试代码, 调试的方法
const vm = new Vue({
el: '#app',
template: '<h3> Hello template</h3>',
render (h) {
return h('h4', 'Hello render')
}
})
六.Vue 初始化过程
四个导出Vue的模块
1.src/platforms/wb/entry runtime with compilrjis
web
平台相关的入口- 重写了平台相关的
$mount()
方法
2.注册了Vue compile()方法,传递一个HTML字符串返回render函数
src/platforms/web/runtime/index.js
web
平台相关- 注册和平台相关的全局指令:
v-model
.v-show
- 注册和平台相关的全局组件:
v-transtion
.v-tansition-group
- 全局方法:
-
patch _
:把虚拟DOM
转换成真实DOM
-
$mount
:挂载方法
-
3.src/core/index.js
- 与平台无关
- 设置了Vue的静态方法,itiltbalaPl(Vue)
4. src/core/instance/index.js
- 与平台无关
- 定义了构造函数,调用了`this.init(options)方法
- 给
Vue
中混入了常用的实例成员
总结
在platforms/web/runtime/index.js
下的文件主要做了以下事情,在这个文件中所有代码都是和平台相关的,注册了平台相关的一些指令,patch
函数以及$mount
这个两个方法
import Vue from 'core/index'
导入了构造函数- 在
core/index.js
中,调用了initGlobalAPI(Vue)
方法,给Vue
的构造函数增加以下静态方法,其他内容都是调用Object.defineProperty
给Vue
增加了一些成员,还有服务端渲染SSR
, core/global-api
初始化了Vue
的静态方法instance/index.js
创建了Vue
构造函数,设置了Vue
实例成员
八.Vue 初始化问题
- Flow 语法红线 “javascript.validate.enable”: false
- TS代码高亮 Babel JavaScript 插件
九. Vue初始化-静态成员
十.Vue初始化-实例成员
instance·文件夹
index.js
定义了Vue
的构造函数,并且调用了initMixin(Vue), stateMixin(Vue),eventsMixin(Vue)
lifecycleMixin(Vue),renderMixin(Vue)
initMixin(Vue)
就是在Vue
的原型上挂载了_init()
方法stateMixin(Vue)
通过Object.defineProperty(Vue.proptotype, '$data', dataDef)
在Vue原型上增加了两个属性eventsMixin(Vue)
分别定义了$on,$once,$off,$emit
事件,使用发布订阅模式lifecycleMixin(Vue)
定义了forceUpdate destory()
renderMixin
这几个函数的作用都是给Vue原型混入一些成员和属性,给Vue对象增加相应的实例成员
// 注册vm的_init()方法, 初始化vm
initMixin(Vue)
// 注册vm 的$data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
//$on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)
11.Vue实例-实例成员-init
12.Vue实例-实例成员-initState
初始化vm
的 _props/methods/_data/computed/watch
以下是insrance/state.js initState()的源码
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
在instance/state.js中,首先获取了Vue实例中的$options
,然后判断options中是否有props,methods,data
以及computed
和watch
这些属性,如果有的话,通过initProps进行初始化
initProps(vm, opts.props)
接收了两个参数,一个是Vue
实例,一个是Props
属性,我们跳转到initProps
函数中,首先给Vue
实例定义了一个_Props
对象, 并且把它存储到了常量里面
const props = vm._props = {}
紧接着,开始遍历PropsOptions
的所有属性,它其实就是initProps
方法中的第二个参数,遍历每个属性,然后通过defineReactive
注入到Props
这个对象上,这个props
其实就是vm._props
所有的成员都会通过defineReacttive
转化为get
和set
,最后在Props
对象上存储,
注意
- 在开发模式中,如果我们直接给这个属性赋值的话,会发出一个警告,
- 生产环境中直接通过
defineReactive
把props
中的属性转化成get
和set
- 最后判断了
props
属性是否在Vue
实例中存在,不存在通过Proxy
这个函数把我们的属性注入到Vue
的实例中
在Proxy
中,通过调用Object.defineProperty(target, key,sharePropertyDefinition)
总结initProps
的作用就是把我们的Props
成员转化成响应式数据,并且注入到Vue
实例里面中
initMethods
在initMethods(vm, opts.methods)
中,也是接收两个参数,Vue实例和选项中的methods
,首先获取了选项中的Props,遍历methods
所有属性,然后判断当前的环境是否是开发或者生产
开发环境会判断methods
是否是functicon
继续往下判断methods
方法的名称是否在Props对象中存在,存在就会发送一个警告,警告在属性在Props中已经存在,因为Props和methods最终都要注入到Vue实例上,不能出现同名
之后判断key是否在Vue中存在,并且调用了isReserved(key),判断我们的key是否以_开头或$开头
最后把methods
注入到Vue实例上来,注入的时候会判断是否是function
,如果不是返回noop
,是的话把函数返回bind(methods[key], vm)
总结 initMethods
作用就是把选项的methods
注入到vue
实例,在注入之前,会先判断我们命名是否在Props
中存在,并且判断了命名的规范,不建议_和$开头
initData(vm)
当options
中有data选项时,会调用initData(vm)
当没有的时候此时会给vm
初始化一个_data
属性observe(vm._data = {}, true)
,然后调用observe
函数,observe
是响应式中的一个函数
在initData
中获取了options
的data
选项,判断了data
选项是否是function
,如果是调用getData(data,vm)
接着获取data中的所有属性,同时获取了props,methods中所有的属性
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
最后做一个响应式处理
observe(data, true)
目前还没整理完成哦