认识
一、认识
二、初始化
-
定义
Vue.prototype._init
、Vue.prototype.$data
、Vue.prototype.$props
、Vue.prototype.$set
、Vue.prototype.$delete
、Vue.prototype.$watch
、Vue.prototype.$on
、Vue.prototype.$once
、Vue.prototype.$off
、Vue.prototype.$emit
、Vue.prototype._update
、Vue.prototype.$forceUpdate
、Vue.prototype.$destroy
、Vue.prototype.$nextTick
、Vue.prototype._render
等方法 -
执行
Vue.prototype._init
函数 -
initInternalComponent()
或者mergeOptions
处理组件配置项-
initInternalComponent()
: 每个子组件初始化时走这里,这里只做了一些性能优化, 将组件配置对象上的一些深层次属性放到vm.$options
选项中,以提高代码的执行效率 -
mergeOptions
: 初始化根组件时走这里,合并Vue
的全局配置到根组件的局部配置,比如Vue.component
注册的全局组件会合并到 根实例的components
选项中
-
-
initLifecycle(vm)
初始化组件实例的关系属性,比如$parent
、$children
、$root
、$refs
等 -
initEvents(vm)
处理自定义事件: 这里需要注意一点,所以我们在<comp @click="handleClick" />
上注册的事件,监听者不是父组件,而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关 -
initRender(vm)
解析组件的插槽信息,得到vm.$slot
,处理渲染函数,得到vm.$createElement
方法,即h
函数 -
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
调用beforeCreate
钩子函数: 数据初始化并未完成,像data
、props
这些属性无法访问到 -
initInjections(vm)
初始化组件的inject
配置项,得到result[key] = val
形式的配置对象,然后对结果数据进行响应式处理,并代理每个key
到vm
实例 -
initState(vm)
处理数据响应式,处理props
、methods
、data
、computed
、watch
-
initProvide(vm)
解析组件配置项上的provide
对象,将其挂载到vm._provided
属性上 -
callHook(vm, 'created')
调用created
钩子函数: 数据已经初始化完成,能够访问data
、props
这些属性,但这时候并未完成dom
的挂载,因此无法访问到dom
元素 -
如果发现配置项上有
el
选项,则自动调用$mount
方法,也就是说有了el
选项,就不需要再手动调用$mount
方法,反之,没提供el
选项则必须调用$mount
-
接下来则进入挂载阶段
三、挂载
-
调用
$mount
,$mount
主要有以下工作:-
处理
render
选项或者template
选项, 如果两个都没有, 通过el
获取DOM
上的outerHTML
字符串 -
通过
compileToFunctions
编译template
, 生成render
函数 和staticRenderFns
并挂载到Vue.$options
上
-
-
调用
$mount > Vue.prototype.$mount > mountComponent
-
在
mountComponent
函数中,主要有以下工作:-
callHook(vm, 'beforeMount')
执行beforeMount
钩子 -
定义
updateComponent
函数,updateComponent
函数可以渲染VNode
-
实例化渲染
Watcher
, 将updateComponent
作为第二个参数传入,updateComponent
后续会作为Watcher
的getter
函数。
-
-
在
Watcher
的constructor
构造函数中, 初始调用this.getter
函数, 进而调用执行updateComponent
函数进行首次渲染 -
在
updateComponent
函数中, 主要有以下工作:-
执行
vm._render
生成组件的VNode
: 在组件渲染的过程中, 用到的数据属性会触发getter
,getter
内部会收集依赖。 -
执行
vm.__patch__
进行首次渲染
-
-
递归遍历
VNode
, 创建各个节点,处理节点上的属性和指令, 如果是自定义组件则创建组件实例, 进行组件的初始化、挂载 -
最终所有
VNode
变成真实的DOM
节点并替换掉页面上的模版内容 -
完成初始渲染
四、更新
-
响应式拦截到数据的更新,
setter
拦截到更新操作 -
调用
dep.notify()
, 循环遍历subs
中的所有Watcher
, 执行Watcher
的update
方法。 -
在
update
方法中, 会将此时的Watcher
加入到渲染队列queue
, 通过nextTick
进行批量更新渲染 -
在
nextTick
中,通过promise.then(flushCallbacks)
将批量更新任务放到了微任务队列, 依次执行任务队列中的任务, 每一个任务就是一个Watcher
, 开始执行每一个Watcher
的run
方法 -
Watcher
中的run
方法调用Watcher
中的get
方法,get
方法调用Watcher
中的getter
函数, 此时的getter
函数就是updateComponent
, 用于初始或者更新渲染 -
首先执行
vm._render
生成组件的vnode
,这时就会执行编译器生成的函数 -
执行
vm.__patch__
进行更新渲染 -
执行
patchVnode
进行VNode
Diff
操作 -
完成更新
五、思考与沉淀
5.1 Vue 的编译过程的设计思想?
Vue.js
在不同的平台下都会有编译的过程, 因此编译过程中的依赖的配置 baseOptions
会有所不同。而编译过程会多次执行,但这同一个平台下每一次的编译过程配置又是相同的,为了不让这些配置在每次编译过程都通过参数传入, Vue.js
利用了函数柯里化的技巧很好的实现了 baseOptions
的参数保留。同样,Vue.js
也是利用函数柯里化技巧把基础的编译过程函数抽出来,通过 createCompilerCreator(baseCompile)
的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开
5.2 Vue 在订阅依赖时所做的优化?
考虑到一种场景,我们的模板会根据 v-if
去渲染不同子模板 a
和 b
,当我们满足某种条件的时候渲染 a
的时候,会访问到 a
中的数据,这时候我们对 a
使用的数据添加了 getter
,做了依赖收集,那么当我们去修改 a
的数据的时候,理应通知到这些订阅者。那么如果我们一旦改变了条件渲染了 b
模板,又会对 b
使用的数据添加了 getter
,如果我们没有依赖移除的过程,那么这时候我去修改 a
模板的数据,会通知 a
数据的订阅的回调,这显然是有浪费的。
因此 Vue
设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b
模板的时候去修改 a
模板的数据,a
数据订阅回调已经被移除了,所以不会有任何浪费,真的是非常赞叹 Vue
对一些细节上的处理。