vue3.0尝鲜与对比

1.实现数据响应的不同

Vue 2.0 使用 Object.defineProperty()实现数据响应
Object.observe()被废弃后,Object.defineProperty()成为了大家实现数据劫持的首选。
vue递归遍历data中的数据,使用 Object.defineProperty()劫持 getter和setter,在getter中做数据依赖收集处理,在setter中监听数据的变化,并通知订阅当前数据的地方。

这么做的问题有:

  • 检测不到对象属性的添加和删除:Object.defineProperty()是在原对象上进行修改的,当你在对象上新加了一个属性newProperty,当前新加的这个属性并没有加入vue检测数据更新的机制(因为是在初始化之后添加的)。vue. s e t 是 能 让 v u e 知 道 你 添 加 了 属 性 , 它 会 给 你 做 处 理 , set是能让vue知道你添加了属性, 它会给你做处理, setvue,set内部也是通过调用Object.defineProperty()去处理的
  • vue2.0不监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。(这里有一个大误区,js是可以通过Object.defineProperty()监控数组下标变化的,而vue的设计者出于性能考虑取消了这个功能,并不是原生就不支持)详细可以看参考内容(强烈推荐看看)
    在这里插入图片描述
  • 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,当data中数据比较多且层级很深的时候,会有性能问题,因为要遍历data中所有的数据并给其设置成响应式的。
Vue 3.0 中的Proxy
const p = new Proxy({}, {
    get(target, propKey) {
        return '哈哈,你被我拦截了';
    }
});

console.log(p.name);
// 哈哈,你被我拦截了

Proxy支持的拦截操作一共 13 种,详细的可以查看 MDN。
vue3.0尝鲜与对比_第1张图片
为什么使用 Proxy 可以解决上面的问题呢?主要是因为Proxy是拦截对象,并返回一个新的被proxy包裹的对象,外界对该对象的访问,都必须先通过这层拦截。无论访问对象的什么属性,之前定义的还是新增的,它都会走到拦截中。

Reflect(ES6引入) 是一个内置的对象,它提供拦截 JavaScript 操作的方法。将Object对象一些明显属于语言内部方法(比如Object.defineProperty())放到Reflect对象上。修改某些Object方法的返回结果,让其变得更合理。让Object操作都变成函数行为。具体内容查看MDN

Reflect.get(target, prop, receiver)到底有什么用呢?
先看下面的例子:

let People = new Proxy({
  _name:'zky',
  get name(){
    return this._name
  }
},{
  get:function(target,prop,receiver){
    return target[prop]
    }
  })
let Man = {_name:'zky_man'}
Man.__proto__ = People // Man继承People
console.log(Man._name) // zky_man
console.log(Man.name) // zky

问题来了,Man中已经存在_name属性,但这里Man.name返回的却是原型链上的_name属性,
原因很好解释:get name中的this默认绑定为People。

那怎么解决这个问题呢?这里就该receiver上场了,修改上面的例子:

let People = new Proxy({_name:'zky'},{
  get:function(target,prop,receiver){ // receiver指向的是get的调用者
    return Reflect.get(target,prop,receiver) // 调用get name函数时,this被绑定到receiver
    }
  })
let Man = {
  _name:'zky_man',
  get name(){
    return this._name
  }
  }
Man.__proto__ = People // Man继承People
console.log(Man._name)// zky_man
console.log(Man.name) // zky_man

举个例子总结:有一栋大楼,我们想给里面的住户发邮件,Object.defineProperty()就是只有注册了电子邮箱的的住户才能可能收到我们的邮件,后搬来的住户没有注册邮箱,就无法和我们交流,除非我们强制他再注册一个邮箱(使用vue.$set),而使用Proxy就是建立一个收发室,所有要进行交流的邮箱先通过收发室的拦截,再由收发室通知每一位住户信息,这样新搬来的住户也可以和我们正常交流了。

2.函数式编程

<template>
  <button @click="increment">
    Count is: {{ count }}
  </button>
</template>

<script>
  import { ref, watchEffect, onMounted } from 'vue'

  export default {
    setup() {
      const count = ref(0)
      function increment() {
        count.value++
      }
      watchEffect(() => console.log(count.value))
      onMounted(() => console.log('mounted!'))
      return {
        count,
        increment,
      }
    },
  }
</script>

第一次看这段代码可能会觉得神似react hooks!
Vue 的函数式编程和react hooks最大的不同就是setup()入口函数只执行一遍,setup() 函数返回值就是注入到页面模版的变量。
ref()(以前被命名为value()) 返回的是一个对象,通过 .value 才能访问到其真实值。
为何 ref()返回的是 Wrappers (包装对象)而非具体值呢?原因是 Vue 采用双向绑定,只有对象形式访问值才能保证访问到的是最终值,这一点类似 React 的 useRef() API 的 .current 规则。
那既然所有 ref() 返回的值都是 Wrapper,那直接给模版使用时要不要调用 .value 呢?答案是否定的,直接使用即可,模版会自动 Unwrapping。关于包装对象(对于原生js:当对number、string、boolean这三种数据类型调用方法时候,js会先将他们转换为对应的包装对象,并且这个对象是临时的,调用完包装对象的方法后,包装对象随即被丢弃)
关于watch,因为在 Vue 中,setup 函数仅执行一次,所以不像 React Function Component,每次组件 props 变化都会重新执行,因此无论是在变量、props 变化时如果想做一些事情,都需要包裹在 watch 中。

3.Vue3.0 与 React hooks的区别与优势

  1. 结构区别
    因为客观上存在Immutable 与 Mutable、JSX 与 Template 的区别,vue中的setup 函数仅执行一次,看上去与 React 函数完全不一样(React 函数每次都执行),但其实 Vue 将渲染层(Template)与数据层(setup)分开了,而 React 合在了一起。虽然我们可以利用 React Hooks 将数据层与渲染层完全隔离,源于 JSX 与 Template 的根本区别,JSX 使模版与 JS 可以写在一起,因此数据层与渲染层可以耦合在一起写(也可以拆分),但 Vue 采取的 Template 思路使数据层强制分离了,这也使代码分层更清晰了。

  2. 语法区别
    vue3.0尝鲜与对比_第2张图片
    React 返回的 count 是一个数字,是因为 Immutable 规则,而 Vue 返回的 count 是个对象,拥有 count.value 属性,也是因为 Vue Mutable 规则导致,这使得 Vue 定义的所有变量都类似 React 中 useRef 定义变量,因此不存 React capture value (捕捉值:每次 Render 的内容都会形成一个快照并保留下来,因此当状态变更而 Rerender 时,就形成了 N 个 Render 状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State)的特性。

  3. Vue 的代码使用更符合js原本的习惯
    这句话直截了当戳中了 JS 软肋,JS 并非是针对 Immutable 设计的语言,所以 Mutable 写法非常自然,而 Immutable 的写法就比较别扭。
    当 Hooks 要更新值时,Vue 只要用等于号赋值即可,而 React Hooks 需要调用赋值函数,当对象类型复杂时,还需借助第三方库才能保证进行了正确的 Immutable 更新。

  4. 对 Hooks 使用顺序无要求,而且可以放在条件语句里
    对 React Hooks 而言,调用必须放在最前面,而且不能被包含在条件语句里(可以参考React hooks 学习笔记 - useState ),这是因为 React Hooks 采用下标方式寻找状态,一旦位置不对或者 Hooks 放在了条件中,就无法正确找到对应位置的值。
    而 Vue Function API 中的 Hooks 可以放在任意位置、任意命名、被条件语句任意包裹的,因为其并不会触发 setup 的更新,只在需要的时候更新自己的引用值即可,而 Template 的重渲染则完全继承 Vue 2.0 的依赖收集机制,它不管值来自哪里,只要用到的值变了,就可以重新渲染了。

  5. 不会在每次渲染重复调用,减少垃圾回收压力
    这确实是 React Hooks 的一个问题,所有 Hooks 都在渲染闭包中执行,每次重渲染都有一定性能压力,而且频繁的渲染会带来许多闭包,虽然可以依赖垃圾回收机制回收,但会给垃圾回收带来不小的压力,并且必须要包裹useCallback 函数确保不让子元素频繁重渲染。而 Vue Hooks 只有一个引用,所以存储的内容就非常精简,也就是占用内存小,而且当值变化时,也不会重新触发 setup 的执行,所以确实不会造成垃圾回收压力,Mutable + 依赖自动收集就可以做到最小粒度的精确更新,根本不会触发不必要的 Rerender,因此 useMemo 这个概念也不需要了。

4.虚拟dom与静态标记/事件缓存

先看一段代码

<div id="app">
    <h1>今天</h1>
    <p>大家好!</p>
    <div>{{name}}</div>
</div>

在vue2.0中,会转换为:
function render() {
  with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, [_c('h1', [_v("今天")]), _c('p', [_v("大家好!")]), _c('div', [_v(
      _s(name))])])
  }
    }

其中的

今天

大家好!

两个节点完全是静态节点,也要加入diff算法的比较过程中来,所以造成了一定的性能消耗。

在vue3.0中,会转换为:

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", { id: "app" }, [
    _createVNode("h1", null, "今天"),
    _createVNode("p", null, "大家好!"),
    _createVNode("div", null, _toDisplayString(_ctx.name), 1 /* TEXT */)
  ]))
}

注意看第二个_createVNode结尾的“1”,Vue在运行时会生成number(大于0)值的PatchFlag,用作标记。只有带这个参数的,才会被真正的追踪,静态节点不需要遍历,这个就是vue3优秀性能的主要来源。PatchFlag的值有以下多种类型:
vue3.0尝鲜与对比_第3张图片
如果一个节点涉及以上两种绑定,进行位运算就可以得出最终结果了。例如:
vue3.0尝鲜与对比_第4张图片
当我们的节点中涉及到事件函数时:

<div id="app">
  <button @click="handleClick">戳我</button>
</div>

转换为:

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", { id: "app" }, [
    _createVNode("button", {
      onClick: _cache[1] || (_cache[1] = $event => (_ctx.handleClick($event)))
    }, "戳我")
  ]))
}

传入的事件会自动生成并缓存一个内联函数再cache里,变为一个静态节点。这样就算我们自己写内联函数,也不会导致多余的重复渲染。

5.优化性能的不同选择

react使用fiber实现这个目标
简单对描述就是在一个时间片里尽量做更多事,快速响应用户,让用户觉得够快。利用浏览器的空闲时间来做diff,如果超过了16ms,有动画或者用户交互的任务,就把主进程控制权还给浏览器,等空闲了继续。
对fiber细节感兴趣对可以看看我之前写的学习笔记React Fiber学习笔记
vue3.0尝鲜与对比_第5张图片
vue优化模版渲染实现这个目标
除了刚才上面说得“静态标记/事件缓存”外,在vue2.0中也有关于静态节点编译对优化,不过这个静态节点特指对是静态根节点,而“静态标记/事件缓存”是为静态子节点服务的。
compile 的内容非常多,大致分为三块主要内容,我也称他们是Vue的 渲染三巨头:
就是 parse,optimize,generate
虽然分为三块,但是要明确一点:compile 的作用是解析模板,生成渲染模板的 render,再继而产生vnode。
vue3.0尝鲜与对比_第6张图片
parse:接收 template 原始模板,按照模板的节点 和数据 生成对应的 ast(抽象语法树)
vue3.0尝鲜与对比_第7张图片
optimize:遍历递归每一个ast节点,标记静态的根节点(没有绑定任何动态数据),这样就知道那部分不会变化,于是在页面需要更新时,减少去比对这部分DOM从而达到性能优化的目的。
vue3.0尝鲜与对比_第8张图片
generate:把前两步生成完善的 ast 组装成 render 字符串(这个 render 变成函数后是可执行的函数,不过现在是字符串的形态,后面会转成函数)
vue3.0尝鲜与对比_第9张图片
关于js的编译原理可以参考这个最简单的js编译器( 介绍:今天我们将一起写一个编译器,但不是真正意义上的编译器,只是一个极简编译器。简单到只要你把说明文字去掉,只剩下200行左右的代码)强烈推荐看看

你可能感兴趣的:(vue3.0尝鲜与对比)