1 简答题

1、当我们点击按钮的时候动态给 data 增加的成员是否是响应式数据,如果不是的话,如何把新增成员设置成响应式数据,它的内部原理是什么。

  1. let vm = new Vue({
  2. el: '#el'
  3. data: {
  4. o: 'object',
  5. dog: {}
  6. },
  7. method: {
  8. clickHandler () {
  9. // 该 name 属性是否是响应式的
  10. this.dog.name = 'Trump'
  11. }
  12. }
  13. })

答:
不是,在vue 中通过 Vue.set() 方法可以设置响应式数据,或者vm.$set()设置。Vue.set() 方法,先去新建了一个 Dep 实例,再去判断类型是数组或者对象,然后把新添加的值添加到数组或者对象上,调用 defineReactive 方法,方法先将 value 设置为 observer 对象,里面会递归设置所有属性为响应式,然后用 Objec.definePropery 去监听 Object 的 key,在 getter 里面,判断 Dep.target 存在与否,存在就把 target 赋值给 Dep 实例的 subs 数组中,然后再 Vue.set 方法中的最后面调用 dep.notify() 方法,调用 dep 实例下 subs 数组中所有成员的 update 方法

2、请简述 Diff 算法的执行过程
答:
当调用 patch 函数时,先去判断旧节点是否为 vnode,不是则转换为 vnode;然后判断新旧 vnode 是否为 sameVnode(sel/key相等),如果是则比对更新 DOM,如果不是则创建 DOM 并插入到 DOM 树中。sameVnode 的情况下,执行patchVnode 函数比对更新,内部会判断新 vnode 是否有 text 属性,如果有再去判断新旧 vnode 的 text 是否相等,不相等则移除旧的 vnode,设置新的 vnode 的 textContent; 如果新的 vnode 没有 text 属性,而且新旧 vnode 都要children,且两个不相等,此时会调用 updateChildren 方法,此时才是真正 diff 算法执行的地方;
在 updateChildren 中,传入的新旧 vnode 的children 数组,比对两个数组先从两个数组的第一个开始,如果相同则比对的指针后移到下一轮循环,如果不同则比对从两个数组的最后一个,相同则前移到下一轮循环,如果还不同则从旧节点 children 数组的第一个跟新节点 children 数组的最后一个对比,如果相同则把旧 children 数组的第一个移动到当前指针的最后面,更新对应指针到下一轮循环;如果还不同则比对 旧 children 数组的最后一个和新 children 数组的第一个,如果相同则把旧 children 数组的最后一个移动到当前指针的最前面,更新指针到下一轮循环;
如果这四种情况都不满足,则拿当前旧 children 的前后指针遍历后,根据 key 生成 map,以新 children 数组当前指针对象的key 去map 中找,如果找不到说明是新元素,则插入到 dom 中,如果找到了再去比对 map 对应的旧 children 指针节点的sel 和当前遍历新 children 中指针节点的sel 是否相同,不同则插入 DOM,相同则递归调用 patchVnode,并且插入 DOM;
最后,对比两个数组的开始和结束指针,如果在开始小于等于结束指针,判断如果旧节点数组开始指针大于结束指针,说明新节点没遍历完,此时根据新节点数组的开始结束指针,把数组剩下的内容插入到 DOM 中;如果旧节点数组开始指针小于等于结束指针,说明旧节点没遍历完,根据指针移除剩余的旧节点;

二、编程题

1、模拟 VueRouter 的 hash 模式的实现,实现思路和 History 模式类似,把 URL 中的 # 后面的内容作为路由的地址,可以通过 hashchange 事件监听路由地址的变化。
答:过程:

  • 新建VueRouter 类,添加静态 install(vue) 方法,方法内部判断 VueRouter 是否已安装,没有的话把插件置为安装状态,继续执行;存储install 方法传入的vue 对象,留在后续调用;把创建vue 实例时传入的 router 对象注入到 vue 实例上,需要通过vue 的mixin 混入来解决 this 的问题;在beforeCreate 钩子里给 vue 原型上添加 $router,并调用$router 的 init方法
  • init 方法中初始化 RouterLink 和 RouterView 组件
  • RouterLink 组件中通过 render 函数添加点击方法,点击后更新 current
  • RouterView 组件中通过 render 函数,动态生成视图
  • 挂载 hashchange 事件,通过改变响应式属性 current 来改变视图 ```javascript // vuerouter/index.js let _Vue = null

export default class VueRouter { static install(vue) { if(VueRouter.install.installed) { return } VueRouter.install.installed = true _Vue = vue

  1. _Vue.mixin({
  2. beforeCreate() {
  3. if(this.$options.router) {
  4. _Vue.prototype.$router = this.$options.router
  5. this.$options.router.init()
  6. }
  7. },
  8. })

}

constructor(options) { this.options = options this.routerMap = {} this.data = _Vue.observable({ current: window.location.hash.substr(1) }) }

init() { this.createRouterMap() this.initComponents(_Vue) this.initEvent() }

createRouterMap () { this.options.routes.forEach(route => { this.routerMap[route.path] = route.component }) }

initComponents (Vue) { Vue.component(‘router-link’, { name: ‘RouterLink’, props: { to: String }, render(h) { return h(‘a’, { attrs: { href: this.to }, on: { click: this.clickHandler } },[this.$slots.default]) }, methods: { clickHandler(e) { // 调用 pushstate 方法改变地址栏的地址 history.pushState({},’’,’#’ + this.to) // 跟换视图 this.$router.data.current = this.to // 阻止浏览器的默认行为 e.preventDefault(); } }, })

  1. const self = this
  2. Vue.component('router-view', {
  3. render(h) {
  4. // 当前路由地址
  5. console.log(self.routerMap)
  6. console.log(self.data.current)
  7. const component = self.routerMap[self.data.current]
  8. return h(component)
  9. }
  10. })

}

initEvent() { window.addEventListener(‘hashchange’, () => { console.log(‘hashchange’, window.location.hash.substr(1)) this.data.current = window.location.hash.substr(1) }) } }

  1. 2、在模拟 Vue.js 响应式源码的基础上实现 v-html 指令,以及 v-on 指令。
  2. ```javascript
  3. // 调用指令的方法
  4. update (node, key, attrName) {
  5. let name = attrName.split(':')[0]
  6. let event = attrName.split(':')[1]
  7. let updateFn = this[name + 'Updater']
  8. updateFn && updateFn.call(this, node, this.vm[key], key, event)
  9. }
  10. // 处理 v-html 指令
  11. htmlUpdater (node, value, key) {
  12. node.innerHTML = this.vm[key]
  13. new Watcher(this.vm, key, newValue => {
  14. node.innerHTML = newValue
  15. })
  16. }
  17. // 处理 v-on 指令
  18. onUpdater (node, value, key, event) {
  19. let methodsStrArr = key.split('(')
  20. let methodName = methodsStrArr[0];
  21. let params = methodsStrArr[1] && methodsStrArr[1].split(')')[0]
  22. let [...arg] = params && params.split(',') || []
  23. let originEvent = false
  24. if(arg[arg.length - 1] === '$event') {
  25. arg.splice(arg.length - 1,1)
  26. originEvent = true
  27. }
  28. node.addEventListener(event, e => {
  29. if(originEvent) {
  30. this.vm.$options.methods[methodName](...arg, e)
  31. } else {
  32. this.vm.$options.methods[methodName](...arg)
  33. }
  34. })
  35. }

3、参考 Snabbdom 提供的电影列表的示例,利用Snabbdom 实现类似的效果
1 拷贝 html、css

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  5. <title>Reorder animation</title>
  6. <link rel="stylesheet" href="./src/07-top10Movies.css">
  7. </head>
  8. <body>
  9. <div id="container"></div>
  10. <script src="./src/07-top10movie.js"></script>
  11. </body>
  12. </html>
  1. // 07-top10Movies.css
  2. body {
  3. background: #fafafa;
  4. font-family: sans-serif;
  5. }
  6. h1 {
  7. font-weight: normal;
  8. }
  9. .btn {
  10. display: inline-block;
  11. cursor: pointer;
  12. background: #fff;
  13. box-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
  14. padding: 0.5em 0.8em;
  15. transition: box-shadow 0.05s ease-in-out;
  16. -webkit-transition: box-shadow 0.05s ease-in-out;
  17. }
  18. .btn:hover {
  19. box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
  20. }
  21. .btn:active,
  22. .active,
  23. .active:hover {
  24. box-shadow: 0 0 1px rgba(0, 0, 0, 0.2), inset 0 0 4px rgba(0, 0, 0, 0.1);
  25. }
  26. .add {
  27. float: right;
  28. }
  29. #container {
  30. max-width: 42em;
  31. margin: 0 auto 2em auto;
  32. }
  33. .list {
  34. position: relative;
  35. }
  36. .row {
  37. overflow: hidden;
  38. position: absolute;
  39. box-sizing: border-box;
  40. width: 100%;
  41. left: 0px;
  42. margin: 0.5em 0;
  43. padding: 1em;
  44. background: #fff;
  45. box-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
  46. transition: transform 0.5s ease-in-out, opacity 0.5s ease-out,
  47. left 0.5s ease-in-out;
  48. -webkit-transition: transform 0.5s ease-in-out, opacity 0.5s ease-out,
  49. left 0.5s ease-in-out;
  50. }
  51. .row div {
  52. display: inline-block;
  53. vertical-align: middle;
  54. }
  55. .row > div:nth-child(1) {
  56. width: 5%;
  57. }
  58. .row > div:nth-child(2) {
  59. width: 30%;
  60. }
  61. .row > div:nth-child(3) {
  62. width: 65%;
  63. }
  64. .rm-btn {
  65. cursor: pointer;
  66. position: absolute;
  67. top: 0;
  68. right: 0;
  69. color: #c25151;
  70. width: 1.4em;
  71. height: 1.4em;
  72. text-align: center;
  73. line-height: 1.4em;
  74. padding: 0;
  75. }

2 写 js

  1. // 07-top10movie.js
  2. // 引入模块
  3. import { init } from 'snabbdom/build/package/init'
  4. import { h } from 'snabbdom/build/package/h';
  5. import { classModule } from 'snabbdom/build/package/modules/class';
  6. import { propsModule } from 'snabbdom/build/package/modules/props';
  7. import { styleModule } from 'snabbdom/build/package/modules/style';
  8. import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners';
  9. // 初始化
  10. var patch = init([classModule, propsModule, styleModule, eventListenersModule])
  11. var vnode
  12. var nextKey = 11
  13. var margin = 8
  14. var sortBy = 'rank'
  15. var totalHeight = 0
  16. var originalData = [
  17. { 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 },
  18. { 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 },
  19. { 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 },
  20. { 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 },
  21. { 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 },
  22. { 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 },
  23. { 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 },
  24. { 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 },
  25. { 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 },
  26. { 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 },
  27. ]
  28. var data = [
  29. originalData[0],
  30. originalData[1],
  31. originalData[2],
  32. originalData[3],
  33. originalData[4],
  34. originalData[5],
  35. originalData[6],
  36. originalData[7],
  37. originalData[8],
  38. originalData[9],
  39. ]
  40. // 排序
  41. function changeSort (prop) {
  42. sortBy = prop
  43. data.sort((a, b) => {
  44. if (a[prop] > b[prop]) {
  45. return 1
  46. }
  47. if (a[prop] < b[prop]) {
  48. return -1
  49. }
  50. return 0
  51. })
  52. render()
  53. }
  54. // 新增
  55. function add () {
  56. var n = originalData[Math.floor(Math.random() * 10)]
  57. data = [{ rank: nextKey++, title: n.title, desc: n.desc, elmHeight: 0 }].concat(data)
  58. render()
  59. }
  60. // 删除
  61. function remove (movie) {
  62. data = data.filter((m) => {
  63. return m !== movie
  64. })
  65. render()
  66. }
  67. // 列表渲染
  68. function movieView (movie) {
  69. return h('div.row', {
  70. key: movie.rank,
  71. style: {
  72. opacity: '0',
  73. transform: 'translate(-200px)',
  74. delayed: { transform: `translateY(${movie.offset}px)`, opacity: '1' },
  75. remove: { opacity: '0', transform: `translateY(${movie.offset}px) translateX(200px)` }
  76. },
  77. hook: { insert: (vnode) => { movie.elmHeight = vnode.elm.offsetHeight } }, // 插入之前赋值
  78. }, [
  79. h('div', { style: { fontWeight: 'bold' } }, movie.rank),
  80. h('div', movie.title),
  81. h('div', movie.desc),
  82. h('div.btn.rm-btn', { on: { click: () => {
  83. remove(movie)
  84. }} }, 'x'),
  85. ])
  86. }
  87. // 动态计算每个行的位置
  88. function render () {
  89. data = data.reduce((acc, m) => {
  90. var last = acc[acc.length - 1]
  91. m.offset = last ? last.offset + last.elmHeight + margin : margin
  92. return acc.concat(m)
  93. }, [])
  94. totalHeight = data.length === 0
  95. ? 0
  96. : data[data.length - 1].offset + data[data.length - 1].elmHeight
  97. vnode = patch(vnode, view(data))
  98. }
  99. // 视图渲染
  100. function view (data) {
  101. return h('div', [
  102. h('h1', 'Top 10 movies'),
  103. h('div', [
  104. h('a.btn.add', { on: { click: add } }, 'Add'),
  105. 'Sort by: ',
  106. h('span.btn-group', [
  107. h('a.btn.rank', { class: { active: sortBy === 'rank' }, on: { click: ()=>{changeSort('rank')} } }, 'Rank'),
  108. h('a.btn.title', { class: { active: sortBy === 'title' }, on: { click: ()=>{changeSort('title')} } }, 'Title'),
  109. h('a.btn.desc', { class: { active: sortBy === 'desc' }, on: { click: ()=>{changeSort('desc')} } }, 'Description'),
  110. ]),
  111. ]),
  112. h('div.list', { style: { height: totalHeight + 'px' } }, data.map(movieView)),
  113. ])
  114. }
  115. // 首次加载
  116. window.addEventListener('DOMContentLoaded', () => {
  117. var container = document.getElementById('container')
  118. vnode = patch(container, view(data))
  119. render()
  120. })