vue 阶段性总结

时间:2021-06-07 00:27:13

  使用vue开发已经有一段时间了,本文主要是记录平时使用过程中踩过的一些坑,以及一些心得。一方面可以自我总结提高,另一方面可以将自己的经验分享出来。

  一、为什么要用框架

    现在前端行业发展飞快,我们在选用技术栈的时候,一方面要易于上手,另一方面要适合自己的项目。vue就是这样一个前端框架,易于上手,有成熟的文档可以参考、同样有成熟的社区可以讨论问题。最不济可以阅读源码,vue的源码还是比较易读的。

    为什么我们开发的时候需要使用框架?以前的jquery不好吗?原生js开发不好吗?使用这些框架能够解决哪些痛点?我就来说说我自己的一些浅见。

    1、组件化

    组件化开发可以解耦,便于复用,我跟人而言组件化是模块化更进一步的解耦。最初我们在开发前端项目的时候,浏览器原生并不支持模块开发,我们需要使用一些库来给模块化的js提供一个运行平台,例如之前的 requireJs、seaJs等。如果你经历过原始的开发再转型到模块化开发,你就会明白模块化的好处。因为当你在没有模块的开发过程当中,你会发现所有的变量都是全局的,所有的方法都是全局的(当然局部变量除外)。当你在你的js文件当中使用了某个变量的时候,你都找不到这个变量是定义在当前的js文件当中还是定义在其他的js文件中,这就很头疼了。还有就是你引用了别人的js文件,你无法确认你写的方法是不是覆盖了别人的方法。当你使用了模块化开发的时候,你就能通过模块引入的方式来进行你需要依赖的方法、类、对象、常量、变量的引入。并且模块中所有定义的方法变量都不是全局的,这样你会少了很多麻烦。

    上面简要谈了谈模块化的好处,模块化对你的js逻辑来说基本上是够用了,但是我们前端不仅有js逻辑,还有html、css这些界面样式。这些东西怎么去解耦呢?怎么拥有自己的局部作用域呢?怎么去复用呢?组件就很好地解决了这个问题。试想一下你需要在页面上做一个拥有提交功能的按钮,你需要在html中写入元素,然后再在js中给这个按钮添加事件。当你的页面上有很多这种按钮的时候,你需要去复制多份,这样一没有起到解耦的效果,二无法复用。现在如果浏览器厂商特意为你的需求定制了一个标签,你只需要在你的html文件中使用这个标签就能完成你所需要的功能,这样是不是很简单,如果你需要多份这样的按钮,你要做的只是将这个标签写到其他地方。当然浏览器不可能为你定制这样一个标签,所以你需要自己为自己定制这样一个标签。这就是组件化的思想。

    2、数据绑定

    这个可以说是前端三大框架真正能够解决痛点的地方了,因为前面所说的组件化,浏览器马上就要原生支持 webcomponent 了。在原来的开发模式中我们修改页面中的某个元素中的value值,我们需要获取到相应的元素,然后再修改元素的value。获取元素的过程就是操作DOM的过程,浏览器为js提供了操作DOM的接口,前端就是展示页面,也就是改变html及css。改变html和css需要使用脚本语言才能动态地修改,这就是js存在的重要意义。但是操作DOM的过程是繁琐的,而数据绑定可以帮我们从操作DOM的过程中解放出来。试想一下我们如果只需要改变 value的值就可以让页面发生相应的更改,这样我们就可以免去操作DOM的麻烦。我们称这种改变UI的方式为数据绑定。

    不管框架是如何实现数据绑定的,我们需要关心的只有数据,因为UI是被数据所驱动的。只要数据发生改变UI就能发生相应的更改。其实细想起来,我们做的UI页面就是一个html DOM树,这颗DOM树完全可以通过内存中的对象来实现数据映射。框架要做的工作就是将内存中的对象与DOM树发生绑定关系,其实内存中的这个对象就是我们经常看到的虚拟DOM。

  二、vue的数据绑定原理

    vue这个框架很好地实现了以上两个优点。我曾经读过一些vue的源码,下面说说我对vue发生数据绑定的一些理解,如果掌握了vue的工作原理,我们可以在工作当中很好地解释一些难以理解的问题,我认为还是很有必要的。

 

    一般来说我们改变了数据浏览器并不知道需要重新渲染哪些DOM,浏览器只知道当我们显式地改变了DOM的时候才会去重新渲染。很显然改变数据然后通知浏览器去渲染就是我们的vue起的作用。那么vue是怎么知道我们改变了数据的呢?

    第一个问题:vue如何知道我们修改了数据?

    其实vue并不是万能的,它并不知道我随手写的一个变量是否发生了改变。也就是说vue所监控的变量是有要求的,这个要求就是显式地声明在data和props中的对象属性。也就是说我们改变data对象或者对象中的属性的时候vue是知道我们修改了这些属性的。vue是如何知道的呢?

    在我们实例化Vue这个类的时候,它会递归地对data对象设置监听,监听的方式是 使用浏览器支持的 Object.defineProperty() 这个类静态方法对这些属性或对象设置get、set方法。这两个方法有点类似钩子函数,当你去改变data中的某个属性时就会触发set方法,当你去获取data中的某个属性时就会触发get方法。很显然你可以在get和set方法中执行一些操作去修改页面中的DOM。

    vue就是这样对我们的数据进行监控的。前面说了 vue是在初始化的时候递归地对属性进行监听的,当你改变了data中某个属性的引用的时候vue会重新对新的对象进行递归地监听。注意这里说的是改变引用的时候才会触发监听绑定,也就是说当你的对象引用没有发生改变,只是给对象增加了一个属性的时候vue是无法对新的属性进行监听的。这也就是为什么你在没有在data中声明属性而是后面添加的属性vue没有办法监听到的原因。通常我们还会犯一个错误,就是我们把开始声明的对象引用改变了,但是新的引用中的属性跟原来的引用的属性有所区别,这样原来的属性就会丢失引用。例如

  

 1 data () {
 2     return {
 3         pro1: {  // 初始声明的引用具有a b两个属性
 4             a: 1,
 5             b: 2
 6         }
 7     }
 8 }
 9 
10 ...
11 
12 // 后面的某个时刻
13 this.pro1 = {a: 3, c: 4};
14 
15 // 在这里你会发现原来 模板中与 pro1.b 发生绑定关系已经被丢失了

 

  就上面的问题而言,我们平时的工作当中如何避免这种情况呢。现阶段我采用的方法是 使用 Object.assign()静态方法或者自己编写一个merge方法,进行数据的合并操作。由于Object.assign()方法存在局限性,我们自己编写的merge方法可以更加灵活,所以我们会采用merge方法进行vue的数据变更。merge方法的大概实现如下:

/**
 * 数据合并方法
 * @param {Object} target 目标对象
 * @param {Object} origin 源对象
 * @param {String} stand 合并标准,默认为左树标准
 * @returns {void} 无返回值
 */
export function mergeData (target, origin, stand = 'left') {
  if (!target || !origin) {
    return
  }
  if (Utils.dataType(target) !== Utils.dataType(origin)) {
    console.error('目标对象与源对象的数据类型不同,无法实现合并')
    return
  }

  let flag = stand === 'left'
  for (let prop in target) {
    if (Utils.dataType(target[prop]) === 'object') {
      // target[prop] = (target[prop].constructor === Array) ? [] : {}// 三元运算,将s[prop]初始化为数组或者对象
      mergeData(target[prop], origin[prop])
    } else if (Utils.dataType(target[prop]) === 'array') {
      // 兼容处理
      if (!origin[prop]) {
        origin[prop] = []
      }
      if (origin[prop].length > 0) {
        // 该条件是为了剔除重复的数据
        target[prop].length = 0
      }
      target[prop].push(...origin[prop])
    } else {
      let defaultVal
      switch (Utils.dataType(target[prop])) {
        case 'object':
          defaultVal = flag ? (target[prop] || {}) : (origin[prop] || {})
          break
        case 'array':
          // defaultVal = target[prop] || []
          defaultVal = flag ? (target[prop] || []) : (origin[prop] || [])
          break
        case 'string':
          // defaultVal = target[prop] || ''
          defaultVal = flag ? (target[prop] || '') : (origin[prop] || '')
          break
        case 'number':
          // defaultVal = target[prop] || 0
          defaultVal = flag ? (target[prop] || 0) : (origin[prop] || 0)
          break
        case 'boolean':
          // defaultVal = target[prop] || false
          defaultVal = flag ? (target[prop] || false) : (origin[prop] || false)
          break
      }
      target[prop] = (origin[prop] || defaultVal)
    }
  };
}

     第二个问题:Vue怎么知道我们数据改变之后需要对哪些UI状态做出修改?

    Vue是通过一点一点的截断的方式对模板文件进行解析的,例如:Vue在解析模板的过程当中遇到了<div>{{msg}}</div>,首先会解析出来div标签做为一个node节点,然后接着去截取{{msg}}作为文本节点,并且发现这个文本节点中有特殊的{{}},就会将msg作为一个表达式,并且认为msg是自己需要绑定的UI数据。最后会解析到</div>来最终确定div节点的闭合。实际过程中Vue把模板解析完成之后会生成对应的虚拟DOM树,这个DOM树是存在于内存当中,并且会标记其中需要关心的变量和表达式,将这些需要关系的变量或者表达式与自己data作用域中的数据进行绑定,绑定的过程就是在对数据监听的get、set方法中来进行相应的VNODE修改,VNODE修改流程结束之后会触发render方法,从而达到改变指定UI的效果。

   三、组件数据共享

    分组件开发当然能简化我们的工作,数据绑定又让我们只用关注于数据,因为vue接管了我们的UI,它可以将我们操作的数据动作映射为UI状态的改变。所以我们在平时开发的过程当中需要关心数据的改变已经数据的传递。数据的传递指的是组件之间数据的传递,因为我们在开发工作当中往往都会有多个组件共享数据的情况。这些组件之间的关系可能是父子组件、兄弟组件、祖先后代组件这些关系。这里我想说一下最简单的父子组件之间的数据传递方式。因为兄弟组件和祖先后代组件这种组件关系Vue提供了Vuex和事件总线这种解决方案。

    从我的工作经验来说父子组件之间的数据传递方式有两大类,一类就是Vue官方推荐的做法,父传子通过props来进行传递,子传父通过$emit这种发布订阅模式来进行。第二类就是父传子通过props进行传递,子传父通过改变对象属性引用的方式来进行.

如果多层级组件之间使用vue $emit来改变父组件的数据很闲的很繁琐,我们可以使用改变对象属性的方式来改变这一现象。
    
    在Vue中我们不能直接在子组件中直接修改父组件的数据,这一行为Vue会发出警告。这是因为在Vue2.0 中只支持单项数据流,也就是说只能通过父组件传递数据给子组件,并且父组件改变数据时子组件可以同步监控到修改,从而引起UI的修改。但是我们可以在父组件中传入一个对象到子组件,子组件不直接修改父组件传过来的对象的引用,而是修改传过来的对象的属性,这样Vue就不会发出警告,并且父组件可以获得子组件的修改。
// 父组件模板
<ChildComponent :parent-props="testProp" />
// 父组件数据
data: {
  testProp: {
    pro1: 'a',
    pro2: 'b'
  }
}

// 子组件
props: ['testProp'],
methods: {
  doing () {
    this.testProp.a = 'c'
  }
}
    通过以上步骤,可以实现在子组件中修改父组件传递过来的数据的目的,这种不管组件的层级有多深都可以实现数据的传递和修改,这在有些情况下是很有用的。比如在 element-ui 这个组件库当中,就是通过这个数据传递的方式来实现 Form 组件的。
<el-form :model="formData" :rules="formRules">
  <el-form-item prop="name">
    <el-input v-model="formData.name" />
  </el-form-item>
</el-form>

// 数据
data: {
  formData: {
    name: ''
  },
  formRules: {
    name: [
      // ...
    ]
  }
}
    element-ui 就是首先传入一个 formData这个对象到 el-form 组件中的,并且通过prop属性给 el-form-item 组件,让该组件得以跟 formData.name 这条数据进行绑定,从而实现表单校验。
    (完)