五、Vue源码解读 ---kkb

这里使用的是vue2.6.9版本,可以在GitHub上下载

调试vue项目的方式

  • 安装依赖:npm i
  • 安装打包工具:npm i rollup -g
  • 修改package.json里面dev脚本
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
  • 执行打包: npm run dev
  • 修改samples里面的文件,引用新生成的vue.js

整体启动顺序(主线任务):

1、根据package.json 中scripts 的dev命令可知,TARGET:web-full-dev

2、进入scripts/config.js,找到web-full-dev,可以知道它的 entry 是 web/entry-runtime-with-compiler.js

3、进入 platforms/web/entry-runtime-with-compiler.js 

  扩展$mount方法: const mount = Vue.prototype.$mount
  这里可以得知 在new Vue() 的时候,传入的参数优先级如下:render() > template > el      在template中 string > nodeType (template: '#app'   >  template: document.getElementById('app'))  
  如果是template(模板字符串),需要用编译器编译:compileToFunctions()
 
4、srcplatformsweb untimeindex.js
  • 实现$mounte(支线任务) : mountComponent
5、srccoreindex.js
  • 全局API初始化(支线任务):initGlobalAPI(Vue)     --- srccoreglobal-apiindex.js
  set 、 delete、nextTick、observable、initUse、initMixin、initExtend、initAssetRegisters
 
6、srccoreinstanceindex.js (主线流程结束)
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

initMixin(Vue)

initProxy(vm)
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

initMixin -> initLifecycle

做一些初始化操作
// 子组件创建时,父组件已经存在了
  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}
initMixin -> initEvents
添加一些监听器:$on、$off、$once、$emit
初始化事件监听器的方法:updateComponentListeners(vm, listeners)
 
initMixin -> initRender
定义插槽以及定义$createElement 函数:
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject

vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

initMixin -> initInjections

处理从祖代拿到的inject ,做响应式
 
initMixin -> initState(初始化组件各种状态)
处理用户传入的options:props,methods,computed,data,watch
export function initState (vm: Component) {
  vm._watchers = []
  const 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)
  }
}

initMixin -> initProvide

祖代给后代传值
 
 

stateMixin

 挂载$set、$delete、$watch
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)

Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function(){...}
 

eventsMixin

 实现$on、$once、$off、$emit
 
 

lifecycleMixin

 实现:_update、$forceUpdate、$destroy
_update: __patch__  算法的实现(打补丁)
 

renderMixin

 实现:$nextTick、_render
 
---------------------------------------------------------------------------------------华丽的分割线----------------------------------------------------------------------------------------
 

 

数据响应式

 Vue一大特点是数据响应式,数据的变化会作用于UI而不用进行DOM操作。原理上来讲,是利用了JS语言特性 Object.defineProperty() ,通过定义对象属性 setter方法拦截对象属性变更,从而将数值的变化转换为UI的变化。
具体实现是在Vue初始化时,会调用initState,它会初始化data,props等,这里着重关注data初始化
 
srccoreinstancestate.js   

initData() 方法

proxy(vm, `_data`, key)
observe(data, true /* asRootData */)

srccoreobserverindex.js

ob = new Observer(value)
return ob

在Observer的构造函数中:根据data中是Object还是Array,进行不同的响应式处理

constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0
  def(value, '__ob__', this)
  // 判断当前value是数组还是Object
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value, arrayMethods, arrayKeys)
    }
    this.observeArray(value)
  } else { // 对象
    this.walk(value)
  }
}
defineReactive:响应式处理函数

当数据中是对象时

walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}

当数据中是数组时:arrayMethods

srccoreobserverarray.js

覆盖会修改当前数组的7个方法:'push', 'pop',   'shift', 'unshift', 'splice', 'sort', 'reverse'

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
    ob.dep.notify()
    return result
  })
})

虚拟DOM

 概念:虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用的各种状态变化会作用于虚拟DOM,最终映射到DOM上。
 

优点 

 虚拟DOM轻量、快速,当它们发生变化时通过新旧虚拟DOM比对可以得到最小DOM操作量,从而提升性能和用户体验。本质上是使用JavaScript运算成本替换DOM操作的执行成本,前者运算速度要比后者快得多,这样做很划算,因此才会有虚拟DOM。
 

实现

render函数用来返回vnode
srcplatformsweb untimeindex.js
// 整个应用程序的挂载点,根组件会执行它,根组件中的任何子组件也会执行它(mountComponent)
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
import { mountComponent } from 'core/instance/lifecycle'
 
mountComponent中定义了更新函数
updateComponent = () => {
  // 首先执行vm._render 返回VNode
  // 然后VNode作为参数执行update,update就是执行真正的DOM更新
  vm._update(vm._render(), hydrating)
}

// 创建一个组件相关的watcher实例
// $watcher/watch选项会额外创建watcher
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

_render()   srccoreinstance ender.js  :创建出虚拟DOM

const { render, _parentVnode } = vm.$options

vnode = render.call(vm._renderProxy, vm.$createElement)

_update()     srccoreinstancelifecycle.js

if (!prevVnode) {
  // initial render
  // 如果没有老vnode,说明在初始化
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
  // updates
  // 更新周期直接diff,返回新的dom
  vm.$el = vm.__patch__(prevVnode, vnode)
}

patch()     srcplatformsweb untimepatch.js

// the directive module should be applied last, after all
// built-in modules have been applied.
// 扩展操作:把通用模块和浏览器中特有模块合并
const modules = platformModules.concat(baseModules)

// 工厂函数:创建浏览器特有的patch函数,这里主要解决跨平台问题
export const patch: Function = createPatchFunction({ nodeOps, modules })
import { createPatchFunction } from 'core/vdom/patch'     
createPatchFunction    :srccorevdompatch.js
 

patch是如何工作的?

首先说一下patch的核心diff算法:通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当高效的算法。
同层级制作三件事:增删改。具体规则是:new VNode不存在就删;old VNode不存在就增;都存在就比较类型,类型不同直径替换、类型相同执行更新。
 
/*createPatchFunction的返回值,⼀一个patch函数*/
return function patch(oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
  /*vnode不不存在则删*/
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }
  let isInitialPatch = false
  const insertedVnodeQueue = []
  if (isUndef(oldVnode)) {
    /*oldVnode不不存在则创建新节点*/
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue, parentElm, refElm)
  } else {
    /*oldVnode有nodeType,说明传递进来⼀一个DOM元素*/
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      /*是组件且是同⼀一个节点的时候打补丁*/
      patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
    } else {
      /*传递进来oldVnode是dom元素*/
      if (isRealElement) {
        // 将该dom元素清空    
        oldVnode = emptyNodeAt(oldVnode)
      }
      /*取代现有元素:*/
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)
      //创建⼀一个新的dom      
      createElm(
        vnode,
        insertedVnodeQueue,
        oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm)
      )
      if (isDef(parentElm)) {
        /*移除⽼老老节点*/
        removeVnodes(parentElm, [oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        /*调⽤用destroy钩⼦子*/
        invokeDestroyHook(oldVnode)
      }
    }
  }
  /*调⽤用insert钩⼦子*/
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

patchVnode

两个VNode类型相同,就执行更新操作,包括三种类型操作:属性更新props文本更新text子节点更新reorder

patchVnode具体规则如下:

1、如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了v-once,那么只需要替换elm以及componentInstance即可。

2、新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren,这个updateChildren也是diff的核心。

3、如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。

4、当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。

5、当新老节点都无子节点的时候,只是文本的替换。

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
原文地址:https://www.cnblogs.com/haishen/p/11803567.html