目标
- Vue.js 的静态成员和实例成员初始化过程
- 首次渲染过程
- 数据响应式原理
准备工作
Vue 源码获取
- 项目地址: github.com/vuejs/vue
- Fork 一份到自己仓库,克隆到本地,可以自己写注释提交到github
- 为什么分析 Vue2.6
- 到目前为止 Vue3.0的正式版本还没有发布
- 新版本发布后,现有项目不会升级到3.0,2.x还有很长的一段过渡期
- 3.0项目地址:github.com/vuejs/vue-n…
- 源码目录结构
src
├─compiler 编译相关
├─core Vue 核心库
├─platforms 平台相关代码
├─server SSR,服务端渲染
├─sfc .vue 文件编译为 js 对象
└─shared 公共的代码
了解 Flow
- 官网:flow.org
- JavaScript的静态类型检查器
- Flow的静态类型检查错误是通过静态类型推断实现实现的
- 文件开头通过// @flow或者/* @flow */声明
/* @flow */
function square(n: number): number {
return n * n;
}
square("2"); // Error!
调试设置
打包
- 打包工具 Rollup
- Vue.js 源码的打包工具使用的是 Rollup,比Webpack轻量
- Webpack 把所有文件当做模块,Rollup只处理js文件更适合在Vue.js这样的库中使用
- Rollup 打包不会生成冗余的代码
- 安装依赖
npm i
- 设置 sourcemap
- package.josn文件中的dev脚本中添加参数 --sourcemap
"dev":"rollup-w-cscripts/config.js--sourcemap--environmentTARGET:web- full-dev"
- 执行 dev
- npm run dev 执行打包,用的rollup,-w 参数是监听文件的变化,文件变化自动重新打包 调试
- examples 的示例中引入的vue.min.js改为vue.js
- 打开 Chrome 的调试工具中的source
Vue 的不同构建版本
- npm run build 重新打包所有文件
UMD | CommonJS | ES Module | |
---|---|---|---|
Full | vue.js | vue.common.js | vue.esm.js |
Runtime-only | vue.runtime.js | vue.runtime.common.js | vue.runtime.esm.js |
Full (production) | vue.min.js | ||
Runtime-only (production) | vue.runtime.min.js |
术语
- 完整版:同时包含编译器和运行时的版本。
- 编辑器:用来将模板字符串编译成为 JavaScript 渲染函数的代码,体积大,效率低。
- 运行时:用来创建 Vue 示例、渲染并处理虚拟 DOM 等代码,体积小、效率高。基本上就是除去编译器的代码。
- UMD:UMD 版本通用的模块版本,支持多种模块化方式(例如可直接挂到window对象上)。 vue.js 默认文件就是运行时 + 编译器的UMD版本。
- CommonJS(cjs): CommonJS 版本用来配合老的打包工具比如 Browserify 或 webpack 1。
- ES Module:从2.6开始Vue会提供两个ES Modules(ESM)构建文件,为现代打包工具提供的版本。
- ESM 格式被设计为可以被静态分析(编译时处理),所以打包工具可以利用这一点进行"tree-shaking"并将用不到的代码排除出最终的包。
- ES6模块与CommonJS模块的差异 Runtime + Compiler vs. Runtime-only
// Compiler
// 需要编译器,把 template 转换成 render 函数
// const vm = new Vue({
// el: '#app',
// template: '<h1>{{ msg }}</h1>',
// data: {
// msg: 'Hello Vue'
// }
// })
// Runtime
// 不需要编译器
const vm = new Vue({
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 文件中的模板是在构建时预编译的,最终打包后的结果不需要编译器,只需要运行时版本即可
寻找入口文件
- 查看 dist/vue.js 的构建过程
执行构建
npm run dev
# "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET: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 {
return path.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 elements
instead.`
)
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 初始化的过程
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() 方法(使其可以编译模板 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初始化 - 静态成员
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)
Vue初始化 - 实例成员
src/core/instance/index.js
- 定义 Vue 的构造函数
- 初始化 Vue 的实例成员
// 此处不用 class 的原因是因为方便,后续给 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')
}
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)
- initMixin(Vue)
- 初始化 _init() 方法
首次渲染过程
- 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 = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Observer
- src\core\observer\index.js
- 对对象做响应化处理
- 对数组做响应化处理
export class Observer { // 观测对象
value: any;
// 依赖对象
dep: Dep;
// 实例计数器
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
// 初始化实例的 vmCount 为0
this.vmCount = 0
// 将实例挂载到观测对象的 __ob__ 属性,设置为不可枚举
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 数组的响应式处理
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 为数组中的每一个对象创建一个 observer 实例
this.observeArray(value)
} else {
// 对象的响应化处理
// 遍历对象中的每一个属性,转换成 setter/getter
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when * value type is Object.
*/
walk (obj: Object) {
// 获取观察对象的每一个属性
const keys = Object.keys(obj)
// 遍历每一个属性,设置为响应式数据
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
- walk(obj)
- 遍历 obj 的所有属性,为每一个属性调用 defineReactive() 方法,设置 getter/setter
defineReactive()
- src\core\observer\index.js
- defineReactive(obj, key, val, customSetter, shallow)
- 为一个对象定义一个响应式的属性,每一个属性对应一个 dep 对象
- 如果该属性的值是对象,继续调用 observe
- 如果给属性赋新值,继续调用 observe
- 如果数据更新发送通知
对象响应式处理
// 为一个对象定义一个响应式的属性
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
){
// 1. 为每一个属性,创建依赖对象实例
const dep = new Dep()
// 获取 obj 的属性描述符对象
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 提供预定义的存取器函数
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 2. 判断是否递归观察子对象,并将子对象属性都转换成 getter/setter,返回子观察对象
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 如果预定义的 getter 存在则 value 等于getter 调用的返回值
// 否则直接赋予属性值
const value = getter ? getter.call(obj) : val
// 如果存在当前依赖目标,即 watcher 对象,则建立依赖
if (Dep.target) {
// dep() 添加相互的依赖
// 1个组件对应一个 watcher 对象
// 1个watcher会对应多个dep(要观察的属性很多)
// 我们可以手动创建多个 watcher 监听1个属性的变化,1个dep可以对应多个watcher
dep.depend()
// 如果子观察目标存在,建立子对象的依赖关系,将来 Vue.set() 会用到
if (childOb) {
childOb.dep.depend()
// 如果属性是数组,则特殊处理收集数组对象依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// 返回属性值
return value
},
set: function reactiveSetter (newVal) {
// 如果预定义的 getter 存在则 value 等于getter 调用的返回值
// 否则直接赋予属性值
const value = getter ? getter.call(obj) : val
// 如果新值等于旧值或者新值旧值为null则不执行
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// 如果没有 setter 直接返回
// #7981: for accessor properties without setter
if (getter && !setter) return
// 如果预定义setter存在则调用,否则直接更新新值
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 3. 如果新值是对象,观察子对象并返回 子的 observer 对象
childOb = !shallow && observe(newVal)
// 4. 发布更改通知
dep.notify()
}
数组响应式处理
- Observer 的构造函数中
- 处理数组修改数据的方法
- src\core\observer\array.js
Dep 类
- src\core\observer\dep.js
- 依赖对象
- 记录 watcher 对象
- depend() -- watcher 记录对应的 dep 发布通知
Watcher 类
- Watcher 分为三种,Computed Watcher、用户 Watcher (侦听器)、渲染 Watcher
- 渲染 Watcher 的创建时机
-
/src/core/instance/lifecycle.js
-