vue2-实例方法与全局API的实现(二)

vm.$mount

使用: vm.$mount([elementOrSelector])

参数: {Element | String} [elementOrSelector]

返回值: vm,实例本身

用法: 如果Vue.js实例在实例化时没有接受el选项, 则处于“挂载”状态,没有关联DOM元素。我们可以是用vm.$mount手动挂载一个未挂载的实例。 如果没有提供elementOrSelector参数,模板会被 渲染为文档之外的元素, 并且必须使用原生DOM的API把它插入文档中。 这个方法会返回实例自身,因而可以链式调用其他实例方法。

栗子:

var myComponent = Vue.extend({
  template: '
jjjjjjjsdf
' }) // 有el 创建并挂载到#app (会替换#app) new myComponent({el: '#app'}) // $mount有参数 // 创建并挂载到#app (会替换#app) new myComponent().$mount('#app') // 文档之外渲染并且然后挂载 var comp = new myComponent().$mount() document.getElementById('app').appendChild(comp.$el)

事实上,在不同的构建版本中,vm.$mount的表现是不一样的。 差异主要体现在完整版 和 只包换运行时 版本

完整版会包含编译器。 vm.$mount会先检查template和el 选项提供的模板是否已经转换为渲染函数(render函数),如果没有 进入编译过程,把模板编译成渲染函数,之后再进入挂载和渲染流程。

而只包含运行版本的vm.$mount没有编译步骤,默认实例上已经存在渲染函数,如果不存在,则会设置一个渲染函数(返回一个空节点Vnode),已包装执行时不会因为函数不存在而报错。在开发环境下,vue会警告提示让我们提供渲染函数 或者使用完整版。

  • 关系
    完整版 = 编译器 + 只包含运行时版本

完整版vm.$mount的实现原理

首先 我们会使用函数劫持, 把Vue原型上的mount方法被一个新方法覆盖

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {

....

return mount.call(this, el, hydrating)
}

通过劫持,我们可以在原始功能上新增一些其他功能, vm.$mount的原始方法就是mount的核心功能,而再完整版中需要将编译功能 新增到核心功能了上。

之后我们获取 el参数对应的选择器。
如果el是字符串,尝试获取DOM元素,如果获取不到,创建一个空div元素。如果el不是字符串,那么认为它是元素类型,直接返回el(如果执行vm.$mount方法时没有传递el参数,则返回undefined)

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

return mount.call(this, el, hydrating)
function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}

接下来实现 完整版中的主要功能 : 编译器

  • 首先判断是否有渲染函数(render),只有不存在时,才会将模板编译成渲染函数
  • 然后判断 是否有template参数,有的话 获取模板并编译成渲染函数赋值给render选项
  • 如果没有template则 从 el选项中获取模板,然后再编译成渲染函数。

所有在new Vue() 中优先级是 render > template > el > vm.$mount(el) > document.getElementById('app').appendChild(comp.$el)

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

const options = this.$options

  if (!options.render) {
    let template = options.template
    if (template) {
      // 处理 template
    } else if(el){
      // template = getouterHTML(el)
    }
return mount.call(this, el, hydrating)

先看一个 把el转换为template 的实现(会返回参数中提供的DOM元素的HTML字符串)

function getOuterHTML (el: Element): string {
  // 如果有outerHTML配置直接返回
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    // 否则 创建一个div, 克隆 el 添加到div中,并返回 div的内容
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

由于template可以有不同的格式,我们也要处理下

  1. 如果template是#开头的字符串,则它将作为选择符,通过选择符获取DOM元素后,使用innerHTML作为模板
  2. 如果tempalte选项不是字符串,则判定它是否是一个DOM元素,如果是,使用DOM元素的innerHTML作为模板。
  3. 如果tempalte既不是字符串也不DOM元素,vue会警告用户 template无效
 if (!options.render) {
    let template = options.template
    if (template) {
        // 如果template 是 #开头的字符串
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          // 获取 #id 对应的模板
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
        // 如果是元素节点
      } else if (template.nodeType) {
        // template 直接 得到 innerHTML
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        // 直接返回(用户自己设置的模板)
        return this
      }
    } else if (el) {
      // template 不存在  template 等于 获取 el对应的DOM 元素 
      template = getOuterHTML(el)
    }
// 根据 id 获取页面DOM 元素的内容
const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

ok 获取模板之后,我们需要把模板编译成渲染函数。

...
} else if (el) {
      // template 不存在  template 等于 获取 el对应的DOM 元素 
      template = getOuterHTML(el)
}
if (template) {
   const { render } = compileToFunctions(template, {
        ...
      }, this)
      options.render = render
    }
 }
 return mount.call(this, el, hydrating)
}

compileToFunctions 函数可以把模板 编译成渲染函数, 并设置到this.$options上。

compileToFUnctions 其实最终是 在complier/to-function.js中 的createCompileToFunctionFn 返回的

export function createCompileToFunctionFn (compile: Function): Function {
  // 创建缓存对象
  const cache = Object.create(null)

  // 返回编译 template为 render 函数
  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 将options 属性混入到空对象中,目的是让options成为 可选参数
    options = extend({}, options)

    // check cache
    // 检查缓存 是否存在编译后的模板,存在直接返回
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // 编译 后类似于 `width(this){return _c('div', {attrs: {"id": "el"}}, [_v("Hello" + _s(name))])}`
    const compiled = compile(template, options)


    // turn code into functions
    // 把代码字符串 转换为 函数 
    const res = {}
    res.render = createFunction(compiled.render)

    // 缓存结果 并返回
    return (cache[key] = res)
  }
}
// 字符串转换为函数 ,当被调用时,代码字符串会执行
function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
  }
}

实现原理是 先从缓存中获取,如果不存在 ,把template转换为 代码字符串,然后通过createFunction 把代码字符串转换为 render函数,调用的时候就会执行,最后缓存起来并返回。

只包含运行时版本vm.$mount的实现原理

源码位置:platforms/web/runtime/index.js

// 只运行时版本 vm.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

$mount 使用 mountComponent 把vue实例挂载到 DOM元素上。 事实上,将实例挂载DOM元素上指的是 将模板 渲染到指定 DOM元素中,并且是持续化的,当数据(状态)发生变化时, 依然可以渲染到指定的DOM元素中。

实现这个功能需要开启一个watcher。 watcher 将持续观察模板中用到的所有数据(状态),当数据(状态)修改时,进行渲染操作。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 不存在 render 
  if (!vm.$options.render) {
    // render 被赋值 空的 虚拟节点
    vm.$options.render = createEmptyVNode
    // 如果是 非 生成环境 会警告用户 

首先 会判断 实例上是否存在渲染函数,如果不存在设置一个默认的渲染函数 createEmptyVNode(会返回一个 注释类型 的Vnode节点)。
事实上,mountComponent 方法中发现 实例上没有渲染函数, 会将el参数指定页面中的 元素节点 替换成 一个注释节点, 并且在开发环境下会给出警告。

之后会在实例挂载 之前 触发 beforeMount钩子函数
钩子函数出发后,会执行真正的挂载操作。 挂载操作与渲染类似,不同是 挂载是 持续性渲染。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 不存在 render 
  if (!vm.$options.render) {
    // render 被赋值 空的 虚拟节点
    vm.$options.render = createEmptyVNode
    // 如果是 非 生成环境 会警告用户 
    if (process.env.NODE_ENV !== 'production') {
      //  警告用户
    }
  }
  // 执行 beforeMount 生命周期
  callHook(vm, 'beforeMount')

  // 挂载
  new Watcher(vm, ()=>{
    vm._update(vm._render())
  }, noop)

  // 执行 mounted 生命周期
  callHook(vm, 'mounted') 

  return vm
}

其中
_update作用是: 调用虚拟DOM中的patch方法 来执行节点的对比与渲染操作
_render作用是:执行渲染函数,得到一份最新的Vnode节点树

所以 vm._update(vm._render())的作用 是 先调用渲染函数 获取一份最新的Vnode节点树, 然后通过 _update方法 对最新的 Vnode和 旧Vnode进行对比,更新DOM节点。

由于 Watcher 的第二个参数支持 函数, 如果是函数,那么就会观察函数中所有 读取vue实例 上的 响应式数据。

所有原理就是 函数中所有读取的数据都 将被watcher 观察, 这些数据中间任何一个发生变化,watcher都将得到 通知。 触发更新。

全局API的实现原理

Vue.extend(options)
参数: {Object} options
用法: 使用Vue构造器 创建一个子类,其参数是一个包含“组件选项”的对象

全局API和 实例方法不同, 前者是 直接在Vue上挂载翻啊翻, 后者是在Vue的原型上挂载方法(Vue.prototype)

原理是:

  • 先从缓存中获取,如果有,直接返回。
  • 然后判断 name 是否符合 命名规则
  • 创建子类
  • 把父类的原型继承给子类
  • 合并 options 并把 父类保存到子类中
  • 初始化props 和computed
  • 复制父类的 extend minxin use component directive filter方法
  • 新增 superOptions extendOptions sealedOptions 属性
  • 最后缓存自身 并返回
export function initExtend (Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {
    // 获取参数,默认 空对象
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    // 尝试 获取 缓存 ,如果有直接返回
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    // 获取name 
    const name = extendOptions.name || Super.options.name
    // 非生产环境 校验 name命名是否规范
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }

    // 创建 子类
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    // 子类继承 原型
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    // cid ++ 每个类的唯一标识
    Sub.cid = cid++
    // 合并父类 的options 到子类中
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    //  把父类 保存到子类的 super 属性中
    Sub['super'] = Super

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    // 如果有 props,初始化props
    if (Sub.options.props) {
      initProps(Sub)
    }
    // 如果有 computed, 初始化
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    // 复制 父类的  extend  minxin use 方法
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    //  ASSET_TYPES : [  'component', 'directive','filter']
    // 复制 父类 的 component  directive filter
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    // 启用递归自查找
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    // 子类上 新增 superOptions  extendOptions sealedOptions
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    // 缓存自己
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

// 将 key 代理到 _props 中
function initProps (Comp) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

// computed对象中每一项进行定义
function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

Vue.nextTick([callback, context])

参数:

  • {Function} [callback]
  • {Object} [context]

用法: 在下次DOM更新循环结束只有执行 延迟回调,修改数据之后立即使用这个方法获取更新后的DOM.

Vue.nextTick 实现原理和上一篇 的vm.$nextTick https://www.jianshu.com/p/b4737801a416一样

import {
  nextTick,
} from '../util/index'
Vue.nextTick = nextTick

Vue.set

参数:

  • {Object | Array} target
  • {String | Number} key
  • {any} value

用法: 在object上设置一个属性,如果object 是响应式的, 那么添加的属性也会变为响应式。 这个方法可以用来避开 Vue.js不能侦测属性被添加的限制;

返回值:{Function} unwatch
前面文章实现了vm.$set https://www.jianshu.com/p/c68d3c3ab54a.

import { set, del } from '../observer/index'
Vue.set = set

Vue.delete

使用: Vue.delete(target, key)
参数:

  • {Object | Array} target
  • {String | Number} key|index

用法: 删除对象的属性。如果对象是响应式的,需要确保删除能触发更新视图(通知依赖更新)。避开vue.js不能检测属性被删除的限制;

vm.$delete https://www.jianshu.com/p/c68d3c3ab54a.

import { set, del } from '../observer/index'
Vue.delete = del

Vue.directive,Vue.filter, Vue.component

使用: Vue.directive(id, [definition])

参数:

  • {String} id
  • {Function | Object} [definition]

用法: 注册或获取全局指令

// 注册 指令 1
Vue.directive('my-directive', {
  bind: function(){},
  inserted: function(){},
  update: function(){},
  componentUpdated: function(){},
  unbind: function(){},
})

// 注册 指令 2
Vue.directive('my-directive', function(){
  //这里 bind 和 update 调用
})

// 获取已注册的指令
var myDirective = Vue.directive('my-directive')

Vue.filter

使用: Vue.filter('id', [definition])

参数:

  • {String} id
  • {Function | Object} [definition]

用法: 注册或获取全局过滤器

// 注册过滤器
Vue.filter('my-filter', function(v){
  // 返回处理后 的值
})

// 获取过滤器
var myFilter = Vue.filter('my-filter')

Vue.component

用法:Vue.component(id, [definition])

参数:

  • {String} id
  • {Function | Object} [definition]
    用法: 注册或获取全局组件。 注册组件时, 会自动使用的第一个参数id设置组件名称。
    三个方法都在同一个文件中实现的 /core/global-api/index.js
// component filter directive
ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

Vue.options._base = Vue

initAssetRegisters(Vue)

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  // component filter directive
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      // definition 不存在 那么就是读取 直接找到返回
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        // 注册操作
        
        // 开发环境 要 校验 component 的第一个 参数 id 是否 命名规范
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        // 如果是 注册 组件   且  definition 是对象 _toString.call(obj) === '[object Object]'
        if (type === 'component' && isPlainObject(definition)) {
          // 没有设置 组件名 或自动 使用给定 id(第一个参数) 命名
          definition.name = definition.name || id
          // Vue.options._base = Vue
          // Vue.extend(definition) 把definition变成Vue的子类
          definition = this.options._base.extend(definition)
        }
        // 注册指令 如果是函数  默认监听 bind 和 update 两个事件
        // 不是函数 的话,下面直接赋值 给 this.options.directives[id]即可
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        // 把用户 指令 或组件 参数 保存 到 对应的 options上 
        this.options[type + 's'][id] = definition

        // 方法 处理 过的 definition
        return definition
      }
    }
  })
}

Vue.use
用法 Vue.use(plugin)
参数:

  • {Object| Function} plugin

用法: 安装Vue.js插件。 插件如果是对象,必须有install方法, 如果是函数,则它会被作为 install方法。 install 方法只会执行一次。执行时,会把Vue作为 install 方法的第一个参数 执行。(插件中就可以使用Vue了)

/* @flow */

import { toArray } from '../util/index'

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 获取 Vue的 已注册插件列表
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // 如果 已经有该插件 直接返回 避免重复注册
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    // 获取 插件传入的参数 (从第二个参数开始)
    const args = toArray(arguments, 1)
    // 把 Vue 作为第一个参数
    args.unshift(this)
    // 如果插件 有install 方法 并且是函数 执行 install方法
    //  把含有 Vue的参数作为参数执行
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      // 如果 plugin 没有install 方法,当plugin 就是一个 函数
      // 把含有 Vue的参数作为参数执行
      plugin.apply(null, args)
    }
    // 保存当前 插件 到 已注册 插件列表中
    installedPlugins.push(plugin)
    return this
  }
}

Vue.mixin

使用: Vue.mixin(mixin)

参数:

  • {object} mixin
    用法: 全局注册一个混入, 影响注册会后 创建的 所有vue.js 实例。
    插件作者 可以使用 混入 向组件中注入 自定义行为(比如: 监听生命周期钩子)
import { mergeOptions } from '../util/index'


export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    // 把混入 的mixin 与 Vue.options 合并 生成 新的 Vue.options
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

Vue.compile

使用 : Vue.compile(template)
参数:

  • {String} template
    用法: 编译模板字符串并返回 包含渲染函数的对象。 只有完整版中才有效
    /platforms/web/entry-runtime-with-compiler.js
import { compileToFunctions } from './compiler/index'
...
Vue.compile = compileToFunctions
export default Vue

compileToFunctions 上面已经说过了。

Vue.version
提供字符串 形式 的 Vue.js 安装版本号。 这对 社区的插件和组件来说非常有用,可以根据不用的版本号 采取不容的策略。

用法

var version = Number(Vue.version.split('.')[0])
if(version === 2){
  // vuejs v2.x.x
} else if(version === 1){
  // vue.js v1.x.x.
} else {
  // 不支持的版本
}

/core/index.js

Vue.version = '__VERSION__'

vue.version 是一个属性,rollup-plugin-replace在构建文件的过程中, 会读取 package.json中的version,然后替换为 常量 VERSION.

你可能感兴趣的:(vue2-实例方法与全局API的实现(二))