Vue源码分析——Vue的构造函数分解

本次可以说为自己看的第一部分吧,我们按照自顶向下的顺序来看,我是整体按照下面这位大大的分析过程来的,也是尤大自己推荐的一篇vue源码分析的博客文章,可以说是质量很不错的了。
参考文章:HcySunYang-vue源码解析

这里我们按照文章中找到Vue的构造函数这一节,本篇就是分析这一节了 => src/core/instance/index.js, 为什么说这就是vue的构造函数了,大家可以看到 src/core/index.js 里面引入的 Vue 就是这里释出的再加上大概看看代码就一清二楚了。

下面开始

// src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

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)

export default Vue

我们看到构造函数中主要进行的是整体的初始化注入,主要是几点的注入,整体围绕Vue的生命周期进行(自行对照Vue的生命周期图)

  • initMixin => created周期函数之前的操作,即各项初始化,期间调用 beforeCreate 钩子,后续详解
  • stateMixin => 利用 definedProperty 进行静态数据的订阅发布,并在其中实现几项实例 api $set、 $delete、 $watch, 后续详解
  • eventsMixin => 实例事件流的注入, 利用的是订阅发布模式的事件流构造
  • lifecycleMixin => 注入几个Vue原型函数
    • Vue.prototype._update => 调用生命周期钩子 beforeUpdate,其后实现 virtual dom 的更新;
    • Vue.prototype.$forceUpdate => 实现实例 api forceUpdate 强制重新渲染实例,包括其下的子组件(更新了 watcher 队列);
    • Vue.prototype.$destroy => 调用生命周期钩子 beforeDestroy , 其后移除各项实例子组件,拆卸实例的watcher队列及调用实例的 __patch__ 方法将 virtual dom 置空(null),最后调用钩子 destroyed 并解除(实例api:$off)实例所有事件;
  • renderMixin => 实现实例api $nextTick,后续详解,实现 _render 渲染虚拟dom

下面是上述各项详解(尽量吧,因为实在太多了,讲不清楚的,还是需要大家自己去看,可能我就讲个大概思路吧, 没写详解的就不写了,各位自己看吧,太多了哈,我也不是能一次全理解的),需要注意在源码中有 _ 和 $ 两项原型函数区分,而类型检测使用了 flow , 大型项目的多人协作中适合半路引入, 试了下,确实是简单易用,不过没实际项目用过,如果是从0-1的话还是ts吧,虽然我只看过ts的一些语法,不过确实好,自身携带了很多高级的设计模式,这都是ES需要自己写的。

  • _: Vue.prototype._xxx 这种大部分是属于 Vue 在实现过程中自己会调用的函数,不是开放出来给用户调用的,如果强行调用会报警告。例如大家在查看 data 或 props 队列时都会有一个监测者 __ob__,你不会改这个吧,改了可就会影响这个队列的观测者了~不明白的情况下不要乱动;
  • $: Vue.prototype.$xxx 这种就是开放的实例 api 了, 这里我会解析几个,未解析到的大家自己看文档就好;
  • 各位看的时候记得打开 vue 的官方文档,可以对着全局 api 来搜索看作者给的解释后再去看源码就清晰了,最好还开着vue的生命周期对着看。

下面是详解了

一、src/core/instance/init.js => initMixin

这里主要是暴露了 _init 这个自用初始化函数

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-init:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // 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)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = 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')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`${vm._name} init`, startTag, endTag)
    }

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

前面实例的组件选项合并不说了,大家自己看~从选项合并后开始讲个函数

  • initProxy(vm) => ./proxy.js 非生产环境下构建,实例初始化代理属性,只有非生产下能用,因为 Proxy 是ES6的一个概念,它包括了 defineProperty 的所有功能,并能实现如对象下各属性的获取、变更、调用、判断进行监听,比如 get set has apply 等,Proxy 更像是元编程一样的东西,对本语言进行编程,创建了一个拦截器,比起翻译作代理,它更适合叫拦截器, 所以这里就很明了了, 通过判断实例中是否有 render 函数来判断是对各属性的 get 还是 has(has 操作即 key in obj 的判断) 操作进行拦截,这里的拦截当然是为了检测每个数据在vue中的合法性(比如在渲染时,你在模板的 html 内使用了某个属性或数据或函数,但是没有在options内初始化,这里就会出现警告),主要知识点 ES6——Proxy;
  • initLifecycle(vm) => ./lifecycle.js 上面是在非生产下才能检测的,而从这开始就是通用的了,以下普遍围绕生命周期进行,这里是初始化生命周期,实例属性该赋值的赋值,子组件实例该加入 $children 队列的加入,watcher队列置空、初始化绑定状态、摧毁状态等等,不复杂,自己看;
  • initEvents(vm) => ./events.js 先将实例的事件监听全部初始化置空,并且此处还没有触发任何生命周期的钩子,因为是初始化, 后面再查看父级实例有没有事件声明,有的话本实例也添加一下,后续在介绍 eventsMixin 时会简化的写一个 订阅-发布模式的简单事件机制 Vue就是基于这种设计模式;
  • initRender(vm) => ./render.js 处理插槽属性 slot ,遍历插槽中的所有子节点,如果碰到 template 标签的直接将其节点的所有节点以数组 push 入slot队列,其他顺序压入,这里的 slot 为 slots 对象中的具名或匿名 slot 键值 const name = child.data.slot, 当然前面室友判断 child.data 的存在性的,具体要大家去看下 createElemen 的用法,当增加了 functional: true 后,组件变为函数式组件后,会增加context 参数,最后就利用 createElement 函数将一颗 抽象的dom树(一个包含children的对象) 抽象为 dom 元素了,关于模板编译我不是特别了解,不敢乱写太多,大家需要了解的可以去看看 react diff算法的实现,里面已经解释了如何将 dom 抽象为一颗 dom 树,在编译回来, 如果自己写的话,这篇都不够写,还是引用别人的吧如何实现一个 Virtual DOM 算法
  • callHook(vm, ‘beforeCreate’) 此处对照vue的生命周期图顺序,当初始化 事件 与 周期函数 完毕后,调用 beforeCreate 钩子;
  • initInjections(vm) => ./inject.js vue 2.2.x 版本后新增的 inject 属性,要与 provide 一起使用,在父组件定义 provide 后在需要的子孙组件中都可用 inject 注入,与 prop 不支持响应数据(即双向绑定的数据),但你可以 provide 传入可监控的对象,简单来说传入一个代理吧 new Proxy({}, handler),言归正传,这里就是遍历 injdect 注入的属性遍历后,每个都调用 defineReactive 函数,若在非生产环境下会传入警告函数,会在尝试改变值触发 setter 访问器时触发警告函数, 这是组件间传递信息继 props 与 $emit 之后又一单向流方法 Vue provider-inject;
  • initState(vm) => ./state.js 这里初始化的就是大家熟知的属性了

    • initProps(初始化属性),还是 defineReactive 函数去定义,唯一不同是,这里 prop 是单向流的信息传递,所以此处的 setter 访问器一旦触发会直接发出警告去提示不能修改单向流属性;
    • initMethods(初始化订阅发布模式事件函数),下面会放出一个订阅发布模式的自定义事件的最简单的小轮子,也是这种设计模式的标配;
    • initData 这个就不说了,双向绑定数据的绑定,还是 defineReactive 这个函数, 详细的基本原理已经在我很久之前的文章中写过 Vue双向绑定原理;
    • initComputed(计算属性的初始化),还是围绕 defineReactive 函数,但是这里对 getter 访问器的的get 函数是传入我们所写的对应键名的函数或对象;
    • initWatch(监控数据变化),还是根本性的 defineReactive 函数,只是在触发 set 时,发出 update 更新通知后,会将旧数据与新数据同时回调到我们需要 watch 的目标中,而目标都是在 watcher 队列中的,所以大家一定要理解双向绑定的原理是什么, watcher 队列是干嘛的,否则这一阶段的基本看不懂在干嘛,如果需要大家可以直接在vue的源码的这里 /src/core/instance/observer/index.js => defineReactive 函数找到完全体,如果不想看这么复杂的,那么可以直接看我上面提到的那篇文章,大体原理是差不多的,不过 model => view 过程的 render 函数渲染,有所不同,我那篇是以往 vue 1.x 之前的,可以说是最初版本的利用 fragment 去做渲染,而 vue 2.0 后已经是抽象 dom 树的 diff算法了,跟react一样。
  • initProvide(vm) => ./inject 跟props差不多的,非常简单,这里不说了,这里真的实现的很简单~后面就是回调生命周期钩子函数 created 了 callHook(vm, 'created'), 并在其后判断是否存在 el 所指引的 dom 目标,存在则触发 vm.$mount(vm.$options.el) 绑定,拉出单独说说$mount

  • src/platforms/web/runtime/index.js => $mount 这里 return 了 mountComponent 这个组件绑定函数,这个函数就在 lifecycle.js 里,在判断是否存在 render 选项后就调用了生命周期函数钩子 callHook(vm, 'beforeMount'), 最后当虚拟dom还未生成时,触发一次钩子 callHook(vm, 'mounted'),这就按照 Vue 生命周期图的顺序一步步下来了。

下面是 initMethods 中所说的demo,这里只是最简单的例子,为了不污染全局变量,大家可利用原型模式,将其写成构造函数,即类,或者利用闭包做一个局部作用域,甚至可以利用闭包做离线缓存和预事件触发哈哈。

    var Event = (function () {
        var clientList = {},
            listen,
            trigger,
            remove;
        listen = function (key, fn) {
          if (!clientList[key]) {
            clientList[key] = [];
          }
          clientList[key].push(fn);
        }

        trigger = function () {
          var key = Array.prototype.shift.call(arguments),
              fns = clientList[key];
          if (!fns || fns.length === 0) {
            return false;
          }
          for (var i = 0, fn;fn = clientList[key][i++];) {
            fn.apply(this, arguments);
          }
        }

        remove = function (key, fn) {
          var fns = clientList[key];
          if (!fns) {
            return false;
          }
          if (!fn) {
            fns && (fns.length = 0)
          } else {
            for (var l = fns.length; l >= 0; l--) {
              var _fn = fns[l];
              if (fn === _fn) {
                fns.splice(l, 1);
              }
            }
          }
        }

        return {
          listen: listen,
          trigger: trigger,
          remove: remove
        }
      })()

      Event.listen('squareMeter88', function (price) {
        console.log('价格 = ' + price);
      })
      Event.trigger('squareMeter88', 20000);

二、src/core/instance/state.js => stateMixin

这里就是几个实例原型的api

  • $data: 利用 defineProperty 的 getter 访问器订阅,当获取 vm.$data => return this._data, 如果触发 setter 访问器 => 警告不能改变root $data;
  • $props: 与 $data 同样;
  • $set: 在 Vue 原型中注入 set 函数, 用于在 initData 后才添加的数组或对象也能添加进订阅发布模式去监控, 两种情况
    • set(Array, index, val) => 直接在目标数组中使用 splice 去替换对应下标值, splice 会触发 vue 的数组处理的变异方法,从而实现监控
    • set(Object, key, val) => 若 key 为 target 对象中的自有属性,直接赋值替换 => target[key] = val => 若不为 target 的自有属性,先查看 target 是否已在观察者队列,若不在则直接使用 defineReactive 方法重新 defineProperty 新的 target 进入 watcher 队列并发送通知告知观测者需要更新model => view ( ob.dep.notify() );
  • $del: 与 $set 过程一致,数组时直接 splice 删除不替换,对象时若不是自有属性直接 return,若是则直接 delete 并发送通知 ob.dep.notify();
  • $watch: vm.$watch(exp | fn, callback, [options]) => 直接根据当前 vm 创建一个 watcher 丢进当前vm watch 队列中, 大家都用过 watch了也不用说太多,重点说 options 中的 2 个配置
    • deep: true => 官方文档告诉我们它会检测对象或数组内部的属性,实际上再创建 watcher 对象时,如果是对象则会使用 traverse 函数遍历对象并创建一个纯净的 Set 对象去 set 目标对象内的属性,其中是利用 Object.keys 变为数组遍历创建的,数组则直接遍历即可,每个值会在监控器 ob 下递增一个 id => dep.id => /core/instance/dep.js,这样就实现了对象或数组内的值都会对应一个 setId,每次变化都会触发,此时返回对应 setId 的 watcher.value 即可(这里没具体追的太细)
    • immediate: 这个就简单多了,创建 watcher 后,立刻将传入的回调函数 callback.call(vm, watcher.value) 调用一次;

三、src/core/instance/events.js => eventsMixin

也是几个实例的api

  1. $on: 循环组件 virtual dom 中各标签的事件属性(v-on:click、 @click、、、), 在该组件下创建对象 _events 对象用于保存所有元素的click、keyup 等事件流, 事件名为键名,键值为对应绑定的函数数组,所以这里每个事件名会push多个函数,最后会统一根据标签进行初始化vm:{ _events: { click: [fn1, fn2] || [{ fn: fn1 }, { fn: fn2 }], keyup: [fn1, fn2] || [{ fn: fn1 }, { fn: fn2 }] } };
  2. $off: 传入需要解除的事件名及函数名, 四种传参方式
    • 若只有事件名且为字符串,则解除 vm._events 中对应事件名的所有绑定函数 => vm._events[event] = Object.create(null) => ret
    • 若无事件名也无函数,及无参数, 即解除所有事件流 => vm._events = Object.create(null) => reutrn vm;
    • 若传入事件名为数组,则循环递归第 1 步 => return vm;
    • 若传入事件名为字符串且传入了需解绑函数,循环事件名所绑定的函数数组循环vm._events[event] => 匹配绑定的函数 fn => 删除函数数组中对应函数 splice(index, 1) => return vm;
  3. $once: 先 $on(event, on), on 函数内为先 $off 对应 event 然后立刻将绑定函数 fn.apply(vm, arguments), 最后 return vm, 需注意vm._events[event] 可能有两种形式 ,所以删除时源码中有 cb === fn || cb.fn === fn 这样的比较或赋值
    • [fn1, fn2, fn3]
    • [{fn: fn1}, {fn: fn2}, {fn: fn3}]
  4. $emit: 传入需要触发的函数名,在 vm._events[event] 找到函数数组并循环,这里不说了,跟上述的事件流的机制是一样的,唯一不一样的是通过 this.$emit(event, args[Array]) 触发的函数,可以传入默认的参数,因为通过 emit 触发时你可以预先传入参数, 实际上就是fn.apply(vm, args) 你们懂的吧,不多说了。

    四、src/core/instance/render.js => renderMixin

    我略过了 lifecycleMixin 函数,因为我上面写大概要说的概论时大概已经说过,不具体说了,大家自行跟着看源码就行,并不难,这里说一个实例api nextTick, 一个自用函数 _render, _render 这里就不说了,前面有给大家的参考解读文章。

    vue中 nextTick 思路:
    其实 nextTick 就两点,第一利用闭包实现单例模式的队列任务, 第二利用 Promise、MutationObserver、setTimeout 做 js 运行环境下的兼容性事件队列。如果明白这两点,后面的可以不看,都是废话!

    1. 直接返回闭包并初始化 pending(是否处于等待状态)为 false,传入的函数会 push 到一个 nextTickHandler 的 callback 队列中若 pending 为 false直接执行任务队列, 若为 true 则继续 push 事件入队列, 等待队列中上一任务队列开始执行才改变 pending 为 false,才能进行下一次任务以此实现异步队列的顺序调用,若上一次任务队列未开始,则只会将新任务push到上次任务队列中;只有上次任务开始了才会开启新队列等待下一次调用,调用后直接进行了任务队列的遍历,每个任务队列调用为一个 nextTickHandler;
    2. 实现多个任务队列的顺序遍历,优先 Promise,其次 dom 的 MutationObserver 创建文本节点并改变值来实现异步监测,最后则为一次性定时器启用 microTask 来进行顺序调用,三者实质都是微任务的调用,微任务调用顺序根据主函数中nextTick的调用顺序而定,在主函数执行完后开始;
    3. $nextTick(fn, context)每次都是同一闭包进行任务队列的重置及添加,但同一闭包中只会存在最多2个 nextTickHandler 队列,上一个队列开始调用时,会初始化下一个队列,此时 pending 改为 false,可进行下一次任务队列的调用,一个为正在调用的队列,一个为下次准备调用的队列;
    4. 实现过程中的 pending 实际为一个保险锁,保证了只会有一个队列正在调用,上个队列正在调用时,本次队列不会进行调用,所以需注意的是正在执行的任务队列是只会存在一个的,只有可能上个队列正在执行的过程中,开始再下一队列中增加任务,而不会执行。

五、放到最后

已经大概说完了 Vue 构造函数的分解过程,整个过程如果大家自顶向下看完,会发现就是围绕着 Vue 的生命周期图来写的,我们在看源码学习时,一定要自顶向下,围绕生命周期的思想去看整个大框架,我这里也是按照自己的理解解读,肯定会存在很多遗漏与错误,因为我也是在深入学习,如果有错,大家可以一起交流~最后放上生命周期图,官方文档也可以随便找到。
Vue 生命周期

你可能感兴趣的:(vue学习记录)