大家好,我是林一一,这是一篇关于 vue 的原理面试题,如果能够完全弄懂相信对大家很有帮助。
MPA 多页面应用。
html 构成,URL/cookie/localStorage会重新加载SPA单页面应用不会重新加载iframe 实际上是
MPA,但是可以实现SPA的一些效果,但是本身由不少问题。
目的:借鉴后端的思想,职责划分和分层
view。MVC模式
MVC.jpg
单向的数据,用户的每一步操作都需要重新请求数据库来修改视图层的渲染,形成一个单向的闭环。比如
jQuery+underscore+backbone。
model 数据存放层view:视图层 页面controller:控制器 js 逻辑层。
controller控制层将数据层model层的数据处理后显示在视图层view层,同样视图层view层接收用户的指令也可以通过控制层controller,作用到数据层model。所以MVC的缺点是视图层不能和数据层直接交互。
MVVM模式
隐藏了
controller控制层,直接操控View视图层和Model数据层。
MVVM.jpg
双向的数据绑定:
model数据模型层通过数据绑定Data Bindings直接影响视图层View,同时视图层view通过监听Dom Listener也可以改变数据模型层model。
viewModel 层 Vue 主要做的事。也就是说:只要将 数据模型层Model 的数据挂载到 ViewModel 层 Vue 就可以实现双向的数据绑定。vuex/redux 可以作为 vue和react 的 model 数据层。
var vm = new Vue()vm 就是
view-model数据模型层,data:就是vmview-model层所代理的数据。
controller 属于单向链接。MVVM 隐藏了控制层 controller,让视图层和数据层可以直接交互 属于双向连接。小tip:响应式数据指的是数据发生了变化,视图可以更新就是响应式的数据
vue 中实现了一个 definedReactive 方法,方法内部借用 Object.definedProperty() 给每一个属性都添加了 get/set 的属性。definedReactive 只能监控到最外层的对象,对于内层的对象需要递归劫持数据。push pop shift unshift reverse sort splice 来给数组做数据拦截,因为这几个方法会改变原数组// src\core\observer\index.js export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 准备给属性添加一个 dep 来依赖收集 Watcher 用于更新视图。 const dep = new Dep() // some code
// observe() 用来观察值的类型,如果是属性也是对象就递归,为每个属性都加上get/set let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // 这里取数据时依赖收集 const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() // childOb 是对对像进行收集依赖 if (childOb) { childOb.dep.depend()
//这里对数组和内部的数组进行递归收集依赖,这里数组的 key 和 value 都有dep。 if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { // 属性发生改变,这里会通知 watcher 更新视图 }
}) }上面的 Dep(类) 是用来干嘛的?答:用来收集渲染的
Watcher,Watcher又是一个啥东西?答:watcher是一个类,用于更新视图的
definedProperty() 来数据拦截,而是通过重写数组的方法push pop shift unshift reverse sort splice。对象和数组)的话会进行数据拦截。arr[0] = 1, arr.length = 2 都不会有响应式// src\core\observer\array.js const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'] methodsToPatch.forEach(function (method) { const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.ob let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } // 新增的类型再次观察 if (inserted) ob.observeArray(inserted) // 手动调用 notify 派发更新 ob.dep.notify() return result }) })dep.jpg
tip:
Dep是一个用来负责收集Watcher的类,Watcher是一个封装了渲染视图逻辑的类,用于派发更新的。需要注意的是Watcher 是不能直接更新视图的还需要结合Vnode经过patch()中的diff算法才可以生成真正的DOM
dep 属性,来存放依赖的 Watcher,属性发生变化后会通知 Watcher 去更新。getter) 数据时 Vue 给每一个属性都添加了 dep 属性来(collect as Dependency)收集 Watcher。在用户 setting 设置属性值时 dep.notify() 通知 收集的Watcher 重新渲染。详情见上面的 defineReactive()Dep依赖收集类 其和 Watcher类 是多对多双向存储的关系Watcher 类,因为属性可能在不同的组件中被使用。Watcher 类 也可以对应多个属性。Vue中模板编译:其实就是将
template转化成render函数。说白了就是将真实的DOM(模板)编译成虚拟dom(Vnode)
template 模板字符串转换成 ast 语法树(parser 解析器),这里使用了大量的正则来匹配标签的名称,属性,文本等。static 标记,主要用来做虚拟 DOM 的渲染优化(optimize优化器),这里会遍历出所有的子节点也做静态标记ast语法树 重新生成 render 函数 代码字符串 code。(codeGen 代码生成器)为什么要静态标记节点,如果是静态节点(没有绑定数据,前后不需要发生变化的节点)那么后续就不需要 diff 算法来作比较。
Vue.mixin({})混入的钩子或生命周期中定义了多个函数,vue 内部会调用mergeHook() 对钩子进行合并放入到队列中依次执行// src\core\util\options.js function mergeHook ( parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function> ): ?Array<Function> { const res = childVal ? parentVal ? parentVal.concat(childVal) // 合并 : Array.isArray(childVal) ? childVal : [childVal] : parentVal return res ? dedupeHooks(res) : res }beforeCreate: 刚开始初始化 vue 实例,在数据观测observer之前调用,还没有创建 data/methods 等属性created: vue 实例初始化结束,所有的属性已经创建。beforeMount: 在 vue 挂载数据到页面上之前,触发这个钩子,render 函数此时被触发。mounted: el 被 创建的vm.$el替换,vue 初始化的数据已经挂载到页面之上,这里可以访问到真实的 DOM。一般会在这里请求数据。beforeUpdate: 数据更新时调用,也就是在虚拟 dom 重新渲染之前。updated: 数据变化导致虚拟 dom 发生重新渲染之后发生。beforeDestroy: 实例销毁之前调用该钩子,此时实例还在。vm.$destroy 触发两个方法。destroyed: Vue 实例销毁之后调用。所有的事件监听都会被接触。请求数据要看具体的业务需求决定在哪里发送
ajax
mergeOptions() 方法采用策略模式针对不同的属性合并。混入的数据和组件的数据有冲突就采用组件本身的。Vue.mixin({}) 缺陷,1.可能会导致混入的属性名和组件属性名发生命名冲突;2. 数据依赖的来源问题export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object { // some code if (!child._base) { if (child.extends) { parent = mergeOptions(parent, child.extends, vm) } if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm) } } }
// 递归遍历合并组件和混入的属性 const options = {} let key for (key in parent) { mergeField(key) } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key) } } function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) } return options }data 函数中返回的对象引用地址不同,就能保证不同组件之间的数据不相互污染。Vue.mixin() 中如果混入data属性,那么 data 也必须是一个函数。因为Vue.mixin()也可以多处使用。data可以是一个对象也可以是一个函数,因为我们一个页面一般只初始化一个Vue实例(单例)在 dom 更新循环结束后调用,用于获取更新后的 dom 数据vm.$nextTick(cb) 是一个异步的方法为了兼容性做了很多降级处理依次有 promise.then,MutationObserver,setImmediate,setTimeout。在数据修改后不会马上更新视图,而是经过 set 方法 notify 通知 Watcher 更新,将需要更新的 Watcher 放入到一个异步队列中,nexTick 的回调函数就放在 Watcher 的后面,等待主线程中同步代码执行借宿然后依次清空队列中,所以 vm.nextTick(callback) 是在 dom 更新结束后执行的。上面将对列中
Watcher依次清空就是vue 异步批量更新的原理。提一个小思考:为什么不直接使用setTimeout代替?因为setTimeout是一个宏任务,宏任务多性能也会差。关于事件循环可以看看 JS 事件循环
computed 内部就是根据 Object.definedProperty() 实现的computed 具备缓存功能,依赖的值不发生变化,就不会重新计算。watch 是监控值的变化,值发生变化时会执行对应的回调函数。computed 和 watch 都是基于 Watcher类 来执行的。
computed缓存功能依靠一个变量dirty,表示值是不是脏的默认是true,取值后是false,再次取值时dirty还是false直接将还是上一次的取值返回。
// src\core\instance\state.js computed 取值函数
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) { // 判断值是不是脏 dirty
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}// src\core\instance\state.js watch 实现
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
// 实例化 watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}谢谢大家阅读到这里,如果觉得写的还可以,欢迎三连呀,我是林一一,下次见。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。