vue2.x核心源码深入浅出,我还是去看源码了

  平常的工作就是以vue2.x进行开发,因为我是个实用主义者,以前我就一直觉得,你既然选择了这个框架开发你首先就要先弄懂这玩意怎么用,也就是先熟悉vue语法和各种api,而不是去纠结实现它的原理是什么。甚至我可以这么说,你没有看过源码,只通过官方文档也能用这个框架解决绝大部分业务需要,解决大部分bug,而且大部分情况下,别人是不会管你知不知道原理的。但我不是说阅读源码不好,至少在解决另一小部分bug的时候会让你少走很多弯路,知道为什么会导致这样的bug,还有一点,至少在面试的时候还是很有用的,手动狗头。

  先放上vue2.x版本官方文档:https://v2.cn.vuejs.org/v2/guide/instance.html,然后gayhub上的vue2.x源码地址:https://github.com/vuejs/vue/tree/v2.7.10,由于vue2.x还在迭代更新中,目前最新tag是v2.7.10,所以我们这次分析此分支下的代码。本次分析的代码主要在src/core下,建议谷歌浏览器安装Octo tree插件,一款在线以树形格式展示github项目代码结构的插件(如图左侧),效果真的很棒。

vue2.x核心源码深入浅出,我还是去看源码了

1. 实例挂载

  大家都会在入口文件main.js写上

let app = new Vue({   el: '#app',   data: {     message: 'Hello Vue!'   } })

  其实这块代码套用官方的话呢,就是通过vue函数,给你创建一个vue实例。关于vue这个对象,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/instance/index.ts#L9

function Vue(options) {   if (__DEV__ && !(this instanceof Vue)) {     warn('Vue is a constructor and should be called with the `new` keyword')   }   this._init(options) }

  是不是感觉特别短,它只是说明了vue这个函数,必须要通过new关键字来进行初始化,而且,重头戏在this._init(options)这行代码里,这里调用的_init方法源码是定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/instance/init.ts#L16中的initMixin(),但重点是下面这些代码(L38~L66):

// merge options 合并配置 if (options && options._isComponent) {   // optimize internal component instantiation   // since dynamic options merging is pretty slow, and none of the   // internal component options needs special treatment.   initInternalComponent(vm, options as any) } else {   vm.$options = mergeOptions(     resolveConstructorOptions(vm.constructor as any),     options || {},     vm   ) } /* istanbul ignore else */ if (__DEV__) {   initProxy(vm) // 初始化代理属性 } else {   vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) // 初始化生命周期 initEvents(vm) // 初始化事件中心 initRender(vm) // 初始化渲染 callHook(vm, 'beforeCreate', undefined, false /* setContext */) // 初始化beforeCreate钩子 initInjections(vm) // resolve injections before data/props initState(vm) // 初始化props、methods、data、computed、watch initProvide(vm) // resolve provide after data/props callHook(vm, 'created') // 初始化created钩子

 

2.双向绑定 

  实现vue双向绑定的3个核心类:observe类,dep类和watcher类,在src/core/observer文件夹下,分别对应index.ts文件、dep.ts文件、watcher.ts文件,首先,我们先看index.ts中对observe类的定义,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/index.ts#L49

export class Observer {   dep: Dep   vmCount: number // number of vms that have this object as root $data    constructor(public value: any, public shallow = false, public mock = false) {     // this.value = value     this.dep = mock ? mockDep : new Dep()     this.vmCount = 0     def(value, '__ob__', this)     if (isArray(value)) {       if (!mock) {         if (hasProto) {           /* eslint-disable no-proto */           ;(value as any).__proto__ = arrayMethods           /* eslint-enable no-proto */         } else {           for (let i = 0, l = arrayKeys.length; i < l; i++) {             const key = arrayKeys[i]             def(value, key, arrayMethods[key])           }         }       }       if (!shallow) {         this.observeArray(value)       }     } else {       /**        * Walk through all properties and convert them into        * getter/setters. This method should only be called when        * value type is Object.        */       const keys = Object.keys(value)       for (let i = 0; i < keys.length; i++) {         const key = keys[i]         defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock)       }     }   }

  observe类会在vue实例被创建的时候,去遍历data里的每一个属性,先调用Array.isArray()判断是不是数组。第一个考点就来了,vue是怎么监控数组的?可以看到,如果属性是数组,就会直接将arrayMethods直接赋值给监控数组的_proto_上以达到重写数组方法的目的,所以实际上我们调用的这几个数组方法已经是经过mutator()重写过了的(所以官方称这些为数组变更方法),在这里重写数组方法的好处是只对想要监控的数组生效,不用担心会污染到全局的Array方法。还有一点,虽然现在的浏览器基本都支持这种非标准属性(_proto_)的写法,因为这种写法本身就是早期浏览器自身厂商对原型属性规范的实现,但是为了以防有些浏览器不支持,源码这里还是对浏览器做了兼容,如果不支持,就将这些变异方法一个个绑定到监控的数组上。

  arrayMethods定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/array.ts,这里写了一个拦截器methodsToPatch用来拦截数组原有的7个方法并进行重写,这就是为什么vue只能通过变异方法来改变data里的数组,而不能使用array[0]=newValue的原因。官网文档说是由于 JavaScript 的限制,Vue 不能检测数组和对象的变化其实就是因为defineProperty方法只能监控对象,不能监控数组。

const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto)  const methodsToPatch = [   'push',   'pop',   'shift',   'unshift',   'splice',   'sort',   'reverse' ]  /**  * Intercept mutating methods and emit events  */ methodsToPatch.forEach(function (method) {   // cache original method   const original = arrayProto[method]   def(arrayMethods, method, function mutator(...args) {     const result = original.apply(this, args)     const ob = this.__ob__     let inserted     switch (method) {       case 'push':       case 'unshift':         inserted = args         break       case 'splice':         inserted = args.slice(2)         break     }     if (inserted) ob.observeArray(inserted)     // notify change     if (__DEV__) {       ob.dep.notify({         type: TriggerOpTypes.ARRAY_MUTATION,         target: this,         key: method       })     } else {       ob.dep.notify()     }     return result   }) })

  继续接上上面的observe类源码说,如果是属性是对象的话,则会对对象的每一个属性调用defineReactive()。源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/index.ts#L131。其重点是下面这些代码(L157~L213)。

Object.defineProperty(obj, key, {   enumerable: true,   configurable: true,   get: function reactiveGetter() {     const value = getter ? getter.call(obj) : val     if (Dep.target) {       if (__DEV__) {         dep.depend({           target: obj,           type: TrackOpTypes.GET,           key         })       } else {         dep.depend()       }       if (childOb) {         childOb.dep.depend()         if (isArray(value)) {           dependArray(value)         }       }     }     return isRef(value) && !shallow ? value.value : value   },   set: function reactiveSetter(newVal) {     const value = getter ? getter.call(obj) : val     if (!hasChanged(value, newVal)) {       return     }     if (__DEV__ && customSetter) {       customSetter()     }     if (setter) {       setter.call(obj, newVal)     } else if (getter) {       // #7981: for accessor properties without setter       return     } else if (!shallow && isRef(value) && !isRef(newVal)) {       value.value = newVal       return     } else {       val = newVal     }     childOb = !shallow && observe(newVal, false, mock)     if (__DEV__) {       dep.notify({         type: TriggerOpTypes.SET,         target: obj,         key,         newValue: newVal,         oldValue: value       })     } else {       dep.notify()     }   } })

  这就是双向绑定最核心的部分了,利用object.defineProperty()给每个属性添加getter和setter。getter里主要是调用了Dep类的depend(),Dep类的源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/dep.ts#L21,depend()主要是调用了Dep.target.addDep(),可以看到Dep类下有个静态类型target,它就是一个DepTarget,这个DepTarget接口是定义在#L10,而Watcher类则是对DepTarget接口的实现,所以addDep()的定义需要在Watcher类中去寻找,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/watcher.ts#L160,它又调用回dep.addSub(),其作用是将与当前属性相关的watcher实例之间的依赖关系存进一个叫subs的数组里,这个过程就是依赖收集。那么问题来了:为什么这里要调过来调过去,直接调用不行么,这也是考点之一,vue的双向绑定采用的是什么设计模式?看了这段代码,你就知道了,它采用的是发布者-订阅者模式,而不是观察者模式,因为Dep类就充当了发布者订阅者中的一个消息中转站,就是所谓的调度中心,这样发布者和订阅者就不受对方干扰,实现解耦。

  然后setter里主要是调用了dep.notify(),notify()源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/dep.ts#L51,其作用是遍历subs数组,然后通知到与当前属性相关的每个watcher实例,调用watcher.update()触发视图更新,这个过程叫做派发更新

export default class Dep {   static target?: DepTarget | null   id: number   subs: Array<DepTarget>    constructor() {     this.id = uid++     this.subs = []   }    addSub(sub: DepTarget) {     this.subs.push(sub)   }    removeSub(sub: DepTarget) {     remove(this.subs, sub)   }    depend(info?: DebuggerEventExtraInfo) {     if (Dep.target) {       Dep.target.addDep(this)       if (__DEV__ && info && Dep.target.onTrack) {         Dep.target.onTrack({           effect: Dep.target,           ...info         })       }     }   }    notify(info?: DebuggerEventExtraInfo) {     // stabilize the subscriber list first     const subs = this.subs.slice()     if (__DEV__ && !config.async) {       // subs aren't sorted in scheduler if not running async       // we need to sort them now to make sure they fire in correct       // order       subs.sort((a, b) => a.id - b.id)     }     for (let i = 0, l = subs.length; i < l; i++) {       if (__DEV__ && info) {         const sub = subs[i]         sub.onTrigger &&           sub.onTrigger({             effect: subs[i],             ...info           })       }       subs[i].update()     }   } }

   然后,我们再看看Watcher类,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/watcher.ts。主要看下列代码,在#L196。

  update() {     /* istanbul ignore else */     if (this.lazy) {       this.dirty = true     } else if (this.sync) {       this.run()     } else {       queueWatcher(this)     }   }

  前面说到,派发更新会触发相关watcher实例的update(),而update()主要是执行了queueWatcher(),这个queueWatcher()定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/scheduler.ts#L166,代码如下,主要是起到了对watcher实例去重,然后会在flushSchedulerQueue队列中进行排序,并一个个调用了队列中的watcher.run(),最后用nextTick去异步执行flushSchedulerQueue使视图产生更新。

export function queueWatcher(watcher: Watcher) {   const id = watcher.id   if (has[id] != null) {     return   }    if (watcher === Dep.target && watcher.noRecurse) {     return   }    has[id] = true   if (!flushing) {     queue.push(watcher)   } else {     // if already flushing, splice the watcher based on its id     // if already past its id, it will be run next immediately.     let i = queue.length - 1     while (i > index && queue[i].id > watcher.id) {       i--     }     queue.splice(i + 1, 0, watcher)   }   // queue the flush   if (!waiting) {     waiting = true      if (__DEV__ && !config.async) {       flushSchedulerQueue()       return     }     nextTick(flushSchedulerQueue)   } }

   这里可以看下wacther.run()的代码,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/watcher.ts#L211,其重点是它调用了Watcher类自身的get(),本质是调用data中的get() ,其作用是开启新一轮的依赖收集。

    pushTarget(this)     let value     const vm = this.vm     try {       value = this.getter.call(vm, vm)     } catch (e: any) {       if (this.user) {         handleError(e, vm, `getter for watcher "${this.expression}"`)       } else {         throw e       }     } finally {       // "touch" every property so they are all tracked as       // dependencies for deep watching       if (this.deep) {         traverse(value)       }       popTarget()       this.cleanupDeps()     }     return value   }

 

3.diff算法

  vue更新节点并不是直接暴力一个个节点全部更新,而是对新旧节点进行比较,然后进行按需更新:创建新增的节点,删除废除不用的节点,然后对有差异的节点进行修改或移动。diff算法主要是靠patch()实现的,主要调用的是patchVnode()和updateChildren()这两个方法,源码分别定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/vdom/patch.ts#L584和#L413,前者的作用是先对比了新老节点,然后对一些异步占位符节点(#603的oldVnode.isAsyncPlaceholder,这个属性在vnode.ts中没有注释,vnode源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/vdom/vnode.ts#L8,应该是可以理解为异步组件的占位符)或是静态节点(#617的vnode.isStatic)且含有一样key值的节点且是【克隆节点(#620的vnode.isCloned)或v-once指令绑定的节点(#620的vnode.isOnce,只渲染一次)】不予更新,以提升性能。不是文本节点(#638的vnode.text)的话,就需要对比新旧子节点,对新旧子节点进行按需更新:新子节点有旧子节点没有则新建addVnodes(),新子节点没有旧子节点有则删除removeVnodes(),其他的更新updateChildren()。如果节点是文本节点且文本不一样的,直接将旧节点的文本设置为新节点的文本。

if (isTrue(oldVnode.isAsyncPlaceholder)) {   if (isDef(vnode.asyncFactory.resolved)) {     hydrate(oldVnode.elm, vnode, insertedVnodeQueue)   } else {     vnode.isAsyncPlaceholder = true   }   return }  // reuse element for static trees. // note we only do this if the vnode is cloned - // if the new node is not cloned it means the render functions have been // reset by the hot-reload-api and we need to do a proper re-render. if (   isTrue(vnode.isStatic) &&   isTrue(oldVnode.isStatic) &&   vnode.key === oldVnode.key &&   (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) {   vnode.componentInstance = oldVnode.componentInstance   return }

  而后者的作用是进行是定义了新旧子节点数组的头和尾,然后新旧子节点数组头尾交叉对比,它只在同层级进行比较,不会跨层级比较,这是有考量的,因为前端实际的操作中,很少会把dom元素移到其他层级去。比较完子节点之后,就开始递归调用patchVnode()更新子节点了,这里考点就来了:vue的diff算法是深度优先算法还是广度优先算法?从这个更新流程可以看出来,正常调用顺序是patch()->patchVnode()->updateChildren()->patchVnode()->updateChildren()->....这是深度优先算法,同层比较,深度优先。

 function updateChildren(     parentElm,     oldCh,     newCh,     insertedVnodeQueue,     removeOnly   ) {     let oldStartIdx = 0     let newStartIdx = 0     let oldEndIdx = oldCh.length - 1     let oldStartVnode = oldCh[0]     let oldEndVnode = oldCh[oldEndIdx]     let newEndIdx = newCh.length - 1     let newStartVnode = newCh[0]     let newEndVnode = newCh[newEndIdx]     let oldKeyToIdx, idxInOld, vnodeToMove, refElm      // removeOnly is a special flag used only by <transition-group>     // to ensure removed elements stay in correct relative positions     // during leaving transitions     const canMove = !removeOnly      if (__DEV__) {       checkDuplicateKeys(newCh)     }      while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {       if (isUndef(oldStartVnode)) {         oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left       } else if (isUndef(oldEndVnode)) {         oldEndVnode = oldCh[--oldEndIdx]       } else if (sameVnode(oldStartVnode, newStartVnode)) {         patchVnode(           oldStartVnode,           newStartVnode,           insertedVnodeQueue,           newCh,           newStartIdx         )         oldStartVnode = oldCh[++oldStartIdx]         newStartVnode = newCh[++newStartIdx]       } else if (sameVnode(oldEndVnode, newEndVnode)) {         patchVnode(           oldEndVnode,           newEndVnode,           insertedVnodeQueue,           newCh,           newEndIdx         )         oldEndVnode = oldCh[--oldEndIdx]         newEndVnode = newCh[--newEndIdx]       } else if (sameVnode(oldStartVnode, newEndVnode)) {         // Vnode moved right         patchVnode(           oldStartVnode,           newEndVnode,           insertedVnodeQueue,           newCh,           newEndIdx         )         canMove &&           nodeOps.insertBefore(             parentElm,             oldStartVnode.elm,             nodeOps.nextSibling(oldEndVnode.elm)           )         oldStartVnode = oldCh[++oldStartIdx]         newEndVnode = newCh[--newEndIdx]       } else if (sameVnode(oldEndVnode, newStartVnode)) {         // Vnode moved left         patchVnode(           oldEndVnode,           newStartVnode,           insertedVnodeQueue,           newCh,           newStartIdx         )         canMove &&           nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)         oldEndVnode = oldCh[--oldEndIdx]         newStartVnode = newCh[++newStartIdx]       } else {         if (isUndef(oldKeyToIdx))           oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)         idxInOld = isDef(newStartVnode.key)           ? oldKeyToIdx[newStartVnode.key]           : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)         if (isUndef(idxInOld)) {           // New element           createElm(             newStartVnode,             insertedVnodeQueue,             parentElm,             oldStartVnode.elm,             false,             newCh,             newStartIdx           )         } else {           vnodeToMove = oldCh[idxInOld]           if (sameVnode(vnodeToMove, newStartVnode)) {             patchVnode(               vnodeToMove,               newStartVnode,               insertedVnodeQueue,               newCh,               newStartIdx             )             oldCh[idxInOld] = undefined             canMove &&               nodeOps.insertBefore(                 parentElm,                 vnodeToMove.elm,                 oldStartVnode.elm               )           } else {             // same key but different element. treat as new element             createElm(               newStartVnode,               insertedVnodeQueue,               parentElm,               oldStartVnode.elm,               false,               newCh,               newStartIdx             )           }         }         newStartVnode = newCh[++newStartIdx]       }     }     if (oldStartIdx > oldEndIdx) {       refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm       addVnodes(         parentElm,         refElm,         newCh,         newStartIdx,         newEndIdx,         insertedVnodeQueue       )     } else if (newStartIdx > newEndIdx) {       removeVnodes(oldCh, oldStartIdx, oldEndIdx)     }   }

  上面updateChildren()源码其实还有一个考点:vue中key的作用,或是说v-for的时候为什么推荐写上key?在#495中,有一个createKeyToOldIdx(),这个方法是创建key=>index的map映射,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/vdom/patch.ts#L56,对于新节点,可以看到如果没有key值得话,它会通过findIdxInOld()遍历旧的节点,而有key值的话,它会直接从map结构中取到对应的节点数据,相对于遍历,map结构明显会更有效率。

  

  对于core文件夹下的源码就分析到这里,完结。

  最后说点题外话,这篇文章躺在我的随笔列表里好久了,其实一年半前就开始写这篇文章了,一直缝缝补补,还好现在是写完了。一是因为确实东西很多,不知道从何写起,原本我打算写的是src下所有的文件夹的主要源码分析,现在看来光是这src/core文件夹下的主要源码就花了这么长的时间,当然有一部分原因是我比较懒,至于其他文件夹下的源码,如果以后有时间可能会新开文章写;二是当时写的时候是以main分支源码为基础的写的,但是vue2.x还是有一直更新的,刚开始写的时候vue版本还在v2.6.10+,现在最新版本都到v2.7.10了,更没想到vue2.x也会投入ts的怀抱,这就导致了之前写的文章里的源码与所在链接和行数是不对应的,有种错乱的感觉,所以这次我将v2.7.10作为版本快照固定下来,在最新的tag上进行源码分析,放上对应的源码链接。三就是我前几个月不是有一段面试经历,加入了一点我面试中经常遇到和vue相关的问题,即考点,希望能帮助大家更好的理解源码在实战中的应用。

发表评论

相关文章