现在的前端页面元素越来越多,结构也变得越来越复杂,当数据和视图混合在一起的时候对它们的处理会十分复杂,同时也很容易出现错误,而现代框架使用声明式语法,描述组件对象的嵌套关系,并自动生成与dom对象的对应关系参考1open in new window、参考2open in new window
| vue生命周期 | 描述 |
|---|---|
| beforeCreate | 组件实力被创建,el和数据对象都为undefined,还未初始化 |
| create | 数据已经被初始化,并且初始化了Vue内部事件,但是DOM还未生成 |
| befroeMount | 完成了模板的编译。把data对象里面的数据和vue的语法写的模板编译成了虚拟DOM |
| mouted | 执行了render函数,将渲染出来的内容挂载到了DOM节点上 |
| beforeUpdate | 组件更新之前:数据发生变化时,会调用beforeUpdate,然后经历DOM diff |
| updated | 组件更新后 |
| actived | keep-alive组件被激活 |
| deactivated | keep-alive移除 |
| beforeDestroy | 组件销毁前 |
| destroyed | 组件销毁后 |
可以问数据变动如何和视图联系在一起?
Vue是采用数据劫持结合发布者-订阅者模式的方式, Vue相应系统有三大核心:observe,dep,watcher;精简版Vue代码参考open in new window
Observeopen in new window:当一个Vue实例创建时,initData阶段,vue会遍历data选项的属性(observe),用 Object.defineProperty 将它们转为 getter/setter并且在内部追踪相关依赖(dep),在属性被访问和修改时通知变化。Compiteopen in new window:调用compile方法解析模版,当视图中有用到vue.data中数据的时候,会调用实例化watcher方法进行依赖收集Watcheropen in new window:是Observer和Compile之间通信的桥梁,当视图中遇到绑定的数据时,在watcher方法中会获取这个数据,此时会触发observe中的getter方法,Depopen in new window:发布订阅模式,observe中数据的getter被触发时会收集依赖的watcher(dep.depend方法)observe中数据的setter,此时会调用dep.notify方法给所有订阅的watcher发通知(通过回掉方式)进行视图更新,此时会进行diff流程: 
vue中的data为对象,是引用类型,当重用组件时,一个组件对data做了更改,那么另一个组件也会跟着改,而使用返回一个函数返回数据,则每次返回都是一个新对象,引用地址不用,所以就不会出现问题
虚拟DOM是一个JavaScript对象,包含了当前DOM的基本结构和信息,它的存在是为了减少对操作无用DOM所带来的性能消耗,是实现一种声明式的、状态驱动的 UI 开发理念的策略,在大量的、频繁的数据更新下能够对视图进行合理的高效的更新(细粒度的精准修改),让js业务逻辑和DOM操作解耦;同时也抽象了原来的渲染过程,实现了跨平台的能力;其缺点就是无法进行极致优化
精简源码open in new window;采用深度优先遍历,把树形结构按照层级分解,只比较同级元素;当数据发生改变时,set方法会让调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁(两个重要函数patchVnode和updateChildren):
sameVnode,如果不是的化,就会创建新的根结点并进行替换sameVnode,则进入patchVnode函数,其基本判断 oldVnode === vnode则直接return新节点是文本节点,则判断新旧文本节点是否一致,不一致(oldVnode.text !== vnode.text)则替换新节点不是文本节点,则开始比较新旧节点的子节点oldCh和ch:子节点都存在,则进行updateChildren计算(稍后讲)只有新子节点存在,则如果旧节点有文本节点,则移除文本节点,然后将新子节点拆入只有旧子节点存在,则移除所有子节点均无子节点且旧节点是文本节点,则移除文本节点(此时新节点一定不是文本节点)updateChildren函数做细致对比 idxInOld) 找到了idxInOld,如果是相同节点则移动旧节点到新的对应的地方,否则虽然key相同但元素不同,当作新元素节点去创建没有找到idxInOld,则创建节点老节点先遍历完,则新节点比老节点多,将新节点多余的插入进去新节点先遍历完,则就节点比新节点多,将旧节点多余的删除参考1open in new window参考2open in new window;主要是为了复用节点,高效的``准确的更新虚拟DOM,另外,在使用标签元素过渡效果时也会用到key,同时,以避免“原地复用”带来的副作用,如果没有key并且某些节点有绑定数据(表单)状态,会出现状态错位
initComputed,Watcher实例,并在内实例化一个Dep消息订阅器用作后续收集依赖,computed引用的时候会第一次执行计算属性,调用watcher的evaluate方法,将dirty设置为false,并将结果保存在this.value中进行缓存,computed会这直接返回this.valuecomputed所依赖的属性发生变化时会调用watcher的update方法将dirty设置为true,下次调用computed时就会重新计算class Watcher{
……
evaluate () {
this.value = this.get()
this.dirty = false
}
……
}
class initComputed{
……
//计算属性的getter 获取计算属性的值时会调用
createComputedGetter (key) {
return function computedGetter () {
//获取到相应的watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
//watcher.dirty 参数决定了计算属性值是否需要重新计算,默认值为true,即第一次时会调用一次
if (watcher.dirty) {
/*每次执行之后watcher.dirty会设置为false,只要依赖的data值改变时才会触发
watcher.dirty为true,从而获取值时从新计算*/
watcher.evaluate()
}
//获取依赖
if (Dep.target) {
watcher.depend()
}
//返回计算属性的值
return watcher.value
}
}
}
……
}
参考1open in new window、参考1open in new window、参考2open in new window
计算属性顾名思义就是通过其他变量计算得来的,它的值是基于其所依赖的属性来进行缓存的,只有在其所依赖的属性发生变化时才会从新求值watch是监听一个变量,当变量发生变化时,会调用对应的方法
vue实现响应式并不是数据一更新就立刻触发dom变化,而是按照一定的策略对dom进行更新,源码位置open in new window,原理:
callbacks数组中,$nextTick没有传cb回掉,则返回一个promisecallbacks的执行时机 promise,则用promise.resolve().then来执行callbacksMutationObserver,则用实例化的MutationObserver监听文本变化来执行回掉,setImmediate,则用setImmediate(cb)来执行回掉setTimeout(fn,0)来执行vue2.5.X版本open in new window中对于像v-on这样的DOM交互事件,默认走macroTimerFunc,也就是,跳过第一步promise的判断,initProps时,会对props进行defineReactive操作,传入的第四个参数是自定义的set报错判断函数,该函数会在触发props的set方法时执行// src/core/instance/state.js 源码路径
function initProps (vm: Component, propsOptions: Object) {
...
for (const key in propsOptions) {
if (process.env.NODE_ENV !== 'production') {
...
defineReactive(props, key, value, () => {
// 如果不是跟元素并且不是更新子元素
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})}
...
}
}
// src/core/observer/index.js
export function defineReactive (obj,key,val,customSetter,shallow) {
const property = Object.getOwnPropertyDescriptor(obj, key)
const getter = property && property.get
const setter = property && property.set
Object.defineProperty(obj, key, {
...
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
加载过程:父组件beforeCreate => 父组件created => 父组件beforeMount => 子组件beforeCreate => 子组件created => 子组件 beforeMount => 子组件mounted => 父组件mounted
更新过程:父组件beforeUpdate => 子组件beforeUpdate => 子组件updated => 父组件updated
销毁过程:父组件beforeDestroy => 子组件 beforeDestroy => 子组件 destoryed => 父组件 destoryed
Vue.use(MyPlugin)使用,本质上是调用MyPlugin.install(Vue)new Vue()启动应用之前完成,实例化之前就要配置好。Vue.use多次注册相同插件,那只会注册成功一次(if (plugin.installed) {return})。beforeEach 守卫。beforeRouteUpdate 守卫 (2.2+)。beforeEnter。beforeRouteEnter。beforeResolve 守卫 (2.5+)。afterEach 钩子。beforeRouteEnter 守卫中传给 next 的回调函数。router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用可以为<router-view>增加一个key
<router-view :key="$route.fullPath">
在正常模式下,直接修改state可以成功不会报错,只有在严格模式下(strict: true)才会报错,推荐在mutiation中修改state是为了更好的跟踪vuex状态的更改,保存状态快照,在调试工具中也可以看到每次的修改;在严格模式下,会通过$watch来监控每次date的变化;当state更改是判断store._commiting(只有在提交mutiation时才会变为true)是否为true;如果不是则报错
slot="slotName"替换成v-slot:slotName使用函数式组件(functional)v-for不和v-if一起使用v-if和v-showmixinObject.freeze()使数据不会变成响应式errorCaptured捕获组件级的错误,避免服务端渲染错误导致白屏const errorBoundary = Vue => {
Vue.component('ErrorBoundary', {
data: () => ({ error: null }),
errorCaptured(err, vm, info) {
this.error = `${err.stack}\n\nfound in ${info} of component`
SentryCapture(err, 1) //异常上报到sentry
return false
},
render() {
return (this.$slots.default || [null])[0] || null
}
})
}
// 全局注册
errorBoundaryVue.use(errorBoundary)
lru-cache缓存//nuxt.config.js配置
const LRU = require("lru-cache");
module.exports = {
render: {
bundleRenderer: {
cache: LRU({
max: 1000,
// 缓存队列长度
maxAge: 1000 * 60 // 缓存1分钟
})
}
}
};
// 需要做缓存的 vue 组件
export default {
name:'test',
serverCacheKey () {
// 缓存10秒
return Math.floor(Date.now() / 10000)
},
}
const LRU = require("lru-cache");
let cachePage = new LRU({
max: 100,// 缓存队列长度
maxAge: 1000 * 60// 缓存1分钟
});
export default function(req, res, next) {
let url = req._parsedOriginalUrl;
let pathname = url.pathname;
// 通过路由判断,只有首页才进行缓存
if (["/home"].indexOf(pathname) > -1) {
const existsHtml = cachePage.get("homeData");
if (existsHtml) {
return;
res.end(existsHtml.html, "utf-8");
} else {
res.original_end = res.end;
// 重写res.end
res.end = function(data) {
if (res.statusCode === 200) {
// 设置缓存
cachePage.set("homeData", {
html: data
});
}
// 最终返回结果
res.original_end(data, "utf-8");
};
}
}
next();
}
// nuxt.config.js
//针对home路由做缓存
export default {
serverMiddleware: [
{
path: "/home",
handler: "~/middleware/cache.js"
}
]
}
参考open in new window、参考open in new window参考open in new window
监控数组下标的能力,并且和对象表现基本一致(对于已有下标数组的更改可以监控到,对于新增的下标需要重新observe);但是出于性能等性价比的考虑,放弃这个特性,反而通过改写数组的方法,并将这些方法放在数组的__proto__来实现对数组操作的劫持现在的前端页面元素越来越多,结构也变得越来越复杂,当数据和视图混合在一起的时候对它们的处理会十分复杂,同时也很容易出现错误,而现代框架使用声明式语法,描述组件对象的嵌套关系,并自动生成与dom对象的对应关系参考1open in new window、参考2open in new window
| vue生命周期 | 描述 |
|---|---|
| beforeCreate | 组件实力被创建,el和数据对象都为undefined,还未初始化 |
| create | 数据已经被初始化,并且初始化了Vue内部事件,但是DOM还未生成 |
| befroeMount | 完成了模板的编译。把data对象里面的数据和vue的语法写的模板编译成了虚拟DOM |
| mouted | 执行了render函数,将渲染出来的内容挂载到了DOM节点上 |
| beforeUpdate | 组件更新之前:数据发生变化时,会调用beforeUpdate,然后经历DOM diff |
| updated | 组件更新后 |
| actived | keep-alive组件被激活 |
| deactivated | keep-alive移除 |
| beforeDestroy | 组件销毁前 |
| destroyed | 组件销毁后 |
可以问数据变动如何和视图联系在一起?
Vue是采用数据劫持结合发布者-订阅者模式的方式, Vue相应系统有三大核心:observe,dep,watcher;精简版Vue代码参考open in new window
Observeopen in new window:当一个Vue实例创建时,initData阶段,vue会遍历data选项的属性(observe),用 Object.defineProperty 将它们转为 getter/setter并且在内部追踪相关依赖(dep),在属性被访问和修改时通知变化。Compiteopen in new window:调用compile方法解析模版,当视图中有用到vue.data中数据的时候,会调用实例化watcher方法进行依赖收集Watcheropen in new window:是Observer和Compile之间通信的桥梁,当视图中遇到绑定的数据时,在watcher方法中会获取这个数据,此时会触发observe中的getter方法,Depopen in new window:发布订阅模式,observe中数据的getter被触发时会收集依赖的watcher(dep.depend方法)observe中数据的setter,此时会调用dep.notify方法给所有订阅的watcher发通知(通过回掉方式)进行视图更新,此时会进行diff流程: 
vue中的data为对象,是引用类型,当重用组件时,一个组件对data做了更改,那么另一个组件也会跟着改,而使用返回一个函数返回数据,则每次返回都是一个新对象,引用地址不用,所以就不会出现问题
虚拟DOM是一个JavaScript对象,包含了当前DOM的基本结构和信息,它的存在是为了减少对操作无用DOM所带来的性能消耗,是实现一种声明式的、状态驱动的 UI 开发理念的策略,在大量的、频繁的数据更新下能够对视图进行合理的高效的更新(细粒度的精准修改),让js业务逻辑和DOM操作解耦;同时也抽象了原来的渲染过程,实现了跨平台的能力;其缺点就是无法进行极致优化
精简源码open in new window;采用深度优先遍历,把树形结构按照层级分解,只比较同级元素;当数据发生改变时,set方法会让调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁(两个重要函数patchVnode和updateChildren):
sameVnode,如果不是的化,就会创建新的根结点并进行替换sameVnode,则进入patchVnode函数,其基本判断 oldVnode === vnode则直接return新节点是文本节点,则判断新旧文本节点是否一致,不一致(oldVnode.text !== vnode.text)则替换新节点不是文本节点,则开始比较新旧节点的子节点oldCh和ch:子节点都存在,则进行updateChildren计算(稍后讲)只有新子节点存在,则如果旧节点有文本节点,则移除文本节点,然后将新子节点拆入只有旧子节点存在,则移除所有子节点均无子节点且旧节点是文本节点,则移除文本节点(此时新节点一定不是文本节点)updateChildren函数做细致对比 idxInOld) 找到了idxInOld,如果是相同节点则移动旧节点到新的对应的地方,否则虽然key相同但元素不同,当作新元素节点去创建没有找到idxInOld,则创建节点老节点先遍历完,则新节点比老节点多,将新节点多余的插入进去新节点先遍历完,则就节点比新节点多,将旧节点多余的删除参考1open in new window参考2open in new window;主要是为了复用节点,高效的``准确的更新虚拟DOM,另外,在使用标签元素过渡效果时也会用到key,同时,以避免“原地复用”带来的副作用,如果没有key并且某些节点有绑定数据(表单)状态,会出现状态错位
initComputed,Watcher实例,并在内实例化一个Dep消息订阅器用作后续收集依赖,computed引用的时候会第一次执行计算属性,调用watcher的evaluate方法,将dirty设置为false,并将结果保存在this.value中进行缓存,computed会这直接返回this.valuecomputed所依赖的属性发生变化时会调用watcher的update方法将dirty设置为true,下次调用computed时就会重新计算class Watcher{
……
evaluate () {
this.value = this.get()
this.dirty = false
}
……
}
class initComputed{
……
//计算属性的getter 获取计算属性的值时会调用
createComputedGetter (key) {
return function computedGetter () {
//获取到相应的watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
//watcher.dirty 参数决定了计算属性值是否需要重新计算,默认值为true,即第一次时会调用一次
if (watcher.dirty) {
/*每次执行之后watcher.dirty会设置为false,只要依赖的data值改变时才会触发
watcher.dirty为true,从而获取值时从新计算*/
watcher.evaluate()
}
//获取依赖
if (Dep.target) {
watcher.depend()
}
//返回计算属性的值
return watcher.value
}
}
}
……
}
参考1open in new window、参考1open in new window、参考2open in new window
计算属性顾名思义就是通过其他变量计算得来的,它的值是基于其所依赖的属性来进行缓存的,只有在其所依赖的属性发生变化时才会从新求值watch是监听一个变量,当变量发生变化时,会调用对应的方法
vue实现响应式并不是数据一更新就立刻触发dom变化,而是按照一定的策略对dom进行更新,源码位置open in new window,原理:
callbacks数组中,$nextTick没有传cb回掉,则返回一个promisecallbacks的执行时机 promise,则用promise.resolve().then来执行callbacksMutationObserver,则用实例化的MutationObserver监听文本变化来执行回掉,setImmediate,则用setImmediate(cb)来执行回掉setTimeout(fn,0)来执行vue2.5.X版本open in new window中对于像v-on这样的DOM交互事件,默认走macroTimerFunc,也就是,跳过第一步promise的判断,initProps时,会对props进行defineReactive操作,传入的第四个参数是自定义的set报错判断函数,该函数会在触发props的set方法时执行// src/core/instance/state.js 源码路径
function initProps (vm: Component, propsOptions: Object) {
...
for (const key in propsOptions) {
if (process.env.NODE_ENV !== 'production') {
...
defineReactive(props, key, value, () => {
// 如果不是跟元素并且不是更新子元素
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})}
...
}
}
// src/core/observer/index.js
export function defineReactive (obj,key,val,customSetter,shallow) {
const property = Object.getOwnPropertyDescriptor(obj, key)
const getter = property && property.get
const setter = property && property.set
Object.defineProperty(obj, key, {
...
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
加载过程:父组件beforeCreate => 父组件created => 父组件beforeMount => 子组件beforeCreate => 子组件created => 子组件 beforeMount => 子组件mounted => 父组件mounted
更新过程:父组件beforeUpdate => 子组件beforeUpdate => 子组件updated => 父组件updated
销毁过程:父组件beforeDestroy => 子组件 beforeDestroy => 子组件 destoryed => 父组件 destoryed
Vue.use(MyPlugin)使用,本质上是调用MyPlugin.install(Vue)new Vue()启动应用之前完成,实例化之前就要配置好。Vue.use多次注册相同插件,那只会注册成功一次(if (plugin.installed) {return})。beforeEach 守卫。beforeRouteUpdate 守卫 (2.2+)。beforeEnter。beforeRouteEnter。beforeResolve 守卫 (2.5+)。afterEach 钩子。beforeRouteEnter 守卫中传给 next 的回调函数。router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用可以为<router-view>增加一个key
<router-view :key="$route.fullPath">
在正常模式下,直接修改state可以成功不会报错,只有在严格模式下(strict: true)才会报错,推荐在mutiation中修改state是为了更好的跟踪vuex状态的更改,保存状态快照,在调试工具中也可以看到每次的修改;在严格模式下,会通过$watch来监控每次date的变化;当state更改是判断store._commiting(只有在提交mutiation时才会变为true)是否为true;如果不是则报错
slot="slotName"替换成v-slot:slotName使用函数式组件(functional)v-for不和v-if一起使用v-if和v-showmixinObject.freeze()使数据不会变成响应式errorCaptured捕获组件级的错误,避免服务端渲染错误导致白屏const errorBoundary = Vue => {
Vue.component('ErrorBoundary', {
data: () => ({ error: null }),
errorCaptured(err, vm, info) {
this.error = `${err.stack}\n\nfound in ${info} of component`
SentryCapture(err, 1) //异常上报到sentry
return false
},
render() {
return (this.$slots.default || [null])[0] || null
}
})
}
// 全局注册
errorBoundaryVue.use(errorBoundary)
lru-cache缓存//nuxt.config.js配置
const LRU = require("lru-cache");
module.exports = {
render: {
bundleRenderer: {
cache: LRU({
max: 1000,
// 缓存队列长度
maxAge: 1000 * 60 // 缓存1分钟
})
}
}
};
// 需要做缓存的 vue 组件
export default {
name:'test',
serverCacheKey () {
// 缓存10秒
return Math.floor(Date.now() / 10000)
},
}
const LRU = require("lru-cache");
let cachePage = new LRU({
max: 100,// 缓存队列长度
maxAge: 1000 * 60// 缓存1分钟
});
export default function(req, res, next) {
let url = req._parsedOriginalUrl;
let pathname = url.pathname;
// 通过路由判断,只有首页才进行缓存
if (["/home"].indexOf(pathname) > -1) {
const existsHtml = cachePage.get("homeData");
if (existsHtml) {
return;
res.end(existsHtml.html, "utf-8");
} else {
res.original_end = res.end;
// 重写res.end
res.end = function(data) {
if (res.statusCode === 200) {
// 设置缓存
cachePage.set("homeData", {
html: data
});
}
// 最终返回结果
res.original_end(data, "utf-8");
};
}
}
next();
}
// nuxt.config.js
//针对home路由做缓存
export default {
serverMiddleware: [
{
path: "/home",
handler: "~/middleware/cache.js"
}
]
}
参考open in new window、参考open in new window参考open in new window
监控数组下标的能力,并且和对象表现基本一致(对于已有下标数组的更改可以监控到,对于新增的下标需要重新observe);但是出于性能等性价比的考虑,放弃这个特性,反而通过改写数组的方法,并将这些方法放在数组的__proto__来实现对数组操作的劫持