原文地址
vue(前端框架)解决了什么问题?
现在的前端页面元素越来越多,结构也变得越来越复杂, 当数据和视图混合在一起的时候对它们的处理会十分复杂,同时也很容易出现错误,而现代框架 使用声明式语法,描述组件对象的嵌套关系,并自动生成与 dom 对象的对应关系
参考 1
vue 生命周期
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 是采用数据劫持结合发布者 - 订阅者模式的方式, Vue 相应系统有三大核心:observe
,dep
,watcher
; 精简版 Vue 代码参考
-
Observe
:当一个 Vue 实例创建时,initData 阶段,vue 会遍历 data 选项的属性(observe),用 Object.defineProperty 将它们转为 getter/setter 并且在内部追踪相关依赖(dep),在属性被访问和修改时通知变化。 -
Compite
:调用compile
方法解析模版, 当视图中有用到 vue.data 中数据的时候,会调用实例化watcher
方法进行依赖收集 -
Watcher
:是Observer
和Compile
之间通信的桥梁,当视图中遇到绑定的数据时, 在watcher
方法中会获取这个数据,此时会触发observe
中的getter
方法, -
Dep
:发布订阅模式,observe
中数据的getter
被触发时会收集依赖的watcher
(dep.depend 方法) - 当有数据被改动时会触发
observe
中数据的setter
,此时会调用dep.notify
方法给所有订阅的watcher
发通知(通过回掉方式)进行视图更新,此时会进行 diff 流程:
vue 中 data 为什么必须要是一个函数
vue 中的 data 为对象,是引用类型,当重用组件时,一个组件对 data 做了更改,那么另一个组件也会跟着改,而使用返回一个函数返回数据,则每次返回都是一个新对象,引用地址不用,所以就不会出现问题
Virtual DOM 是什么
虚拟 DOM 是一个 JavaScript 对象,包含了当前 DOM 的基本结构和信息,它的存在是为了减少对操作无用 DOM 所带来的性能消耗,在大量的、频繁的数据更新下能够对视图进行合理的高效的更新(细粒度的精准修改),同时也抽象了原来的渲染过程,实现了跨平台的能力
简述 vue 中的 DOM DIFF 算法
精简源码;当数据发生改变时,set
方法会让调用 Dep.notify
通知所有订阅者 Watcher
,订阅者就会调用patch
给真实的 DOM 打补丁 (两个重要函数patchVnode
和updateChildren
):
- 先判断根结点及变化后的节点是否是
sameVnode
,如果不是的化,就会创建新的根结点并进行替换 -
如果是
sameVnode
,则进入patchVnode
函数,其基本判断- 如果两个节点是相等
oldVnode === vnode
则直接return
- 如果
新节点是文本节点
,则判断新旧文本节点是否一致,不一致(oldVnode.text !== vnode.text
) 则替换 - 如果
新节点不是文本节点
,则开始比较新旧节点的子节点oldCh
和ch
: - 如果
子节点都存在
,则进行updateChildren
计算(稍后讲) - 如果
只有新子节点存在
, 则如果旧节点有文本节点,则移除文本节点,然后将新子节点拆入 - 如果
只有旧子节点存在
,则移除所有子节点 - 如果
均无子节点且旧节点是文本节点
,则移除文本节点(此时新节点一定不是文本节点)
- 如果两个节点是相等
-
updateChildren
函数做细致对比- start && oldStart 对比
- end && oldEnd 对比
- start && oldEnd 对比
- end && oldStart 对比
- 生成 map 映射,(key: 旧子节点上的 key,value: 旧子节点在自己点中的位置), 根据 key 记录下老节点在新节点的位置(
idxInOld
)
1) 如果找到了 idxInOld
, 如果是相同节点
则移动旧节点到新的对应的地方,否则虽然key
相同但元素不同,当作新元素节点去创建
2) 如果没有找到 idxInOld
,则创建节点 - 如果
老节点先遍历完
,则新节点比老节点多, 将新节点多余的插入进去 - 如果
新节点先遍历完
,则就节点比新节点多, 将旧节点多余的删除
vue 中 key 的作用
主要是为了复用节点,高效的更新虚拟 DOM,另外,在使用标签元素过渡效果时也会用到 key
computed 的原理
- vue 对象初始化的同时对计算属性进行初始化
initComputed
, - computed 会对初始化的 Watcher 传入
lazy: true
就会触发 Watcher 中的watcher.dirty=true
(dirty 决定了当前属性是否更新), - 当视图中有对
computed
引用的时候会第一次执行计算属性,并将dirty
设置为false
, 并将结果保存在this.value
中进行缓存, - 如果依赖没有更改,则下次获取
computed
会这直接返回this.value
, 只有当computed
所依赖的属性发生变化时会将dirty
设置为true
,并重新计算
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
}
}
}
……
}
计算属性 computed 和 watch 的区别
计算属性
顾名思义就是通过其他变量计算得来的,它的值是基于其所依赖的属性来进行 缓存
的, 只有在其所依赖的属性发生变化时才会从新求值 watch
是监听一个变量,当变量发生变化时,会调用对应的方法
对 $nextTick 的理解
vue 实现响应式并不是数据一更新就立刻触发 dom 变化,而是按照一定的策略对 dom 进行更新,源码位置,原理:
- 首先会将所有的 nextTick 放到一个函数中,然后放在
callbacks
数组中,$nextTick
没有传cb
回掉,则返回一个promise
-
接下来就是
callbacks
的执行时机- 首先如果浏览器是否兼容
promise
,则用promise.resolve().then
来执行callbacks
- 如果浏览器兼容
MutationObserver
, 则用实例化的MutationObserver
监听文本变化来执行回掉, - 如果兼容
setImmediate
, 则用setImmediate(cb)
来执行回掉 - 最后降级为用
setTimeout(fn,0)
来执行
- 首先如果浏览器是否兼容
- 在
vue2.5.X
版本中对于像v-on
这样的 DOM 交互事件,默认走macroTimerFunc
,也就是,跳过第一步promise
的判断,
子组件为何不可以修改父组件传递的 Prop, 是如何监控并给出错误提示的
- 单向数据流,易于监测数据的流动,出现了错误可以更加迅速的定位到错误发生的位置
- 在
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-router 的导航解析流程
官网
- 导航被触发。
- 在失活的组件里调用离开守卫。
- 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫 (2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫 (2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 用创建好的实例调用
beforeRouteEnter
守卫中传给 next 的回调函数。
router.beforeResolve
注册一个全局守卫。这和 router.beforeEach
类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用