Vue vue2源码解析

2021-01-09  本文已影响0人  爱吃馒头不吃辣

入口文件

vue-2.6.11\src\platforms\web\entry-runtime-with-compiler.js

  1. 从vue原型中拿出$mount进行覆盖,

    // 17行
    const mount = Vue.prototype.$mount
    
  2. 解析模板相关选项

    // 18行
    Vue.prototype.$mount = function (
      el?: string | Element,// 宿主元素
      hydrating?: boolean
    ): Component {
      //获取真实DOM
      el = el && query(el)
    // 34行
    // 先判断有没有render,render函数优先级最高
    // 优先级:render > template > el
    if (!options.render) {
        let template = options.template
        // 再判断有没有template并解析
        if (template) {
          if (typeof template === 'string') {
            if (template.charAt(0) === '#') {
              template = idToTemplate(template)
              /* istanbul ignore if */
              if (process.env.NODE_ENV !== 'production' && !template) {
              }
            }
          } else if (template.nodeType) {
            template = template.innerHTML
          } else {
            return this
          }
        // 最后查看el选项
        } else if (el) {
          template = getOuterHTML(el)
        }
        // 处理模板的方式,编译它,最终目标是render
        if (template) {
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            mark('compile')
          }
    
          const { render, staticRenderFns } = compileToFunctions(template, {
            outputSourceRange: process.env.NODE_ENV !== 'production',
            shouldDecodeNewlines,
            shouldDecodeNewlinesForHref,
            delimiters: options.delimiters,
            comments: options.comments
          }, this)
          // 重新复制给选项
          options.render = render
          options.staticRenderFns = staticRenderFns
    
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            mark('compile end')
            measure(`vue ${this._name} compile`, 'compile', 'compile end')
          }
        }
      }
    

初始化挂载

vue-2.6.11\src\platforms\web\runtime\index.js

  1. 安装平台特有的补丁(patch)函数:做初始化和更新,为了实现跨平台,平台特有的操作。

    // 33行
    Vue.prototype.__patch__ = inBrowser ? patch : noop
    
  2. 实现$mount:初始化挂载。

    1. $mount('#app')=>mountComponent:render()=>vdom=>patch()=>dom=>appendChile()
    2. 初始化时执行$mount('#app') ,在内部调用mountComponent,调用内部的render函数,核心目标把当前虚拟DOM树传给patch()函数变成真实DOM,真实DOM挂载到#app上。
    // 36行
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && inBrowser ? query(el) : undefined
      return mountComponent(this, el, hydrating)
    }
    

初始化全局API

vue-2.6.11\src\core\index.js

  1. 初始化全局API:Vue.use,component,directive,filter,mixin,set,extend,delete

    // 6行
    initGlobalAPI(Vue)
    

Vue的构造函数

vue-2.6.11\src\core\instance\index.js

  1. 构造函数:new Vue(options)

    // 8行
    function Vue (options) {
      // 初始化
      this._init(options)
    }
    
  2. 声明实例属性和方法,把构造函数传递进去,通过混入模式把原型加上_init方法。

    initMixin(Vue)
    // 熟悉的其他梳理属性和方法,有下面这些混入
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)
    

初始化

vue-2.6.11\src\core\instance\init.js

  1. 原型挂载_init实现原型方法,在任何实例都可使用vm._init

    Vue.prototype._init = function (options?: Object)
    
  2. 合并选项

    // 30行
    // 合并选项:new Vue传入的时用户配置,需要和系统配置合并
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
       )
    }
    
  3. 初始化操作

    // 52行
    // 生命周期初始化 组件实例相关的属性初始化,$parent,$root,$children,$refs
    initLifecycle(vm)
    // 事件初始化 监听自定义事件
    initEvents(vm)
    // 解析插槽,$slots,$scopeSlots,$createElement()
    initRender(vm)
    callHook(vm, 'beforeCreate')// beforeCreate生命周期
    
    // 组件状态相关的数据操作
    // inject/provide 注入祖辈传递下来的数据。
    initInjections(vm) // 
    // 把自己内部的状态进行响应式的处理,`props`,`methods`,`data`,`computed`,`watch`
    initState(vm)
    // 提供给后代,用来隔代传递参数
    initProvide(vm) 
    callHook(vm, 'created')// created生命周期
    
  4. initEvents(vm)

    1. 为什么要从父组件找Listeners?
      1. 子组件上有事件选项,但是在父组件内声明的。
      2. 派发和监听者都是子组件本身(this.$on(),this.$emit());
      3. 事件的真正监听者时子组件自己。
      4. updateComponentListeners从父组件身上拿出事件给子组件。
    export function initEvents (vm: Component) {
      vm._events = Object.create(null)
      vm._hasHookEvent = false
      // 找到父组件的选项
      const listeners = vm.$options._parentListeners
      if (listeners) {
        updateComponentListeners(vm, listeners)
      }
    }
    
  5. initRender(vm)

    // 34行
    // 这里的$createElement就是render(h)
    vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
    
  6. 如果设置了el,自动执行$mount()

    if (vm.$options.el) {
       vm.$mount(vm.$options.el)
    }
    

Vue执行流程

  1. 进入src\core\instance\index.js文件内的Vue函数,执行this._init(options)

  2. 再进入src\core\instance\init.js文件内的initMixin函数。

    1. 32行此时的options还是用户设置的初始化选项,datael等。

    2. 先判断是否为isComponent,若不是执行mergeOptions将用户设置和系统设置进行合并。

      1. 合并完成后vm.$options是合并后的结果,可使用全局组件等{components{KeepAlive,Transition,TransitionGroup},data,el,filters}
    3. 执行initProxy进行代理设置。

    4. 依次执行初始化操作,

      1. initLifecycle(vm),vm新增属性$children,$parent,$refs但是值为空,$rootVue根实例。

      2. initEvents(vm),vm新增属性_events事件监听

      3. callHook(vm, 'beforeCreate')派发生命周期,所以此时不可以操作数据。

      4. initState(vm),数据的代理和响应式发生在这里,vm新增数据相应式,新增_data(响应式有_ob_),$data(非响应式)

        1. 进入文件src\core\instance\state.js,执行initState函数,如果有重名按处理顺序,谁先占用谁使用。

          1. if (opts.props) initProps(vm, opts.props)处理props

          2. if (opts.methods) initMethods(vm, opts.methods)处理methods

          3. if (opts.data) { initData(vm) } else {observe(vm._data = {}, true) }处理data

            1. 如果有data执行initData(vm)
              1. 执行initData函数,判断是函数还是对象再做相应处理。
              2. 校验命名冲突,根据propsmethods
              3. 最后执行observe(data, true)递归响应式处理。
            2. 如果没有data执行observe(vm._data = {}, true)
              1. 进入文件core\observer\index.js
              2. 获取Ob实例,是否作为根数据。
                1. 如果有_ob_直接返回,ob = value.__ob__
                2. 如果没有则创建,ob = new Observer(value)
                  1. 初始化时先给data的值创建一个ob,结果对象就有几个ob
                  2. 创建dep实例this.dep = new Dep():对象也需要dep,对象如果动态增减属性。
                  3. Observer区分对象还是数组。
                  4. 判断类型,指定ob实例。
                    1. 如果时数组执行数组响应式this.observeArray(value)
                      1. 进入文件src\core\observer\array.js
                      2. 默认的7个方法不会通知更新,将原有7个方法拦截修改。
                      3. 获取数组原型,const arrayProto = Array.prototype
                      4. 克隆一份新原型,export const arrayMethods = Object.create(arrayProto)
                      5. 7个变更方法需要覆盖,const methodsToPatch = [ 'push', 'pop', 'shift','unshift', 'splice', 'sort', 'reverse']。因为这7个会改变数组,其他的返回新数组。
                      6. 遍历7个方法,每次拿出一个方法保存原始方法const original = arrayProto[method]开始覆盖。
                      7. 执行默认方法const result = original.apply(this, args)
                      8. 对新加入的元素进行响应式if (inserted) ob.observeArray(inserted)
                      9. 变更通知const ob = this.__ob__
                      10. ob内部有dep,让dep通知更新ob.dep.notify()
                      2. 如果是对象执行对象响应式this.walk(value),执行defineReactive(obj, keys[i])
                      1. 每个key对应一个depconst dep = new Dep()
                      2. 依赖收集dep.depend(),vue2中一个组件是一个Watcher
                      1. dep:n=>wtacher:1,多对一。
                      2. 通过diff算法比对变化。
                      3. 进入文件src\core\observer\dep.js
                      1. depend函数内执行的Dep.target.addDep(this)watcher.addDep()
                      2. 进入文件src\core\observer\watcher.jsaddDep函数。
                      1. 相互添加引用的过程。
                      2. watcher添加depthis.newDepIds.add(id)this.newDeps.push(dep)
                      3. dep添加watcherdep.addSub(this)
                      3. 用户手动创建Watcherthis.$watch(key,cb)
                      1. dep:n=>wtacher:n,多对多。
                      4. 子ob也要做依赖收集工作childOb.dep.depend()
          4. if (opts.computed) initComputed(vm, opts.computed)处理computed

          5. if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) }处理watch

    5. 执行挂载过程vm.$mount(vm.$options.el)

  3. 进入文件src\platforms\web\runtime\index.js执行Vue.prototype.$mount挂载函数。

    1. 执行mountComponent(this, el, hydrating)将虚拟DOM转换成真实DOM。
  4. 进入文件src\core\instance\lifecycle.js

    1. 执行mountComponent函数内部,挂载callHook(vm, 'beforeMount')生命周期钩子。
    2. 声明updateComponent组件更新函数函数未执行。
      1. vm._render()渲染函数。
      2. vm._update()更新函数。
    3. new Watcher内部会执行updateComponent,这次才真正执行updateComponent,执行函数内部的vm._render()渲染函数。
      1. 进入文件src\core\instance\render.js,执行Vue.prototype._render,渲染函数的目的是得到虚拟DOM。
        1. vnode = render.call(vm._renderProxy, vm.$createElement)获得虚拟DOM。
    4. 再执行lifecycle.js文件内执行vm._update()更新函数。
      1. 判断是否有虚拟DOM。
        1. 若没有则通过vm._patch_创建,虚拟DOM将变成真实DOM。
        2. 若有则通过vm._patch_更新。
vue执行流程

new Vue({})发生了什么?

  1. 选项的合并,用户选项和系统选项的合并。
  2. 组件实例相关的属性初始化,$parent,$root,$children,$refs
  3. 监听自定义事件。
  4. 解析插槽,$slots,$scopeSlots,$createElement()
  5. 注入祖辈传递下来的数据。
  6. 把自己内部的状态进行响应式的处理,props,methods,data,computed,watch
  7. 提供给后代,用来隔代传递参数

Vue数据响应式

  1. Vue中利用了JS语言特性
    Object.defineProperty(),通过定义对象属性getter/setter拦截对属性的访问。
    具体实现是在Vue初始化时,会调用initState,它会初始化data,props等,这里着重关注data初始
    化。
  2. Vue1最大问题watcher过多,项目大到一定程度容易崩溃,所以导致Vue1性能很差。
  3. Vue2的解决方法,把watcher粒度降低,一个组件一个watcher。
  4. 如果有数据发生变化,通过Diff算法把两次计算的结果比较,才能知道那里变化并进行更新。
  5. Watcher和属性key之间的关系是1对N,正好跟Vue1的一对N是相反。
  6. 当用户自己编写表达式this.$watch('foo',function(){})时,会产生新的watcher实例,此时'foo'又跟多个watcer产生关系,N对N。
  7. 在依赖收集过程中相互添加关系,watcher.addDep()
  8. 一个对象一个Observer,内部有个大管家dep。
    1. 大管家负责对象新增或删除属性的更新通知。
  9. 一个key一个小管家dep。
    1. 小关节负责对应key的值变化的更新通知。


上一篇下一篇

猜你喜欢

热点阅读