Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!


2013年7月28日,尤雨溪第一次在 GItHub 上为 Vue.js 提交代码;2015年10月26日,Vue.js 1.0.0版本发布;2016年10月1日,Vue.js 2.0发布。

最早的 Vue.js 只做视图层,没有路由, 没有状态管理,也没有官方的构建工具,只有一个库,放到网页里就可以直接用了。

后来,Vue.js 慢慢开始加入了一些官方的辅助工具,比如路由(Router)、状态管理方案(Vuex)和构建工具(Vue-cli)等。此时,Vue.js 的定位是:The Progressive Framework。翻译成中文,就是渐进式框架。

Vue.js2.0 引入了很多特性,比如虚拟 DOM,支持 JSX 和 TypeScript,支持流式服务端渲染,提供了跨平台的能力等。Vue.js 在国内的用户有阿里巴巴、百度、腾讯、新浪、网易、滴滴出行、360、美团等等。

Vue 已是一名前端工程师必备的技能,现在就让我们开始深入学习 Vue.js 内部的核心技术原理吧!


什么是生命周期

百科全书定义的生命周期的概念是:生命周期就是指一个对象的生老病死。比如一个生物的出生、长大、成年、老年、死亡的全过程,相信这个大家应该都很好理解。

那么对于Vue.js来说,生命周期是什么呢?我们所创建的每一个 Vue.js实例,都要经历一系列的初始化过程,比如对状态设置数据监听(响应式基础)、编译模板(template)、将实例挂载到指定的 DOM 元素上、当数据改变时更新 DOM。

在这个过程中,Vue.js 会在响应的阶段执行指定的生命周期钩子函数,在每一个生命周期钩子函数里,我们可以去执行我们自己的代码,来确保功能的完成和整体性能优化。


生命周期图示

下图给出了 Vue.js 生命周期图示,分为 4 个阶段,初始化阶段、模板编译阶段、挂载阶段和卸载阶段。

初始化阶段

从 new Vue() 到 created 之间的阶段叫做初始化阶段,在本阶段,主要是在Vue.js 的实例上初始化一些属性、事件和响应式数据,比如 props、methods、data、computed、watch、inject 和 provide 等。

模板编译阶段

从 created 到 beforeMounted 之间是模板编译阶段,主要是将模板编译成渲染函数,有关模板编译的详细内容,请移步Vue 进阶系列丨Patch 和模板编译

挂载阶段

从 beforeMounted 到 mounted 之间是挂载阶段,主要是将实例挂载到指定的 DOM 元素上,在挂载的过程中,会开启状态监听,当状态发生变化时,虚拟 DOM 会收到监听器发送的通知,重新渲染 DOM,已达到响应式。在渲染视图之前会触发 beforeUpdate 函数,渲染视图之后,触发 updated 函数,这个状态会一致持续到 Vue.js 实例组件被销毁。

卸载阶段

从 beforeDestory 到 destoryed 之间是卸载阶段,当我们调用Vue.$destory 方法后,Vue.js 会进入卸载阶段。Vue.js 会将自身从父组件删除、清空所有依赖追踪和所有事件监听器。


new Vue() 被调用时发生了什么

当调用 new Vue() 时,会先进行一些初始化阶段,然后进入模板编译阶段,最后是挂载阶段。那么刚刚调用时,Vue 内部都做了哪些操作呢?我们引用Vue.js 源码来看一看。

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)
}

从源码可以看出,首先进行安全检查,在非生产环境中,如果没有用new来调用 Vue,会在控制台抛出错误警告。

然后调用 this._init(options) 来执行生命周期的初始化阶段。下面我们来看下

this._init 函数的源码。

Vue.prototype._init = function (options?: Object) {// merge optionsvm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)// expose real selfvm._self = vminitLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm, 'beforeCreate')initInjections(vm) // resolve injections before data/propsinitState(vm)initProvide(vm) // resolve provide after data/propscallHook(vm, 'created')if (vm.$options.el) {vm.$mount(vm.$options.el)}}

在 this._init() 方法内部,首先我们将用户传递的 options 选项和当前构造函数的 options 属性及其父级实例构造函数的 options 属性,合并成一个新的options 并赋值给 $options 属性。

resolveConstructorOptions(vm.constructor) 是用来获取当前实例中构造函数的 options 属性和其父级构造函数的 options。

然后是通过 initLifecycle(vm)、initEvents(vm)、initRender(vm)、initInjections(vm)、initState(vm)、initProvide(vm) 来进行相应的初始化操作。并且在初始化过程中,通过 callHook 函数触发相应的生命周期钩子函数。


初始化实例属性

接初始化实例属性是 Vue 整个生命周期的第一步,既包含 Vue 内部自己需要用到的属性,比如 vm._watcher,也有提供给外部使用的属性,比如vm.$parent。

以下划线“_”开头的属性是提供给 Vue 内部使用的,也就是源码内部逻辑使用的,以“$”开头的属性是提供给外部使用的,也就是暴漏给外部用户使用的属性。

实例化属性是在 initLifecycle 函数中实现的,我们先看一下源码部分:

export function initLifecycle (vm: Component) {const options = vm.$options// locate first non-abstract parentlet parent = options.parentif (parent && !options.abstract) {while (parent.$options.abstract && parent.$parent) {parent = parent.$parent}parent.$children.push(vm)}vm.$parent = parentvm.$root = parent ? parent.$root : vmvm.$children = []vm.$refs = {}vm._watcher = nullvm._inactive = nullvm._directInactive = falsevm._isMounted = falsevm._isDestroyed = falsevm._isBeingDestroyed = false
}

实现方法很简单,就是在 Vue.js 的实例上设置一些属性并且提供相应的默认值。

但是 vm.$parent 属性并不是简单的赋值操作,需要找到第一个非抽象类型的父级。通过 while 循环,直到遇到第一个非抽象类的父级时,才将其赋值给 vm.$parent。

vm.$children 保存的是当前组件的子组件,是由子组件自己添加的,当子组件找到 vm.$parent 后,通过 parent.$children.push(vm),将自己添加到父级组件的 vm.$children 属性中。

还有一个就是 vm.$root,表示的是当前组件树的根组件,循环自己的父组件,知道父组件的 $root 属性还是父组件本身的时候,才会将其赋值到当前组件的 $root 中。如果当前组件没有父组件,那么自己就是根组件,就将自身设置为 $root 属性。


初始化事件

初始化事件,是指初始化父组通过 v-on(也就是@),向子组件的事件系统中添加事件。当我们通过父子组件传参时,会遇到父组件通过 v-on 监听子组件的事件触发,子组件可以通过 this.$emit 来触发事件。

如果 v-on 写在了组件标签上,那么这个事件就会注册到自组件的事件系统中,如果是写在了 HTML 标签上,会注册到浏览器事件中。

子组件在初始化的时候,可能会接受到父组件向子组件注册的事件,也可能接收到自身在 HTML 标签上注册的事件,但是只有在渲染的时候才会根据虚拟 DOM 的对比结果,来确定到底是注册事件还是解绑事件。

所以初始化事件指的是父组件向子组件注册的事件。这段实现,是在initEvents 函数中实现的,我们先看一下源码部分。

export function initEvents (vm: Component) {vm._events = Object.create(null)vm._hasHookEvent = false// init parent attached eventsconst listeners = vm.$options._parentListenersif (listeners) {updateComponentListeners(vm, listeners)}
}

vm._events 是用来存储事件的,所有通过 vm.$on 注册的事件都会保存在这里。

vm.$options._parentListeners 用来存储父组件向自己注册的事件。

下面我们来看 updateComponentListeners 内部实现。

export function updateComponentListeners (vm: Component,listeners: Object,oldListeners: ?Object
) {target = vmupdateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)target = undefined
}

updateComponentListeners 的逻辑很简单,就是循环vm.$options._parentListeners,判断新的事件列表和旧的事件列表,通过add 和 remove 方法,将事件注册到 this._events 中,还是从 this._events中移除事件。

下面是 add 和 remove 方法源码部分

function add (event, fn) {target.$on(event, fn)
}function remove (event, fn) {target.$off(event, fn)
}

updateListeners 函数就是来对比新的事件列表和旧的事件列表,并且进行新增和删除操作的。

源码部分如下:

export function updateListeners (on: Object,oldOn: Object,add: Function,remove: Function,createOnceHandler: Function,vm: Component
) {let name, def, cur, old, eventfor (name in on) {cur = on[name]old = oldOn[name]event = normalizeEvent(name)if (isUndef(cur)) {process.env.NODE_ENV !== 'production' && warn(`Invalid handler for event "${event.name}": got ` + String(cur),vm)} else if (isUndef(old)) {if (isUndef(cur.fns)) {cur = on[name] = createFnInvoker(cur, vm)}add(event.name, cur, event.capture, event.passive, event.params)} else if (cur !== old) {old.fns = curon[name] = old}}for (name in oldOn) {if (isUndef(on[name])) {event = normalizeEvent(name)remove(event.name, oldOn[name], event.capture)}}
}

初始化 inject

inject 和 provide 是成对出现的,目的是允许祖先组件向子孙组件注入依赖,也可以说是传递数据。我们平时使用的 vm.$parent 仅仅保证我们可以获取父组件的相关内容,但是当我们想要拿到祖先组件的内容时,这种方法就有点麻烦了。

provide 选项是祖先组件注入依赖的过程,像是祖先组件提前声明好了一些内容,这些内容是可以向子孙组件暴露的,可以是一个对象或者一个返回对象的函数。对象也可以使用 Symbol 作为 key。

inject 选项是子孙组件用来拿到注入内容的,通过 inject 可以拿到祖先组件通过 provide 选项暴露的那些数据,当然你可以选择性拿,不用全拿。inject可以是一个数组,也可以是一个对象。对象的 key 是你本地的绑定名,value是一个 key(字符串或Symbol),也就是 provide 开始暴露的 key,用来在已注入的内容里面搜索。

如果是一个对象的话,具备两个属性:

  • name:一个key(字符串或Symbol),用来在已注入的内容里面搜索

  • default:在已注入的内容里面搜索不到之后的默认值

我们通过下面的示例代码,来加深对 provide 和 inject 的熟悉

// 子孙组件拿到注入的值
var Provider = {provide:{name:'zhangsan'}
}
var Injecter = {inject:['name'],created(){console.log(this.name) // zhangsan}
}
----------------------------------------------------------------
// 子孙组件拿到注入的值,重新命名且设置默认值
var Provider = {provide:{name:'zhangsan'}
}
var Injecter = {inject:{name2:{from:'name',default:'没拿到name'}},created(){console.log(this.name2) // zhangsan}
}
----------------------------------------------------------------
// 也可以在data/props里拿到注入的值,因为初始化状态阶段是在初始化inject之后进行的,
// 所以在data/props里可以拿到inject的值
var Provider = {provide:{name:'zhangsan'}
}
var Injecter = {inject:['name'],props:{name2:{return this.name // 使用inject作为props的默认值}}
}
var Injecter2 = {inject:['name'],data(){return {name2:this.name // 使用inject作为name2的数据入口}}
}

inject的内部原理

知道了怎么使用了之后,现在来看一下他的内部实现是怎么样的。我们可以看到初始化 inject 和初始化 provide 并不是一起进行的,而是在 data/props 之前初始化 inject,之后初始化 provide。目的就是为了确保 data/props 里可以访问到 inject 里的内容。

inject 的实现逻辑也很简单,通过使用阶段可以得知,inject 其实就是拿到祖先组件 provide 的值。我们只需要一级一级向上查找父级的 provide 里有没有我们想要的值就可以了,如果没找到,继续向上级查找,直到找到了根组件,还没找到的话,就将默认值保存到当前实例中(this)上,找到了,就将找到的内容保存到实例(this)中,这样就可以直接通过 this 访问 inject 导入的内容了。

相应源码如下:

function initInjections (vm) {var result = resolveInject(vm.$options.inject, vm);if (result) {Object.keys(result).forEach(function (key) {/* istanbul ignore else */{defineReactive$$1(vm, key, result[key], function () {warn("Avoid mutating an injected value directly since the changes will be " +"overwritten whenever the provided component re-renders. " +"injection being mutated: \"" + key + "\"",vm);});}});}}

其中,resolveInject 函数的作用是通过用户配置的 inject,自底向上搜索可用的注入内容,找到后返回结果内容。然后循环结果内容,依次调用 defineReactive 函数,将他们设置到 Vue.js 实例上。


初始化状态

在我们实际开发项目的时候,经常使用一些状态,比如 props、methods、data、computed 和 watch,这些状态在 Vue 内部是怎么初始化的呢?我们先看一下源码部分

export function initState (vm: Component) {vm._watchers = []  const opts = vm.$optionsif (opts.props) initProps(vm, opts.props)if (opts.methods) initMethods(vm, opts.methods)if (opts.data) {initData(vm)} else {observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)}
}

vm._watchers 是用来存储当前实例的监听器的,Vue.js 每个状态变化时,通知到的监听器就在这个里面,当变化时,循环 vm._watchers,找到对应的监听器,执行监听器,通知虚拟 DOM,虚拟 DOM 进行 diff 算法,然后重新渲染 DOM,更新页面。

而且只有当我们使用了这个状态,这个状态才会初始化。比如我们只使用了data,那么只会初始化 data 即可。

这里需要注意一点的是,watch 是最后初始化的,这样可以确保我们可以在watch 中既可以监听 props 的变化,也可以监听 data 的变化。


初始化 props

当进行父子组件传参时,子组件通过 props,接收父组件传递的数据,并且可以选择性接收。Vue.js 内部会将子组件接收的 props 属性筛选出来,然后添加到子组件的上下文中。

1. 规格化 props

我们通过 props 接收数据时,既可以通过数组形式接收,也可以通过对象形式接收,子组件被实例化时,会先对 props 进行规格化处理,规格化处理后的 props 为对象的格式,目的是统一格式,方便后期处理。规格化部分的源码部分如下。

function normalizeProps (options: Object, vm: ?Component) {const props = options.props// 先判断有没有propsif (!props) return// 声明res,保存规格化之后的结果const res = {}let i, val, name// 判断是否是数组if (Array.isArray(props)) {i = props.length// 循环数组while (i--) {val = props[i]// props的名称是String,使用camelize函数将props名称驼峰化,就是将a-b转为aBif (typeof val === 'string') {name = camelize(val)res[name] = { type: null }} else if (process.env.NODE_ENV !== 'production') {// 否则,在非生产环境打印警告warn('props must be strings when using array syntax.')}}// 不是数组,判断是不是对象} else if (isPlainObject(props)) {// 循环对象for (const key in props) {val = props[key]name = camelize(key)// 设置一个key为名的属性,值为{type:val}的属性res[name] = isPlainObject(val)? val: { type: val }}} else if (process.env.NODE_ENV !== 'production') {// 不是数组,也不是对象,打印警告warn(`Invalid value for option "props": expected an Array or an Object, ` +`but got ${toRawType(props)}.`,vm)}// 将规格化后的数据重新保存options.props = res
}

2. 初始化 props

初始化的步骤是通过规格化后的 props,从他的父组件身上传进来的数据里筛选出需要的数据,保存在 vm._props 中,然后在 vm 上设置一个代理,让我们可以通过 vm.x 访问到 vm._props.x,也就是可以通过 this 访问到 props。相关代码如下:

function initProps (vm: Component, propsOptions: Object) {// 保存父组件传入或者用户通过propsData传入的propsconst propsData = vm.$options.propsData || {}// 指向vm._props的指针const props = vm._props = {}// 指向vm.$options._propKeys的指针const keys = vm.$options._propKeys = []// 判断是不是根组件const isRoot = !vm.$parentif (!isRoot) {// 不是根组件,不需要将props数据转换成响应式数据toggleObserving(false)}// 循环propsOptionsfor (const key in propsOptions) {// 将key添加到keys中keys.push(key)// 调用validateProp函数将得到的props数据,通过defineReactive函数设置到vm._props上const value = validateProp(key, propsOptions, propsData, vm)defineReactive(props, key, value)}// 循环vm,为vm._props设置代理if (!(key in vm)) {proxy(vm, `_props`, key)}}toggleObserving(true)
}

初始化 methods

初始化 methods 很简单,就是循环模板选项中的 methods 对象,将每个属性都挂载到 vm 上。相关代码如下:

function initMethods (vm: Component, methods: Object) {// 用来判断methods中的方法是否和props中的重复了const props = vm.$options.props // 循环methodsfor (const key in methods) {// 检查方法是否合法if (process.env.NODE_ENV !== 'production') {// 检查方法是不是只有key,没有valueif (typeof methods[key] !== 'function') {warn(`Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +`Did you reference the function correctly?`,vm)}// 检查方法是否已经存在props里了if (props && hasOwn(props, key)) {warn(`Method "${key}" has already been defined as a prop.`,vm)}// 检查方法是否已经存在了,或者是否是以_或者$开头的if ((key in vm) && isReserved(key)) {warn(`Method "${key}" conflicts with an existing Vue instance method. ` +`Avoid defining component methods that start with _ or $.`)}}// 将方法挂载到vm上vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)}
}

初始化 data

初始化 data,data 是保存在 vm._data 属性中的,通过在 vm 上设置一个代理,使得通过 vm.x 可以访问到 vm._data 中的 x 属性,最后通过一个叫做Observe 的函数将 data 转换成响应式数据,就是添加监听器。于是,data就完成了响应式。相关代码如下

function initData (vm: Component) {// 拿到模板中的data对象let data = vm.$options.data// 判断data的类型,如果是函数就先执行函数,并将返回值赋值给data和vm._data// getData也是执行函数,将返回值赋值给data和vm._data。// 只不过里面有try...catch 捕获data函数有可能出现的错误data = vm._data = typeof data === 'function'? getData(data, vm) : data || {}// 判断是否是Object类型,如果不是且在非生产环境就发出警告if (!isPlainObject(data)) {data = {}process.env.NODE_ENV !== 'production' && warn('data functions should return an object:\n' +'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',vm)}// 将data代理到实例上const keys = Object.keys(data)const props = vm.$options.propsconst methods = vm.$options.methodslet i = keys.length// 循环datawhile (i--) {const key = keys[i]// 判断是否是恒产环境if (process.env.NODE_ENV !== 'production') {// 不是生产环境,判断当前循环的key是否存在于methods中,如果存在,就说明重复了,发出警告if (methods && hasOwn(methods, key)) {warn(`Method "${key}" has already been defined as a data property.`,vm)}}// 不是生产环境,判断当前循环的key是否存在于props中,如果存在,就说明重复了,发出警告if (props && hasOwn(props, key)) {process.env.NODE_ENV !== 'production' && warn(`The data property "${key}" is already declared as a prop. ` +`Use prop default value instead.`,vm)} else if (!isReserved(key)) {// 调用proxy函数实现代理功能proxy(vm, `_data`, key)}}// 将数据转换为响应式observe(data, true /* asRootData */)
}

初始化 computed

计算属性在我们平时项目中也是经常出现的一个状态,他和 watch 的不同之处就在于计算属性具有缓存特性,只有计算属性依赖的值发生变化时,计算属性才会重新计算,否则会反复读取计算属性,而计算属性函数不会反复执行。相关代码如下:

function initComputed (vm: Component, computed: Object) {// vm._computedWatchers用来保存所有的计算属性的监听器watcher实例const watchers = vm._computedWatchers = Object.create(null)// 循环computed对象for (const key in computed) {// 保存用户设置的计算属性定义const userDef = computed[key]// 通过userDef获取getter函数const getter = typeof userDef === 'function' ? userDef : userDef.get// 如果是非生产环境且getter为null,则发出警告if (process.env.NODE_ENV !== 'production' && getter == null) {warn(`Getter is missing for computed property "${key}".`,vm)}// 为计算属性创建监听器watcher实例watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions)// 判断当前循环到的属性是否已经存在于vm中if (!(key in vm)) {// 不存在,使用defineComputed函数在vm上设置个计算属性defineComputed(vm, key, userDef)} else if (process.env.NODE_ENV !== 'production') {// 已经存在,在非生产环境下发出警告if (key in vm.$data) {warn(`The computed property "${key}" is already defined in data.`, vm)} else if (vm.$options.props && key in vm.$options.props) {warn(`The computed property "${key}" is already defined as a prop.`, vm)} else if (vm.$options.methods && key in vm.$options.methods) {warn(`The computed property "${key}" is already defined as a method.`, vm)}}}
}

初始化 watch

初始化的最后一步是初始化 watch,目的就是可以在 watch 中既可以监听data,也可以监听 props,当然也可以监听 computed 的变化。实现方式很简单,就是循环模板中的 watch 选项,然后依次使用 vm.$watch方法,观察被监听状态的变化。相关源码如下:

function initWatch (vm: Component, watch: Object) {// 循环模板中的watch选项for (const key in watch) {// 通过key得到watch的值,赋值给handlerconst handler = watch[key]// 如果watch是一个数组形式的,就遍历数组,依次调用createWatcher函数创建对状态的监听器watcherif (Array.isArray(handler)) {for (let i = 0; i < handler.length; i++) {createWatcher(vm, key, handler[i])}} else {// 不是数组,就直接调用createWatcher函数创建对状态的监听器watchercreateWatcher(vm, key, handler)}}
}

初始化 provide

生命周期初始化阶段的最后是初始化 provide,初始化 provide 不像 inject那样麻烦,我们只需要将 provide 中的内容保存在一个地方,然后 inject 去这个地方查找就好了。相关代码如下:

export function initProvide (vm: Component) {// 拿到模板中的provideconst provide = vm.$options.provideif (provide) {// 判断是不是函数,如果是就先执行,将执行结果赋值给vm._provided,否则直接将变量赋值给vm._providedvm._provided = typeof provide === 'function'? provide.call(vm): provide}
}

Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!

叶阳辉

HFun 前端攻城狮

往期精彩:

Vue 进阶系列丨生命周期相关推荐

  1. Vue 进阶系列丨自定义指令实现按钮权限功能

    Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的.若感本文对您有所帮助请点个赞吧! 2013年7月28日,尤雨溪第一次 ...

  2. Vue 进阶系列丨权限控制 addRoute()

    Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的.若感本文对您有所帮助请点个赞吧! 2013年7月28日,尤雨溪第一次 ...

  3. Vue 进阶系列丨vuex持久化

    Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的.若感本文对您有所帮助请点个赞吧! 2013年7月28日,尤雨溪第一次 ...

  4. Vue 进阶语法和生命周期

    文章目录 Vue 进阶语法和生命周期 16.Vue:生命周期[了解] 17.Vue:computed计算属性 18.Vue:watch监控属性 Vue 进阶语法和生命周期 a. 每个 Vue 应用都是 ...

  5. day4 vue 学习笔记 组件 生命周期 数据共享 数组常用方法

    系列文章目录 day1学习vue2笔记 vue指令 day2 学习vue2 笔记 过滤器 侦听器 计算属性 axios day3 vue2 学习笔记 vue组件 day4 vue 学习笔记 组件 生命 ...

  6. Vue 进阶系列(一)之响应式原理及实现

    Vue进阶系列汇总如下,欢迎阅读. Vue 进阶系列(一)之响应式原理及实现 Vue 进阶系列(二)之插件原理及实现 Vue 进阶系列(三)之Render函数原理及实现 什么是响应式Reactivit ...

  7. vue手机端回退_从外链回退到vue应用不触发生命周期、beforeRouterEnter等钩子函数的问题...

    在iphoneX及以上版本从外链回退不触发事件,7P,7没发现这个bug 安卓上自测没有发现这个问题 最近做项目中发现了一个问题,iphoneX及以上版本从当前vue应用中跳转到外部链接然后在回退到v ...

  8. Vue之MVVM、Vue实例对象、生命周期

    1.初识Vue Vue是一套用于构建用户界面的渐进式框架,Vue 被设计为可以自底向上逐层应用,Vue的核心是只关注视图层(如何理解),不仅易于上手,还便于与第三方库或既有项目整合. 渐进式框架:渐进 ...

  9. Vue基础进阶 之 实例方法--生命周期

    在上一篇博客中我们知道生命周期的方法: 生命周期: vm.$mount:手动挂载Vue实例: vm.$destroy:销毁Vue实例,清理数据绑定,移除事件监听: vm.$nextTick:将方法中的 ...

最新文章

  1. CVPR2020:训练多视图三维点云配准
  2. php全局变量GLOBAL
  3. Python Django 多表插入之重写save()方法代码示例
  4. wxWidgets:wxRegKey类用法
  5. PAT_B_1049_C++(20分)
  6. Idea新建modules后无法自动导入pom.xml
  7. 统计学习方法笔记(李航)———第三章(k近邻法)
  8. 转---猫大叫一声,所有的老鼠都开始逃跑,主人被惊醒 [观察者模式]
  9. linux创建网络连接,使用nmcli创建网络连接
  10. C++vector基础容器3.0
  11. 一步步教你轻松学主成分分析PCA降维算法
  12. db2数据库基础知识
  13. qq linux五笔输入法,qq五笔输入法
  14. 【Lingo】线性规划
  15. 黑马 docker 学习笔记
  16. 阿里云服务器和 hexo 博客实战
  17. 华厦眼科上市:募资31亿市值393亿 挂靠厦门大学
  18. nginx设置代理后端服务器增加前缀
  19. 如何写出同事看不懂的Java代码?
  20. Java速成学习小结

热门文章

  1. http error - listen tcp 127.0.0.1:1087: bind: address already in use - Mac
  2. IPsec虚拟专用网络
  3. word 排版大师 最新版本 版本发布啦。https://pan.baidu.com/s/1jIIi6xg
  4. RuntimeError: Failed to init API, possibly an invalid tessdata
  5. EVE-NG使用手册学习
  6. Windows Store apps开发[72]Windows 8 开发31日-第15日-虚拟键盘
  7. 运用Python编写程序,模拟猫狗大战
  8. 怎么从一段视频中分割出片头片尾
  9. vsco怎么两个滤镜叠加_别再乱套滤镜了!这4款VSCO滤镜好看到哭!
  10. android日记管理,Android日志管理Logger框架的简单介绍