• 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项目代码结构的插件(如图左侧),效果真的很棒。

    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
    
      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 
        // 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相关的问题,即考点,希望能帮助大家更好的理解源码在实战中的应用。

    __EOF__

  • 相关阅读:
    [iOS开发]离屏渲染优化方案
    Web安全—Web漏扫工具NetSparker安装与使用
    推荐系统笔记(六):LightGCN代码实现
    statistic learning outlook
    (零代) MDD 开创低代码领行设计模式
    01 【Vue简介 初识Vue 模板语法和数据绑定】
    【牛客网】斐波那契数列
    一个斜杠引发的CDN资源回源请求量飙升
    unity urp 棉麻织物渲染
    Day9 ---- 用户注册与登录
  • 原文地址:https://www.cnblogs.com/jdWu-d/p/14558813.html