3-1作业


一、简单题

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

let vm = new Vue({
 el: '#el'
 data: {
  o: 'object',
  dog: {}
 },
 method: {
  clickHandler () {
   // 该 name 属性是否是响应式的
   this.dog.name = 'Trump'
  }
 }
})

通过this.dog.name = 'Trump'并不是响应式数据,因为在Vue中当吧一个普通的JavaScript对象传入Vue实例作为data选项,Vue将便利所有对象的所有property,并使用Object.defineProperty把这些property全部转换为getter/setter。Vue无法检测property添加或移除。由于Vue会在初始化实例时对property执行getter/setter转化,所以property必须在data对象上存在才能让Vue将它转换为响应式。

对于对象

对于已创建的实例,Vue不允许动态添加跟级别的响应式property。但是,可以使用

Vue.set(object, propertyName, value)方法向嵌套对象添加响应式property。例如:

Vue.set(vm.someObject, 'b', 2)

还可以使用vm.$set实例方法,这也是Vue.set方法的别名:

this.$set(this.someObject, 'b', 2)

当需要为已有对象赋值多个新property,比如使用Object.assign()或_.extend()。但是这样添加到对象的新property不会触发更新。这种情况下,应该使用原对象与要混合进去的对象的property一起创建一个新的对象。

// 代替 Object.assgin(this.someObject, { a: 1, b: 2 })
this.someObject = Object.assgin({}, this.someObject, { a: 1, b: 2 })

对于数组

Vue不能检测一下数组的变动:

  1. 当里利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

举个例子:

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x'  // 不是响应式的
vm.items.length = 2  // 不是响应式的

解决第一类问题(利用数组索引设置一个数组项vm.items[indexOfItem] = newValue):

  • ```js
    // Vue.set
    Vue.set(vm.items, indexOfItem, newValue)
    
    - ```js
      // Array.property.splice
      vm.items.splice(indexOfItem, 1, newValue)

解决第二类问题(修改数组长度vm.items.length = newLength

  • ```js
    vm.items.splice(newLength)
    
    *数组更新检查*
    
    ***变更方法***
    
    - *push():向尾部添加元素。改变原数组。*
    - *pop():从数组中删除最后一个元素,并返回该元素的值。并重置数组的长度。改变原数组。*
    - *shift():删除**第一个**元素并返回该元素的值,并重置数组的长度。改变原数组。*
    - *unshift():将一个或多个元素添加到数组的**开头**,并返回该数组的**新长度**。改变原数组。*
    - *splice():通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。改变原数组。*
    - *sort():方法用[原地算法](https://en.wikipedia.org/wiki/In-place_algorithm)对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的。改变原数组。*
    - *reverse():将数组中元素的位置颠倒,并返回该数组。数组的第一个元素会变成最后一个,数组的最后一个元素变成第一个。改变原数组。*
    
    ***替换数组***
    
    *变更方法,顾名思义,会变更调用了这些方法的原始数组。相比之下,也有非变更方法,例如 `filter()`、`concat()` 和 `slice()`。它们不会变更原始数组,而**总是返回一个新数组**。当使用非变更方法时,可以用新数组替换旧数组:*
    
    ```js
    example1.items = example1.items.filter(function (item) {
      return item.message.match(/Foo/)
    })

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

在执行Diff算法的过程就是调用名为patch的函数,比较新旧节点。一边比较一边给真实DOM打补丁。patch函数接收两个参数:oldVnodevnode,分别代表旧节点和新节点。patch函数内部会先执行pre钩子函数。

执行完成后,使用isVnode(函数内部查看oldVnode是否有sel属性)判断传入的oldVnode是否为DOM对象,如果不是vnode,则使用emptyNodeAt(函数内部找到id/class/tagName,调用vnode()函数将其转换为vnode对象:{api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm})将真实DOM转换为vnode对象。

isVnode

function isVnode (vnode: any): vnode is VNode {
  return vnode.sel !== undefined
}

emptyNodeAt

function emptyNodeAt (elm: Element) {
  const id = elm.id ? '#' + elm.id : ''
  const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''
  return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)
}

vnode

export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined): VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}

接下来使用sameVnode(函数内部判断两个oldVnodevnodekeysel是否都相同,如果都想同则认为是同一个vnode对象)函数判断oldVnodevnode是否为同一个vnode

sameVnode

function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}
  • 如果为相同节点,则使用patchVnode(函数细节在下面讲)函数对比新旧节点差异,并更新到DOM上。

    patchVnode

    function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
      const hook = vnode.data?.hook
      hook?.prepatch?.(oldVnode, vnode)
      const elm = vnode.elm = oldVnode.elm!
      const oldCh = oldVnode.children as VNode[]
      const ch = vnode.children as VNode[]
      if (oldVnode === vnode) return
      if (vnode.data !== undefined) {
        for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
        vnode.data.hook?.update?.(oldVnode, vnode)
      }
      if (isUndef(vnode.text)) {
        if (isDef(oldCh) && isDef(ch)) {
          if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
        } else if (isDef(ch)) {
          if (isDef(oldVnode.text)) api.setTextContent(elm, '')
          addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        } else if (isDef(oldCh)) {
          removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        } else if (isDef(oldVnode.text)) {
          api.setTextContent(elm, '')
        }
      } else if (oldVnode.text !== vnode.text) {
        if (isDef(oldCh)) {
          removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        }
        api.setTextContent(elm, vnode.text!)
      }
      hook?.postpatch?.(oldVnode, vnode)
    }

    patchVnode函数内部,首先获取vnode中的hook钩子函数并执行prepatch,然后通过oldVnode.elm!获取elm属性,并赋值给vnode.elmelm,通过oldVnode.children as Vnode[]获取老vnode子元素列表oldCh,通过vnode.children as Vnode[]获取新Vnode子元素列表ch

    判断vnode.data如果不为undefined,遍历循环cbs并执行update钩子函数(for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode))。

    判断vnode.text是否为undefined

    • 如果为undefined,再此判断oldChch是否为undefined
      • 如果oldChch存在,则判断如果oldCh不等于ch,执行updateChildren函数(下面详解)
      • 如果oldCh不存在,ch存在
        • 如果oldVnode.text存在,使用api.setTextContent(elm, ''),设置elmtextContent内容
        • 调用addVnodes添加节点
      • 如果oldCh存在,ch不存在
        • 调用removeVnodes删除老节点
      • 如果oldVnode.text存在,设置elmtextContent内容
    • 如果vnode.text不为undefinedoldVnode.text !== vnode.text
      • 如果oldCh存在,使用removeVnode删除oldCh节点
      • 给elm设置vnode.text
  • 如果不为相同节点,

    • 获取oldVnodeelm属性(oldVnode.elm!)并赋值给elm

    • 获取oldVnode的parent父元素(api.parentNode(elm) as Node

    • 使用createElm(函数内部返回vnode.elm)函数将elm属性赋值给vnode.elm

      createElm

      function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
        // 执行用户设置的init钩子函数
        let i: any
        let data = vnode.data
        if (data !== undefined) {
          const init = data.hook?.init
          if (isDef(init)) {
            init(vnode)
            data = vnode.data
          }
        }
    • 接下来判断parent是否为空,如果不为空,调用api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))将其插入到parent元素中的兄弟节点(一般为文本节点)之后,然后使用removeVnodes函数删除原来的oldVnode

      removeVnode

      function removeVnodes (parentElm: Node,
        vnodes: VNode[],
        startIdx: number,
        endIdx: number): void {
        // 参数一:要删除节点的父节点
        // 参数二:要删除的节点
        // 参数三:开始索引
        // 参数四:结束索引
        for (; startIdx <= endIdx; ++startIdx) {
          //
          let listeners: number
          let rm: () => void
          const ch = vnodes[startIdx]
          if (ch != null) {
            if (isDef(ch.sel)) {
              // 内部触发了vnode的destroy钩子函数
              invokeDestroyHook(ch)
              // 防止重复删除DOM元素
              listeners = cbs.remove.length + 1
              // createRmCb高阶函数,内部返回真正删除dom元素的函数
              rm = createRmCb(ch.elm!, listeners)
              // remove钩子函数内部会真正调用rm(删除DOM元素)
              for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
              // 用户是否传入remove钩子函数
              const removeHook = ch?.data?.hook?.remove
              if (isDef(removeHook)) {
                removeHook(ch, rm)
              } else {
                rm()
              }
            } else { // Text node
              api.removeChild(parentElm, ch.elm!)
            }
          }
        }
      }

接下来遍历insertVnodeQueue(保存新插入节点的队列,为了触发钩子函数),执行create钩子函数。然后遍历cbs中的post,调用(cbs.post[i]())post钩子函数。最后返回vnode对象。

for (i = 0; i < insertedVnodeQueue.length; ++i) {
  insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
return vnode

二、编程题

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

实现思路(构建类图如下):

  • + options
  • +data
  • +routeMap
  • +Constructor(Options): VueRouter
  • _install(Vue): void
  • +init(): void
  • +initEvent(): void
  • +createRouteMap(): void
  • initComponents(Vue): void

代码实现地址:https://gitee.com/coder5leo/fed-e-task-03-01/blob/master/codes/vue-router-hash/src/vuerouter/index.js

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

代码实现地址:https://gitee.com/coder5leo/fed-e-task-03-01/tree/master/codes/minivue

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

代码实现地址:https://gitee.com/coder5leo/fed-e-task-03-01/tree/master/codes/snabbdom-movie


文章作者: 5coder
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 5coder !
  目录