前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >石桥码农:Vue3 与 Vue2 在响应机制的实现上有什么差别?

石桥码农:Vue3 与 Vue2 在响应机制的实现上有什么差别?

作者头像
LIYI
发布2020-02-13 11:57:21
2.1K2
发布2020-02-13 11:57:21
举报
文章被收录于专栏:艺述论专栏

文 / 李艺

代码语言:javascript
复制
目录

一、问题:vue2 通过数组索引改变数据不能触发视图更新是怎么回事?
二、分析:在 vue3 不存在这个问题,vue2 与 vue3 的响应机制分别是怎么实现的?
三、实践:现在如何体验 vue3 框架,四种体验方式的归纳
    3.1、在 vue2 项目中复用
    3.2、从 vue-next 源码编译
    3.3、基于项目模板
    3.4、基于插件自动转化
四、总结与思考

一、问题:vue2 通过数组索引改变数据不能触发视图更新是怎么回事?

vue 是目前最流行的前端框架之一,在中国区至少有 70 万的开发者在使用。

vue 开发者可能都遇到过这样一个问题:如果模板中数据绑定的是一个数组,我们在 js 代码里面,直接以索引方式改变数组元素的值,有时候视图并不会按照我们的期许更新。下面,我们创建一个代码示例,再现这个场景:

代码语言:javascript
复制
<template>
  <div>
    <p>{{arr}}</p>
    <button v-for="(item,index) in arr" :key="index" @click="change(item,index)">{{item}}</button>
    <button @click="arr.push(4)">push</button>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data(){
    return {
      arr:[1,2,3]
    }
  },
  methods:{
    change(item,index){
      this.arr[index]=0
    }
  }
}
</script>

这是一个 vue 组件,在一个独立的文件里,在它的数据对象 data 中有一个数组 arr,被它的模板以 v-for 循环的方式渲染在视图中。当我们单击这些动态渲染的带有数字的按钮时,视图并不会改变。

在上面的 js 代码中,我们明明通过索引改变了数组元素,为什么视图会没有效果呢?

现在我们运行一下,看看这个组件的实际运行效果:

我们看到,在运行效果中,一共有 4 个按钮,前 3 个是通过 v-for 循环动态渲染的,最后一个push按钮用于数据的动态添加。在运行中发现,我们单击前 3 个按钮,按钮文本不会改变,只有单击push按钮时,视图才会更新。

这是为什么?为什么通过数组索引改变元素的值,视图不能及时更新呢?这是不是 vue 框架的一个 bug 呢?

事实上这个问题 vue 团队也是承认的,并且在官方文档上也有提示,我们可以在这个链接:

https://cn.vuejs.org/v2/guide/list.html#数组更新检测

找到这样一段话:

由于 js 的限制,Vue 不能检测以下数组的变动,这些情况包括:

当你利用索引直接设置一个数组项时: vm.items[indexOfItem] = newValue

以及当你修改数组的长度时: vm.items.length = newLength

举个例子:

代码语言:javascript
复制
var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的```

在官方文档中还明示,直接通过数组索引改变数组元素或直接修改数组长度,均不能触发视图更新。

那么在实际开发中,如果需要改变数组元素,应该怎么做呢?官方文档也给出了解决方案,可以使用Vue.set方法或使用数组的特定操作方法,例如splice。具体的示例代码是这样的:

代码语言:javascript
复制
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

既然有问题,那么为什么 vue 框架没有优化解决呢?是不能解决吗?

答案是可以解决的。

现在我们修改一下上面测试代码中的change方法,在改变数组元素后,打印一下数组元素的值:

代码语言:javascript
复制
change(item,index){
  this.arr[index]=0
  // 数据可以改变,但视图不会更新
  console.log(this.arr[index]);
}

运行效果是这样的:

我们看到,当我们单击数字按钮时,即使视图没有更新,数据其实已已经更新了。

vue框架里,有这样一个forceUpdate方法:

代码语言:javascript
复制
vm.$forceUpdate()

将这个方法放在修改数组元素之后调用,其实也可以强制视图更新。也就是说,这个问题vue框架其实是可以解决的,并不是像文档中所说的“因为受js限制”不能解决。

事实上在前面的测试中,我们也发现当单击push按钮时,我们往数组推入了一个新数据项,这个时候所有视图都更新了,包括前面的数字按钮。

那么,为什么push按钮可以触发视图更新?这是因为vue框架虽然没有监听数组索引和数组长度的改变,但是对个别特殊的数组方法进行了钩子改造。当我们调用下面这 7 个数组方法时:

代码语言:javascript
复制
push、pop、shift、unshift、splice、sort、reverse

都会触发视图的更新响应。

关于数组索引更新的这个问题,在社区有人提出过疑问,vue团队也做出过回答:

从截图中可以看到,是因为性能的考虑,解决这个问题并维持响应机制的一致性,会影响用户体验和程序性能,投入产出不成正比,所以没有实现。

问题到这里已经明白了,接下来我们看看在vue3中这个问题是如何解决的。

二、分析:在 vue3 不存在这个问题,vu2 与 vu3 的响应机制分别是怎么实现的?

上面的问题在vue3得到了完美的解决。与前面的示例代码实现同样的功能,作者用vue3再实现一下,代码是这样的:

代码语言:javascript
复制
<template>
  <div>
    <button v-for="(item,index) in arr" :key="index" @click="change(item,index)">{{item}}</button>
    <button @click="arr.push(4)">push</button>
  </div>
</template>

<script>
import {ref} from 'vue'

export default {
  setup(){
    let arr = ref([1,2,3])
    function change(item,index){
      // 这行代码就可以起作用
      arr.value[index]=0
      // 数据变,视图亦变
      window.console.log(arr.value[index]);
    }
    return {arr, change}
  }
}
</script>

这是vue3的代码,setup是它独特的标志。主要代码都是在setup函数内实现,功能和上面示例是一样的,我们看一下运行效果:

从效果来看,当以数组索引改变数据时,不但数据更新了,视图也有更新。

那么问题来了,相同的代码逻辑,在vue2中存在的问题,在vue3中不是问题了,为什么?这也是这篇文章作者想探讨的核心问题。

答案在于vue2vue3的响应机制的实现方式不同,vue2的响应机制是基于Object.defineProperty实现的,而vue3是通过Proxy实现的。

可能有读者会问了,为什么不用vue3的实现方法将vue2优化一下呢,这样vue2不就没有问题了吗?

这个问题作者觉得可能有两个方面原因:第一个,2013 年vue第一个版本开始编写的时候,那个时候还没有Proxy语法可以使用,Proxyes6的语法,es62015 年颁布的;还有另外一个原因,在有了es6 Proxy语法以后,基于旧版本优化牵涉的代码太多,所以vue团队索性重新拉了一个新版本,当然还有其它新特性的考虑,统一都在新版本中一起做了。

那么这两种响应机制的实现方式,具体是怎样的呢,下面我们通过模拟、还有查看源码的方式,来对比它们之间的差别。

首先我们看这段代码:

代码语言:javascript
复制
function reactive(target) {
  for (let key in target) {
    let value = target[key]
    if(typeof value == 'object') {
      reactive(value)
    }else{
      Object.defineProperty(target, key, {
        get() {
          return value
        },
        set(newVal) {
          if (newVal !== value) {
            console.log('value change:', key, newVal)
            value = newVal
          }
        }
      })
    }
  }
}
console.log('init');

let data = {kind:1, arr:[1,2,3]}
reactive(data)
data.arr[2] = 0 //此行还会触发更新
data.arr.push(4)
data.arr[3] = 0 //此行不会触发更新
data.kind = 0

在这段代码中,我们模拟实现了vue2的响应机制。我们期望的是,当data.arr[2]data.arr[3]改变数据时,value change会打印出来。

运行时打印的结果是这样的:

代码语言:javascript
复制
init
value change: 2 0
value change: kind 0

data变量相当于vue数据源,当我们以数组索引的方式改变数据时,关于value change的打印并没有出现。

现在我们看一下Object.defineProperty这个方法。通过查看mdn文档,有这样一个描述:

代码语言:javascript
复制
Object.defineProperty() 
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

语法:
Object.defineProperty(obj, prop, descriptor)

具体的参数说明可以查看这个文档:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

vue2就是通过这样一个方法实现它的响应机制的。

在模拟了vue2的实现后,接下来我们来模拟vue3响应机制的实现,看这段代码:

代码语言:javascript
复制
function reactive2(target) {
 let handler = {
   get(target, key, receiver) {
     let value = Reflect.get(target, key, receiver)
     return typeof value === 'object' ? reactive2(value) : value
   },
   set(target, key, newVal, receiver) {
     console.log("value change2:",key, newVal);
     let res = Reflect.set(target, key, newVal, receiver);
     return res;
   }
 }
 return new Proxy(target, handler);
}
let proxy = reactive2({name: 'yangbo', arr:[1,2,3]})
proxy.name = 'yb';
console.log(proxy.name)
proxy.arr[2] = 0
console.log(proxy.arr[2])
proxy.arr.push(4)
proxy.arr[3] = 0
console.log(proxy.arr[3])

在这段代码中,我们通过Proxy创建了一个数据对象的代理,变量proxy相当于vue组件的数据源对象data

现在我们运行一下,运行时打印的结果是这样的:

代码语言:javascript
复制
init2
value change2: name yb
yb
value change2: 2 0
0
value change2: 3 4
value change2: length 4
value change2: 3 0

从运行效果来看,基于Proxy实现的响应体制,不仅能监听数组索引的修改,对数组长度的变化也有感知。也难怪vue3基于它进行响应机制的重构了,有了这个机制原来vue2对数组方法的钩子改造也不再需要了。

到这里,读者可能会有一个疑问:“这是你自己实现的响应机制,vue框架也是这么实现了吗?”

确认这个问题很容易,查看vue源码就可以了。我们先看vue2的源码。

大家可以在线打开或者下载这个vue仓库:

https://github.com/vuejs/vue

打开这个index文件:

vue/src/core/observer/index.js

大约 135 行有这样的代码:

代码语言:javascript
复制
// Define a reactive property on an Object.
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  ...
) {
  const dep = new Dep()
  ...

  Object.defineProperty(obj, key, {
    ...
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()//在这里收集的依赖,只有初始化阶段Dep.target有值,为真
        ...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      ...
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify() //在这里实现的视图节点更新
    }
  })
}

可以看到源码与作者的实现方式是类似的,主要是通过Object.defineProperty定义了一个get和一个set。为了方便大家查看,作者把不相关的代码给略去了。在这里值得一提的是,关于dep这个对象,它是一个vue自实现的观察者模式对象,它在初始化阶段收集数据依赖,在数据更新时通过dep.notify()方法通知虚拟DOM节点更新视图。vue2判断是不是初始化阶段,是通过“dep.target”这个属性判断的,只有当初始化时,这个target属性才有值。

查看了vue2的源码,我们再看一下vue3响应机制的实现方式。

打开或下载这个vue-next仓库:

https://github.com/vuejs/vue-next

找到这个reactive文件:

packages/reactivity/src/reactive.ts

大约 108 行有这样一段代码:

代码语言:javascript
复制
function createReactiveObject(
  target: unknown,
  ...
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  ...
  // 在初始化阶段使用collectionHandlers,运行时阶段使用baseHandlers
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  observed = new Proxy(target, handlers)
  ...
  return observed
}

在这段代码中,通过new Proxy实现了响应机制。在初始化阶段,使用collectionHandlers作为handlers的实参;在运行时阶段,使用baseHandlers作为实参。collectionHandlers是用于收集数据依赖的,重点我们看baseHandlers的实现。

再看一下这个baseHandlers文件:

packages/reactivity/src/baseHandlers.ts

大约 140 行有这样一段代码:

代码语言:javascript
复制
export const mutableHandlers: ProxyHandler<object> = {
  get,// from createGetter
  set,// from createSetter
  deleteProperty,
  has,
  ownKeys
}

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    ...
    const res = Reflect.get(target, key, receiver)
    ...
    return ...res
  }
}

function createSetter(isReadonly = false, shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    ...
    const oldValue = (target as any)[key]
    ...
    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    ...
    return result
  }
}

这是上面提到的baseHandlers的实现代码,与作者实现的vue3响应机制的代码具有相似的脉路,都是通过创建一个Proxy对象,代理对实际的数据对象的访问。关于collectionHandlers的实现,大家可以自己查看源码,这里不多做介绍了。

到这里,我们的问题基本上已经讲完了,不知道作者有没有讲明白,大家是不是都清楚了呢。上面涉及的所有示例代码,都可以从这里下载:

https://git.code.tencent.com/shiqiaomarong/vue-go-rapiddev-example/tags/20200206

关于vue2的示例代码在这里查看:

vue-and-go-example/vue-next2/src/components/HelloWorld.vue

vue3的示例代码在这里查看:

vue-and-go-example/hello-vue3-sfc/src/components/HelloWorld.vue

通过源码编译体验vue3框架的示例代码在这里查看:

vue-and-go-example/vue3-from-source/index2.html

三、实践:现在如何体验vue3 框架,四种体验方式归纳

目前vue3还没有正式发布,还处于alpha阶段,读者可能想提前试用vue3,作者整理一下,大概有 4 种体验方式,分享给大家。

3.1、在 vue2 项目中复用

第一个方式,就是直接在vue2项目里,通过安装一个插件,体验vue3的组合API编码风格,这个插件是@vue/composition-api,具体的体验步骤是这样的:

代码语言:javascript
复制
// 1 使用 vue cli 创建普通的 vue2 项目
vue create vue-next2

// 2 安装插件
yarn add @vue/composition-api

// 3 修改 main.js,启用插件:
import VueCompositionApi from "@vue/composition-api";
Vue.use(VueCompositionApi);

// 4 引用并使用
import CompositionApi from './components/CompositionApi.vue'

具体示例可以查看:

vue-and-go-example/vue-next2/src/components/CompositionApi.vue

这种方式使用的还是vue2的框架,并不是vue3的框架。那么能不能直接使用vue3的框架呢,因为目前vue3已经发布alpha4第 4 个版本了,离真正发布越来越近了?

答案是可以的。

3.2、从 vue-next 源码编译

目前通过指令安装,安装的仍然是vue2版本,但我们可以通过源码编译的方式,直接使用vue3框架。这就是第二种方式,步骤大概是这样的:

代码语言:javascript
复制
// 1 下载源码
git clone https://github.com/vuejs/vue-next.git --depth=1

// 2 安装依赖,准备编译
cd vue-next
yarn

// 3 编译源码
yarn build vue -f global

// 4 在html页面中使用
packages/vue/dist/vue.global.js

使用这种方式,有一个html示例代码供参考:

代码语言:javascript
复制
<div id="app">
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
  <counter></counter>
</div>

<script src="vue.global.js"></script>

<script>
  let { reactive, ref, computed } = Vue

  const Counter = {
    template: `
        <div>
            <span>{{ count }}</span>
        </div>
        <div>
            <button @click="increase">add</button>
        </div>
    `,
    setup() {
      const count = ref(0)
      function increase() {
        count.value++;
      }
      return {
        count,
        increase
      }
    }
  }

  Vue.createApp({
    components: {
      Counter
    },
    setup() {
      const state = reactive({
        count: 0,
        double: computed(() => state.count * 2)
      })

      function increment() {
        state.count++
      }

      return {
        state,
        increment
      }
    }
  }).mount('#app')
</script>

在这段代码中,由于没有工程化编译支持,是通过script标签加载vue.global.js文件的。vue3的业务逻辑都集中在setup这个函数里。vue3也支持components组件复用,在上面代码中,Counter就是一个独立的vue组件。

这种源码编译的体验方式比较麻烦,由于国内网络环境的原因,在使用yarn指令安装插件依赖时,可能会出现安装失败。那么有没有更简单的方法体验vue3框架呢?

答案也是有的。

3.3、基于项目模板

第三种方式就是直接使用vue团队提供的预设项目模板:

https://github.com/vuejs/vue-next-webpack-preview

使用步骤很简单:

代码语言:javascript
复制
// 1 下载源码
git clone https://github.com/vuejs/vue-next-webpack-preview.git --depth=1

// 2 安装、运行
yarn
yarn serve

在这个vue3项目模板里,运行vue3代码所需的全部依赖都已经设置好了,基于这个源码直接修改就可以试用。

在前不久,大概 2020 年 1 月 13 日 vue 团队公布了一个插件,名称叫vue-cli-plugin-vue-next。这个插件的可以将我们普通的vue2项目自动转为一个vue3项目,它会自动修改工程配置以满足需要,这就是第 4 种体验方式。

3.4、基于插件自动转化

这个插件是vue-cli-plugin-vue-next,地址在这里:

https://github.com/vuejs/vue-cli-plugin-vue-next

这种方式体验起来也很简单,但是网络必须要好。

先是用vue cli创建一个普通的vue2项目,然后再安装这个插件vue-cli-plugin-vue-next。这个插件会自动修改项目的package.json配置文件、还有部分文件源码。具体步骤是这样的:

代码语言:javascript
复制
// 1 更新工具、创建 vue2 项目
yarn global add @vue/cli
vue create hello-vue3-sfc

// 2 安装插件
cd hello-vue3-sfc
vue add vue-next

// 3 启动、运行项目
yarn & yarn serve

这个插件的 0.0.3 版本有一个 bug,作者写这篇文章时发现了这个问题。在 main.js 文件里的这行代码:

createApp().mount(App, '#app')

应该改为:

createApp(App).mount('#app')

这行错误代码会引发一个莫名奇妙的异常:

代码语言:javascript
复制
error:
Uncaught TypeError: parent.appendChild is not a function

但随后作者发现,这个 Bug 在新版本 0.0.4 中被修复了。如果你安装的是 0.0.3 版本,直接将这行代码修改就可以正常运行。如果是使用淘宝镜像源安装的,不能及时获取新版本,可以尝试切换到官方源:

yarn config set registry https://registry.yarnpkg.com

安装完成以后,可以再切换回来:

yarn config set registry https://registry.npm.taobao.org

这个插件在安装时,会询问“插件会修改代码,要不要继续?”:

代码语言:javascript
复制
There are uncommited changes in the current repository, it's recommended to commit or stash them first.
? Still proceed? Y

在终端中直接敲入Y或Yes就可以完成。那么,这个插件它是如何工作的呢?可以通过查看源码得知,在插件源码的generator目录下,放置的是替换逻辑,指定vue2项目中哪些文件哪些内容被替换为什么内容,感兴趣的读者可以在线查看。

关于这个插件的使用,有一个老外还录制了一个很不错的视频教程,可以通过这个地址查看:

https://www.youtube.com/watch?v=o-jiS563yI8

好啦,以上就是目前提前体验vue3框架的 4 种方式。读者朋友们可以根据自己的情况灵活选择。

四、总结与思考

值得注意的是,以上 4 种方式,无论是哪一种,vue 团队都有明确的警示,现在vue3框架还处于alpha阶段,不是正式版,不建议在真实的生产环境中使用,在体验过程中也可能会遇到各种 Bug,这些都是正常的。

在体验 vue3 框架时,如何知道哪些方法可以使用、以及如何使用呢,在 vue2 中实现的功能在 vue3 中应该如何实现呢,在哪里查看这些说明,有一个文档可以帮助我们:

https://vue-composition-api-rfc.netlify.com/#basic-example

最后我们总结一下,今天这篇文章主要讲了一个问题,就是vue2vue3在响应机制的实现上有哪些差别,还有vue2项目里使用数组更新数据时视图不更新的问题在vue3中是如何完美解决的,并简略对比了两个版本的源码实现。不知道作者有没有讲明白,读者朋友们有什么问题,欢迎在评论区留言探讨。

vue2的实现方式是在数据源对象上通过Object.defineProperty方法递归创建属性实现的,这些属性是属于被创建对象的;而vue3的实现方式,是通过给数据对象创建一个Proxy代理实现的,访问这个数据对象的任何属性都会通过这个代理。在vue3中并没有创建多余的对象属性,无论从代码的优雅程度上,还是从性能上考虑,vue3的方案都更胜一筹。在实际的项目开发中,如果我们也需要 对一个对象的属性施加监听,也可以考虑通过Proxy代理的方式实现。

最后谈一点作者对vue3框架的体验感受吧:首先,vue3的响应机制的实现更加高效了,效率更高了,同时它也解决了vue2中遗留的数组更新问题;其次,在vue3项目中,使用组合风格的 Api 编写业务逻辑更加自由。目前vue3已经发布了alpha4版本,离正式发布已经很接近了。作者很期待新版本的发布。相信新版本发布后,会有更多的开发者使用和喜欢这个框架的。不知道读者朋友们在体验后怎么看,欢迎留言。

参考链接

  • https://juejin.im/post/5dac69bf5188252b51183982 vue2.0 响应式到vue3.0响应式原理
  • https://juejin.im/post/5c8ce1005188257ade024ed6 基于Object.defineProperty之后,vue源码解析依赖收集、依赖触发
  • https://juejin.im/post/5e1c19856fb9a0301d11ae0b vue为什么不能检测数组的变化
  • https://www.vuemastery.com/blog/vue-3-start-using-it-today/ 开始试用 vue 3
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty 关于 Object.defineProperty()
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-02-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 艺述论 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、问题:vue2 通过数组索引改变数据不能触发视图更新是怎么回事?
  • 二、分析:在 vue3 不存在这个问题,vu2 与 vu3 的响应机制分别是怎么实现的?
  • 三、实践:现在如何体验vue3 框架,四种体验方式归纳
    • 3.1、在 vue2 项目中复用
      • 3.2、从 vue-next 源码编译
        • 3.3、基于项目模板
          • 3.4、基于插件自动转化
          • 四、总结与思考
          • 参考链接
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档