一、简答题
1、当我们点击按钮的时候动态给 data 增加的成员是否是响应式数据,如果不是的话,如何把新增成员设置成响应式数据,它的内部原理是什么。
let vm = new Vue({
el: '#el'
data: {
dog: {}
},
methods: {
clickHandler () {
// 该 name 属性是否是响应式的
this.dog.name = 'Trump'
}
}
})
答:
不是响应式数据,可以使用 Vue.set 或者 this.$set 将新增成员设置成响应式数据。
Vue.set(this.dog, 'name', 'Trump')
内部原理: 当创建 vue 实例的时候,循环遍历了 data 中的所有属性将其转换为 get/set,并在 get 方法中收集依赖,在 set 方法中触发观察者的 update 方法进行页面的更新。而直接给 data 添加成员并没有实现以上操作,而Vue.set 方法则将新添加的属性也转换为了 get/set,并在页面对应的位置生成了观察者实例。
2、请简述 Diff 算法的执行过程
diff 算法用于查找两棵树每一个节点的差异。当数据变化后,不直接操作 DOM,而是先比较 js 对象(vnode)是否发生变化,找到所有变化的位置,只最小化的更新变化的位置,从而提高性能。因为 DOM 操作时很少会跨级别操作节点,所以可以只比较同级别的节点,从而减少比较次数。
sameVnode 比较的就是 key 和 sel 相同。如果2个节点是 sameVnode,会重用之前的旧节点对应的 DOM,patchVnode 会对比差异然后将更新应用到重用的 DOM 元素上,如果文本元素和子元素也相同的话,就无需再操作 DOM。
同级别节点比较,直到新子节点或者旧子节点有一个全部遍历完
1. oldCh 的开始节点 == newCh 的开始节点
当比较两个节点的时候,先会比较两个节点的开始节点是否为 sameVnode,如果是则调用 patchVnode 比较差异然后更新到真实 DOM 上,然后将开始节点置为第2个节点接着比较。
2. oldCh 的结束节点 == newCh 的结束节点
当比较到开始节点不再相等,就比较两个节点的结束节点,,如果是 sameVnode,则调用 patchVnode 比较差异然后更新到真实 DOM 上,然后将开始节点置为倒数第2个节点接着比较。
3. oldCh 的开始节点 == newCh 的结束节点
比较 oldCh 的开始节点和 newCh 的结束节点,如果是 sameVnode,则调用 patchVnode 比较差异然后更新到真实 DOM 上,并将 oldCh 的开始节点对应的 DOM 元素移动到 parentElm 的最后(不是移动 oldCh,而是移动真实 DOM),然后将 oldCh 的第2个节点置为开始节点,将 newCh 的倒数第2个节点置为结束节点,接着比较。
4. oldCh 的结束节点 == newCh 的开始节点
比较 oldCh 的结束节点和 newCh 的开始节点,如果是 sameVnode,则调用 patchVnode 比较差异然后更新到真实 DOM 上,并将 oldCh 的结束节点对应的 DOM 元素移动到 parentElm 的开始(不是移动 oldCh,而是移动真实 DOM),然后将 oldCh 的倒数第2个节点置为结束节点,将 newCh 的第2个节点置为开始节点,接着比较。
5. oldCh 的开始节点 !== newCh 的任何节点
不是上述4种情况的,遍历 oldCh 中的节点,没有查找到 newCh 的开始节点,说明 newCh 中的开始节点是一个新的节点,此时需要创建新的 DOM 元素,并插入到 oldCh 对应的 DOM 元素的最前面。
6. oldCh 的开始节点 == newCh 的中间某一节点
不是上述4种情况的,遍历 oldCh 中的节点,其中查找到了 newCh 的开始节点,则调用 patchVnode 比较差异然后更新,再将这个节点移动到 oldCh 对应的 DOM 元素的最前面。
结束遍历后,处理剩余节点
1. 老节点先遍历完
说明新节点有剩余,把新节点的剩余节点调用 addVnodes 批量插入到 oldCh 对应的 DOM 元素的最右边
2. 新节点先遍历完
说明老节点有剩余,调用 removeVnodes 把老节点的剩余节点批量删除
二、编程题
1、模拟 VueRouter 的 hash 模式的实现,实现思路和 History 模式类似,把 URL 中的 # 后面的内容作为路由的地址,可以通过 hashchange 事件监听路由地址的变化。
hash模式: 通过location.hash修改hash,触发更新; 通过监听hashchange事件监听浏览器前进或后退,触发更新。
history模式: 通过history.pushState修改浏览器地址,触发更新; 通过监听popstate事件监听浏览器前进或后退,触发更新。
/* eslint-disable */
let _Vue = null
export default class VueRouter{
static install(Vue) {
if (VueRouter.install.installed) return
VueRouter.install.installed = true
_Vue = Vue
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$route = this.$options.router
}
}
})
}
constructor(options) {
this.options = options
this.routeMap = {}
this.data = _Vue.observable({ current: '/' })
this.init()
}
init() {
this.createRouteMap()
this.initComponent(_Vue)
this.initEvent()
}
createRouteMap() {
this.options.routes.forEach((route) => {
this.routeMap[route.path] = route.component
})
}
initComponent(Vue) {
const self = this
Vue.component('router-link', {
props: { to: String },
render(h) {
return h('a', {attrs: { href: this.to }, on: { click: this.clickHandler }}, [this.$slots.default])
},
methods: {
clickHandler(e) {
e.preventDefault()
// history 模式
// history.pushState({}, "", this.to)
// hash 模式
window.location.hash = this.to
self.data.current = this.to
}
}
})
Vue.component('router-view', {
render(h) {
return h(self.routeMap[self.data.current])
}
})
}
initEvent() {
window.addEventListener('hashchange', () => {
this.data.current = window.location.hash.substring(1)
})
// window.addEventListener('popState', () => {
// this.data.current = window.location.pathname
// })
}
}
2、在模拟 Vue.js 响应式源码的基础上实现 v-html 指令,以及 v-on 指令。
vue.js 中添加 _setMethods 方法将 methods 都添加到 vm 实例上:
constructor(options) {
this._ProxyData(this.$data)
+ this._setMethods(this.$options.methods)
}
+ _setMethods(methods) {
+ Object.keys(methods).forEach(key => {
+ this[key] = methods[key]
+ })
+ }
compiler 中修改 update 方法,将 v-on 后跟的事件名拆分出来:
update(attrName, node, key) {
// v-on:click 需要特殊处理
let eventName
[attrName, eventName] = attrName.split(':')
let updateFn = this[attrName.substr(2) + 'Updater']
updateFn && updateFn.call(this, node, key, this.vm[key], eventName)
}
compiler 中添加 v-on 和 v-html 的处理函数:
// 实现 v-html 和 v-on
htmlUpdater(node, key, value) {
node.innerHTML = value
new Watcher(this.vm, key, (newValue) => {
node.innerHTML = newValue
})
}
onUpdater(node, key, value, eventName) {
node.addEventListener(eventName, () => {
this.vm[key]()
})
}