【作业】手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法
- 【简答题】一、当我们点击按钮的时候动态给 data 增加的成员是否是响应式数据,如果不是的话,如何把新增成员设置成响应式数据,它的内部原理是什么。
- 【简答题】二、请简述 Diff 算法的执行过程
- 【编程题】一、模拟 VueRouter 的 hash 模式的实现,实现思路和 History 模式类似,把 URL 中的 # 后面的内容作为路由的地址,可以通过 hashchange 事件监听路由地址的变化。
- 【编程题】二、在模拟 Vue.js 响应式源码的基础上实现 v-html 指令,以及 v-on 指令。
- 【编程题】三、参考 Snabbdom 提供的电影列表的示例,利用Snabbdom 实现类似的效果,如图:
【简答题】一、当我们点击按钮的时候动态给 data 增加的成员是否是响应式数据,如果不是的话,如何把新增成员设置成响应式数据,它的内部原理是什么。
let vm = new Vue({
el: '#el'
data: {
o: 'object',
dog: {
}
},
method: {
clickHandler () {
// 该 name 属性是否是响应式的
this.dog.name = 'Trump'
}
}
})
不是想响应式数据
当创建好 Vue 实例后,新增一个成员,此时 data 并没有定义该成员,data 中的成员是在创建 Vue 对象的时候 new Observer 来将其设置成响应式数据,当 Vue 实例化完成之后,再添加一个成员,此时仅仅是给 vm 上增加了一个js属性而已,因此并不是响应式的
Vue 文档中给出了解决方案 当新增一个属性时,如何将其转化为响应式数据
对于已经创建的实例,Vue不允许动态添加根级别的响应式属性。但是可以使用
Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式属性。您还可以使用vm.$set
实例方法,这也是全局Vue.set
方法的别名。
// this.$set()的源码
vue.property.$set = set
Vue.set 内部原理:
源码位置: vue/src/core/observer/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
...
// 判断当前target是不是数组,并且key的值是有效的数组索引
// 这块代码意思是在修改数组时调用set方法时让我们能够触发响应的代码
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 类似$vm.set(vm.$data.arr, 0, 3)
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// target为对象, key在target或者target.prototype上。
// 并且key不是Object原型上的属性
// 说明这个key本来就在对象上面已经定义过了的,直接修改值就可以了,可以自动触发响应
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 以上都不成立, 即开始给target创建一个全新的属性
// vue给响应式对象都加了一个__ob__属性,如果一个对象有这个__ob__属性,
// 那么就说明这个对象是响应式对象,我们修改对象已有属性的时候就会触发页面渲染
// 获取Observer实例
const ob = (target: any).__ob__
// Vue 实例对象拥有 _isVue 属性, 即不允许给 Vue 实例对象添加属性
// 也不允许Vue.set/$set 函数为根数据对象(vm.$data)添加属性
// 即 当前的target对象是vue实例对象或者是根数据对象,那么就会抛出错误警告
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// target本身就不是响应式数据, 不需要响应,那么直接赋值返回即可
if (!ob) {
target[key] = val
return val
}
// 进行响应式处理
// 给新加的属性添加依赖,以后再直接修改这个新的属性的时候就会触发页面渲染
defineReactive(ob.value, key, val)
// 触发当前的依赖(这里的依赖依然可以理解成渲染函数),所以页面就会进行重新渲染
ob.dep.notify()
return val
}
【简答题】二、请简述 Diff 算法的执行过程
- 执行过程:
- 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引
- 在对开始和结束节点比较的时候,总共有四种情况
- oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
- oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
- oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点)
- oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)
- 开始节点和结束节点比较,这两种情况类似
- oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
- oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
- 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同)
- 调用 patchVnode() 对比和更新节点
- 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++
- oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同
- 调用 patchVnode() 对比和更新节点
- 把 oldStartVnode 对应的 DOM 元素,移动到右边
- 更新索引
- oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同
- 调用 patchVnode() 对比和更新节点
- 把 oldEndVnode 对应的 DOM 元素,移动到左边
- 更新索引
- 如果不是以上四种情况
- 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点
- 如果没有找到,说明 newStartNode 是新节点
- 创建新节点对应的 DOM 元素,插入到 DOM 树中
- 如果找到了
- 判断新节点和找到的老节点的 sel 选择器是否相同
- 如果不相同,说明节点被修改了
- 重新创建对应的 DOM 元素,插入到 DOM 树中
- 如果相同,把 elmToMove 对应的 DOM 元素,移动到左边
- 循环结束
- 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束
- 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束
- 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边
- 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批量删除
【编程题】一、模拟 VueRouter 的 hash 模式的实现,实现思路和 History 模式类似,把 URL 中的 # 后面的内容作为路由的地址,可以通过 hashchange 事件监听路由地址的变化。
let _Vue = null
export default class VueRouter {
static install (Vue) {
// 1.判断当前插件是否已经被安装
// 如果插件已经安装直接返回
if (VueRouter.install.installed && _Vue === Vue) return
VueRouter.install.installed = true
// 2.把 Vue 构造函数记录到全局变量
_Vue = Vue
// 3.把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上