# Task-03-01 **Repository Path**: fishlyn/Task-03-01 ## Basic Information - **Project Name**: Task-03-01 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-07-12 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Part3 模块一作业 ## 一、简答题 ### 1、当我们点击按钮的时候动态给 data 增加的成员是否是响应式数据,如果不是的话,如何把新增成员设置成响应式数据,它的内部原理是什么。 ```js let vm = new Vue({ el: '#el' data: { o: 'object', dog: {} }, method: { clickHandler () { // 该 name 属性是否是响应式的 this.dog.name = 'Trump' } } }) ``` 答:**动态给 data 新增的成员是响应式数据**,因为在实例化 vue 的时候,给 data 添加了 set 方法,当修改 data 对象的时候会触发 set 方法,从而触发将数据转换成响应式数据的方法,将新增的对象转换成响应式 > 但是给 vm 实例对象直接添加属性的时候并不会转换成响应式数据,因为并没有触发内部的转换成响应式数据的方法,此时我们可以向外暴露一个转换的方法,将要转换的对象传入这个方法,再通过内部的转换成响应式数据的方法转换下,再添加到实例属性中 ### 2、请简述 Diff 算法的执行过程 答:Diff 算法是为了解决,用对象来描述 DOM 树,通过对比两个对象的差异,从而更新 DOM 树的操作 一般我们最先想到的是将两个对象的每个属性依次对比,以此来确定两个对象的差异,但是这样的方式时间复杂度是 O(n^3) 为了减少时间复杂度,结合在实际的开发过程中,比较的都是相似的 DOM 树结构,不会将完全不同或者父子关系的对象进行对比,因此,我们可以只比较同级别的子属性,然后再找下一级别的子属性进行比较 Diff 算法执行过程,会依次执行不同的算法 1、首先会判断两个对象开始节点,使用 sameVnode 判断是否是相同节点,若是相同节点,则执行 patchVnode 比较节点的差异和更新节点,并将索引往后移,比较下一个节点,若是不相同,则进行下一个比较方式 2、判断结束节点是否相同,和判断开始节点一样,若是相同节点,则执行 patchVnode 比较和更新节点,并将索引往前移,若是节点不同,则进行下一个比较方式 3、判断旧开始节点和新结束节点是否相同,若是相同,则将旧开始节点移动到最末端,旧开始节点索引后移,新结束节点索引前移,继续比较,若是不同,则进行下一个比较方式 4、判断旧结束节点和新开始节点是否相同,若是相同,则将旧结束节点移动到最前端,旧结束节点索引前移,新开始节点索引后移,若是不同,则进行下一个比较方式 5、以上所有的执行算法是互斥的,用 if esle 判断,所以当以上情况都不满足时,执行最后一种情况,判断新开始节点在旧节点数组内是否能找到,若是找不到,则是新节点,将新节点插入到旧节点数组的前面,若是找到了,则将该节点从旧节点数组中取出,重新插入到旧节点数组中对应的索引位置 6、将以上所有的情况判断完毕后,将会把旧节点数组中和新节点数组中一致的节点梳理成一致的,通过对比此时的数组,能明确的找出旧节点数组中和新节点数组中的差异,然后对旧节点数组中的节点进行添加和删除节点的操作,最后渲染到页面上 ## 二、编程题 ### 1、模拟 VueRouter 的 hash 模式的实现,实现思路和 History 模式类似,把 URL 中的 # 后面的内容作为路由的地址,可以通过 hashchange 事件监听路由地址的变化。 答:使用 location.hash 改变锚地址,然后通过监听 hashchange 对锚地址的改变做处理 ```js let _Vue = null export default class vueRouter { constructor (options) { this.options = options this.routeMap = {} this.data = _Vue.observable({ current: '/' }) } static install (Vue) { if (vueRouter.install.installed) { return } vueRouter.install.installed = true _Vue = Vue _Vue.mixin({ beforeCreate () { if (this.$options.router) { _Vue.prototype.$router = this.$options.router this.$options.router.init() } } }) } init () { this.createRouteMap() this.initComponents(_Vue) this.initEvent() } createRouteMap () { this.options.routes.forEach(route => { this.routeMap[route.path] = route.component }) } initComponents (Vue) { 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) { // 通过 location.hash 的方式修改 url 的锚地址 location.hash = `#${this.to}` this.$router.data.current = this.to e.preventDefault() } } }) const _this = this Vue.component('router-view', { render (h) { const component = _this.routeMap[_this.data.current] return h(component) } }) } initEvent () { // 通过监听锚地址的变化修改 data 的 current 值,从而改变 view 的内容 window.addEventListener('hashchange', () => { this.data.current = location.hash.slice(1) }) } } ``` ### 2、在模拟 Vue.js 响应式源码的基础上实现 v-html 指令,以及 v-on 指令。 答:v-html 可以通过 innerHTML 添加字符串 DOM,v-on 可以在判断出是添加事件监听,通过 addEventListener 往DOM对象添加事件监听 ```js class Compiler { constructor(vm){ this.el = vm.$el this.vm = vm this.compile(this.el) } compile(el) { let childNodes = el.childNodes Array.from(el.childNodes).forEach(node => { if(this.isTextNode(node)){ this.compileText(node) }else if(this.isElementNode(node)){ this.compileElement(node) } }) } compileElement(node) { Array.from(node.attributes).forEach(attr => { let attrName = attr.name if(this.isDirection(attrName)){ attrName = attrName.substr(2) const key = attr.value if(this.isEvent(attrName)){ attrName = attrName.substr(3) this.handler(node, key, attrName) }else{ this.update(node, key, attrName) } } }) } compileText(node) { const reg = /\{\{(.+?)\}\}/ const value = node.textContent if(reg.test(value)){ const key = RegExp.$1.trim() node.textContent = value.replace(reg, this.vm[key]) } } update(node, key, attrName) { let updateFn = this[`${attrName}Updater`] updateFn && updateFn(node, this.vm[key]) } // 添加 v-on 指令,前提是将 options 对象的 methods 对象的所有方法添加到 Vue 实例下,方便调用 handler(node, key, attrName) { node.addEventListener(attrName, this.vm[key]) } // 添加 v-html 指令 htmlUpdater(node, value) { node.innerHTML = value } textUpdater(node, value) { node.textContent = value } isDirection(attrName) { return attrName.startsWith('v-') } isEvent(eventName) { return eventName.startsWith('on') } isTextNode(node) { return node.nodeType === 3 } isElementNode(node) { return node.nodeType === 1 } } ``` ### 3、参考 Snabbdom 提供的电影列表的示例,利用 Snabbdom 实现类似的效果,如图: ![img](./images/image-06.png) 答:分析需求,需要实现列表的排序、新增、删除,以及列表的排序动画 1、先将列表的静态结构写好,方便之后抽离成虚拟DOM 2、将列表数据对象准备好 3、编写列表虚拟DOM视图 4、编写完整虚拟DOM视图,并将列表虚拟DOM导入 5、编写 render 函数,将每次修改的虚拟DOM数据重新渲染到页面 6、监听首次页面渲染,在页面结构渲染完成后,将容器内容替换为虚拟DOM 7、编写新增列表函数 add 8、编写删除列表函数 del 9、编写列表排序函数 changSort 10、给列表变化添加动画,因此需要将列表变成定位布局,通过给列表数据循环添加 offset 属性来定位每条数据的位置,offset 的值由 上条数据的 offset ,每条数据的高度和margin值确定,每条数据的高度可以通过 hook 生命周期的 insert 方法通过 vnode.elm.offsetHeight 方法获取到,之后可以将 offset 属性的值赋给每条数据的 translateY ,通过该属性的变化,用 transition 实现过渡效果 以下是效果图: ![20200811_185808](./images/image-07.gif) ```html Document
``` ```js // 逻辑代码 // 导入 snabbdon 的 h 函数 和 init 函数 import { h, init } from 'snabbdom' // 导入 snabbdon 的处理模块 import style from 'snabbdom/modules/style' import classModules from 'snabbdom/modules/class' import props from 'snabbdom/modules/props' import eventlisteners from 'snabbdom/modules/eventlisteners' // 初始化 const patch = init([style, classModules, props, eventlisteners]) let vnode let softBy = 'rank' let nextRank = 10 let margin = 8 // 电影列表数据 let movieList = [ { rank: 1, title: 'The Shawshank Redemption', desc: 'Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.', elmHeight: 0 }, { rank: 2, title: 'The Godfather', desc: 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.', elmHeight: 0 }, { rank: 3, title: 'The Godfather: Part II', desc: 'The early life and career of Vito Corleone in 1920s New York is portrayed while his son, Michael, expands and tightens his grip on his crime syndicate stretching from Lake Tahoe, Nevada to pre-revolution 1958 Cuba.', elmHeight: 0 }, { rank: 4, title: 'The Dark Knight', desc: 'When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, the caped crusader must come to terms with one of the greatest psychological tests of his ability to fight injustice.', elmHeight: 0 }, { rank: 5, title: 'Pulp Fiction', desc: 'The lives of two mob hit men, a boxer, a gangster\'s wife, and a pair of diner bandits intertwine in four tales of violence and redemption.', elmHeight: 0 }, { rank: 6, title: 'Schindler\'s List', desc: 'In Poland during World War II, Oskar Schindler gradually becomes concerned for his Jewish workforce after witnessing their persecution by the Nazis.', elmHeight: 0 }, { rank: 7, title: '12 Angry Men', desc: 'A dissenting juror in a murder trial slowly manages to convince the others that the case is not as obviously clear as it seemed in court.', elmHeight: 0 }, { rank: 8, title: 'The Good, the Bad and the Ugly', desc: 'A bounty hunting scam joins two men in an uneasy alliance against a third in a race to find a fortune in gold buried in a remote cemetery.', elmHeight: 0 }, { rank: 9, title: 'The Lord of the Rings: The Return of the King', desc: 'Gandalf and Aragorn lead the World of Men against Sauron\'s army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.', elmHeight: 0 }, { rank: 10, title: 'Fight Club', desc: 'An insomniac office worker looking for a way to change his life crosses paths with a devil-may-care soap maker and they form an underground fight club that evolves into something much, much more...', elmHeight: 0 }, ] // 方便处理列表数据 let data = [ movieList[0], movieList[1], movieList[2], movieList[3], movieList[4], movieList[5], movieList[6], movieList[7], movieList[8], movieList[9], ] // 排序函数 function changeSort(prop){ softBy = prop data.sort((a, b) => { if (a[prop] > b[prop]) { return 1 }else if (a[prop] < b[prop]) { return -1 } return 0 }) render() } // 新增 function add(){ const item = movieList[Math.floor(Math.random() * 10)] data.unshift({rank: nextRank++, title: item.title, desc: item.desc}) render() render() } // 删除 function del(movie) { console.log(movie) data = data.filter(item => { return item !== movie }) render() } // 渲染电影列表 function movieView(movie) { return h('div.row', { key: movie.rank, style: { opacity: '0', transform: 'translate(-200px)', delayed: { transform: `translateY(${movie.offset}px)`, opacity: '1' }, remove: { opacity: '0', transform: `translateY(${movie.offset}px) translateX(200px)` } }, hook: { insert: (vnode) => { movie.elmHeight = vnode.elm.offsetHeight } }, }, [ h('div.rank', movie.rank), h('div.title', movie.title), h('div.desc', movie.desc), h('div.del', {on: { click: [del, movie] } }, 'x') ]) } // 渲染完整视图 function view(data) { return h('div', [ h('h1', 'Movie List'), h('div', [ '排序', h('button.soft', {class: { active: softBy === 'rank' }, on: { click: [changeSort, 'rank'] } }, 'Rank'), h('button.soft', {class: { active: softBy === 'title' }, on: { click: [changeSort, 'title'] } }, 'Title'), h('button.soft', {class: { active: softBy === 'desc' }, on: { click: [changeSort, 'desc'] } }, 'desc'), h('button.add', { on: { click: add } }, 'add'), ]), h('div.content', data.map(movieView)) ]) } // render 渲染函数 function render() { data = data.reduce((acc, m) => { let last = acc[acc.length - 1] m.offset = last ? last.offset + m.elmHeight + margin : margin return acc.concat(m) }, []) vnode = patch(vnode, view(data)) } // 监听页面渲染完成后将虚拟DOM渲染到容器内 window.addEventListener('DOMContentLoaded', () => { const container = document.getElementById('container') vnode = patch(container, view(data)) render() }) ```