前言 最近看源码看得有点上头,趁着周末有空就记录下。之前和同事谈论过,他们对源码都不以为意,停留在一种框架停留在能用就行了,看看官方文档,能解决覆盖80%,甚至更多的业务场景了。对于我来说,使用vue已经很长一段时间了,如何对内部实现还是不清楚的话,我自己本身也说不过去的,关键是有时候遇到一些问题,只有理解内部是如何进行的,才能很好地找到合理的解决方案,而不是老是黑人问号“为什么会这样?”
概述 众所周知,vue内部遍历data对象,对数据进行每个劫持,重新定义了get, set方法。set方法很好理解,就是改变值时候触发,那get方法是什么时候触发的呢?
引用官方文档的概述,如下:
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
 
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) 代替。
 
总结 对vue 2.x的源码分析至此over, 在debug的过程中,曾经怀疑过是否值得花这么多时间去做这个事情,但最终还是走过来了,深入debug源码我觉得有两点是非常重要的:
我对自己对要求是,是否吃透了JavaScript,是否达到为所欲为的程度?很显然,离这个水平还有很长的路要走,那就继续努力吧。接下来转战vue 3.0了,我可能会再去撸一遍vue 3.0的源码,大概就是想学好typescript😬