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: '<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方法被一个新方法覆盖
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可以有不同的格式,我们也要处理下
- 如果template是#开头的字符串,则它将作为选择符,通过选择符获取DOM元素后,使用innerHTML作为模板
- 如果tempalte选项不是字符串,则判定它是否是一个DOM元素,如果是,使用DOM元素的innerHTML作为模板。
- 如果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.