如何找到对应的代码行开始看源码
看到很多资料都是通过new Vue的时候debug直接定位进入源码的,我以工程化的角度去说一下流程,为什么是从src\core\instance\index.js开始的
下载
git clone -b v2.6.14 https://github.com/vuejs/vue.git
package.json
工程化项目绕不开的的各种元数据和依赖关系记录文件,包括脚本定义;
- 里边有一个看着非常眼熟的脚本,平时没少用的
npm run dev执行
scripts:{"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",}
我们可以看到一个路径scripts/config.js,并且输入了TARGET:web-full-dev作为入参
scripts/config.js
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
genConfig内部通过入参TARGET读取常量builds的web-full-dev
'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 => {
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
找到构造函数Vue
- 进入
src/platforms/web/entry-runtime-with-compiler.js - 发现里Vue是通过
import Vue from './runtime/index'引入的 - 进入后发现Vue又是通过
import Vue from 'core/index'引入的 - 进入
core/index后发现还有import Vue from './instance/index' - 进入后,我们总算找到构造函数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)
}
实例化Vue
此小节会描述函数执行的大致逻辑
Vue从src\core\instance开始
文件定义和暴露构造函数Vue,且实例化的时候,调用了this._init 此外还给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)
}
initMixin(Vue)// 定义了_init方法,初始化Vue实例initLifecycle,initEvents,initRender,initState,initProvide,initInjections等
stateMixin(Vue)//在Vue的原型对象挂载$data,$props,$set,$delete,$watch
eventsMixin(Vue)//在Vue的原型对象挂载$on,$once,$off,$emit
lifecycleMixin(Vue)//在Vue的原型对象挂载_update,$forceUpdate,$destroy
renderMixin(Vue)//给Vue的原型对象挂载一些辅助方法,以及$nextTick,_render
export default Vue
initMixin(Vue)
在Vue的原型对象上定义了_init函数,初始化Vue实例的时候会调用,_init函数内部调用了initLifecycle,initEvents,initRender,initState,initProvide,initInjections等
initInternalComponent
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
1. 设置内部组件的特定属性和配置:
- 初始化一些内部组件特有的标识或状态,以便在后续的渲染和更新过程中进行特殊处理。
2. 处理父子组件关系:
- 建立内部组件与父组件之间的联系,包括设置父组件的引用、确定父子组件之间的通信方式等。
- 可能会处理组件的继承关系,确保内部组件能够正确地继承父组件的属性和方法。
3. 处理组件选项合并:
- 将内部组件的选项与父组件的选项进行合并,以确定最终的组件配置。这可能涉及合并数据、计算属性、方法、生命周期钩子等。
4. 准备组件的数据和响应式系统:
- 如果内部组件有自己的数据,可能会对其进行响应式处理,确保数据的变化能够触发视图的更新。
- 可能会设置一些内部组件特有的数据观察机制,以满足特定的业务需求。
5. 初始化组件的生命周期钩子:
-
确保内部组件的生命周期钩子能够正确地被调用,以便在不同的阶段执行特定的逻辑。
-
可能会对内部组件的生命周期钩子进行特殊处理,以适应内部组件的特殊行为。
mergeOptions
直接创建一个普通的 Vue 实例,而不是一个组件实例时。普通 Vue 实例可能没有设置 _isComponent 这个标识,就不会进入initInternalComponent,而是进入vm.$options = mergeOptions(...)
resolveConstructorOptions(vm.constructor):
- 这个函数用于解析 Vue 实例的构造函数的选项。它可能会遍历构造函数的原型链,收集所有通过 `Vue.extend` 或 `Vue.component` 定义的选项以及全局混入(`Vue.mixin`)的选项等,以获取该实例的完整构造函数选项。
-
options || {}:- 这里传入的
options通常是在创建 Vue 实例时用户自定义的选项对象。如果options不存在(为null或undefined),则使用一个空对象。
- 这里传入的
-
mergeOptions(...):-
调用
mergeOptions函数将前面两个来源的选项对象进行合并。具体来说:- 它会整合构造函数中的默认选项和用户自定义的选项,包括数据(
data)、方法(methods)、计算属性(computed)、生命周期钩子(如created、mounted等)、监听器(watch)等各个方面的选项。 - 对于父子组件的关系,这个过程也会处理父组件传递给子组件的选项,确保子组件能够正确继承和扩展父组件的选项。
- 对于一些特殊的选项,如
props、emits、slots等,会按照特定的规则进行合并,以保证组件之间的属性传递和交互正常进行。
- 它会整合构造函数中的默认选项和用户自定义的选项,包括数据(
-
-
vm.$options:-
最后,将合并后的选项对象赋值给
vm.$options,使得 Vue 实例可以通过this.$options访问到完整的选项配置。在后续的实例生命周期中,Vue 内部的各个部分可以根据这个选项对象来进行相应的操作,例如在生命周期钩子中执行特定的逻辑、根据data选项初始化响应式数据等。
-
initLifecycle(vm)
初始化一些属性,给父节点收集children
initEvents(vm)
初始化事件,拿到父组件的事件,并以自身为媒介(类似于$BUS),将事件绑定到自身上
initRender(vm)
-
创建用于渲染的元素和属性相关的变量:
- 例如,创建了 vm._vnode 用于存储组件的虚拟节点。
-
初始化与插槽相关的内容:
- 处理插槽的配置和数据结构,为组件的插槽功能做好准备。
-
定义一些用于操作 DOM 的辅助方法:
- 如 vm._c 用于创建虚拟节点,vm.$createElement 用于更方便地创建元素节点等。
-
建立组件与父组件、子组件之间的关联,方便在渲染和更新过程中进行通信和协调。
- 初始化一些与事件监听和派发相关的变量和方法:为组件处理事件提供基础支持。
-
包括把listeners变成响应式的,方便后续有变化的时候更新
callHook(vm, 'beforeCreate')
调用beforeCreate钩子
initInjections(vm)
先注入,再提供
-
如果先提供数据,然后再处理注入,可能会导致注入的数据覆盖或干扰已经提供的数据。因此,在初始化数据之前,先处理注入。
initState(vm)
基本就是初始化prop data methods computed watch这些属性
Vue实例化的数据响应式处理也是在initState内部执行的,initData(vm)/observe(vm._data = {}, true /* asRootData */)
- 相关的initXXX函数都在
src\core\instance\state.js内
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)
}
}
initComputed(vm, opts.computed)
在 Vue 中,initComputed 函数主要用于初始化计算属性。
- 它会遍历定义在组件选项中的计算属性对象,为每个计算属性创建一个
Watcher对象,并通过Object.defineProperty对计算属性进行劫持。
具体来说,它做了以下关键步骤:
-
遍历计算属性对象:获取每个计算属性的名称和对应的计算函数。
-
创建 Watcher :为每个计算属性创建一个 Watcher ,用于跟踪其依赖的数据变化。
-
定义属性的 get 和 set 方法:
- get 方法用于获取计算属性的值,在获取值时会执行计算函数,并收集依赖。
- set 方法通常是一个空操作,因为计算属性默认是只读的。
-
缓存计算结果:第一次获取计算属性的值时,会将结果缓存起来,后续再次获取时,如果依赖的数据没有变化,直接返回缓存的结果,提高性能。
-
通过 initComputed ,使得计算属性能够根据其依赖的数据自动更新,并提供高效的访问和缓存机制。
initWatch(vm, opts.watch)
可以看到initWatch通过调用createWatcher创建了watcher
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
initProvide(vm)
提供数据
callHook(vm, 'created')
调用钩子,没啥说的
可能会调用$mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
stateMixin(Vue)
在Vue的原型对象挂载$data,$props,$set,$delete,$watch
$data,$props
- 把给
$data,$props设置getter,但是不设置setter,确保其只可读,不可修改
$set,$delete
- 把响应式相关的set和delete方法挂载到Vue的原型对象上,所以可以在实例直接使用
this.$set()给响应式数据新增响应式属性- set和delete来源于
vue-2.6\src\core\observer\index.jsObserver和defineReactive等响应式核心处理逻辑也在observer\index
- set和delete来源于
$watch
在原型上挂载$watch,方便js动态创建监听
返回了一个unwatchFn函数,该函数调用的是watcher实例的teardown,移除监听,并且在当前实例的deps移除相关dep
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}
eventsMixin(Vue)
在Vue的原型对象挂载$on,$once,$off,$emit,实现事件的发布 - 订阅模式;
Vue 实例提供了强大的事件处理机制,使得开发者可以方便地实现组件之间的通信和交互。
lifecycleMixin(Vue)
在Vue的原型对象挂载_update,$forceUpdate,$destroy
_update
主要用于将虚拟 DOM 转换为实际的 DOM 操作,以更新视图
- 里面调用了
__patch__(内部执行diff算法) _update会在调用mounted->mountComponent=>hook(beforeMount)的内部被封装,提供给wather当做函数更新视图时调用
$forceUpdate
当组件的数据变化没有被 Vue 的响应式系统自动检测到,或者在某些特殊情况下需要立即更新组件的视图时,可以调用 $forceUpdate 方法。
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
$destroy
销毁实例
- 内部调用相关钩子,callHook-beforeDestroy+destroyed
- 拆卸相关watcher
- 取消事件监听
- 释放相关相关变量
renderMixin(Vue)
给Vue的原型对象挂载$nextTick,_render以及很多辅助方法
$nextTick
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
-
用于在下次 DOM 更新循环结束之后执行延迟回调。这在需要操作更新后的 DOM 时非常有用
-
在浏览器环境中:
- 如果支持 Promise ,则使用 Promise.then 来实现异步执行。
- 如果不支持 Promise 但支持 MutationObserver (用于监听 DOM 变化),则使用它来实现异步。
- 在 Vue 的 nextTick 中使用 MutationObserver 时,通常不会监听特定的某个元素。而是创建一个新的文本节点,并将对这个文本节点的变化监听作为触发 nextTick 回调的机制。
- 如果上述都不支持,就会采用 setTimeout 来实现异步。
-
nextTick 所使用的 Promise.then 和 MutationObserver 属于微任务,而 setTimeout 属于宏任务。
-
微任务通常在当前脚本执行完毕后,在执行下一个宏任务之前进行处理,并且微任务的执行优先级高于宏任务。
_render
用于将组件的渲染函数转换为虚拟 DOM。
mounted->mountComponent内部封装updateComponent函数的时候,函数内部调用_update的时候会把vm._render()当做参数
- 关于
updateComponent后面会说
vm._update(vm._render(), hydrating)
辅助方法
installRenderHelpers(Vue.prototype)
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
callHook
说一下这个函数,位置在vue-2.6\src\core\instance\lifecycle.js
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
一个非官方推荐,但可行的用法
可以看到callHook调用钩子,都是使用vm.$emit('hook:' + hook)这种方式派发的,结合平时实际使用,$on订阅后,可以在合适的时机调用vm.$emit来派发事件;
- @其实是v-on的语法糖,相关事件最终会被解析编译为对应的
vm.$on,监听了hook:created这个事件,在Vue实例化的时候,调用了callHook('created')就会派发时间,触发订阅(hook可以是其他钩子) - 下面示例helloW的vm就是helloW该实例
<helloW @hook:created='doSame' @hook:beforeDestroy='resetSame'/>
类似场景,引入了第三方组件,但希望created的时候监听其创建了,或者计数器+1等功能的时候,不期望重写第三方组件,入侵性较小
挂载($mount)
从梦开始的地方看Vue,对Vue.prototype.$mount有个重写
- 路径
vue-2.6\src\platforms\web\entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function(el){
if (!options.render){
if (template){...}
else if(el){...}
}
}
render,template,el的优先级
一般实例化写入配置的时候有3种方式,render,template,el
- 从源码看,render优先级最高,其次到template,最后到el
const _vue=new Vue({
tempalte:'<div />',
el:'#app',
render(){
},
data(){
return {
msg:'hello world'
}
}
}).$mount('#app')
mount
我们从入口文件entry-runtime-with-compiler.js可以看到有引入Vue:import Vue from './runtime/index';点进去会发现最初始对Vue的原型对象挂载的$mounted
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
mountComponent 主要做了以下几件重要的事情:
-
创建一个 Watcher 实例:用于监听数据的变化并触发更新操作。
-
调用 updateComponent 方法来进行初始渲染:
- 这个方法会获取最新的渲染结果(虚拟 DOM)。
- 然后将虚拟 DOM 转换为真实 DOM 并进行挂载。
-
设置更新回调:当数据变化时,会触发 Watcher 的回调,再次调用 updateComponent 进行组件的重新渲染。
-
处理错误:在渲染过程中捕获可能出现的错误,并进行适当的处理和报告。
-
例如,当组件首次被挂载时,mountComponent 会启动整个渲染流程,确保组件的视图正确地显示在页面上。之后,当组件中的数据发生变化,通过之前创建的 Watcher 机制,再次调用相关方法进行视图的更新。
-
里面的new Watch的时候会控制Dep.target的指针是否存自身,如果存在,则表示有watcher在监听
mountComponent
- 目录
'core/instance/lifecycle' - 挂载的时候实际就是调用这个函数
export function mountComponent (
vm,
el,
hydrating
){
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')
let updateComponent = () => {
//_render创建vnode
//_update转换为实际DOM,内部调用__patch__执行diff算法打补丁
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
updateComponent是内部封装生成的
- Vue.prototype._update 方法主要用于将_render生成的虚拟 DOM 转换为实际的 DOM 操作,以更新视图。
- 具体来说,它会执行以下主要操作:
- 对比新的虚拟 DOM 和旧的虚拟 DOM 之间的差异。
- 根据差异来决定是创建新的 DOM 节点、更新现有 DOM 节点的属性、删除不再需要的 DOM 节点等操作。
- 最终将这些操作应用到实际的 DOM 中,以实现视图的更新。
- 例如,如果新的虚拟 DOM 中添加了一个节点,_update 方法会在实际的 DOM 中创建相应的节点。如果某个节点的属性发生了变化,它会更新该节点的属性值。
- 这是 Vue 实现高效视图更新的关键步骤之一。
- 具体来说,它会执行以下主要操作:
- updateComponent提供给生成的wather实例,在更新视图的时候调用
如有错误欢迎指正,响应式相关和wather后面会另出一篇文章