文章目录
到目前为止 Vue 3.0 的正式版还没有发布新版本发布后,现有项目不会升级到 3.0,2.x 还有很长的一段过渡期
3.0 项目地址:https://github.com/vuejs/vue-next
一、源码目录结构
src
├─compiler 编译相关
├─core Vue核心库
├─platforms 平台相关代码
├─server SSR,服务端渲染
├─sfc .vue 文件编译为 js 对象
└─shared 公共的代码
了解 Flow
官网:https://flow.org/
JavaScript 的静态类型检查器
Flow 的静态类型检查错误是通过 静态类型推断 实现的
文件开头通过 // @flow 或者 /* @flow */ 声明
/* @flow */functionsquare(n: number): number {
returnn*n;
}
square("2"); // Error!12345
二、准备工作-调试
1、打包
- 打包工具 Rollup
- Vue.js 源码的打包工具使用的是 Rollup,比 Webpack 轻量
- Webpack 把所有文件当做模块,Rollup 只处理 js 文件更适合在 Vue.js 这样的库中使用
- Rollup 打包不会生成冗余的代码
- 安装依赖 npm i
- 设置sourcemap
- package.json 文件中的 dev 脚本中添加参数 --sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
- 执行 dev
- npm run dev执行打包,用的是 rollup,-w 参数是监听文件的变化,文件变化自动重新打包
- 结果
调试 - examples 的示例中引入的 vue.min.js 改为 vue.js
- 打开 Chrome 的调试工具中的 source
三、准备工作-Vue的不同构建版本
- npm run build 重新打包所有文件
- https://cn.vuejs.org/v2/guide/installation.html#对不同构建版本的解释
- dist\README.md
术语 - 完整版:同时包含编译器(3000多行代码)和运行时的版本。
- 编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码,体积大、效率低。
- 运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码,体积小、效率高。基本上就是除去编译器的代码。
- UMD https://github.com/umdjs/umd:UMD 版本通用的模块版本,支持多种模块方式。vue.js默认文件就是运行时 + 编译器的UMD 版本
- CommonJS(cjs) http://wiki.commonjs.org/wiki/Modules/1.1:CommonJS 版本用来配合老的打包工具比如Browserify http://browserify.org/或webpack 1 https://webpack.github.io/。
- ES Module http://exploringjs.com/es6/ch_modules.html:从 2.6 开始 Vue 会提供两个 ES Modules (ESM) 构建文件,为现代打包工具提供的版本。
- ESM 格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包。
- http://es6.ruanyifeng.com/#docs/module-loader#ES6-模块与-CommonJS-模块的差异
基于vueCLI创建的项目,默认导入的vue就是运行时版本,并且是ESM也就是es6的模块化方式
// Compiler
// 需要编译器,
把 template 转换成 render 函数
// const vm = new Vue({
// el: '#app',
// template: '<h1>{{ msg }}</h1>',
// data: {
// msg: 'Hello Vue'
// }
// })
// Runtime
// 不需要编译器
const vm = newVue({
el: '#app',
render (h) {
return h('h1', this.msg)
},
data: {
msg: 'Hello Vue'
}
})
- 推荐使用运行时版本,因为运行时版本相比完整版体积要小大约 30%
- 基于 Vue-CLI 创建的项目默认使用的是vue.runtime.esm.js
- 通过查看 webpack 的配置文件
vue inspect > output.js
- 注意:*.vue文件中的模板是在构建时预编译的,最终打包后的结果不需要编译器,只需要运行时版本即可
vue inspect这个命令可以查看webpack的配置
可以输出到一个文件中来查看 vue inspect > output.js
推荐使用运行时版本
开发项目时会有很多单文件组件,这些单文件组件浏览器是不支持的,所以打包时会把单文件组件转换成js对象。在转换成js对象的过程中还会把这些模板转换成render函数,所以单文件组件运行时也是不需要编译器的
编译器的作用是把template换成render函数
运行时是不需要编译器的。比完整版体积小,执行效率高。推荐使用运行时版本
四、寻找入口文件
- 查看 dist/vue.js 的构建过程
执行构建
npm run dev
# "dev": "rollup -w -c scripts/config.js --sourcemap --environmentTARGET:web-full-dev"
# --environment TARGET:web-full-dev 设置环境变量 TARGET
- script/config.js的执行过程
- 作用:生成 rollup 构建的配置文件
- 使用环境变量 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)
}
- genConfig(name)
- 根据环境变量 TARGET 获取配置信息
- builds[name] 获取生成配置的信息
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
- resolve()
- 获取入口和出口文件的绝对路径
const aliases=require('./alias')
const resolve=p=> {
// 根据路径中的前半部分去alias中找别名
const base=p.split('/')[0]
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length+1))
} else {
returnpath.resolve(__dirname, '../', p)
}
}
结果
- 把 src/platforms/web/entry-runtime-with-compiler.js 构建成 dist/vue.js,如果设置 --sourcemap 会生成 vue.js.map
- src/platform 文件夹下是 Vue 可以构建成不同平台下使用的库,目前有 weex 和 web,还有服务器端渲染的库
五、从入口开始
- 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')
}
})
- 阅读源码记录
- el 不能是 body 或者 html 标签
- 如果没有 render,把 template 转换成 render 函数
- 如果有 render 方法,直接调用 mount 挂载 DOM
// 1. 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 elementsinstead.`
)
return this
}
const options=this.$options
if (!options.render) {
// 2. 把 template/el 转换成 render 函数
......
}
// 3. 调用 mount 方法,挂载 DOM
return mount.call(this, el, hydrating)
- 调试代码
- 调试的方法
const vm = new Vue({
el: '#app',
template: '<h3>Hello template</h3>',
render (h) {
return h('h4', 'Hello render')
}
})
Vue 的构造函数在哪?
Vue 实例的成员/Vue 的静态成员从哪里来的?
Vue 的构造函数在哪里
- src/platform/web/entry-runtime-with-compiler.js 中引用了 ‘./runtime/index’
- src/platform/web/runtime/index.js
- 设置 Vue.config
- 设置平台相关的指令和组件
- 指令 v-model、v-show
- 组件 transition、transition-group
- 设置平台相关的__patch__方法(打补丁方法,对比新旧的 VNode)
- 设置 $mount 方法,挂载 DOM
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// install platform patch function
Vue.prototype.__patch__=inBrowser?patch : noop
// public mount method
Vue.prototype.$mount=function (
el?: string|Element,hydrating?: boolean
): Component {
el=el&&inBrowser?query(el) : undefined
return mountComponent(this, el, hydrating)
}
- src/platform/web/runtime/index.js 中引用了 ‘core/index’
- src/core/index.js
- 定义了 Vue 的静态方法
- initGlobalAPI(Vue)
- src/core/index.js 中引用了 './instance/index
- 'src/core/instance/index.js
- 定义了 Vue 的构造函数
function Vue (options) {
if (process.env.NODE_ENV!=='production'&&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new`keyword')
}
// 调用 _init() 方法
this._init(options)
}
// 注册 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)
四个导出 Vue 的模块
- src/platforms/web/entry-runtime-with-compiler.js :核心增加了编译的功能
- web 平台相关的入口
- 重写了平台相关的 $mount() 方法:
- 让$mount这个方法内部可以去编译模板,也就是把template模板转换成render函数
- 注册了 Vue.compile() 方法,传递一个 HTML 字符串返回 render 函数
- src/platforms/web/runtime/index.js
- web 平台相关
- 注册和平台相关的全局指令:v-model、v-show
- 注册和平台相关的全局组件: v-transition、v-transition-group
- 全局方法:
- patch:把虚拟 DOM 转换成真实 DOM
- $mount:挂载方法 入口中是重写这边是定义
- src/core/index.js
- 与平台无关
- 设置了 Vue 的静态方法,initGlobalAPI(Vue)
- src/core/instance/index.js 与实例相关
- 与平台无关
- 定义了构造函数,调用了 this._init(options) 方法
- 给 Vue 中混入了常用的实例成员
六、Vue初始化
1、两个问题
a. settings.json中
//设置不检查js语法问题,方式flow报错
“javascript.validate.enable”: false
b.使用flow泛型后代码不高亮问题:安装vscode babel javascript插件 方可高亮显示.但是超链接的功能丢失,暂时无法解决
2、静态成员
src/core/global-api/index.js
- 初始化 Vue 的静态方法
// 注册 Vue 的静态属性/方法
initGlobalAPI(Vue)
// src/core/global-api/index.js
// 初始化 Vue.config 对象
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
// 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们
Vue.util= {
warn,
extend,
mergeOptions,
defineReactive
}
// 静态方法 set/delete/nextTick
Vue.set=set
Vue.delete=del
Vue.nextTick=nextTick
// 2.6 explicit observable API
// 让一个对象可响应
Vue.observable=<T>(obj: T): T=> {
observe(obj)
return obj
}
// 初始化 Vue.options 对象,并给其扩展
// components/directives/filters/_base
Vue.options=Object.create(null)
ASSET_TYPES.forEach(type=> {
Vue.options[type+'s'] =Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base=Vue
// 设置 keep-alive 组件
extend(Vue.options.components, builtInComponents)
// 注册 Vue.use() 用来注册插件
initUse(Vue)
// 注册 Vue.mixin() 实现混入
initMixin(Vue)
// 注册 Vue.extend() 基于传入的 options 返回一个组件的构造函数
initExtend(Vue)
// 注册 Vue.directive()、 Vue.component()、Vue.filter()
initAssetRegisters(Vue)
3、实例成员
src/core/instance/index.js
- 定义 Vue 的构造函数
- 初始化 Vue 的实例成员
// 此处不用 class 的原因是因为方便,后续给 Vue 实例混入实例成员
function Vue (options) {
if (process.env.NODE_ENV!=='production'&&! (thisinstanceofVue)
) {
warn('Vue is a constructor and should be called with the `new`keyword') }
this._init(options
)}
// 注册 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)
4、实例成员-init
- initMixin(Vue)
- 初始化 _init() 方法
5、实例成员-initState
6、调试
七、首次渲染过程
- Vue 初始化完毕,开始真正的执行
- 调用 new Vue() 之前,已经初始化完毕
- 通过调试代码,记录首次渲染过程
数据响应式原理
通过查看源码解决下面问题 - vm.msg = { count: 0 },重新给属性赋值,是否是响应式的?
- vm.arr[0] = 4,给数组元素赋值,视图是否会更新
- vm.arr.length = 0,修改数组的 length,视图是否会更新
- vm.arr.push(4),视图是否会更新
响应式处理的入口
整个响应式处理的过程是比较复杂的,下面我们先从 - src\core\instance\init.js
- initState(vm) vm 状态的初始化
- 初始化了_data、_props、methods 等
- src\core\instance\state.js
// 数据的初始化
if (opts.data) {
initData(vm)
} else {
observe(vm._data= {}, true/* asRootData */)
}
- initData(vm) vm 数据的初始化
function initData (vm: Component) {
let data=vm.$options.data
// 初始化 _data,组件中 data 是函数,调用函数返回结果
// 否则直接返回 data
data=vm._data=typeof data==='function'?getData(data, vm) : data|| {}
......
// proxy data on instance
// 获取 data 中的所有属性
const keys=Object.keys(data)
// 获取 props / methods
const props=vm.$options.props
const methods=vm.$options.methods
let i=keys.length
// 判断 data 上的成员是否和 props/methods 重名......
// observe data
// 数据的响应式处理
observe(data, true/* asRootData */)
}
- src\core\observer\index.js
- observe(value, asRootData)
- 负责为每一个 Object 类型的 value 创建一个 observer 实例
export function observe (value: any, asRootData: ?boolean): Observer|void{
// 判断 value 是否是对象
if (!isObject(value) ||value instanceof VNode) {
return
}
let ob: Observer|void
// 如果 value 有 __ob__(observer对象) 属性结束
if (hasOwn(value, '__ob__') && value.__ob__instanceof Observer) {
ob=value.__ob__
} else if (
shouldObserve&&!isServerRendering() && (Array.isArray(value) ||isPlainObject(value)) && Object.isExtensible(value) && !value._isVue
) {
// 创建一个 Observer 对象
ob=newObserver(value)
}
if (asRootData&&ob) {o
b.vmCount++
}
returnob
}
数组的响应式:把会改变数组元素的这些方法给它进行重新的修补,当这些方法被调用的时候,我们调用dep的notify方法。还有个方法observeArray,遍历数组中的所有元素,把这些对象的元素转换成响应式的对象
watcher分为3种,Computed Watcher、用户Watcher(侦听器)、渲染watcher
渲染Watcher的创建时机:./src/core/instance/lifecycle.js
当数据放生变化后我们调用dep的notify方法去通知watcher,先把watcher放到一个队列里面。然后遍历这个队列调用这个队列所有watcher中的run方法。run方法中调用了渲染watcher的uodateComponent函数
渲染 wacher 创建的位置 lifecycle.js 的 mountComponent 函数中
Wacher 的构造函数初始化,处理 expOrFn (渲染 watcher 和侦听器处理不同)
调用 this.get() ,它里面调用 pushTarget() 然后 this.getter.call(vm, vm) (对于渲染 wacher 调 用 updateComponent),如果是用户 wacher 会获取属性的值(触发get操作) 当数据更新的时候,dep 中调用 notify() 方法,notify() 中调用 wacher 的 update() 方法
update() 中调用 queueWatcher()
queueWatcher() 是一个核心方法,去除重复操作,调用 flushSchedulerQueue() 刷新队列并执行 watcher
flushSchedulerQueue() 中对 wacher 排序,遍历所有 wacher ,如果有 before,触发生命周期 的钩子函数 beforeUpdate,执行 wacher.run(),它内部调用 this.get(),然后调用 this.cb() (渲染 wacher 的 cb 是 noop)
整个流程结束
vm. d e l e t e 功 能 删 除 对 象 的 属 性 。 如 果 对 象 是 响 应 式 的 , 确 保 删 除 能 触 发 更 新 视 图 。 这 个 方 法 主 要 用 于 避 开 V u e 不 能 检 测 到 属 性 被 删 除 的 限 制 , 但 是 你 应 该 很 少 会 使 用 它 。 注 意 : 目 标 对 象 不 能 是 一 个 V u e 实 例 或 V u e 实 例 的 根 数 据 对 象 。 例 : v m . delete 功能 删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是你应该很少会使用它。 注意: 目标对象不能是一个 Vue 实例或 Vue 实例的根数据对象。 例:vm. delete功能删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开Vue不能检测到属性被删除的限制,但是你应该很少会使用它。注意:目标对象不能是一个Vue实例或Vue实例的根数据对象。例:vm.delete(vm.obj,‘msg’)
vm.
w
a
t
c
h
v
m
.
watch vm.
watchvm.watch( expOrFn, callback, [options] )
功能
观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只 接受监督的键路径。对于更复杂的表达式,用一个函数取代。
参数
expOrFn:要监视的 $data 中的属性,可以是表达式或函数
callback:数据变化后执行的函数
函数:回调函数
对象:具有 handler 属性(字符串或者函数),如果该属性为字符串则 methods 中相应 的定义
options:可选的选项
deep:布尔类型,深度监听
immediate:布尔类型,是否立即执行一次回调函数 示例
const vm = new Vue({
el: '#app',
data: {
a: '1',
b: '2',
msg: 'Hello Vue',
user: {
firstName: '诸葛',
lastName: '亮'
}
}
})
// expOrFn 是表达式
vm.$watch('msg', function (newVal, oldVal) {
console.log(newVal, oldVal)
})
vm.$watch('user.firstName', function (newVal, oldVal) {
console.log(newVal)
})
// expOrFn 是函数 vm.$watch(function () {
return this.a + this.b
}, function (newVal, oldVal) {
console.log(newVal)
})
// deep 是 true,消耗性能
vm.$watch('user', function (newVal, oldVal) {
// 此时的 newVal 是 user 对象
console.log(newVal === vm.user)
}, {
deep: true // 深度监听
})
// immediate 是 true
vm.$watch('msg', function (newVal, oldVal) {
console.log(newVal)
}, {
immediate: true // 可以立即执行
})
三种类型的 Watcher 对象
没有静态方法,因为
w
a
t
c
h
方
法
中
要
使
用
V
u
e
的
实
例
W
a
t
c
h
e
r
分
三
种
:
计
算
属
性
W
a
t
c
h
e
r
、
用
户
W
a
t
c
h
e
r
(
侦
听
器
)
、
渲
染
W
a
t
c
h
e
r
创
建
顺
序
:
计
算
属
性
W
a
t
c
h
e
r
、
用
户
W
a
t
c
h
e
r
(
侦
听
器
)
、
渲
染
W
a
t
c
h
e
r
v
m
.
watch 方法中要使用 Vue 的实例 Watcher 分三种:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher 创建顺序:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher vm.
watch方法中要使用Vue的实例Watcher分三种:计算属性Watcher、用户Watcher(侦听器)、渲染Watcher创建顺序:计算属性Watcher、用户Watcher(侦听器)、渲染Watchervm.watch()
src\core\instance\state.js
源码
Vue.prototype.$watch = function (expOrFn: string | Function, cb: any,
options?: Object
): Function {
// 获取 Vue 实例 this
const vm: Component = this if (isPlainObject(cb)) {
// 判断如果 cb 是对象执行 createWatcher
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 标记为用户 watcher
options.user = true
// 创建用户 watcher 对象
const watcher = new Watcher(vm, expOrFn, cb, options) // 判断 immediate 如果为 true
if (options.immediate) {
// 立即执行一次 cb 回调,并且把当前值传入 try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
// 返回取消监听的方法
return function unwatchFn () {
watcher.teardown()
}
}
查看渲染 watcher 的执行过程
当数据更新,defineReactive 的 set 方法中调用 dep.notify()
调用 watcher 的 update()
调用 queueWatcher(),把 wacher 存入队列,如果已经存入,不重复添加
- 循环调用 flushSchedulerQueue()
- 通过 nextTick(),在消息循环结束之前时候调用 flushSchedulerQueue() 调用
- wacher.run()
- 调用 wacher.get() 获取最新值
- 如果是渲染 wacher 结束
- 如果是用户 watcher,调用 this.cb()
异步更新队列-nextTick()
- Vue 更新 DOM 是异步执行的,批量的
- 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更
新后的 DOM。
- 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更
- vm.$nextTick(function () { /* 操作 DOM */ }) / Vue.nextTick(function () {})
vm.$nextTick() 代码演示
<div id="app">
<p ref="p1">{{ msg }}</p>
</div>
<script src="../../dist/vue.js"></script> <script>
const vm = new Vue({
el: '#app',
data: {
msg: 'Hello nextTick', name: 'Vue.js',
title: 'Title'
},
mounted() {
this.msg = 'Hello World' this.name = 'Hello snabbdom' this.title = 'Vue.js'
this.$nextTick(() => { console.log(this.$refs.p1.textContent)
}) }
})
</script>
定义位置
src\core\instance\render.js
Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this)
}
源码
手动调用 vm.$nextTick()
在 Watcher 的 queueWatcher 中执行 nextTick() src\core\util\next-tick.js