美文网首页
vue2-实例方法与全局API的实现(二)

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

作者: AAA前端 | 来源:发表于2021-05-25 15:28 被阅读0次

    vm.$mount

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

    参数: {Element | String} [elementOrSelector]

    返回值: vm,实例本身

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

    栗子:

    var myComponent = Vue.extend({
      template: '<div>jjjjjjjsdf</div>'
    })
    
    // 有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方法保存到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的实现(二)

          本文链接:https://www.haomeiwen.com/subject/nnjqsltx.html