• vue 源码详解(一):原型对象和全局 `API`的设计


    vue 源码详解(一):原型对象和全局 API的设计

    1. 从 new Vue() 开始

    我们在实际的项目中使用 Vue 的时候 , 一般都是在 main.js 中通过 new Vue({el : '#app , ...options}) 生成根组件进行使用的, 相关的配置都通过 options 传入。 Vue 的原型对象会帮我们初始化好很多属性和方法, 我们可以通过 this.property 直接调用即可; 而 Vue 这个类也通过类的静态方法初始化了一些全局的 api, 我们可以通过类名直接调用, 比如 Vue.component()Vue 的原型对象和全局 API 是通过混入的方式融入 Vue 中的。

    如下面代码所示, import Vue from './instance/index' 引入 Vue 的构造函数,在用户调用之前, Vue 先做了一些初始化工作, 具体做了哪些工作看 vue/src/core/instance/index.js(点击跳转) 中的代码(下边第二段):

    1. function Vue (options) { ... } 定义了 Vue 构造函数, 我们调用 new Vue 时,只会执行一句代码, 即 this._init(options);
    2. 定义完构造函数,依次调用 initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) , 从而将 Vue 的初始化函数、状态初始化函数、事件初始化函数、生命周期初始化函数、渲染函数混入到 Vue 的原型对象。这才使得每个组件都有了便捷的功能。初始化函数具体都做了什么工作, 且看后续的分析。

    vue/src/core/index.js :

    import Vue from './instance/index' // 1. 引入 Vue 构造函数
    import { initGlobalAPI } from './global-api/index' // 2. 引入初始化全局 API 的依赖
    import { isServerRendering } from 'core/util/env'
    import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
    
    initGlobalAPI(Vue) // 3. 初始化全局 API
    
    Object.defineProperty(Vue.prototype, '$isServer', {
      get: isServerRendering
    })
    
    Object.defineProperty(Vue.prototype, '$ssrContext', {
      get () {
        /* istanbul ignore next */
        return this.$vnode && this.$vnode.ssrContext
      }
    })
    
    // expose FunctionalRenderContext for ssr runtime helper installation
    Object.defineProperty(Vue, 'FunctionalRenderContext', {
      value: FunctionalRenderContext
    })
    
    Vue.version = '__VERSION__'
    
    export default Vue
    

    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) // 1. Vue 实例初始化
    }
    
    initMixin(Vue) // 2
    stateMixin(Vue) // 3
    eventsMixin(Vue) // 4
    lifecycleMixin(Vue) // 5
    renderMixin(Vue) // 6
    
    export default Vue
    

    注释 1 处, new Vue() 时, 只执行了一个初始化工作 this._init(options) ; 值得注意的是, 在定义完成构造函数后,此时尚未有 new Vue 的调用, 即在实例创建之前, 会执行注释 2 3 4 5 6 处的初始化工作, 让后初始化全局 API ,至此准备工作已经就绪, 通过调用 new Vue 生成 Vue 实例时,会调用 this._init(options) 。接下来,探索一下 Vue 生成实例前, 依次做了哪些工作。

    1.1 initMixin (vuesrccoreinstanceinit.js)

    let uid = 0
    
    export function initMixin (Vue: Class<Component>) {
      Vue.prototype._init = function (options?: Object) {
        const vm: Component = this // 1. vm 即 this, 即 Vue 的实例对象
        // a uid
        vm._uid = uid++ // 每个 Vue 实例对象都可以看成一个组件, 每个组件有一个 _uid 属性来标记唯一性
    
        let startTag, endTag
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
          startTag = `vue-perf-start:${vm._uid}`
          endTag = `vue-perf-end:${vm._uid}`
          mark(startTag)
        }
    
        // a flag to avoid this being observed
        vm._isVue = true
        // merge options 
        // 合并参数, options 是我们调用 `new Vue({ el : 'app'chuand, ...args})` 时传入的参数
        // 合并完成后将合并结果挂载到当前 `Vue` 实例
        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( // 合并完成后将合并结果挂载到当前 `Vue` 实例
            // 这个函数会检查当前 Vue 实例的否早函数和其父类、祖先类上的 options 选项, 并能监听是否发生了变化, 将 祖先类、父类和当前 Vue 实例的 options 合并到一起
            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) // 1. 初始化声明周期
        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(`vue ${vm._name} init`, startTag, endTag)
        }
    
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    }
    

    1.1.1 initLifecycle

    上边代码给每个实例标记一个唯一的 _uid 属性, 然后标记是否为 Vue 实例, 将用户传入的参数和 Vue 自有参数合并后,挂载到 Vue$options 属性 。

    export function initLifecycle (vm: Component) {
      const options = vm.$options
    
      // locate first non-abstract parent
      // 这个注释已经很明了了, 就是查找当前 vue 实例的第一个非抽象父组件
      // 找到后会将当前的组件合并到父组件的 `$children` 数组里
      // 从而建立了组件的父子关系
      let parent = options.parent
      if (parent && !options.abstract) {
        while (parent.$options.abstract && parent.$parent) {
          parent = parent.$parent
        }
        parent.$children.push(vm)
      }
    
      vm.$parent = parent
      vm.$root = parent ? parent.$root : vm
    
      vm.$children = []
      vm.$refs = {}
    
      vm._watcher = null
      vm._inactive = null // 这俩先忽略, 俺也不知道干嘛的
      vm._directInactive = false // 这俩先忽略, 俺也不知道干嘛的
      vm._isMounted = false
      vm._isDestroyed = false
      vm._isBeingDestroyed = false
    }
    

    如上, 初始化声明周期的时候, 会简历当前组件与其他组件的父子关系, 如果找到父组件, 会将 $root 指针指向父组件,找不到的话, 指向当前 Vue 实例。接下来 vm.$children = [] 初始化子组件列表, vm.$refs = {} 初始化引用列表, vm._watcher = null 初始化观察者列表, 此时还没有观察者,无法检测数据变化, vm._isMounted = false 标记当前组件尚未挂载到 DOM, vm._isDestroyed = false 标记当前组件并不是一个被销毁的实例,这与垃圾回收有关系的, vm._isBeingDestroyed = false 标记当前组件是否正在销毁工作。

    至此, 声明周期的初始化已经完成了。

    1.1.2 initEvents

    vue/src/core/instance/events.js :

    1.2 stateMixin : 状态初始化

    vue/src/core/instance/state.js :

    export function stateMixin (Vue: Class<Component>) {
      // flow somehow has problems with directly declared definition object
      // when using Object.defineProperty, so we have to procedurally build up
      // the object here.
      const dataDef = {}
      dataDef.get = function () { return this._data }
      const propsDef = {}
      propsDef.get = function () { return this._props }
      if (process.env.NODE_ENV !== 'production') {
        dataDef.set = function () {
          warn(
            'Avoid replacing instance root $data. ' +
            'Use nested data properties instead.',
            this
          )
        }
        propsDef.set = function () {
          warn(`$props is readonly.`, this)
        }
      }
      Object.defineProperty(Vue.prototype, '$data', dataDef) // 1
      Object.defineProperty(Vue.prototype, '$props', propsDef) // 2
    
      Vue.prototype.$set = set // 3
      Vue.prototype.$delete = del // 4
    
      Vue.prototype.$watch = function (
        expOrFn: string | Function,
        cb: any,
        options?: Object
      ): Function {
        const vm: Component = this
        if (isPlainObject(cb)) {
          return createWatcher(vm, expOrFn, cb, options)
        }
        options = options || {}
        options.user = true
        const watcher = new Watcher(vm, expOrFn, cb, options)
        if (options.immediate) {
          try {
            cb.call(vm, watcher.value)
          } catch (error) {
            handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
          }
        }
        return function unwatchFn () {
          watcher.teardown()
        }
      }
    }
    

    上边代码对实例的状态做了初始化。 在注释 1 2 两个地方分别给 Vue 原型对象增加了 $data $props 两个属性, 这两个属性的值分别是当前 vm_data _props 属性, 并且设置这两个属性是不可以修改的。

    注释 3 4 处为 vm 添加了 setdelete 方法, setdelete 是干嘛的就不用介绍了吧, Vue 对象本身也有 Vue.setVue.delete 这两个方法, 都是来源于下边 set 这个函数, 他的作用体现在下边代码注释的 1 2 处:

    参数 target 为对象或者数组, target 有一个 __ob__ 属性, 这个属性的来源是在 Observer 这个类中的构造函数,其中有一句是 def(value, '__ob__', this) , value 是待观测的对象, 也就是我们写代码时传入的 data中的属性, 然后我们传入的 data 其实都被代理到 __ob__ 这个属性上了,以后我们操作 data 中的数据或者访问 data 中的数据都会被代理到 __ob__ 这个属性。

    之后又在原型对象挂载了 $watcher 方法, 该方法的返回值是一个销毁 watcher 的方法。 至于 watcher 是个啥, 以及 watcher 的作用,后边再谈。

    /**
     * Set a property on an object. Adds the new property and
     * triggers change notification if the property doesn't
     * already exist.
     */
    export function set (target: Array<any> | Object, key: any, val: any): any {
      if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
      ) {
        warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        target.splice(key, 1, val)
        return val
      }
      if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
      }
      const ob = (target: any).__ob__
      if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Avoid adding reactive properties to a Vue instance or its root $data ' +
          'at runtime - declare it upfront in the data option.'
        )
        return val
      }
      if (!ob) {
        target[key] = val
        return val
      }
      defineReactive(ob.value, key, val) // 1
      ob.dep.notify() // 2
      return val
    }
    

    vuesrccoreutillang.js :

    /**
     * Define a property.
     */
    export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
      Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
      })
    }
    

    1.3 事件初始化

    其实就是在 Vue 原型对象上挂载了一些方法 ($on $once $off $emit) , 基于发布订阅模式,实现了一个事件响应系统, 与 nodejs 中的 eventEmitter 是极其相似的。这就是我们常用的事件总线机制的来源。

    简单解析一下下面的代码 :

    $on 是事件的订阅, 通过他的参数 (event: string | Array<string>, fn: Function) 可知, 可以一次订阅多个事件,他们共享一个处理函数, 然后将所有的处理函数以键值对的形式({eventName : handler[]})存储在 vm._events 对象中,等待事件发布。一旦事件发布, 就会根据事件类型( eventName )去事件处理函数列表(handler[])中,读取处理函数并执行。

    $emit 是事件的发布, 生产环境中对事件名称(也就是类型),进行了大小写转换, 不用区分事件名称的大小写了, 当然我们编码不能这样粗狂的去写哈。 然后 cbs 是根据事件名称读取的处理函数的列表, const args = toArray(arguments, 1) 是处理事件的参数, 函数 toArray$emit 函数的参数除掉第一个以后, 最终传入了我们的订阅函数中。 即
    vm.$emit('render', 'a',124) 代码最终调用结果是 vm._events['render'] 列表中所有的函数都以 ('a', 123) 为参数运行一次。

    $off 是将事件的订阅函数从订阅列表中删除, 它提供了两个参数 (event?: string | Array<string>, fn?: Function), 两个参数都是可选的, 并且不能只穿第二参数。 如果实参列表为空, 则当前 vm 上订阅的所有事件和事件的处理函数都将被删除;如果第二参数为空, 则当前 vmvm._events[event] 中所有的处理函数将被清空; 如果第二个参数 fn 不为空, 则只将 vm._events[event] 事件处理列表中的 fn 函数删除。

    $once 表示事件处理只执行一次, 多次发布事件,也只会执行一次处理函数。这个函数有点小技巧。先建立一个 on 函数, 然后把事件处理函数 fn 挂载到这个函数对象上, 函数也是对象,可以有自己的属性,这个没有疑问吧。 on 函数中只有两句代码 vm.$off(event, on), 让 vm 解除 on 函数的订阅, 这就可以保证以后不会再执行 on 函数了; 下一句fn.apply(vm, arguments) 调用 fn , 这保证了 fn 被执行了一次。 哈哈哈, 666.

    事件的初始化就这样讲完了。

    export function eventsMixin (Vue: Class<Component>) {
      const hookRE = /^hook:/
      Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
        const vm: Component = this
        if (Array.isArray(event)) {
          for (let i = 0, l = event.length; i < l; i++) {
            vm.$on(event[i], fn)
          }
        } else {
          (vm._events[event] || (vm._events[event] = [])).push(fn)
          // optimize hook:event cost by using a boolean flag marked at registration
          // instead of a hash lookup
          if (hookRE.test(event)) {
            vm._hasHookEvent = true
          }
        }
        return vm
      }
    
      Vue.prototype.$once = function (event: string, fn: Function): Component {
        const vm: Component = this
        function on () {
          vm.$off(event, on)
          fn.apply(vm, arguments)
        }
        on.fn = fn
        vm.$on(event, on)
        return vm
      }
    
      Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
        const vm: Component = this
        // all
        if (!arguments.length) {
          vm._events = Object.create(null)
          return vm
        }
        // array of events
        if (Array.isArray(event)) {
          for (let i = 0, l = event.length; i < l; i++) {
            vm.$off(event[i], fn)
          }
          return vm
        }
        // specific event
        const cbs = vm._events[event]
        if (!cbs) {
          return vm
        }
        if (!fn) {
          vm._events[event] = null
          return vm
        }
        // specific handler
        let cb
        let i = cbs.length
        while (i--) {
          cb = cbs[i]
          if (cb === fn || cb.fn === fn) {
            cbs.splice(i, 1)
            break
          }
        }
        return vm
      }
    
      Vue.prototype.$emit = function (event: string): Component {
        const vm: Component = this
        if (process.env.NODE_ENV !== 'production') {
          const lowerCaseEvent = event.toLowerCase()
          if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
            tip(
              `Event "${lowerCaseEvent}" is emitted in component ` +
              `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
              `Note that HTML attributes are case-insensitive and you cannot use ` +
              `v-on to listen to camelCase events when using in-DOM templates. ` +
              `You should probably use "${hyphenate(event)}" instead of "${event}".`
            )
          }
        }
        let cbs = vm._events[event]
        if (cbs) {
          cbs = cbs.length > 1 ? toArray(cbs) : cbs
          const args = toArray(arguments, 1)
          const info = `event handler for "${event}"`
          for (let i = 0, l = cbs.length; i < l; i++) {
            invokeWithErrorHandling(cbs[i], vm, args, vm, info)
          }
        }
        return vm
      }
    }
    

    1.4 lifecycleMixin 生命周期初始化

    代码如下, 在 Vue 的原型对象上增加了三个方法 _update $forceUpdate $destroy, 依次来看下都做了什么事吧。

    vm._update 通过 __patch__ 函数把虚拟节点 vnode 编译成真实 DOM. 并且, 组件的更新也是在这里完成虚拟节点到真实 DOM 的转换。并且父组件更新后, 子组件也会更新。

    vm.$forceUpdate 若果当前组件上有观察者, 则直接更细组件。

    vm.$destroy 销毁组件, 如果当前组件正在走销毁的流程,则直接返回, 等待继续销毁。 否则, 会触发 beforeDestroy 这个声明周期, 并将当前组件标记为正在销毁的状态。 然后将当前组件从父组件中删除, 然后销毁所有的 watcher, 销毁 vm._data__ob__ , 标记组件状态为 已销毁,重新生成真实 DOM , 触发 destroyed 生命周期方法, 移除当前组件订阅的事件和事件的处理函数, 将当前组件对父组件的引用清空。

    vue/src/core/instance/lifecycle.js

    export function lifecycleMixin (Vue: Class<Component>) {
      Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
        const vm: Component = this
        const prevEl = vm.$el
        const prevVnode = vm._vnode
        const restoreActiveInstance = setActiveInstance(vm)
        vm._vnode = vnode
        // Vue.prototype.__patch__ is injected in entry points
        // based on the rendering backend used.
        if (!prevVnode) {
          // initial render
          vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
        } else {
          // updates
          vm.$el = vm.__patch__(prevVnode, vnode)
        }
        restoreActiveInstance()
        // update __vue__ reference
        if (prevEl) {
          prevEl.__vue__ = null
        }
        if (vm.$el) {
          vm.$el.__vue__ = vm
        }
        // if parent is an HOC, update its $el as well
        if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
          vm.$parent.$el = vm.$el
        }
        // updated hook is called by the scheduler to ensure that children are
        // updated in a parent's updated hook.
      }
    
      Vue.prototype.$forceUpdate = function () {
        const vm: Component = this
        if (vm._watcher) {
          vm._watcher.update()
        }
      }
    
      Vue.prototype.$destroy = function () {
        const vm: Component = this
        if (vm._isBeingDestroyed) {
          return
        }
        callHook(vm, 'beforeDestroy')
        vm._isBeingDestroyed = true
        // remove self from parent
        const parent = vm.$parent
        if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
          remove(parent.$children, vm)
        }
        // teardown watchers
        if (vm._watcher) {
          vm._watcher.teardown()
        }
        let i = vm._watchers.length
        while (i--) {
          vm._watchers[i].teardown()
        }
        // remove reference from data ob
        // frozen object may not have observer.
        if (vm._data.__ob__) {
          vm._data.__ob__.vmCount--
        }
        // call the last hook...
        vm._isDestroyed = true
        // invoke destroy hooks on current rendered tree
        vm.__patch__(vm._vnode, null)
        // fire destroyed hook
        callHook(vm, 'destroyed')
        // turn off all instance listeners.
        vm.$off()
        // remove __vue__ reference
        if (vm.$el) {
          vm.$el.__vue__ = null
        }
        // release circular reference (#6759)
        if (vm.$vnode) {
          vm.$vnode.parent = null
        }
      }
    }
    
    

    1.5 renderMixin 渲染函数初始化

    也是向 Vue 的原型对象挂载一些方法。

    installRenderHelpers(Vue.prototype) 向 vm 增加了模板的解析编译所需要的一些方法;

    $nextTick 即我们在写代码时常用的 this.$nextTick() , 它返回一个 Promise 实例 p, 我们可以在 pthen 函数中访问到更新到 DOM 元素的数据, 也可以向 this.nextTick 传递一个回调函数 ff 也可以访问更新到 DOM 元素的数据。

    _render 方法生成虚拟节点。详见后边的代码。

    vue/src/core/instance/render.js

    export function renderMixin (Vue: Class<Component>) {
      // install runtime convenience helpers
      installRenderHelpers(Vue.prototype)
    
      Vue.prototype.$nextTick = function (fn: Function) {
        return nextTick(fn, this)
      }
    
      Vue.prototype._render = function (): VNode {
        const vm: Component = this
        const { render, _parentVnode } = vm.$options
    
        if (_parentVnode) {
          vm.$scopedSlots = normalizeScopedSlots(
            _parentVnode.data.scopedSlots,
            vm.$slots,
            vm.$scopedSlots
          )
        }
    
        // set parent vnode. this allows render functions to have access
        // to the data on the placeholder node.
        vm.$vnode = _parentVnode
        // render self
        let vnode
        try {
          // There's no need to maintain a stack because all render fns are called
          // separately from one another. Nested component's render fns are called
          // when parent component is patched.
          currentRenderingInstance = vm
          vnode = render.call(vm._renderProxy, vm.$createElement)
        } catch (e) {
          handleError(e, vm, `render`)
          // return error render result,
          // or previous vnode to prevent render error causing blank component
          /* istanbul ignore else */
          if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
            try {
              vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
            } catch (e) {
              handleError(e, vm, `renderError`)
              vnode = vm._vnode
            }
          } else {
            vnode = vm._vnode
          }
        } finally {
          currentRenderingInstance = null
        }
        // if the returned array contains only a single node, allow it
        if (Array.isArray(vnode) && vnode.length === 1) {
          vnode = vnode[0]
        }
        // return empty vnode in case the render function errored out
        if (!(vnode instanceof VNode)) {
          if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
            warn(
              'Multiple root nodes returned from render function. Render function ' +
              'should return a single root node.',
              vm
            )
          }
          vnode = createEmptyVNode()
        }
        // set parent
        vnode.parent = _parentVnode
        return vnode
      }
    }
    

    2. Vue 全局 API

    Vue 中全局 API 一共有一下 12 个。全局 API 是通过构造函数 Vue 直接调用的, 有一些方法在实例上也做了同步, 可以通过实例对象去调用。 比如常用的 Vue.nextTick , 可以通过 this.$nextTick 进行调用。下面就依次分析一下每个全局 API 的使用和实现思路吧。

    • Vue.extend
    • Vue.nextTick
    • Vue.set
    • Vue.delete
    • Vue.directive
    • Vue.filter
    • Vue.component
    • Vue.use
    • Vue.mixin
    • Vue.compile
    • Vue.observable
    • Vue.version

    src/core/global-api/index.js

    2.1 Vue.extend

  • 相关阅读:
    KMP算法
    模板特化
    css 绘制三角形和斜边
    709. 转换成小写字母
    numpy.ceil(), numpy.floor()
    warm_up
    Tensor基础概念
    模型微调
    @函数装饰器
    optimizer.step(), scheduler.step()
  • 原文地址:https://www.cnblogs.com/darkterror/p/15127446.html
Copyright © 2020-2023  润新知