Vue笔记

143 阅读8分钟

nextTick

Vue官方对nextTick这个API的描述:

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

例如,当你设置 vm.someData = 'new value' ,该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数情况我们不需要关心这个过程,但是如果你想在 DOM 状态更新后做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

Vue 响应式原理

整体思路是数据劫持+观察者模式

对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。

[手写Vue2.0源码(一)-响应式数据原理|技术点评]  juejin.cn/post/693534… 

// 是否可以使用__proto__
const hasProto = '__proto__' in {}
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
​
class Observer {
  constructor (value) {
    // this.value = value
    // def(value, "__proto__",  this)
        
    
    if( Array.isArray(value) ) {
      if(hasProto) {  
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys )
      }
​
      this.observeArray(value)
    }
    this.walk(value)
  }
​
  walk(data) {
    let keysArr = Object.keys(data)
    for (let i = 0; i < keysArr.length; i++) {
      let key = keysArr[i]      
      let value = data[key]
      
      defineReactive(data, key, value)
    }
  }
​
  
  observeArray(arrs) {
    for (let i = 0; i < arrs.length; i++) {
      observe(arrs[i])    
    }
  }
}
​
function protoAugment(target, src) {
  target.__proto__ = src
}
​
function copyAugment(target, src, keys) {
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    def(target, keys, src[keys])
  }
}
​
function def(obj, key, val, enumerable) {
  Object.definePropety(obj, key, {
    value: val,
    enumerable: !!enumerable,
    configurable: true,    
    writable: true
  })
}
​
​
function  defineReactive(obj, key, value) {
  
  observe(value); // 递归关键
​
  /* Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性) */
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if( property && property.configurable === false ) {
    return
  }
​
  Object.defineProperty(data, key, {
    get() {
      console.log("获取值");
      return value
    },
​
​
    set(newVal) {
      if( newVal === value ) return
      value = newVal
    }
  })
​
}
​
​
export function observe(value) { 
  if(
    value.constructor === Object || 
    value.constructor === Array
  ) {
    let _observe = new Observer(value)
    return _observe
  }
}

路由原理 history 和 hash 两种路由方式的特点

hash 模式

  1. location.hash 的值实际就是 URL 中#后面的东西 它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
  2. 可以为 hash 的改变添加监听事件
window.addEventListener("hashchange", funcRef, false);
复制代码

每一次改变 hash(window.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了

特点:兼容性好但是不美观

history 模式

利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。

这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。

特点:虽然美观,但是刷新会出现 404 需要后端进行配置

diff

传统diff

传统的Diff算法通过循环递归对节点进行比较,然后判断每个节点的状态以及要做的操作(add,remove,change),最后 根据Virtual DOM进行DOM的渲染。

优化后的diff(React)

diff 只进行同级比较(同层比较)

不同类型节点的比较

如果发现新旧两个节点类型不同时,Diff算法会直接删除旧的节点及其子节点并插入新的节点,这是由于前面提出的不同组件产生的DOM结构一般是不同的,所以可以不用浪费时间去比较。注意的是,删除节点意味着彻底销毁该节点,并不会将该节点去与后面的节点相比较。

相同类型节点的比较

若是两个节点类型相同时,Diff算法会更新节点的属性实现转换。

列表节点的比较

列表节点的操作一般包括添加、删除和排序,列表节点需要我们给它一个key才能进行高效的比较。

Vue Diff算法的实现

Vue的Diff算法与上面的思路大体相似,只比较同级的节点,若找不到与新节点类型相同的节点,则插入一个新节点,若有相同类型的节点则进行节点属性的更新,最后删除新节点列表中不包含的旧节点。具体的实现在vue源码的src/core/vdom/patch.js中的updateChildren方法中

就地复用 在Diff中会使用到一种就地复用的策略。就地复用是指Vue会尽可能复用之前的DOM,尽可能不发生DOM的移动。

Vue判断新旧节点是否为相同节点(也就是上面的sameVnode方法),这个相同节点的意思并不是两个完全相同的节点,实际上它仅判断是否为同类节点(同类节点为类型相同且节点数据一致,如前后两个span,span标签上的属性没有改变,但是里面的内容变了,这样就算作同类节点),如果是同类节点,那么Vue会直接复用旧DOM节点,只要更新节点中的内容即可。这样可以大大减少列表中节点的移动操作。

Vue 的父子组件生命周期钩子函数执行顺序

  • 加载渲染过程

父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted

  • 子组件更新过程

父 beforeUpdate->子 beforeUpdate->子 updated->父 updated

  • 父组件更新过程

父 beforeUpdate->父 updated

  • 销毁过程

父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

虚拟 DOM 是什么 有什么优缺点

由于在浏览器中操作 DOM 是很昂贵的。频繁的操作 DOM,会产生一定的性能问题。这就是虚拟 Dom 的产生原因。Vue2 的 Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象。

优点

  1. 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
  2. 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  3. 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点

  1. 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
  2. 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。

v-model 原理

v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:

  • text 和 textarea 元素使用 value property 和 input 事件;
  • checkbox 和 radio 使用 checked property 和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。

V3和V2区别

简单对比 vue2.x 与 vue3.x 响应式

其实在 Vue3.x 还没有发布 bate 的时候, 很火的一个话题就是Vue3.x 将使用 Proxy 取代 Vue2.x 版本的 Object.defineProperty。 没有无缘无故的爱,也没有无缘无故的恨。为何要将Object.defineProperty换掉呢,咋们可以简单聊一下。 我刚上手 Vue2.x 的时候就经常遇到一个问题,数据更新了啊,为何页面不更新呢?什么时候用$set更新,什么时候用$forceUpdate强制更新,你是否也一度陷入困境。后来的学习过程中开始接触源码,才知道一切的根源都是 Object.defineProperty。 对这块想要深入了解的小伙伴可以看这篇文章 为什么 Vue3.0 不再使用 defineProperty 实现数据监听?要详细解释又是一篇文章,这里就简单对比一下Object.defineProperty 与 Proxy

  1. Object.defineProperty只能劫持对象的属性, 而 Proxy 是直接代理对象

由于Object.defineProperty只能劫持对象属性,需要遍历对象的每一个属性,如果属性值也是对象,就需要递归进行深度遍历。但是 Proxy 直接代理对象, 不需要遍历操作

2.Object.defineProperty对新增属性需要手动进行Observe

因为Object.defineProperty劫持的是对象的属性,所以新增属性时,需要重新遍历对象, 对其新增属性再次使用Object.defineProperty进行劫持。也就是 Vue2.x 中给数组和对象新增属性时,需要使用$set才能保证新增的属性也是响应式的, $set内部也是通过调用Object.defineProperty去处理的。

加载渲染过程

->父beforeCreate -> 父created -> 父beforeMount

->\子beforeCreate -> 子created -> 子beforeMount -> 子mounted

-> 父mounted

  • 子组件更新过程

->父beforeUpdate

-> 子beforeUpdate -> 子updated

-> 父updated

  • 父组件更新过程

父beforeUpdate -> 父updated

  • 销毁过程

-> 父beforeDestroy

-> 子beforeDestroy -> 子destroyed

-> 父destroyed

SSR和预渲染

SSR服务端渲染

优点:

  1. 更好的SEO优化,爬虫工具可以直接看到整个页面
  2. 更快的响应速度,提升用户体验

缺点:

  1. 构建设置部署麻烦
  2. 更多的服务器压力

vue-server-renderer