前言
其实之间也写过关于vue2的一些源码分析的笔记,但是因为各种各样的原因导致先帝创业未半而中道崩殂这样的局面。因为自己的目标是成为前端架构师,为了往这个方向靠拢一直在学习各方面的知识,前端后端运维相关都在学习,自己感觉很累。然后平时学习的东西大部分工作中用不上,这又让自己感觉浪费了很多时间和精力。左脚踩着学习,右脚踩着躺平,所以前段子就一直在这种状态下反复横跳。但是最近在B站上看到技术胖给的建议:https://www.bilibili.com/video/BV1Tf4y1G7dq?spm_id_from=333.880.my_history.page.click
首先解决态度问题,心态上放稳,放弃一些东西 得到一些东西。确定目标。然后四个方法一个流程的走
1.精熟技术 2.揣摩实现 3.打造轮子 4.找到调性
所以我打算这段时间精熟技术,揣摩实现,主要是根据黄轶老师的Vue.js技术揭秘博客来学习Vue源码,然后自己补充一些其他的内容,希望这段时间能够利用业余时间专心地把这系列笔记写好,有写的错误地方欢迎指出,respect
1. Vuejs源码如何构建
vue2基于rollup构建的,它的构建相关配置都在 scripts 目录下 当在开发执行 npm run dev 的时候, 实际上就会执行 rollup -w -c scripts/config.js --environment TARGET:web-full-dev , 于是rollup 找到根目录下的script目录 下的config.js 文件,
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
根据参数TARGET:web-full-dev 调用genConfig 获取当前环境的配置
'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
},
以web-full-dev 配置为例,它的entry 是resolve('web/entry-runtime-with-compiler.js') ,先来看下resolve 函数的定义
const aliases = require('./alias')
const resolve = p => {
const base = p.split('/')[0]
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
return path.resolve(__dirname, '../', p)
}
}
取数组第一个元素设置为base ,在我们这个例子中,参数p 是web/entry-runtime-with-compiler.js ,base 为web 。base 并不是真的路径,而是借助了别名的配置,现在看一下别名配置的代码,在script/alias 中
const path = require('path')
module.exports = {
vue: path.resolve(__dirname, '../src/platforms/web/entry-runtime-with-compiler'),
compiler: path.resolve(__dirname, '../src/compiler'),
core: path.resolve(__dirname, '../src/core'),
shared: path.resolve(__dirname, '../src/shared'),
web: path.resolve(__dirname, '../src/platforms/web'),
weex: path.resolve(__dirname, '../src/platforms/weex'),
server: path.resolve(__dirname, '../src/server'),
entries: path.resolve(__dirname, '../src/entries'),
sfc: path.resolve(__dirname, '../src/sfc')
}
这里 web 对应的真实的路径是 path.resolve(__dirname, '../src/platforms/web') ,这个路径就找到了 Vue.js源码的 web 目录。然后 resolve 函数通过 path.resolve(aliases[base], p.slice(base.length + 1)) 找到了最终路径,它就是 Vue.js 源码 web 目录下的 entry-runtime-with-compiler
2.从入口开始
我们之前提到过 Vue.js 构建过程,在 web 应用下,我们来分析 Runtime + Compiler 构建出来的 Vue.js,它的入口是src/platforms/web/entry-runtime-with-compiler.js ,
import config from "core/config";
import { warn, cached } from "core/util/index";
import { mark, measure } from "core/util/perf";
import Vue from "./runtime/index";
import { query } from "./util/index";
import { compileToFunctions } from "./compiler/index";
import {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
} from "./util/compat";
export default Vue;
那么,当我们的代码执行 import Vue from 'vue' 的时候,就是从这个入口执行代码来初始化 Vue
3.Vue的入口
3.1 找到入口
在这个入口 JS 的上方我们可以找到 Vue 的来源:import Vue from './runtime/index' ,我们先来看一下这块儿的实现,它定义在 src/platforms/web/runtime/index.js 中
import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser, isChrome } from 'core/util/index'
import {
query,
mustUseProp,
isReservedTag,
isReservedAttr,
getTagNamespace,
isUnknownElement
} from 'web/util/index'
import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'
export default Vue
这里关键的代码是 import Vue from 'core/index' ,之后的逻辑都是对 Vue 这个对象做一些扩展,可以先不用看,我们来看一下真正初始化 Vue 的地方,在 src/core/index.js 中:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
return this.$vnode && this.$vnode.ssrContext
}
})
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue
这里有 2 处关键的代码,import Vue from './instance/index' 和 initGlobalAPI(Vue) ,初始化全局 Vue API(稍后介绍),我们先来看第一部分,在 src/core/instance/index.js 中
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
我们终于看见Vue 的真面目,实际上是用一个 Function 实现的类,我们只能通过 new Vue取实例化,为什么这里不用ES6的 Class 去实现呢?往后面我们可以看到里面有很多 xxxMixin 的函数调用,并把Vue作为参数传入,它们的功能都是给 Vue 的prototype 上扩展一些方法,Vue按功能把这些扩展分散到多个模块中去实现,而不是在一个模块中实现所有,这是用 Class 难以实现的。这样做的好处是非常方便代码的维护和管理,这种编程技巧也非常值得我们去学习。
3.2 扩展Vue的一些mixin调用
initMixin(Vue) :在Vue的原型对象上挂载了 _init 方法,当执行 new Vue()的时候,才会执行 function Vue 中的 this._init
stateMixin(Vue) :通过Object.defineProperty 在Vue.prototype 上定义了两个属性 $data 和 $props ,通过重写的get 返回对应的私有属性,如果在开发环境下通过$data 或$props 去改变值,就会报对应的警告,因为这个两个属性是只读的,然后又在Vue.prototype(Vue) 上定义了 $set/$delete/$watch 方法,这三个方法在后面会讲解,知道是在这里定义的就行
eventsMixin(Vue) :在Vue.prototype 上定义了$on ,$once , $off ,$emit 等方法,这些方法如何实现自行了解,比如说面试题实现事件总线,定义一个全局的_event 对象,然后存储emit 发射的方法…
lifecycleMixin(Vue) :在Vue.prototype 上定义了 _update(目前知道有这个东西就行),$forceUpdate (这个就是强制触发依赖更新,99%情况下不要用);然后$destroy 方法
renderMixin(Vue) :Vue.prototype 上定义了 $nextTick 和 _render
4.initGlobalAPI
Vue.js 在整个初始化过程中,除了给它的原型 prototype 上扩展方法,还会给 Vue 这个对象本身扩展全局的静态方法,它的定义在 src/core/global-api/index.js 中
export function initGlobalAPI (Vue: GlobalAPI) {
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
这里就是在 Vue 上扩展的一些全局方法的定义,Vue 官网中关于全局 API 都可以在这里找到,这里不会介绍细节,会在之后的章节我们具体介绍到某个 API 的时候会详细介绍。有一点要注意的是,Vue.util 暴露的方法最好不要依赖,因为它可能经常会发生变化,是不稳定的
4.1 configDef
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
这里劫持了Vue的config属性,使的无法对其进行修改。
4.2 Vue.util
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
4.2.1 warn
首先看 warn,它实际来自于/src/core/util/debug.js
warn = (msg, vm) => {
const trace = vm ? generateComponentTrace(vm) : ''
if (config.warnHandler) {
config.warnHandler.call(null, msg, vm, trace)
} else if (hasConsole && (!config.silent)) {
console.error(`[Vue warn]: ${msg}${trace}`)
}
}
tip = (msg, vm) => {
if (hasConsole && (!config.silent)) {
console.warn(`[Vue tip]: ${msg}` + (
vm ? generateComponentTrace(vm) : ''
))
}
}
用过Vue的warnHandler 功能应该都知道,这是一个自定义警告处理函数。其实也就是我们可以通过自定义warnHandler 函数做一些项目警告的收集,同样的功能还有errorHandler ,如果有需要可以去官方文档看看。generateComponentTrace 方法会追踪到项目警告组件的踪迹,也就是一个定位功能。如果没有定义warnHandler ,在写组件不规范的情况下,就会在控制台打印错误
4.2.2 extend
顺着找下去来自于 /share/utils
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
作用就是将源对象的属性混入到目标对象。
4.2.3 mergeOptions
在/core/util/options.js 下
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
作用是将父子的策略项合并为一个checkComponents 就是检查组件,内部遍历了传入的child 的components 属性检查组件名字是否规范。 normalizeProps 其作用就是统一将props: [?] 和props: {?} 形式转换为后者的形式。 normalizeInject 也是统一标准化传入的inject , inject 一般是父组件或者祖父组件 provide 的值,也是一种通信方式。 normalizeDirectives 标准化自定义指令,如果传入的是个function , 会被定义成{ bind: function, update: function} 这种形式。 下面还有如果子组件有 extends 或者mixins 将会改变父组件等功能,等遇到具体调用的时候再分析。 mergeField 这个是实际做的事情,它会按照strats 定义的的策略进行合并
strats.el = strats.propsData = function (parent, child, vm, key) {
strats.data = function () {...}
strats.props = ...
strats.methods = ...
strats.computed = ...
strats.watch = ...
...
4.2.4 defineReactive
是Vue 实现响应式的关键,后续会详细再说,这里相当于给util 赋予了这个功能
4.2.5 其他(可了解)
set 和 del 作用就是响应式的添加或删除属性 nextTick 可以在下次 DOM 更新循环结束之后执行延迟回调。与JS的事件运行机制非常像,会单独记录一篇文章。 Vue.observable 这个是 Vue 2.6版本新增的API,很明显的看到了它使用了Vue的Observe ,假如一个小型的项目根本用不上Vuex进行状态管理,可以使用它自定义一个小型的响应式store供全局使用,详情可以参照官方文档。 Vue.options
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
extend(Vue.options.components, builtInComponents) 将builtInCompoents 属性混入Vue.options.components ,里面是一些keep-alive 相关的东西。
initUse(Vue)
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
给Vue 上添加了use ,接收一个插件参数,安装插件。平时我们注册类似router 等插件的时候,就会用到Vue.use(router) 。其内部维护了一个_installedPlugins 数组用来存储所有安装的插件,安装时会判断插件是否实现了install 方法,如果有就会执行插件的install 方法,在这里Vue 巧妙的将自己注入到install 方法的参数中,这样的好处就是,在实现install 方法就不需要import Vue 了。
initMixin
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
全局混入,将mixin 通过合并策略混入全局的options ,一旦使用全局混入,由于局部注册组件,其实是调用了Vue.extend 方法,其调用时也会进行mergeOptions ,所以会影响每一个之后创建的 Vue 实例。应当恰当使用。
initExtend
export function initExtend (Vue: GlobalAPI) {
Vue.cid = 0
let cid = 1
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
if (name) {
Sub.options.components[name] = Sub
}
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
cachedCtors[SuperId] = Sub
return Sub
}
}
这里实现了一个js 的经典继承,此方法用于创建Vue 的子类构造函数。在这里就可以验证了上面说的,调用时会将Super.options 和extendOptions 合并。
initAssetRegisters
export function initAssetRegisters (Vue: GlobalAPI) {
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
ASSET_TYPES 在上面已经提到过了,这里是初始化Vue 的注册器,比如我们要注册组建的时候会调用Vue.component ,要自定义指令时会使用Vue.directive ,就是在这定义的。
|