前言 最近看源码看得有点上头,趁着周末有空就记录下。之前和同事谈论过,他们对源码都不以为意,停留在一种框架停留在能用就行了,看看官方文档,能解决覆盖80%,甚至更多的业务场景了。对于我来说,使用vue已经很长一段时间了,如何对内部实现还是不清楚的话,我自己本身也说不过去的,关键是有时候遇到一些问题,只有理解内部是如何进行的,才能很好地找到合理的解决方案,而不是老是黑人问号“为什么会这样?”
概述 众所周知,vue内部遍历data对象,对数据进行每个劫持,重新定义了get, set方法。set方法很好理解,就是改变值时候触发,那get方法是什么时候触发的呢?
引用官方文档的概述,如下:
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。 这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式化并不同,所以建议安装 vue-devtools 来获取对检查数据更加友好的用户界面。 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
mouted 简单分析一波源码~~那就从入口说起吧。
现在vue2.x版本一般是通过官方脚手架进行搭建,在src/main.js目录下,会看到这样的代码:
1 2 3 4 5 6 new Vue ({ router : router, store, i18n, render : h => h (App ) }).$mount('#app' );
1 2 3 4 5 6 7 Vue .prototype .$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query (el) : undefined return mountComponent (this , el, hydrating) }
至于传入的参数是怎样处理的,我们暂时放到一边,一目了然,new了一个vue实例,并且调用了$mount方法。
new Vue()的时候,调用this._init方法,其中这里对data对象中所有属性利用Object.defineProperty, 进行geter/seter处理。
vue对web/weex不同平台运行环境进行了差异化处理,以web平台分析,我们看到$mount的赋值如下,所有在入口其实是调用了mountComponent()方法,我们继续深入探究mountComponent()内部是怎样实现的。
进入mountComponent,将组件的element元素赋值给$el,触发了beforeMount生命周期,创建了一个Watch实例,最后触发了mounted生命周期,返回vm component对象,核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 export function mountComponent ( vm : Component , el : ?Element , hydrating?: boolean ): Component { vm.$el = el if (!vm.$options .render ) { vm.$options .render = createEmptyVNode } callHook (vm, 'beforeMount' ) let updateComponent updateComponent = () => { vm._update (vm._render (), hydrating) } new Watcher (vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed ) { callHook (vm, 'beforeUpdate' ) } } }, true ) hydrating = false if (vm.$vnode == null ) { vm._isMounted = true callHook (vm, 'mounted' ) } return vm }
watcher 我们把焦点放到new Watcher,这段代码发生了什么?转到Watcher类的定义,首先在constructor将watcher的实例的this,push到vm._watchers数组里面,然后对一序列的参数进行赋值初始化,最后调用了里面的this.get()方法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 export default class Watcher { vm : Component ; cb : Function ; id : number; deep : boolean; user : boolean; lazy : boolean; sync : boolean; dirty : boolean; active : boolean; deps : Array <Dep >; newDeps : Array <Dep >; depIds : SimpleSet ; newDepIds : SimpleSet ; before : ?Function ; getter : Function ; value : any; constructor ( vm : Component , expOrFn : string | Function , cb : Function , options?: ?Object , isRenderWatcher?: boolean ) { this .vm = vm vm._watchers .push (this ) if (options) { this .deep = !!options.deep this .user = !!options.user this .lazy = !!options.lazy this .sync = !!options.sync this .before = options.before } else { this .deep = this .user = this .lazy = this .sync = false } this .cb = cb this .id = ++uid this .active = true this .dirty = this .lazy this .deps = [] this .newDeps = [] this .depIds = new Set () this .newDepIds = new Set () ? expOrFn.toString () : '' if (typeof expOrFn === 'function' ) { this .getter = expOrFn } else { this .getter = parsePath (expOrFn) } this .value = this .lazy ? undefined : this .get () }
依赖收集 在get()处理如下逻辑,先喵一下主流程代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 get () { pushTarget (this ) let value const vm = this .vm try { value = this .getter .call (vm, vm) } catch (e) { if (this .user ) { handleError (e, vm, `getter for watcher "${this .expression} "` ) } else { throw e } } finally { if (this .deep ) { traverse (value) } popTarget () this .cleanupDeps () } return value }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Dep .target = null const targetStack = []export function pushTarget (target : ?Watcher ) { targetStack.push (target) Dep .target = target } export function popTarget () { targetStack.pop () Dep .target = targetStack[targetStack.length - 1 ] }
pushTarget(this) 将当前watcher放到targetStack队列, 同一时间只有一个watcher处于收集状态
this.getter.call(vm, vm) 回顾mountComponent()方法的传参,其实watcher的getter方法就是调用:
1 2 3 4 let updateComponentupdateComponent = () => { vm._update (vm._render (), hydrating) }
而_update的核心是触发__patch__,根据虚拟vnode,生成真实的Dom元素, 下面是_update的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Vue .prototype ._update = function (vnode: VNode, hydrating?: boolean ) { const vm : Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance (vm) vm._vnode = vnode if (!prevVnode) { vm.$el = vm.__patch__ (vm.$el , vnode, hydrating, false ) } else { vm.$el = vm.__patch__ (prevVnode, vnode) } restoreActiveInstance () if (prevEl) { prevEl.__vue__ = null } if (vm.$el ) { vm.$el .__vue__ = vm } if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent ._vnode ) { vm.$parent .$el = vm.$el } }
如何进行依赖收集呢 回到get/seter的处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 Object .defineProperty (obj, key, { enumerable : true , configurable : true , get : function reactiveGetter () { const value = getter ? getter.call (obj) : val if (Dep .target ) { dep.depend () if (childOb) { childOb.dep .depend () if (Array .isArray (value)) { dependArray (value) } } } return value }, set : function reactiveSetter (newVal) { const value = getter ? getter.call (obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env .NODE_ENV !== 'production' && customSetter) { customSetter () } if (getter && !setter) return if (setter) { setter.call (obj, newVal) } else { val = newVal } childOb = !shallow && observe (newVal) dep.notify () } })
我们发现在get里面,触发了dep.depend():
1 2 3 4 5 depend () { if (Dep .target ) { Dep .target .addDep (this ) } }
上面说到,在实例化watcher的时候,pushTarget(this),即 target => watcher, 因此在get的时候,就是触发了watcher里面的addDep, 同一个watcher通过newDepIds维护,根据id避免 收集重复的依赖~~
1 2 3 4 5 6 7 8 9 10 addDep (dep : Dep ) { const id = dep.id if (!this .newDepIds .has (id)) { this .newDepIds .add (id) this .newDeps .push (dep) if (!this .depIds .has (id)) { dep.addSub (this ) } } }
在vnode转化成真实Dom元素的时候,会触发数据绑定中的get, 进行依赖收集,收集完成调用popTarget(),this.cleanupDeps(), 避免造成对下一次依赖收集的污染。
所有依赖塞进一个数组队列subs维护,当数据发生变化,在set方法触发notify(), 遍历subs里面的元素(其实就是watcher对象),调用watcher对象的update()方法, 触run()方法进行派发更新,最终还是回到了vm._update(vm._render(), hydrating),更新渲染。
需要注意的是, 内部根据组件id进行排序,保证更新顺序是由父组件到子组件。
1 queue.sort ((a, b ) => a.id - b.id )
为了提高性能vue实行异步更新watcher队列, 就是将watcher.run()放到任务队列,如:
Promise.resolve(flushCallbacks)
setTimeout(flushCallbacks, 0)
const observer = new MutationObserver(flushCallbacks)
贴出官方文档的解释:
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。 例如,当你设置 vm.someData = ‘new value’,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用
总结 对vue 2.x的源码分析至此over, 在debug的过程中,曾经怀疑过是否值得花这么多时间去做这个事情,但最终还是走过来了,深入debug源码我觉得有两点是非常重要的:
我对自己对要求是,是否吃透了JavaScript,是否达到为所欲为的程度?很显然,离这个水平还有很长的路要走,那就继续努力吧。接下来转战vue 3.0了,我可能会再去撸一遍vue 3.0的源码,大概就是想学好typescript😬