源码调试地址
https://github.com/KingComedy/vue-source-debugger
Vue的初始化
在引入的Vue的时候,定义了Vue的构造函数,以及初始化了Vue的实例方法和静态方法,总结了有以下一些比较重要的方法:
初始化实例方法,主要在src\core\instance\index.js文件里
- _init方法(Vue实例初始化的方法)
- 初始化set、set、delete、$watch方法(用于监听或触发响应式更新的方法)
- 初始化on、on、off、once、once、emit(发布订阅的事件机制)
- 初始化forceUpdate、_update(用于组件更新函数)、forceUpdate、update(用于组件更新函数)、destroy
- 初始化$nextTick
- 初始化_render(用于将组件转为虚拟dom)
- 初始化Vue.prototype.patch (主要讲虚拟dom转为真实dom并挂载 以及 dom的更新时执行的diff对比)。 src\platforms\web\runtime\patch.js中定义
- 初始化Vue.prototype.mount方法 (生命周期create执行之后,mounted执行之前执行),在src\platforms\web\runtime\index.js定义,在src\platforms\web\entry-runtime-with-compiler.js扩展了mount方法(生命周期create执行之后,mounted执行之前执行),在src\platforms\web\runtime\index.js定义,在src\platforms\web\entry−runtime−with−compiler.js扩展了mount方法
初始化静态成员,在src\core\index.js文件里
- 初始化Vue.options 和 Vue.config静态属性
- 初始化Vue.options._base 为Vue构造函数
- 初始化Vue.options的component、directive、filter3个属性
- 初始化Vue.util 的 defineReactive、extend、mergeOptions、warn的四个方法
- 初始化Vue.set、delete、nextTick、observable 四个静态方法(和set、set、delete、$nextTick是同一个方法)
- 注册了keep-alive组件
- 注册了Vue.Use、Vue.mixin、Vue.extend静态方法
Vue的首次渲染
new Vue实例化时,执行_init方法(src\core\instance\init.js)
执行_init方法(在src\core\instance\init.js定义)
主要源码:
// 对传入的配置与默认配置合并
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
vm._self = vm
initLifecycle(vm) // 初始化组件的$parent\$root\$children\$ref属性
initEvents(vm) // 初始化组件的事件监听
initRender(vm) // 初始化$slots属性, 定义 $createElement和vm._c方法(用于生成虚拟dom),对组件的$listeners、$attrs做响应式处理
callHook(vm, 'beforeCreate') // 执行beforeCreate生命周期函数
initInjections(vm) // 获取组件的inject所有属性,并做响应式处理
initState(vm) // 组件数据的初始化以及响应式处理:按顺序 props => methods => data => computed => watch
initProvide(vm) // 获取组件的provide所有属性,并做响应式处理
callHook(vm, 'created') // 执行created生命周期函数
// 处理完组件相关的数据后,开始执行$mount方法
if (vm.$options.el) {
vm.$mount(vm.$options.el) // $mount 在src\platforms\web\entry-runtime-with-compiler.js文件中执行了扩展
}
执行$mount(在src\platforms\web\runtime\index.js定义)
- mount方法是在src\platforms\web\runtime\index.js定义,如果是提供编译器的vue版本,在src\platforms\web\entry-runtime-with-compiler.js文件中对mount方法是在src\platforms\web\runtime\index.js定义,如果是提供编译器的vue版本,在src\platforms\web\entry−runtime−with−compiler.js文件中对mount方法进行扩展
- 扩展的主要内容:获取组件的render函数,优先级:options.render => template => el
- 1 如果vue组件中是否已经有自带的render函数,继续执行$mount
- 2 如果有template,将template转为render函数
- 3 如果有el属性,通过el获取dom,并转为template,再将template转为render函数
- 得到render函数后,赋值给vm.options.render, 继续执行options.render,继续执行mount
- 最终执行mountComponent函数(在src\core\instance\lifecycle.js定义)
执行mountComponent(在src\core\instance\lifecycle.js定义)
- 执行beforeMount生命周期
- 设置组件更新函数updateComponent => vm._update(vm._render(), hydrating )
- 实例化Watcher,传入组件更新函数updateComponent
- Watcher实例化时,会触发一次组件更新,即执行updateComponent
- 先执行vm_render(即$mount里生成的render函数),生成当前组件的虚拟dom 即vnode,并传入vm._update
- 执行vm._update(在src\core\instance\lifecycle.js定义)
- _update主要是执行vm.patch(在src\platforms\web\runtime\index.js定义)
- 最终执行patch(在src\core\vdom\patch.js文件中所返回的patch函数),无论是首次渲染还是组件更新,最后都是执行patch函数
- 因为是首次渲染,所以主要流程是将传入的虚拟dom转为真实dom,并挂载到真实dom上,主要是深入遍历执行createElm
- createElm先执行createComponent(在src\core\vdom\patch.js定义),如果遇到的是自定义组件,则不会继续往下执行
- 如果遇到的是自定义组件,获取组件的init的钩子函数,并执行。(init钩子函数,在vm._render中 执行createComponent时 执行钩子函数的安装)
- init主要是执行组件的实例化,即执行Vue的实例方法_init,并执行实例$mount方法
- 即当遇到子组件时,会走子组件的渲染流程,即 _init => $mount => mountComponent => new Watcher => updateComponent => 子组件的mounted生命周期
- 如果不是自定义组件,创建dom元素,执行createChildren遍历vnode.chidrend,执行createElm,创建子元素,并执行insert添加到父dom元素下
- createChildren深度遍历创建完所有子dom元素后,执行最后的insert,将根元素插入到页面中,整个首次渲染结束
- 执行根组件的mounted生命周期函数
vm._render的主要作用:(在src\core\instance\render.js定义)
- 主要是执行vm.options.render.call(vm._renderProxy, vm.options.render.call(vm.renderProxy,vm.createElement)
- vm._renderProxy即vm实例,vm.$createElement则在initRender(src\core\instance\render.js)中初始化
- vm.$createElement中执行createElement(src\core\vdom\create-element.js中定义)
- createElement对传入的参数做进一步的处理,比如有时传入两个参数或者三个参数,最终执行_createElement,开始创建虚拟dom
- 执行_createElement:通过标签类型创建虚拟dom
- 判断传入的标签tag是否是字符串类型,如果是字符串,判断是否是原生dom标签,如果是直接new VNode创建虚拟dom
- 如果不是,则通过tag在vm.$options.components里找到对应的组件的构造函数Ctor,执行createComponent并传入构造函数Ctor
- 执行createComponent(在src\core\vdom\create-component.js定义)
- installComponentHooks安装组件各阶段相对应的钩子,默认钩子componentVNodeHooks有init、prepatch、insert、destroy 4个管理钩子
- 创建虚拟dom
createElm的主要作用:(在src\core\vdom\patch.js定义)
- 如果遇到的是自定义组件,获取组件的init的钩子函数,并执行。
- init主要是执行组件的实例化,即执行Vue的实例方法_init,并执行实例$mount方法
- 即当遇到子组件时,会走子组件的渲染流程,即 _init => $mount => mountComponent => new Watcher => updateComponent => 子组件的mounted生命周期
- 如果不是自定义组件,创建dom元素,执行createChildren遍历vnode.chidrend,执行createElm,创建子元素,并执行insert添加到父dom元素下
- createChildren深度遍历创建完所有子dom元素后,执行最后的insert,将根元素插入到页面中,整个首次渲染结束
总结
组件的首次渲染过程
- _init => mount => mountComponent => new Watcher => updateComponent => vm._render => vm._update => vm.__patch__ => createElm创建根元素 => createChildren(深度遍历执行createElm创建子元素 ) => 如果遇到子组件child,执行child_init => child.mount => ... => 直到子组件下所有的子元素创建完成 => 执行子组件的mounted => 直到所有子元素创建完成 => 执行组件的mounted
父子组件的生命周期顺序
- beforeCreate 和 created 都是在组件实例的_init里执行,而子组件是在 patch阶段才开始创建
- 只有所有子元素创建并挂载(如果有子组件,要等子组件执行完mounted), 最终才执行父元素的mounted
- 所以顺序为:parent created => child created => child mounted => parent mounted
组件并不是拆分越细越好
- 组件的初始化 要多创建许多属性,和做一些组件所需的一些操作,所以组件抽象需要合理,并不是组件拆的越细越好~