一、简答题

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实例后,新增一个成员,此时的data并没有定义该成员,data中的成员是在创建Vue对象的手new Observer来将其设置成响应式数据,当Vue实例化完成之后,再添加一个成员,此时仅仅是给vm上增加了一个js属性而已,因此并不是响应式的。

把新增成员设置成响应式数据:可以使用Vue.set(object, propertName, value)方法向嵌套对象添加响应式属性。您还可以使用vm.$set实例方法,这也是全局Vue.set方法别名。
Vue.set内部原理:

  1. export function set (target: Array<any> | Object, key: any, val: any): any {
  2. ...
  3. // 判断当前target是不是数组,并且key的值是有效的数组索引
  4. // 在修改数组时调用set方法时让我们能够触发响应的代码
  5. if (Array.isArray(target) && isValidArrayIndex(key)) {
  6. // 类似$vm.set(vm.$data.arr, 0, 3)
  7. // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
  8. target.length = Math.max(target.length, key)
  9. // splice会改变数组从而触发响应式
  10. target.splice(key, 1, val)
  11. return val
  12. }
  13. // target为对象, key在target或者target.prototype上。
  14. // 并且key不是Object原型上的属性
  15. // 说明这个key本来就在对象上面已经定义过了的,直接修改值就可以了,可以自动触发响应
  16. if (key in target && !(key in Object.prototype)) {
  17. target[key] = val
  18. return val
  19. }
  20. // 以上都不成立, 即开始给target创建一个全新的属性
  21. // vue给响应式对象都加了一个__ob__属性,如果一个对象有这个__ob__属性,
  22. // 那么就说明这个对象是响应式对象,我们修改对象已有属性的时候就会触发页面渲染
  23. // 获取Observer实例
  24. const ob = (target: any).__ob__
  25. // Vue 实例对象拥有 _isVue 属性, 即不允许给 Vue 实例对象添加属性
  26. // 也不允许Vue.set/$set 函数为根数据对象(vm.$data)添加属性
  27. // 即 当前的target对象是vue实例对象或者是根数据对象,那么就会抛出错误警告
  28. if (target._isVue || (ob && ob.vmCount)) {
  29. process.env.NODE_ENV !== 'production' && warn(
  30. 'Avoid adding reactive properties to a Vue instance or its root $data ' +
  31. 'at runtime - declare it upfront in the data option.'
  32. )
  33. return val
  34. }
  35. // target本身就不是响应式数据, 不需要响应,那么直接赋值返回即可
  36. if (!ob) {
  37. target[key] = val
  38. return val
  39. }
  40. // 进行响应式处理
  41. // 给新加的属性添加依赖,以后再直接修改这个新的属性的时候就会触发页面渲染
  42. defineReactive(ob.value, key, val)
  43. // 触发当前的依赖(这里的依赖依然可以理解成渲染函数),所以页面就会进行重新渲染
  44. ob.dep.notify()
  45. return val
  46. }

2、请简述 Diff 算法的执行过程

  • 执行过程:

    • 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引
    • 在对开始和结束节点比较的时候,总共有四种情况

      • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
      • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
      • oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点)
      • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)

        1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/416587/1610889171381-9e222a59-da31-4240-8452-88452ab0fc5b.png#align=left&display=inline&height=173&margin=%5Bobject%20Object%5D&name=image.png&originHeight=345&originWidth=764&size=141110&status=done&style=none&width=384)
    • 新旧开始节点比较和新旧结束节点比较,这两种情况类似

      • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
      • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
    • 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同)
      • 调用 patchVnode() 对比和更新节点
      • 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++

image.png

  • oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同

    • 调用 patchVnode() 对比和更新节点
    • 把 oldStartVnode 对应的 DOM 元素,移动到右边
    • 更新索引

      1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/416587/1610889616292-0edfe0d3-555a-46ed-8586-556c5faf8504.png#align=left&display=inline&height=207&margin=%5Bobject%20Object%5D&name=image.png&originHeight=391&originWidth=762&size=173350&status=done&style=none&width=404)
  • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同

    • 调用 patchVnode() 对比和更新节点
    • 把 oldEndVnode 对应的 DOM 元素,移动到左边
    • 更新索引

image.png

  • 如果不是以上四种情况
    • 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点
    • 如果没有找到,说明 newStartNode 是新节点
    • 创建新节点对应的 DOM 元素,插入到 DOM 树中
    • 如果找到了
    • 判断新节点和找到的老节点的 sel 选择器是否相同
    • 如果不相同,说明节点被修改了
    • 重新创建对应的 DOM 元素,插入到 DOM 树中
    • 如果相同,把 elmToMove 对应的 DOM 元素,移动到左边

image.png

  • 循环结束
    • 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束
    • 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束
  • 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边

    image.png

  • 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批量删除

    image.png

二、编程题

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

  1. let _Vue = null
  2. export default class VueRouter {
  3. static install (Vue) {
  4. // 1.判断当前插件是否已经被安装
  5. // 如果插件已经安装直接返回
  6. if (VueRouter.install.installed && _Vue === Vue) return
  7. VueRouter.install.installed = true
  8. // 2.把 Vue 构造函数记录到全局变量
  9. _Vue = Vue
  10. // 3.把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上
  11. // 混入
  12. _Vue.mixin({
  13. beforeCreate () {
  14. // 判断 router 对象是否已经挂载了 Vue 实例上
  15. if (this.$options.router) {
  16. // 把 router 对象注入到 Vue 实例上
  17. _Vue.prototype.$router = this.$options.router
  18. this.$options.router.init()
  19. }
  20. }
  21. })
  22. }
  23. constructor (options) {
  24. this.options = options
  25. // 记录路径和对应的组件
  26. this.routeMap = {}
  27. this.data = _Vue.observable({
  28. // 当前的默认路径
  29. current: '/'
  30. })
  31. }
  32. init () {
  33. this.createRouteMap()
  34. this.initComponents(_Vue)
  35. this.initEvent()
  36. }
  37. createRouteMap () {
  38. // routes => [{ name: '', path: '', component: }]
  39. // 遍历所有的路由信息,记录路径和组件的映射
  40. this.options.routes.forEach(route => {
  41. // 记录路径和组件的映射关系
  42. this.routeMap[route.path] = route.component
  43. })
  44. }
  45. initComponents (Vue) {
  46. _Vue.component('router-link', {
  47. // 接收外部传入的参数
  48. props: {
  49. to: String
  50. },
  51. // 使用运行时版本的 Vue.js
  52. // 此时没有编译器 直接来写一个 render函数
  53. render (h) { // 参数 h 创建虚拟DOM render函数中调用h函数并将结果返回
  54. // h函数 接收三个参数
  55. return h('a', { // 1. 创建的元素对应的选择器
  56. attrs: { // 2. 给标签设置属性 attes 指明DOM对象属性
  57. // history
  58. // href: this.to
  59. // hash
  60. href: '#' + this.to
  61. },
  62. on: { // 给 a标签 注册点击事件
  63. click: this.clickhander
  64. }
  65. }, [this.$slots.default]) // 3. 生成元素的子元素
  66. },
  67. methods: {
  68. clickhander (e) { // 时间参数 e
  69. // 改变浏览器地址栏 pushiState 不向服务器发送请求
  70. // history
  71. // history.pushState({}, '', this.to) // data title url
  72. // hash
  73. window.location.hash = '#' + this.to
  74. this.$router.data.current = this.to // 响应式对象data
  75. e.preventDefault() // 阻止事件默认行为
  76. }
  77. }
  78. // template: '<a :href="to"><slot></slot></a>'
  79. })
  80. const self = this // 保存 this
  81. _Vue.component('router-view', {
  82. render (h) {
  83. // 根据当前路径找到对应的组件,注意 this 的问题
  84. const component = self.routeMap[self.data.current]
  85. return h(component) // 将组件转换为虚拟DOM返回
  86. }
  87. })
  88. }
  89. // initEvent () {
  90. // window.addEventListener('popstate', () => {
  91. // this.data.current = window.location.pathname
  92. // })
  93. // }
  94. // 监听页面 load 和 hashchange 方法,在这个地方有个判断
  95. // 如果当前页面的 hash 不存在,则自动加上 '#/' ,并加载 '/' 的组件
  96. initEvent () {
  97. window.addEventListener('load', this.hashChange.bind(this))
  98. window.addEventListener('hashchange', this.hashChange.bind(this))
  99. }
  100. hashChange () {
  101. if (!window.location.hash) {
  102. window.location.hash = '#/'
  103. }
  104. this.data.current = window.location.hash.substr(1)
  105. }
  106. }

2、在模拟 Vue.js 响应式源码的基础上实现 v-html 指令,以及 v-on 指令。

  • v-html指令

    • 与v-text相似,将textContent更改为innerHTML
      1. class Compiler {
      2. ...
      3. // 处理 v-html 指令
      4. htmlUpdater (node, value, key) {
      5. node.innerHTML = value
      6. new Watcher(this.vm, key, (newValue) => {
      7. node.innerHTML = newValue
      8. })
      9. }
      10. }
  • v-on 指令

  • 在 vue.js 文件 添加变量 methods,把事件注入到 vue 实例
  • 在 compiler.js文件 将on: 修改为空 只保留后面的事件 再处理相应指令 ```javascript class Vue { constructor(options) { … this.$methods = options.methods || {} this._proxyMethods(this.$methods) … } … // 把methods注入vue实例 _proxyMethods(methods) { Object.keys(methods).forEach(key => {
    1. this[key] = this.$methods[key]
    }) } }

class Compiler { update (node, key, attrName) { // 删除 on: 前缀 if (attrName.startsWith(‘on:’)) { attrName = attrName.replace(‘on:’, ‘’) } … } … // 处理 v-on 指令 此处举两个例子 clickUpdater (node, value, key) { node.onclick = value } mouseoverUpdater (node, value, key) { node.onmouseover = value } … }

  1. ```javascript
  2. // 测试代码
  3. <body>
  4. <h1>v-on</h1>
  5. <div v-on:click="clickHandler">点击触发</div>
  6. <div v-on:mouseover="mouseOver">鼠标进入触发</div>
  7. </body>
  8. <script>
  9. let vm = new Vue({
  10. el: '#app',
  11. data: {
  12. html: '<p style="color: skyblue">这是一个p标签</p>',
  13. },
  14. methods: {
  15. clickHandler() {
  16. alert('点击了')
  17. },
  18. mouseOver(){
  19. alert('鼠标进入')
  20. }
  21. }
  22. })
  23. </script>

3、参考 Snabbdom 提供的电影列表的示例,利用Snabbdom 实现类似的效果,如图:

【作业】Part 3 · 模块一 - 图6

  1. // package.json中配置parcel
  2. "scripts": {
  3. "dev": "parcel index.html --open",
  4. "build": "parcel build index.html"
  5. },
  1. // 根据demo写法引入方法和模块
  2. import { init } from 'snabbdom/build/package/init.js'
  3. import { classModule } from 'snabbdom/build/package/modules/class.js'
  4. import { propsModule } from 'snabbdom/build/package/modules/props.js'
  5. import { styleModule } from 'snabbdom/build/package/modules/style.js'
  6. import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners.js'
  7. import { h } from 'snabbdom/build/package/h.js'
  8. // 注册模块
  9. var patch = init([classModule, propsModule, styleModule, eventListenersModule])
  10. var vnode
  11. var nextKey = 11
  12. var margin = 8
  13. var sortBy = 'rank'
  14. var totalHeight = 0
  15. // 电影列表数据
  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. render()
  60. }
  61. // 删除当前数据
  62. function remove (movie) {
  63. data = data.filter((m) => {
  64. return m !== movie
  65. })
  66. render()
  67. }
  68. // 定义别表行
  69. function movieView (movie) {
  70. return h('div.row', {
  71. key: movie.rank,
  72. style: {
  73. opacity: '0',
  74. transform: 'translate(-200px)',
  75. // 进场样式
  76. delayed: { transform: `translateY(${movie.offset}px)`, opacity: '1' },
  77. // 出场样式
  78. remove: { opacity: '0', transform: `translateY(${movie.offset}px) translateX(200px)` }
  79. },
  80. hook: { insert: (vnode) => { movie.elmHeight = vnode.elm.offsetHeight } },
  81. }, [
  82. h('div', { style: { fontWeight: 'bold' } }, movie.rank),
  83. h('div', movie.title),
  84. h('div', movie.desc),
  85. // 新版本的snabbdom已不支持这样的写法,参照上面remove方法,函数接收一个movie参数
  86. // h('div.btn.rm-btn', { on: { click: [remove, movie] } }, 'x'),
  87. h('div.btn.rm-btn', { on: { click: () => {
  88. remove(movie)
  89. } } }, 'x'),
  90. ])
  91. }
  92. function render () {
  93. data = data.reduce((acc, m) => {
  94. var last = acc[acc.length - 1]
  95. m.offset = last ? last.offset + last.elmHeight + margin : margin
  96. return acc.concat(m)
  97. }, [])
  98. totalHeight = data.length === 0
  99. ? 0
  100. : data[data.length - 1].offset + data[data.length - 1].elmHeight
  101. // 新旧Vnode对比后渲染页面
  102. vnode = patch(vnode, view(data))
  103. }
  104. // 返回新Vnode
  105. function view (data) {
  106. return h('div', [
  107. h('h1', 'Top 10 movies'),
  108. h('div', [
  109. h('a.btn.add', { on: { click: add } }, 'Add'),
  110. 'Sort by: ',
  111. h('span.btn-group', [
  112. // h('a.btn.rank', { class: { active: sortBy === 'rank' }, on: { click: [changeSort, 'rank'] } }, 'Rank'),
  113. // h('a.btn.title', { class: { active: sortBy === 'title' }, on: { click: [changeSort, 'title'] } }, 'Title'),
  114. // h('a.btn.desc', { class: { active: sortBy === 'desc' }, on: { click: [changeSort, 'desc'] } }, 'Description'),
  115. h('a.btn.rank', { class: { active: sortBy === 'rank' }, on: { click: () => {
  116. changeSort('rank')
  117. } } }, 'Rank'),
  118. h('a.btn.title', { class: { active: sortBy === 'title' }, on: { click: () => {
  119. changeSort('title')
  120. } } }, 'Title'),
  121. h('a.btn.desc', { class: { active: sortBy === 'desc' }, on: { click: () => {
  122. changeSort('desc')
  123. } } }, 'Description'),
  124. ]),
  125. ]),
  126. h('div.list', { style: { height: totalHeight + 'px' } }, data.map(movieView)),
  127. ])
  128. }
  129. window.addEventListener('DOMContentLoaded', () => {
  130. var container = document.getElementById('container')
  131. vnode = patch(container, view(data))
  132. render()
  133. })