Vue源码中有很多零碎知识点值得学习,本篇博文记录学习Vue中常见API的实现原理。
new Vue初始化流程
- 介绍
Vue初始化是1
2
3
4
5
6new Vue({
el: '#app',
data: {
name: '张三'
}
})
传入的参数是一个object,包含了绑定的根节点#app
和初始化数据data
。
查看Vue源码,Vue构造函数执行初始化方法_init(options)
,_init
依次执行了以下方法
1 | vm.$options = mergeOptions() // 扩展vue $options方法,该方法可以merge对象属性,扩展了vue属性 |
nextTick
介绍
nextTick用于下一次DOM更新执行的方法,当赋值变量之后,DOM节点并没有及时更新,所以此时获取节点内容并不是最新的,因此需要使用nextTick在DOM节点变化之后,获取最新的DOM。原理
nextTick是采用异步控制+优雅降级的方式,以v2.6版本为例,降级策略为根据浏览器兼容性依次判断是否支持Promise -> MutationObserver -> setImmediate -> setTimeout。
其中,Promise和MutationObserver属于微任务队列,setImmediate和setTimeout属于宏任务队列,微任务比宏任务优先执行。
nextTick将回调方法收集到callbacks数组中,在异步任务执行时,将callbacks中的函数依次执行。
例如在使用MutationObserver时,源码中创建了一个node节点,MutationObserver监听此节点内容的变化。当需要执行nextTick的回调函数时,手动触发node节点内容的变化,推动MutationObserver触发监听,执行callbacks中的方法。
双向绑定
介绍
Vue最基本的功能便是双向绑定,与jQuery相比,极大地方便了DOM操作,可以说是革了jQuery的命。使开发者可以专注于数据操作,数据推动DOM变化。原理
Vue初始化绑定data数据是在initState中的initData1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function initState (vm) {
vm._watchers = [];
var 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);
}
}
initData
中做了:
- 判断数据类型是否合规;
- 判断是否有重名的key,比如props中和data中不能有相同的属性;
proxy(vm, "_data", key);
代理data中的对象到this下。实现可以app.name可以调用app._data.nameobserve(data, true /* asRootData */);
绑定数据
再看observe:
1 | function observe (value, asRootData) { |
主要是先判断是否有__ob__
,如果没有,就把__ob__
设为new Observer实例。
再看Observer:
1 | var Observer = function Observer (value) { |
walk
和observeArray
是针对对象和数组进行数据绑定,数组的话,就循环针对每一个属性进行绑定。
walk:
1 | Observer.prototype.walk = function walk (obj) { |
walk就是针对对象中的每一个属性进行循环绑定。defineReactive$$1
这里就是使用Object.defineProperty
的set
,get
进行数据绑定,
1 | get: function reactiveGetter () { |
dep用于依赖管理,收集Watcher。它会用subs收集Watcher,当执行setter时,就将subs中的Watcher依次循环,执行每一个Watcher的notify。
总的数据管理流程就是:Observer -> dep -> watcher
Vue异步更新过程
介绍
当js中的变量发生变化时,DOM上绑定变量的节点内容并没有立刻发生变化,而是通过nextTick将tick之间发生的内容变化,一次性更新在DOM上的。原理
以如下代码为例:1
2
3<div id="app">
<p @click="handleClick">{{ test }}</p>
</div>
1 | const app = new Vue({ |
当点击P标签时,会触发handleClick
,首先要获取this.test
, Vue是通过proxy
将this._data
下的变量代理到this下的。
1 | function proxy (target, sourceKey, key) { |
proxy的get会调用Object.defineProperty
的get。
当给test赋新值时,又通过proxy的set调用Object.defineProperty
的set。
1 | set: function reactiveSetter (newVal) { |
1 | Dep.prototype.notify = function notify () { |
dep中绑定的Watcher,都在subs中。通知每一个Watcher去执行它们的update。
1 | Watcher.prototype.update = function update () { |
update并不是立即去执行更新,而是通过queueWatcher方法把需要更新的事件放进队列
1 | function queueWatcher (watcher) { |
放进队列之后,就通过nextTick去执行队列里的事件。nextTick前面已经介绍过了,nextTick传了一个回调flushSchedulerQueue
,Watcher的更新操作是在这个方法中进行
1 | function flushSchedulerQueue () { |
这样一次tick过程就结束了,总的过程是:setter -> dep.notify -> Watcher.update -> nextTick -> Watcher.run
patch和diff
介绍
Vue在更新DOM时,并不会全部更新DOM,而是把需要更新的节点进行更新,那么他是怎么计算哪些节点需要更新呢,这就要讲到diff算法了。diff算法是一种通过同层的树节点进行比较的高效算法,复杂度只有 O(n)分析
Vue更新操作是在nextTick后执行的flushCallbacks
,依次去执行监听者Watcher中的run方法,然后执行vm._update(vm._render(), hydrating)
, 参数vm._render()
就是需要更新的Vnode。_update中有一句vm.$el = vm.__patch__(prevVnode, vnode);
,这里就是执行patch方法了。
patch
中主要是判断新旧节点的差异:
1.如果vnode不存在,但是oldVnode存在,说明是需要销毁旧节点。
2.如果vnode存在,但是oldVnode不存在,说明是需要创建新节点。
3.当vnode和oldVnode都存在,那就要对节点进行进一步的比较patchVnode
。
patchVnode
主要是对节点本身以及节点属性进行比较,对于他们的子节点的比较,就是updateChildren
1 | function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { |
以上是diff算法在vue中的应用,整个过程是先进行了:
1.旧开始 《-》新开始
2.旧结束 《-》 新结束
3.旧开始 《-》 新结束
4.旧结束 《-》 新开始
这四个方案的比较。如果这四种都不符合,那就再以旧节点为key进行map映射查找是否有新节点相同的节点,如果有,则把新节点放到旧开始节点前边,如果没有找到,则新建节点。
在整个比较过程中,新旧节点顺序始终是不变的,操作的是真实DOM。
这里推荐一篇文章,非常的通俗易懂–>链接
computed原码分析
- 原理分析
初始化computed属性是在initState中1
2
3
4
5function initState (vm) {
...
if (opts.computed) { initComputed(vm, opts.computed); }
...
}
判断如果有computed属性,就执行initComputed初始化computed
1 | function initComputed (vm, computed) { |
接下来就是执行defineComputed
1 | function defineComputed ( |
我们看到该方法是使用Object.defineProperty定义computed中的key,getter就是createComputedGetter(key),看看他是怎么定义的getter
1 | function createComputedGetter (key) { |
getter的定义是从vm._computedWatchers中找到computed的key的watcher实例,创建watcher实例时,dirty是默认为true。第一次获取key,dirty是为true的话,就通过watcher.evaluate去计算key的属性。
1 | Watcher.prototype.evaluate = function evaluate () { |
这里我们看到就是执行我们自定义的computed的key的方法,得到值赋值给了watcher实例,进行缓存,之后将dirty变为false,这样再次获取computed的key,就直接取值,避免再次计算。
那么再看get()
1 | Watcher.prototype.get = function get () { |
这里我们看到去执行我们自定的computed的key的方法,如果方法中用到了data中的响应式属性,就会触发data的属性的getter
1 | get: function reactiveGetter () { |
这样一来,当computed的key所依赖的data的key发生变化时,就会触发data的key的dep.notify()
1 | Dep.prototype.notify = function notify () { |
但是在update()中,并不会立即重新计算computed的key的值,而是把它的dirty变为true
1 | /** |
这是因为,当data的属性发生变化,就会重新渲染视图,触发_render重新渲染,而视图上依赖了computed的key,就会去获取这个key,触发key的getter,在getter中就会由于dirty为true,而再次evaluate()。
这便是整个computed的处理过程。
watch源码分析
- 原理分析
初始化watch的入口是在initState中1
2
3
4
5
6function initState (vm) {
...
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
看一下initWatch
1 | function initWatch (vm, watch) { |
根据观察的key的操作是否是数组还是单个函数,进行createWatcher操作
1 | function createWatcher ( |
返回的vm.$watch就是要去创建一个watcher实例
1 | Vue.prototype.$watch = function ( |
1 | var Watcher = function Watcher ( |
在这里会去通过执行this.get()去获取watcher实例的value,这便是准备开始收集依赖了,和computed类似。
1 | Watcher.prototype.get = function get () { |
在这里我们只需要关心先把全局的Dep.target设为watcher实例,然后就去执行Object.defineProperty的getter,在getter中会判断如果有Dep.target,就会进行依赖收集.
1 | Object.defineProperty(obj, key, { |
这样watch的key就完成了依赖的收集,当data的响应式属性变化时,就会dep.notify()通知subs中的watcher实例进行更新。
1 | Watcher.prototype.update = function update () { |
更新并不是立即更新,而是通过queueWatcher放到队列中,再通过nextTick执行。
以上,便是watch的源码分析。
vue-router源码分析
介绍
vue-router是官方的前端路由库,作为一个单页应用,路由功能可以使单页应用看起来更像个多页应用,可以更好地控制页面跳转。Vue提供的路由方式有hash
,history
,abstract
。本文以v2.8为准来介绍。原理分析
Vue引入插件是通过Vue.use(plugin)
,use
源码是这样定义的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
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
}
}
主要是判断避免重复引入插件。初始化插件是把插件的install
或插件导出的定义在此上下文中执行。
install
的代码如下:
1 | export function install (Vue) { |
该方法使每个组件混入beforeCreate
和destroyed
方法,并响应式声明_route
属性,定义router-view
,router-link
组件,并定义组件生命周期钩子。
install
是定义在VueRouter
上的一个方法,回看VueRouter
类是怎么声明的:
1 | ... |
通过createMatcher
生成路由映射信息,然后对路由模式优雅降级,然后根据模式,选择具体的路由实体类。
router跳转是通过push方法,那么来看看push是怎么定义的:
1 | push (location: RawLocation, onComplete?: Function, onAbort?: Function) { |
1 | transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { |
confirmTransition
主要是判断是否是同一路由之间的跳转,如果不是的话,就把跳转之后需要调的任务放到任务队列中,这些任务主要是些生命周期钩子。
确认跳转之后就通过updateRoute
更新路由,然后再执行push中的回调pushState
,
具体看下updateRoute:
1 | updateRoute (route: Route) { |
这里执行cb,那么cb是在哪定义的呢?其实是在init中:
1 | history.listen(route => { |
又因为在前面定义了响应式属性_route
:
1 | Vue.util.defineReactive(this, '_route', this._router.history.current), |
所以就会触发组件的setter, setter中调用dep中收集的依赖,触发render,再通过nextTick重新渲染。
router-view中组件定义了render方法,在该方法中其实是执行$createElement去渲染组件
Vuex原理分析
介绍
Vuex是一个可以全局组件共享数据的插件,通过actions -> mutations -> state的流程,可以更加规范化数据操作。原理分析
Vue通过install
方法加载Vuex实例,主要是通过mixin
在beforeCreate
时来给Vue扩展方法:1
Vue.mixin({ beforeCreate: vuexInit })
1 | function vuexInit () { |
vuexInit
主要是把store实例挂载到this.$store上,子组件从其父组件引用$store属性,层层嵌套进行设置,以此来达到给全局组件注入store的目的。
那么再来看Store是怎么定义的:
1 | export class Store { |
实例化Store中,把定义的mutation和action都各自放进了一个映射集合,然后在执行时,通过方法名称去执行对应方法,然后再看是如何定义了dispatch和commit的:
1 | dispatch (_type, _payload) { |
commit
和dispatch
类似,只不过多了一句
1 | this._withCommit(() => { |
这个_withCommit就是用来在mutation改变state时,改变一下状态:
1 | _withCommit (fn) { |
再看state和getter是怎么定义的,这两个的定义是在初始化Store时,有个
1 | resetStoreVM(this, state) |
1 | function resetStoreVM (store, state, hot) { |
可以看到,通过创建一个响应式的data和computed来实现state和getters。
我们在在组件中使用Vuex时,经常会通过import { mapGetters, mapActions } from 'vuex'
来直接使用action或mutation的方法,那么他们是怎么实现的呢:
1 | export const mapActions = normalizeNamespace((namespace, actions) => { |
其实就是从_actions方法集合中,找到该方法,dispatch绑定$store上下文,并返回新的方法集合。
v1.5.2