原理深度解析Vue的响应式更新比React快


Posted in Javascript onApril 04, 2020

前言

我们都知道 Vue 对于响应式属性的更新,只会精确更新依赖收集的当前组件,而不会递归的去更新子组件,这也是它性能强大的原因之一。

例子

举例来说 这样的一个组件:

<template>
  <div>
   {{ msg }}
   <ChildComponent />
  </div>
</template>

我们在触发 this.msg = 'Hello, Changed~'的时候,会触发组件的更新,视图的重新渲染。

但是 <ChildComponent /> 这个组件其实是不会重新渲染的,这是 Vue 刻意而为之的。

在以前的一段时间里,我曾经认为因为组件是一棵树,所以它的更新就是理所当然的深度遍历这棵树,进行递归更新。本篇就从源码的角度带你一起分析,Vue 是怎么做到精确更新的。

React的更新粒度

而 React 在类似的场景下是自顶向下的进行递归更新的,也就是说,React 中假如 ChildComponent 里还有十层嵌套子元素,那么所有层次都会递归的重新render(在不进行手动优化的情况下),这是性能上的灾难。(因此,React 创造了Fiber,创造了异步渲染,其实本质上是弥补被自己搞砸了的性能)。

他们能用收集依赖的这套体系吗?不能,因为他们遵从Immutable的设计思想,永远不在原对象上修改属性,那么基于Object.defineProperty 或 Proxy 的响应式依赖收集机制就无从下手了(你永远返回一个新的对象,我哪知道你修改了旧对象的哪部分?)

同时,由于没有响应式的收集依赖,React 只能递归的把所有子组件都重新 render一遍(除了memo和shouldComponentUpdate这些优化手段),然后再通过 diff算法 决定要更新哪部分的视图,这个递归的过程叫做 reconciler,听起来很酷,但是性能很灾难。

Vue的更新粒度

那么,Vue 这种精确的更新是怎么做的呢?其实每个组件都有自己的渲染 watcher,它掌管了当前组件的视图更新,但是并不会掌管 ChildComponent 的更新。

具体到源码中,是怎么样实现的呢?

在 patch  的过程中,当组件更新到ChildComponent的时候,会走到patchVnode,那么这个方法大致做了哪些事情呢?

patchVnode

执行 vnode 的 prepatch 钩子。

注意,只有 组件vnode 才会有 prepatch 这个生命周期,

这里会走到updateChildComponent方法,这个 child 具体指什么呢?

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = vnode.componentOptions
  // 注意 这个child就是ChildComponent组件的 vm 实例,也就是咱们平常用的 this
  const child = vnode.componentInstance = oldVnode.componentInstance
  updateChildComponent(
   child,
   options.propsData, // updated props
   options.listeners, // updated listeners
   vnode, // new parent vnode
   options.children // new children
  )
 },

其实看传入的参数也能猜到大概了,就是做了:

  • 更新props(后续详细讲)
  • 更新绑定事件
  • 对于slot做一些更新(后续详细讲)

如果有子节点的话,对子节点进行 diff。

比如这样的场景:

<ul>
 <li>1</li>
 <li>2</li>
 <li>3</li>
<ul>

要对于 ul 中的三个 li 子节点 vnode 利用 diff 算法来更新,本篇略过。

然后到此为止,patchVnode 就结束了,并没有像常规思维中的那样去递归的更新子组件树。

这也就说明了,Vue 的组件更新确实是精确到组件本身的。

如果是子组件呢?

假设列表是这样的:

<ul>
 <component>1</component>
 <component>2</component>
 <component>3</component>
<ul>

那么在diff的过程中,只会对 component 上声明的 props、listeners等属性进行更新,而不会深入到组件内部进行更新。

注意:不会深入到组件内部进行更新!(划重点,这也是本文所说的更新粒度的关键)

props的更新如何触发重渲染?

那么有同学可能要问了,如果不会递归的去对子组件更新,如果我们把 msg 这个响应式元素通过props传给 ChildComponent,此时它怎么更新呢?

首先,在组件初始化 props的时候,会走到 initProps 方法。

const props = vm._props = {}

 for (const key in propsOptions) {
  // 经过一系列验证props合法性的流程后
  const value = validateProp(key, propsOptions, propsData, vm)
  // props中的字段也被定义成响应式了
  defineReactive(props, key, value)
}

至此为止,是实现了对于 _props 上字段变更的劫持。也就是变成了响应式数据,后面我们做类似于 _props.msg = 'Changed' 的操作时(当然我们不会这样做,Vue内部会做),就会触发视图更新。

其实,msg 在传给子组件的时候,会被保存在子组件实例的 _props 上,并且被定义成了响应式属性,而子组件的模板中对于 msg 的访问其实是被代理到 _props.msg 上去的,所以自然也能精确的收集到依赖,只要 ChildComponent 在模板里也读取了这个属性。

这里要注意一个细节,其实父组件发生重渲染的时候,是会重新计算子组件的 props 的,具体是在 updateChildComponent 中的:

// update props
 if (propsData && vm.$options.props) {
  toggleObserving(false)
  // 注意props被指向了 _props
  const props = vm._props
  const propKeys = vm.$options._propKeys || []
  for (let i = 0; i < propKeys.length; i++) {
   const key = propKeys[i]
   const propOptions: any = vm.$options.props // wtf flow?
   // 就是这句话,触发了对于 _props.msg 的依赖更新。
   props[key] = validateProp(key, propOptions, propsData, vm)
  }
  toggleObserving(true)
  // keep a copy of raw propsData
  vm.$options.propsData = propsData
 }

那么,由于上面注释标明的那段代码,msg 的变化通过 _props 的响应式能力,也让子组件重新渲染了,到目前为止,都只有真的用到了 msg 的组件被重新渲染了。

正如官网 api 文档中所说:

vm.$forceUpdate:迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
—— vm-forceUpdate文档

我们需要知道一个小知识点,vm.$forceUpdate 本质上就是触发了渲染watcher的重新执行,和你去修改一个响应式的属性触发更新的原理是一模一样的,它只是帮你调用了 vm._watcher.update()(只是提供给你了一个便捷的api,在设计模式中叫做门面模式)

slot是怎么更新的?

注意这里也提到了一个细节,也就是 插入插槽内容的子组件:

举例来说

假设我们有父组件parent-comp:

<div>
 <slot-comp>
   <span>{{ msg }}</span>
 </slot-comp>
</div>

子组件 slot-comp:

<div>
  <slot></slot>
</div>

组件中含有 slot的更新 ,是属于比较特殊的场景。

这里的 msg 属性在进行依赖收集的时候,收集到的是 parent-comp 的`渲染watcher。(至于为什么,你看一下它所在的渲染上下文就懂了。)

那么我们想象 msg 此时更新了,

<div>
 <slot-comp>
   <span>{{ msg }}</span>
 </slot-comp>
</div>

这个组件在更新的时候,遇到了一个子组件 slot-comp,按照 Vue 的精确更新策略来说,子组件是不会重新渲染的。

但是在源码内部,它做了一个判断,在执行 slot-comp 的 prepatch 这个hook的时候,会执行 updateChildComponent 逻辑,在这个函数内部会发现它有 slot 元素。

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = vnode.componentOptions
  // 注意 这个child就是 slot-comp 组件的 vm 实例,也就是咱们平常用的 this
  const child = vnode.componentInstance = oldVnode.componentInstance
  updateChildComponent(
   child,
   options.propsData, // updated props
   options.listeners, // updated listeners
   vnode, // new parent vnode
   options.children // new children
  )
 },

在 updateChildComponent 内部

const hasChildren = !!(
  // 这玩意就是 slot 元素
  renderChildren ||        // has new static slots
  vm.$options._renderChildren || // has old static slots
  parentVnode.data.scopedSlots || // has new scoped slots
  vm.$scopedSlots !== emptyObject // has old scoped slots
 )

然后下面走一个判断

if (hasChildren) {
  vm.$slots = resolveSlots(renderChildren, parentVnode.context)
  vm.$forceUpdate()
 }

这里调用了 slot-comp 组件vm实例上的 $forceUpdate,那么它所触发的渲染watcher就是属于slot-comp的渲染watcher了。
总结来说,这次 msg 的更新不光触发了 parent-comp 的重渲染,也进一步的触发了拥有slot的子组件 slot-comp 的重渲染。
它也只是触发了两层渲染,如果 slot-comp 内部又渲染了其他组件 slot-child,那么此时它是不会进行递归更新的。(只要 slot-child 组件不要再有 slot 了)。

比起 React 的递归更新,是不是还是好上很多呢?

赠礼 一个小issue

有人给 Vue 2.4.2 版本提了一个issue,在下面的场景下会出现 bug。

let Child = {
 name: "child",
 template:
  '<div><span>{{ localMsg }}</span><button @click="change">click</button></div>',
 data: function() {
  return {
   localMsg: this.msg
  };
 },
 props: {
  msg: String
 },
 methods: {
  change() {
   this.$emit("update:msg", "world");
  }
 }
};

new Vue({
 el: "#app",
 template: '<child :msg.sync="msg"><child>',
 beforeUpdate() {
  alert("update twice");
 },
 data() {
  return {
   msg: "hello"
  };
 },
 components: {
  Child
 }
});

具体的表现是点击 click按钮,会 alert 出两次 update twice。 这是由于子组件在执行 data 这个函数初始化组件的数据时,会错误的再收集一遍 Dep.target (也就是渲染watcher)。

由于数据初始化的时机是 beforeCreated -> created 之间,此时由于还没有进入子组件的渲染阶段, Dep.target 还是父组件的渲染watcher。

这就导致重复收集依赖,重复触发同样的更新

怎么解决的呢?很简单,在执行 data 函数的前后,把 Dep.target 先设置为 null 即可,在 finally 中再恢复,这样响应式数据就没办法收集到依赖了。

export function getData (data: Function, vm: Component): any {
 const prevTarget = Dep.target
+ Dep.target = null
 try {
  return data.call(vm, vm)
 } catch (e) {
  handleError(e, vm, `data()`)
  return {}
+ } finally {
+  Dep.target = prevTarget
 }
}

后记

如果你对于 Dep.target、 渲染watcher等概念还不太理解,可以看我写的一篇最简实现 Vue 响应式的文章,欢迎阅读:
手把手带你实现一个最精简的响应式系统来学习Vue的data、computed、watch源码

本文也存放在我的Github博客仓库中,欢迎订阅和star。

到此这篇关于原理深度解析Vue的响应式更新比React快的文章就介绍到这了,更多相关Vue的响应式更新比React快内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
JS实现self的resend
Jul 22 Javascript
firebug的一个有趣现象介绍
Nov 30 Javascript
超简单JS二级、多级联动的简单实例
Feb 18 Javascript
js实现双击图片放大单击缩小的方法
Feb 17 Javascript
JS实现生成会变大变小的圆环实例
Aug 05 Javascript
JavaScript程序开发之JS代码放置的位置
Jan 15 Javascript
详解JavaScript异步编程中jQuery的promise对象的作用
May 03 Javascript
Javascript 基础---Ajax入门必看
Jul 06 Javascript
javascript 中事件冒泡和事件捕获机制的详解
Sep 01 Javascript
JavaScript代码异常监控实现过程详解
Feb 17 Javascript
基于aotu.js实现微信自动添加通讯录中的联系人功能
May 28 Javascript
基于Echarts图表在div动态切换时不显示的解决方式
Jul 20 Javascript
Vue的data、computed、watch源码浅谈
Apr 04 #Javascript
VUE table表格动态添加一列数据,新增的这些数据不可以编辑(v-model绑定的数据不能实时更新)
Apr 03 #Javascript
mpvue实现微信小程序快递单号查询代码
Apr 03 #Javascript
mpvue网易云短信接口实现小程序短信登录的示例代码
Apr 03 #Javascript
javascript用defineProperty实现简单的双向绑定方法
Apr 03 #Javascript
JavaScript检测浏览器是否支持CSS变量代码实例
Apr 03 #Javascript
JS内置对象和Math对象知识点详解
Apr 03 #Javascript
You might like
一个php作的文本留言本的例子(一)
2006/10/09 PHP
PHP基于imap获取邮件实例
2014/11/11 PHP
浅谈PHP拦截器之__set()与__get()的理解与使用方法
2016/10/18 PHP
如何在PHP环境中使用ProtoBuf数据格式
2020/06/19 PHP
几行代码轻松搞定jquery实现flash8类似的连接效果
2007/05/03 Javascript
弹出窗口并且此窗口带有半透明的遮罩层效果
2014/03/13 Javascript
浅谈Unicode与JavaScript的发展史
2015/01/19 Javascript
JS实现至少包含字母、大小写数字、字符的密码等级的两种方法
2015/02/03 Javascript
JS弹出可拖拽可关闭的div层完整实例
2015/02/13 Javascript
简单学习JavaScript中的for语句循环结构
2015/11/10 Javascript
Nodejs中session的简单使用及通过session实现身份验证的方法
2016/02/04 NodeJs
jQuery实现漂亮实用的商品图片tips提示框效果(无图片箭头+阴影)
2016/04/16 Javascript
通过JS和PHP两种方法判断用户请求时使用的浏览器类型
2016/09/01 Javascript
JS之相等操作符详解
2016/09/13 Javascript
JavaScript、C# URL编码、解码总结
2017/01/21 Javascript
js实现随机数字字母验证码
2017/06/19 Javascript
React手稿之 React-Saga的详解
2018/11/12 Javascript
javascript实现考勤日历功能
2018/11/29 Javascript
vue实现动态显示与隐藏底部导航的方法分析
2019/02/11 Javascript
基于JavaScript实现猜数字游戏代码实例
2020/07/30 Javascript
[01:04:02]DOTA2-DPC中国联赛 正赛 Elephant vs IG BO3 第二场 1月24日
2021/03/11 DOTA
selenium3+python3环境搭建教程图解
2018/12/07 Python
详解Python Matplot中文显示完美解决方案
2019/03/07 Python
深入了解Python iter() 方法的用法
2019/07/11 Python
python+selenium 脚本实现每天自动登记的思路详解
2020/03/11 Python
keras 多gpu并行运行案例
2020/06/10 Python
Three Graces London官网:英国奢侈品牌
2021/03/18 全球购物
平面设计岗位职责
2013/12/14 职场文书
法警的竞聘演讲稿
2014/01/02 职场文书
《小儿垂钓》教学反思
2014/02/23 职场文书
搞笑爱情保证书
2014/04/29 职场文书
简易版租房协议书范本
2014/10/13 职场文书
《半截蜡烛》教学反思
2016/02/19 职场文书
mysql多表查询-笔记七
2021/04/05 MySQL
vue-cropper组件实现图片切割上传
2021/05/27 Vue.js
Python机器学习之底层实现KNN
2021/06/20 Python