vm options什么意思_解剖new Vue 过程具体发生了什么(一)
废话少说
Vue如何实现双向绑定, Vue如何实现{{...}}模板渲染,为何用过Vue的人都说说它好用,今晚八点前端扫盲带你一起解剖Vue。
new Vue
故事要从new Vue开始讲起,如下:
于是我们直接从源码中找到Vue构造函数位置
这里先判断是否通过new 来创建Vue实例,如果我们直接调用Vue(),则会报warn里面错误。然后this._init(options)初始化相关事务。在构造函数下面还有几个mixin(混入)初始化代码,应该是定义了Vue原型对象上的一些方法,我们下一个个点进去看下具体有哪些实现,方便后面讲解。
在这个initMixin(Vue)里面:
Vue.prototype对象上定义了_init字段
在stateMixin(Vue)里面:
Vue.prototype对象上定义了$data、$props、$set、$delete、$watch字段,具体作用后面再一一突破
在eventsMixin(Vue)里面:
Vue.prototype对象上定义了$on、$once、$off、$emit字段
在lifecycleMixin(Vue)里面:
Vue.prototype对象上定义了_update、$forceUpdate、$destroy字段
在renderMixin(Vue)里面:
Vue.prototype对象上定义了$nextTick、_render字段
综上整理下Vue.prototype的所有字段有:
Vue.prototype = { _init: Function, // 初始化工作 $data: Object, // 传入实例的data对象,经过代理后的 $props: Object, // 父组件传入数据对象 $set: Function, // 实现为响应式对象新增 property 同样是响应式的 $delete: Function,// 删除响应式对象的 property,确保删除能触发更新视图 $watch: Function, // 监听响应式对象的 property 变更 $on: Function, // 为实例添加一个订阅,相同订阅名不会覆盖会叠加 $once: Function, // 为实例添加一个订阅,只会被触发一次 $off: Function, // 为实例解除订阅 $emit: Function, // 为实例触发指定订阅 _update: Function, // 更新实例视图 $forceUpdate: Function, // 强制更新实例视图 $destroy: Function, // 销毁实例 $nextTick: Function, // 将回调函数放到微队列中执行,确保获取到的dom是最新的,没有回调则返回一个Promise _render: Function, // 触发render(new Funtion生成的)函数,生成虚拟dom}
其中"_"开头的属性理解为私有属性,通常是Vue内部自己使用的方法,"$"开头的是对外暴露的属性,运行开发者通过组件实例(this)来调用。
是时候展示真正的技术
接下来我们一个个突破,看看具体是如何实现这些功能的。现在来看下构造函数中this._init具体做了些什么。直接上代码:
Vue.prototype._init = function (options) { var vm = this; vm._uid = uid$3++; // 设置实例的唯一标识 uid var startTag, endTag; /* istanbul ignore if */ if (config.performance && mark) { //... 性能监控代码,忽略 } vm._isVue = true; // 是否是组件 if (options && options._isComponent) { // 初始化组件一些配置 initInternalComponent(vm, options); } else { // 参数合并及初始化 vm.$options = mergeOptions( // 如果构造函数是Vue,则直接返回Vue.options值 // 如果构造函数是VueComponent,则判断VueComponent.superOptions是否改变 // VueComponent 是通过Vue.extend()返回的构造函数,继承了Vue.prototype // 感觉这步没必要,因为此时vm.constructor一定是Vue,如果是VueComponent则会走到上面if流程里面 resolveConstructorOptions(vm.constructor), options || {}, vm ); } /* istanbul ignore else */ { // 通过 Proxy 代理实例对象 initProxy(vm); } // expose real self vm._self = vm; initLifecycle(vm); // 初始化生命周期 initEvents(vm); // 初始化事件 initRender(vm); // 初始化render函数 callHook(vm, 'beforeCreate'); // 执行beforeCreate钩子 initInjections(vm); // 注射祖先组件(可跨层级)提供的数据,主要在开发高阶插件/组件库时使用 initState(vm); // data、props 数据挟持将其注册到vue实例上 initProvide(vm); // 为子孙组件提供跨可层级访问数据,和 inject 配合使用 callHook(vm, 'created'); // 执行created钩子 /* istanbul ignore if */ if (config.performance && mark) { //... 性能监控代码,忽略 } if (vm.$options.el) { vm.$mount(vm.$options.el); // 将el DOM对象挂载到页面 }};
上面我给每行代码都做了注释,理解起来应该没问题。主要是做了一些初始化的工作,具体初始化了哪些事,则我们需要进一步拆解这个函数。
initInternalComponent(vm, options) 函数:
这个是创建组件实例的时候会被调用,主要用来初始化子组件实例$option属性配置,目前我们先跳过组件的相关功能实现,假设当前页面没有引用或定义任何组件,因此我们只分析new Vue 过程。
接着往下看 :
vm.$options = mergeOptions(...),这个过程主要是对构造实例参数options的属性值格式化规范,同时将Vue构造函数上面的options对象和构造实例参数options对象进行merge操作。具体过程如下:
function mergeOptions(parent, child, vm) { { // 主要用来检测child中components声明的组件名称是否规范 checkComponents(child); } // 如果child是构造函数,则获取构造函数上的options对象 if (typeof child === 'function') { child = child.options; } /** * eg: props: ['user-name'] * 格式化props属性规范,将其转成props: { userName: { type : null} } 形式 * 将props中短杆命名属性转成驼峰命名 */ normalizeProps(child, vm); /** * eg: inject: ['loginStatus'] * 格式化inject属性规范,将其转成inject: {loginStatus: { from: 'loginStatus' }} 形式 */ normalizeInject(child, vm); /** * eg: directives: { 'my-click': function(el){ ... }} * 格式化自定义指令格式规范,将其转成 * directives: { 'my-click': {bind: function(el){ ... }, update: function(el){ ... }}} */ normalizeDirectives(child); if (!child._base) { if (child.extends) { // extends 实现,组件扩展 parent = mergeOptions(parent, child.extends, vm); } if (child.mixins) { // mixins 实现,组件混入 for (var i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm); } } } var options = {}; var key; for (key in parent) { mergeField(key); } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key); } } // 字段合并,这边判断了是否采用特殊合并方式strats[key],或者是普通合同defaultStrat合并方式【子属性覆盖父属性】 function mergeField(key) { var strat = strats[key] || defaultStrat; options[key] = strat(parent[key], child[key], vm, key); } // 返回新的 options return options}
上面对mergeOptions 的过程做了详细解释。总结一句话就是将传进构造函数的options选项经过一系列处理,后挂载到实例的vm.$options上。
特殊字段合并方式对应如下:
合并方式介绍
mergeHook: 用在生命周期和props属性的合并上,将其扩展为数组形式
mergeAssets:用在构造函数Vue.options选项和实例参数options选项的合并。示例参数:
const options = { components: { myButton: { ... } } }// 合并后会变成如下options = components: { myButton: { ... } }options.components.prototype = Object.create(Vue.options.components);// 等价于 options.components.__proto__ = Vue.options.components;
mergeDataOrFn:实例参数options.data合并,这个过程会判断如果是子组件则data必须是一个函数且返回一个Object对象。
接下去就是initProxy(vm);
这个过程就是将vm实例通过Proxy方法创建一个代理对象,我们在html模板中引用的变量(如:{{name}}、
initProxy = function initProxy(vm) { // 是否支持Proxy接口 if (hasProxy) { var options = vm.$options; var handlers = options.render && options.render._withStripped ? getHandler : hasHandler; // 创建代理,Proxy 类似Object.defineProperty功能,具体自行百度 vm._renderProxy = new Proxy(vm, handlers); } else { vm._renderProxy = vm; }};
hasHandler 代码:(getHandler 类似)
var hasHandler = { has: function has(target, key) { var has = key in target; // 实例中是否存在key值 // key 格式是否正确 var isAllowed = allowedGlobals(key) // 是否是全局属性如Array、Object、Math... // data对象不允许带_前缀属性 || (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)); if (!has && !isAllowed) { if (key in target.$data) { // data 属性名格式错误报警 warnReservedPrefix(target, key); } else { // 引用值不存在错误报警 warnNonPresent(target, key); } } return has || !isAllowed }};
总结,这个过程目的就是在取值的时候可以进行拦截判断并给出异常提示的作用。
截止这里我们还没进入Vue的核心内容,都是一些对配置项和实例的处理过程,接下去才是Vue的重点内容。
解剖new Vue 过程具体发生了什么(二)
关注公众号一起成长