文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Vue响应式流程及原理是什么

2023-07-02 16:48

关注

本文小编为大家详细介绍“Vue响应式流程及原理是什么”,内容详细,步骤清晰,细节处理妥当,希望这篇“Vue响应式流程及原理是什么”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。

一、组件化流程

1. 整个new Vue阶段做了什么?

执行render。生成vnode

<div id="app">{{ message }}</div>
render (h) {  return h('div', {     attrs: {        id: 'app'      },  }, this.message)}

Vue响应式流程及原理是什么

2. 普通dom元素如何渲染到页面?

Vue响应式流程及原理是什么

3. 组件如何渲染到页面?

这里以如下代码案例讲解更加清晰~没错,就是这么熟悉!就是一个初始化的Vue项目

// mian.jsimport Vue from 'vue'import App from './App.vue'new Vue({  render: h => h(App),}).$mount('#app')
// App.vue<template>    <div id="app">      <p>{{ msg }}</p>    </div></template><script>    export default {        name: 'App',        data () {            return {                msg: 'hello world'            }        }    }</script>

主要讲解组件普通元素的不同之处,主要有2点:

如何生成VNode&mdash;&mdash;创建组件VNodecreateComponent

Vue响应式流程及原理是什么

如何patch&mdash;&mdash;组件new Vue到patch流程createComponent

$vnode:占位符vnode。最终渲染vnode挂载的地方。所有的组件通过递归调用createComponent直至不再存在组件VNode,最终都会转化成普通的dom。

{    tag: 'vue-component-1-App',    componentInstance: {组件实例},    componentOptions: {Ctor, ..., }}

_vnode:渲染vnode。

{    tag: 'div',    {        "attrs": {            "id": "app"        }    },    // 对应占位符vnode: $vnode    parent: {        tag: 'vue-component-1-App',        componentInstance: {组件实例},        componentOptions: {Ctor, ..., }    },    children: [        // 对应p标签        {             tag: 'p',            // 对应p标签内的文本节点{{ msg }}            children: [{ text: 'hello world' }]        }, {          // 如果还有组件VNode其实也是一样的          tag: 'vue-component-2-xxx'        }                  ]}

Vue响应式流程及原理是什么

4. Vue组件化简化流程

相信你看完细粒度的Vue组件化过程可能已经晕头转向了,这里会用一个简化版的流程图进行回顾,加深理解

Vue响应式流程及原理是什么

二、响应式流程

案例代码

// 案例export default {    name: 'App',    data () {        return {            msg: 'hello world',            arr = [1, 2, 3]        }    }}

1. 依赖收集

这里会从Observer、Dep、Watcher三个对象进行讲解,分 objectarray 两种依赖收集方式。

三个核心对象:Observer(蓝)、Dep(绿)、Watcher(紫)

Vue响应式流程及原理是什么

依赖收集准备阶段&mdash;&mdash;Observer、Dep的实例化

// 以下是initData调用的方法讲解,排列遵循调用顺序function observe (value, asRootData) {  if (!isObject(value)) return // 非对象则不处理  // 实例化Observer对象  var ob;  ob = new Observer(value);  return ob}function Observer (value) {  this.value = value; // 保存当前的data  this.dep = new Dep(); // 实例化dep,数组进行依赖收集的dep(对应案例中的arr)  def(value, '__ob__', this);      if (Array.isArray(value)) {    if (hasProto) {      // 这里会改写数组原型。__proto__指向重写数组方法的对象      protoAugment(value, arrayMethods);     } else {      copyAugment(value, arrayMethods, arrayKeys);    }    this.observeArray(value);  } else {    this.walk(value);   }}// 遍历数组元素,执行对每一项调用observe,也就是说数组中有对象会转成响应式对象Observer.prototype.observeArray = function observeArray (items) {  for (var i = 0, l = items.length; i < l; i++) {    observe(items[i]);  }}// 遍历对象的全部属性,调用defineReactiveObserver.prototype.walk = function walk (obj) {  var keys = Object.keys(obj);  // 如案例代码,这里的 keys = ['msg', 'arr']  for (var i = 0; i < keys.length; i++) {            defineReactive(obj, keys[i]);  }}
function defineReactive (obj, key, val) {  // 产生一个闭包dep  var dep = new Dep();  // 如果val是object类型,递归调用observe,案例代码中的arr会走这个逻辑  var childOb = !shallow && observe(val);  Object.defineProperty(obj, key, {        get: function reactiveGetter () {       // 求value的值      var value = getter ? getter.call(obj) : val;      if (Dep.target) { // Dep.target就是当前的Watcher        // 这里是闭包dep        dep.depend();        if (childOb) {          // 案例代码中arr会走到这个逻辑          childOb.dep.depend(); // 这里是Observer里的dep,数组arr在此依赖收集          if (Array.isArray(value)) {            dependArray(value);          }        }      }      return value    },    set: function reactiveSetter (newVal) {      // 下文派发更新里进行讲解    }  });}

Vue响应式流程及原理是什么

依赖收集触发阶段&mdash;&mdash;Wather实例化、访问数据、触发依赖收集

// new Wathcer核心function Watcher (vm, expOrFn, cb, options, isRenderWatcher) {  if (typeof expOrFn === 'function') {  // 渲染watcher中,这里传入的expOrFn是updateComponent = vm.update(vm.render())  // this.getter等价于vm.update(vm.render())    this.getter = expOrFn;   } else {    ...  }  // 这里进行判断,lazy为true时(计算属性)则什么都不执行,否则执行get  this.value = this.lazy    ? undefined    : this.get(); // 本次为渲染Watcher,执行get,继续往下看~}// Watcher的get方法Watcher.prototype.get = function get () {  // 这里很关键,pushTarget就是把当前的Wather赋值给“Dep.target”  pushTarget(this);  var value;  var vm = this.vm;  try {    // 1. 这里调用getter,也就是执行vm.update(vm.render())    // 2. 执行vm.render函数就会访问到响应式数据,触发get进行依赖收集    // 3. 此时的Dep.target为当前的渲染Watcher,数据就可以理所应当的把Watcher加入自己的subs中    // 4. 所以此时,Watcher就能监测到数据变化,实现响应式    value = this.getter.call(vm, vm);  } catch (e) {    ...  } finally {    popTarget();        this.cleanupDeps();   }  return value}

Vue响应式流程及原理是什么

细节太多绕晕了?来个整体流程,从宏观角度再过一遍(computed部分可看完彩蛋后再回来重温一下)

Vue响应式流程及原理是什么

2. 派发更新

派发更新区分对象属性、数组方法进行讲解

如果想要深入了解组件的异步更新,戳这里,了解Vue组件异步更新之nextTick。本文只针对派发更新流程,不会对异步更新DOM进行展开讲解~

这里可以先想一下,以下操作会发生什么?

是的,毫无疑问都会先触发他们之中的get,那再触发什么呢?我们接下来看

对象属性修改触发set,派发更新。this.msg = 'new val'

...Object.defineProperty (obj, key, {    get () {...},    set: function reactiveSetter (newVal) {      var value = getter ? getter.call(obj) : val;      // 判断新值相比旧值是否已经改变      if (newVal === value || (newVal !== newVal && value !== value)) {        return      }      // 如果新值是引用类型,则将其转化为响应式      childOb = !shallow && observe(newVal);      // 这里通知dep的所有watcher进行更新      dep.notify();    }}        ...

Vue响应式流程及原理是什么

数组调用方法。this.arr.push(4)

// 数组方法改写是在 Observer 方法中function Observer () {    if (hasProto) {         // 用案例讲解,也就是this.arr.__proto__ = arrayMethods        protoAugment(value, arrayMethods);     }}   // 以下是数组方法重写的实现var arrayProto = Array.prototype; // 保存真实数组的原型var arrayMethods = Object.create(arrayProto); // 以真数组为原型创建对象// 可以看成:arrayMethods.__proto__ = Array.prototypevar methodsToPatch = [  'push',  'pop',  'shift',  'unshift',  'splice',  'sort',  'reverse'];// 一个装饰器模型,重写7个数组方法methodsToPatch.forEach(function (method) {  // 保存原生的数组方法  var original = arrayProto[method];  // 劫持arrayMethods对象中的数组方法  def(arrayMethods, method, function mutator () {    var args = [], len = arguments.length;    while ( len-- ) args[ len ] = arguments[ len ];    var result = original.apply(this, args);    var ob = this.__ob__; // 当我门调用this.arr.push(),这里就能到数组对象的ob实例    var inserted;    switch (method) {      case 'push':      case 'unshift':        inserted = args;        break      case 'splice':        inserted = args.slice(2);        break    }    if (inserted) { ob.observeArray(inserted); }    // 由于数组对象在new Observer中实例化了一个dep,并通过childOb逻辑收集了依赖,这里就能在ob实例中拿到dep属性    ob.dep.notify();    return result  });})

Vue响应式流程及原理是什么

整个new Vue阶段、到依赖收集派发更新的全部流程就到这里结束了。可以纵观流程图看出,Vue应用就是一个个Vue组件组成的,虽然整个组件化、响应式流程很多,但核心的路径一旦走通,你就会恍然大悟。

三、彩蛋篇

1. computed依赖收集

<template>    <div id="app">        {{ name }}    </div></template><script>export default {    name: 'App',    computed: {      name () {        return this.firstName + this.secondName      }    },    data () {        return {            firstName: 'jing',            secondName: 'boran'        }    }}</script>

Vue响应式流程及原理是什么

根据案例概括一下,加深理解

// 访问computed时触发get的核心代码 function createComputedGetter (key) {  return function computedGetter () {    var watcher = this._computedWatchers && this._computedWatchers[key];    if (watcher) {      if (watcher.dirty) { // dirty第一次为true        watcher.evaluate(); // 这里是对computed进行求值,对computed watcher执行依赖收集      }      if (Dep.target) {        watcher.depend(); // 这里是对渲染Watcher进行依赖收集      }      return watcher.value    }  }}

computed中的name其实就是一个computed Watcher,这个Watcher在init阶段生成

当App组件render的阶段,render函数会访问到模版中的{{ name }},则会触发computed的求值,也就是执行上面代码computedGetter()。执行watcher.evaluate()。也就是执行wathcer.get。上文依赖收集的第3点:依赖收集触发阶段有对get方法进行讲解,忘了的可以上去回顾一下执行watcher.depend()

Watcher.prototype.depend = function depend () {  var i = this.deps.length;  while (i--) {    // 也就是调用Dep.depend => Watcher.addDep => dep.addSub    this.deps[i].depend();   }}
// this.firstName和this.secondName的dep.subsdep.subs: [name的computed watcher, App组件的渲染Watcher]

代码中判断watcher.dirty标志是什么?有什么用?

只有computed的值发生改变(也就是其依赖的数据改变),watcher.dirty才会被设为true

只有watcher.dirtytrue才会对computed进行 求值 或 重新求值

总结:也就是组件每次render,如果computed的值没改变,直接返回value值(是不需要重新计算的),这也是computed的一个特点

讲到这里,我以自己的理解讲解下文章开头引言的问题:为什么Watcher、Dep多对多且相互收集? 这可能也是大家阅读Vue源码中一直存在的一个疑惑(包括我自己刚开始读也是这样)

对的,当然是为了computed中的响应式数据收集渲染Watcher啦!!!

还有!!! 还记得前文中依赖收集的第3点&mdash;&mdash;依赖收集触发阶段的代码讲解中我写了很多注释的cleanupDeps吗?

// 此时flag为true,也就是说msg2没有渲染在页面中<div v-if="flag">{{ msg1 }}</div><div v-else>{{ msg2 }}</div><button @click=() => { this.msg2 = 'change' }>changeMsg2</button>
function cleanupDeps () {  var i = this.deps.length;  while (i--) {    // 这里对watcher所观测的响应式数据的dep进行遍历    // 对的,这样一来,是不是watcher中的deps就发挥作用了呢?    var dep = this.deps[i];    if (!this.newDepIds.has(dep.id)) {      // 这里对当前渲染中没有访问到的响应式数据进行依赖移除      dep.removeSub(this);     }  }  ...}

2. computed派发更新

派发相对来说比较简单了~跟响应式的派发更新基本一致,继续以案例来讲解吧!

当我们修改firstName会发生什么?this.firstName = 'change'

首先触发firstName的set,最终会调用dep.notify()。firstName的dep.subs中有2个watcher,分别执行对应watcher的notify

Watcher.prototype.update = function update () {        if (this.lazy) {    this.dirty = true; // computed会走到这里,然后就结束了  } else if (this.sync) {    this.run();  } else {    queueWatcher(this); // 渲染watcher会走到这里  }}

computed watcher:将dirty属性置为true。

渲染watcher会执行派发更新流程(如本文响应式流程&mdash;&mdash;2.派发更新一致)

nextTick阶段执行flushSchedulerQueue,则会执行watcher.run()

watcher.run会执行watcher.get方法,也就是重新执行render、update的流程

执行render又会访问到name的computed,从而又会执行computedGetter

此时的watcher.dirty在本步骤3已经置为true,又会执行watcher.evaluate()进行computed的求值,执行watcher.depend()......后续的流程就是派发更新的流程了~

3. user Watcher依赖收集

user Watcher的依赖收集相比computed会简单一点,这里不会赘述太多,只说核心区别,还有watch的常用配置immediatedeepsync

user Watcher在init阶段会执行一次watcher.get(),在这里会访问我们watch的响应式数据,从而进行依赖收集。回顾下computed,computed在这个阶段什么也没做。

// 没错,又是这段熟悉的代码this.value = this.lazy  ? undefined  : this.get(); // user Watcher和渲染 Watcher都在new Watcher阶段执行get()

如果userWatcher设置的immediate: true,则会在new Watcher后主动触发一次cb的执行

Vue.prototype.$watch = function (expOrFn, cb, options) {  ...  var watcher = new Watcher(vm, expOrFn, cb, options);  if (options.immediate) {    // immediate则会执行我们传入的callback    try {      cb.call(vm, watcher.value);    } catch (error) {          }  }  return function unwatchFn () {    watcher.teardown();  }};

deep逻辑很简单,大概讲下:深度遍历这个对象,访问到该对象的所有属性,以此来触发所有属性的getter。这样,所有属性都会把当前的user Watcher收集到自己的dep中。因此,深层的属性值修改(触发set派发更新能通知到user Watcher),watch自然就能监测到数据改变~感兴趣的同学可以自己去看看源码中traverse的实现。

sync。当前tick执行,以此能先于渲染Wathcer执行。不设置同步的watcher都会放到nextTick中执行。

Watcher.prototype.update = function update () {  if (this.lazy) {    this.dirty = true; // 计算属性  } else if (this.sync) {    this.run(); // 同步的user Wathcer  } else {    queueWatcher(this); // 普通user Watcher和渲染Watcher  }}

Vue响应式流程及原理是什么

读到这里,这篇“Vue响应式流程及原理是什么”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注编程网行业资讯频道。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯