vue核心面试题(vue响应式数据的原理)
一、谈一下你对MVVM原理的理解1.MVC是Model-View- Controller的简写。即模型-视图-控制器。是最早的架构模型,是从前台到后台统一称之为MVC,比如前端交视图层(View),后端的数据库(Model),用户操作界面想获取数据会向后端服务器发起请求,请求会被路由拦截,这时路由会调用对应的控制器来处理,控制器会获取数据。将结果返回给前端,页面重新渲染 。这种方向是单向的。2.随
响应式数据的原理?
1.首先回答对响应式的个理解,提出核心点是通过Object.defineProperty, Vue 在初始化数据时,会传入一个data对象,内部会默认的对data对象进行遍历,使用 Object.defineProperty 重新定义所有属性,当页面取到对应属性时。会进行依赖收集(收集当前组件的watcher) 如果属性发生变化会通 知相关依赖进行更新操作。
2.为什么要使用Object.defineProperty?
它的特点可以使数据的获取和设置增加一个拦截的功能,我们可以在获取和设置的时候增加一些我们自己的逻辑,这个逻辑叫做依赖收集。比如我们取数据的时候可以收集一些依赖,过一会数据变化了可以告诉这些收集的依赖去更新。
3.原理:在vue初始化的时候,会调用一个方法initData,用来初始化用户传入的data数据,然后new Observer,对数据进行观测,如果数据是个对象类型非数组的话,就会调一个this.walk(value)方法进行对象的处理,将对象进行遍历,然后使用defineReactive重新定义,采用的就是Object.defineProperty。
总结:
Vue采用数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,vue在初始话的时候,在initState方法中会调取initData方法初始化data数据,对data对象进行遍历,在这个方法中会调observe(监听器,观察者)对用户的数据进行监听,在observe中会对对象new Observe实例化创建监听,在observe中对数据进行判断,如果是数组执行observeArray深度进行监听,继续执行observe方法,如果当前传入的是对象则执行this.walk,对对象进行循环,重新定义对象的属性,这时使用的就是defineReactive,它是vue中的一个核心方法,用来定义响应式。在defineReactive方法中实例化了一个Dep(发布者),通过Object.defineProperty对数据进行拦截,把这些 property 全部转为 getter/setter。get数据的时候,通过dep.depend触发Watcher(订阅者)的依赖收集,收集订阅者,如果数据是数组,执行dependArray,对数组中的每个值通过depend都添加到依赖。set时,会对数据进行比较,如果数据发生了变化会通过dep.notify发布通知,通知watcher,更新视图。
4.下面来看源码
文件路径:src/core/instance/state.js
源码:
(1)vue在state数据初始化的时候会掉state.js中的一个initState方法
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (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)
}
}
(2)initData(初始化data)
function initData (vm: Component) {
let data = vm.$options.data // 用户传入的数据
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
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
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
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(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */) // 调observe对用户的数据进行观测
}
(3)observe方法(文件路径:src/core/observe/index.js,专门用来控制数据响应式处理的)
里面会去判断,看数据是否已经被观测过了,如果没又被观测过就会new Observer去观测这个对象
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) { // 不是对象不进行监听
return
}
let ob: Observer | void
// 如果存在__ob__属性,说明该对象没被observe过,不是observer类
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && // 是否可扩展
!value._isVue
) {
ob = new Observer(value) // 监听对象
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
(4)new Observer实例(监听的是两种类型,一种是数组,一种是对象)
export class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) { // 判断是否是数组
if (hasProto) {
protoAugment(value, arrayMethods) // 改写数组原型方法
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value) // 去深度监听数组中的每一项
} else { // 如果是对象走这儿
this.walk(value) // 重新哦定义对象类型数据
}
}
// 对当前传入的对象进行循环,重新定义对象的所有属性
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) // defineReactive是核心的方法,定义响应式
}
}
// 深度监听
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
(5)defineReactive是vue中的核心方法,用来定义响应式,该方法中使用了Object.defineProperty()
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 实例化一个Dep,这个Dep存在在下面的get和set函数的作用域中,用于收集订阅数据更新的Watcher
// 这里一个Dep与一个属性(即参数里的key)相对应,一个Dep可以有多个订阅者。
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val) // 如果还是一个对象,会继续调用observe方法,递归监听
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () { // 数据的取值
// 如果这个属性在转化之前定义过getter,那么调用该getter得到value的值,否则直接返回val。
const value = getter ? getter.call(obj) : val
/*
*注意这里,这里是Dep收集订阅者的过程,只有在Dep.target存在的情况下才进行这个操作
*在Watcher收集依赖的时候才会设置Dep.target
*所以Watcher收集依赖的时机就是Dep收集订阅者的时机。
*调用get的情况有两种
*一是Watcher收集依赖的时候(此时Dep收集订阅者)
*二是模板或js代码里用到这个值,这个时候是不需要收集依赖的,只要返回值就可以了。
*/
if (Dep.target) {
dep.depend() // 收集依赖 watcher,当数据变了,会通知watcher更新数据
// 不仅这个属性需要添加到依赖列表中,如果这个属性对应的值是对象或数组
// 那么这个属性对应的值也需要添加到依赖列表中
if (childOb) {
childOb.dep.depend()
// 如果是数组,那么数组中的每个值都添加到依赖列表里
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) { // 数据更新
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
// 当前的值和数据的值不一样的话,就会掉一个核心的方法dep.notify()
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify() // 触发数据对应的依赖进行更新,它就会通知视图更新
}
})
}
(6)dependArray()方法,如果不是对象是数组调用该方法
在调用这个函数的时候,数组已经被observe过了,且会递归observe。(看上面defineReactive函数里的这行代码:var childOb = observe(val);)
所以正常情况下都会存在__ob__属性,这个时候就可以调用dep添加依赖了。
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend() // 添加依赖
if (Array.isArray(e)) {
dependArray(e) // 如果还是数据继续调用
}
}
}
(7)dep,(用来添加依赖,通知watcher更新数据)
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++ // 每个实例中的dep实例的id都是从0开始累加的
this.subs = []
}
// 添加一个订阅者
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 删除一个订阅者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 追加依赖
//让Watcher收集依赖并添加订阅者。
//Dep.target是一个Watcher, 可以查看Watcher的addDep方法。
//这个方法做的事情是:收集依赖后,调用了Dep的addSub方法,给Dep添加了一个订阅者
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知watcher更新,通过调用subs里面的每个Watcher的update发布更新
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 这儿会执行watcher文件下的update方法去更新数据
}
}
}
5.getter/setter方法拦截数据的不足
(1)当对象增删的时候,是监控不到的,因为在observe data的时候,会遍历已有的每个属性添加getter/setter,而后面设置的属性并没有机会设置getter/setter,所以检测不到变化,同样的,删除对象属性的时候,getter/setter会跟着属性一起被删除掉,拦截不到变化。可以通过vue.set和vue.delete去更新数据。
(2)getter/setter是针对对象的,对于数组的修改也是监控不到的。可以通过调用数组的变异方法,或对数组进行深度拷贝克隆来达到更新视图。
二、相关概念
1.双向数据绑定
M ,即 model,指的是模型,也就是数据;V 即view,指的是视图,也就是页面展现的部分。
双向数据绑定为:每当数据有变更时,会进行渲染,从而更新视图,使得视图与数据保持一致(model到view层);而另一方面,页面也会通过用户的交互,产生状态、数据的变化,这个时候,这时需要将视图对数据的更新同步到数据(view到model层)。
我们上面说的Vue的数据响应式原理其实就是实现数据到视图更新原理,而视图到数据的更新,其实就是此基础上给可表单元素(input等)添加了change等事件监听,来动态修改model和 view。
2.发布-订阅者模型
订阅发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。
Vue中的Dep和Watcher共同实现了这个模型
更多推荐
所有评论(0)