# 基础视频平台树组件踩坑实践

# 前言

树组件是项目中最常见的一种组件,其综合了数据结构的处理、拖拽、点击等前端常见面试题的综合,以下是本次实现过程中遇到的问题汇总

图片

# 问题目录

  • json数据处理
  • $set分析
  • 深拷贝

# 踩坑案例

# json数据处理

[bug描述] 项目中树组件数据所需为三级结构,前两级结构数据与第三级结构数据是不同的接口,商讨过后,决定二级结构后获取三级结构数据,将三级结构数据中的字段获取塞入原数据中,在数据量较大时层级较多时会产生较大延时,影响体验

[bug分析] 大数据量多层级性能较差,需优化

[解决方案] 抽象成两个json数据的映射关系处理

接口1中获取的数据json1如下:

    [
        {
            "name": "组1",
            "id": "g100001",
            "type": "group",
            "children": [
                {
                    "name": "摄像机",
                    "id": "d100001",
                    "type": "device",   
                },
                {
                    "name": "NVR",
                    "id": "d100002",
                    "type": "device",   
                },
                {
                    "name": "摄像机",
                    "id": "d100003",
                    "type": "device",   
                }
            ]
        }
    ]

接口二中获取的数据json2如下:

    [
        {
            "ID": "10001",
            "DeviceID": "10000101001"
        },
        {
            "ID": "10002",
            "DeviceID": "10000101002"
        },
        {
            "ID": "10003",
            "DeviceID": "10000101003"
        }
    ]

最终要将json1中的类型为device的id对应获取到的json2数据组合成最终的数据json如下:

    [
        {
            "name": "组1",
            "id": "g100001",
            "type": "group",
            "children": [
                {
                    "name": "摄像机",
                    "id": "d100001",
                    "type": "device", 
                    ”children": [
                        {
                            "name": ID,
                            "id": DeviceID,
                            "type": "channel"
                        },
                        {
                            "name": ID,
                            "id": DeviceID,
                            "type": "channel"
                        },
                        {
                            "name": ID,
                            "id": DeviceID,
                            "type": "channel"
                        }
                    ]   
                }
            ]
        }
    ]

抽象转化 => [ { "children":[ {...} ], "字段1":"", "字段2":"" } ]

对于多个json数据的映射组合,常见的方法有:1、硬解;2、转AST;3、动态规划

硬解在本场景下时空复杂度还可控制,倘若出现多层结构的深度递归去解会出现爆栈和性能损耗;转ast可以将所有类似结构进行通解,但是需要去写解释执行器也会占据时间和空间,另外ast更适合数据结构变化非常大的,比如像jsx这种要转换成js就需要ast的引入,本场景多层级结构类似;动态规划是对深度递归的一种优化,将每一个问题转化为子问题,从子问题反推,上边的抽象转化是最小子问题,只需将最小子问题进行反向推导,就可以减少内存使用,提升效率

图片

# $set使用及对Watcher的理解

[bug描述] 由于三层数据是由另外一个接口获取的,其获取后需要重新塞入元数据中,然而再次塞入后却没有被监听到,在视图上没有显示

[bug分析] 没有defineReactive,也就无法被dep收集,Watcher就无法监听

[解决方案] 使用$set方法

Vue中的双向数据绑定是通过defineReactive方法实现,其基本是Object.defineProperty的使用

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

对于一般对象的属性新增,defineReactive是不能获取到的,因而需要使用$set方法

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  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
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

只有在defineReactive中才能被dep收集到,才能通过Watcher进行监听

# 深拷贝导致的数据不能及时更新

[bug描述] 树组件重命名后,新的名字未能及时响应到页面上,却在第二轮渲染到了页面上

[bug分析] lodash深拷贝和点击发送事件的回调发生时间未知,因而在defineReactive中的值获取到的时间也是未定的,通常点击事件的回调会阻塞js线程,因而在点击后虽然defineReactive改变了,传到子组件中的数据由于有深拷贝也需要时间响应,然而响应完后,页面已经完成,因此在这一个时间周期中,响应后的数据被放到了下一次的渲染中,当再次点击后才会显示

[解决方案] 去除深拷贝,点击后的数据直接传到子组件中,不需要深拷贝执行

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

  • 加载渲染过程: 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

  • 子组件更新过程: 父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  • 父组件更新过程: 父 beforeUpdate -> 父 updated

  • 销毁过程: 父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

图片

# 总结

树组件是一个比较复杂的组件,同时也是前端基本功的一个很好的测试案例,反复琢磨树组件的基础功能实现会发现面试中常见题目都有体现,其实面试中的常见题目都是日常工作中的抽象化的考察,除了个别的完全不靠谱的题目,大部分题目还是在日常开发中有很好的体现的,比如本次踩坑实践中就有:1、算法考察:动态规划;2、Object基础api考察;3、深拷贝浅拷贝问题;4、EventLoop问题;5、Vue源码...所以,对前端基本功的打磨和修炼才能使自己的前端能力有一个较大的提升,共勉!!!