• 五、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、当新老节点都无子节点的时候,只是文本的替换。

     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
  • 相关阅读:
    Buffer cache spillover: only buffers
    11g中如何禁用自动统计信息收集作业
    OTN中文技术论坛清净的ORACLE讨论之地
    关闭磁盘自动运行
    #error
    在VC++中实现无标题栏对话框的拖动
    string,CString,char*之间的转化
    关于注册表
    #pragma once
    extern "C"用法详解
  • 原文地址:https://www.cnblogs.com/haishen/p/11803567.html
Copyright © 2020-2023  润新知