vue源码学习

1、vue.js响应式原理

参考:https://cn.vuejs.org/v2/guide/reactivity.html

https://github.com/answershuto/learnVue

注:learnVue讲解的vue版本是2.3.0,我粘贴的源码是2.6.10

Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。

  当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

  受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

var vm = new Vue({
  data:{
    a:1 // `vm.a` 是响应式的
  }
})
vm.b = 2 // `vm.b` 是非响应式的

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值。

var vm = new Vue({
  data: {
    message: ‘‘ // 声明 message 为一个空值字符串
  },
  template: ‘<div>{{ message }}</div>‘
})
vm.message = ‘Hello!‘ // 之后设置 `message`

如果你未在 data 选项中声明 message,Vue 将警告你渲染函数正在试图访问不存在的属性。

  Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。例如,当你设置 vm.someData = ‘new value‘,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。

  在initData中会调用observe这个函数将Vue的数据设置成observable的。当_data数据发生改变的时候就会触发set,对订阅者进行回调。那么问题来了,需要对app._data.text操作才会触发set。为了偷懒,我们需要一种方便的方法通过app.text直接设置就能触发set对视图进行重绘。那么就需要用到代理。

function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === ‘function‘
    ? getData(data, vm)
    : data || {};
  // proxy data on instance
  var keys = Object.keys(data);
  var i = keys.length;
  while (i--) {
    var key = keys[i];
    proxy(vm, "_data", key);
  }
  // observe data
  observe(data, true /* asRootData */);
}
let app = new Vue({
    el: ‘#app‘,
    data: {
        text: ‘text‘
    }
})

那么问题来了,需要对app._data.text操作才会触发set。为了偷懒,我们需要一种方便的方法通过app.text直接设置就能触发set对视图进行重绘。那么就需要用到代理。这样我们就把data上面的属性代理到了vm实例上。

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

2、vue.js依赖收集

new Vue({
    template:
        `<div>
            <span>text1:</span> {{text1}}
            <span>text2:</span> {{text2}}
        <div>`,
    data: {
        text1: ‘text1‘,
        text2: ‘text2‘,
        text3: ‘text3‘
    }
});

按照之前《响应式原理》中的方法进行绑定则会出现一个问题——text3在实际模板中并没有被用到,然而当text3的数据被修改(this.text3 = ‘test‘)的时候,同样会触发text3的setter导致重新执行渲染,这显然不正确。

当对data上的对象进行修改值的时候会触发它的setter,那么取值的时候自然就会触发getter事件,所以我们只要在最开始进行一次render,那么所有被渲染所依赖的data中的数据就会被getter收集到Dep的subs中去。在对data中的数据进行修改的时候setter只会触发Dep的subs的函数。

Dep(依赖)

var Dep = function Dep () {
  this.id = uid++;
  this.subs = [];
};
Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};
/*依赖收集*/
Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};

Watcher(观察者)

/*添加一个依赖关系到Deps集合中*/
Watcher.prototype.addDep = function addDep (dep) {
  var id = dep.id;
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    if (!this.depIds.has(id)) {
      dep.addSub(this);
    }
  }
};

将观察者Watcher实例赋值给全局的Dep.target,然后触发render操作只有被Dep.target标记过的才会进行依赖收集。有Dep.target的对象会将Watcher的实例push到subs中,在对象被修改触发setter操作的时候dep会调用subs中的Watcher实例的update方法进行渲染。

function defineReactive$$1 (obj, key, val, customSetter, shallow) {
  var dep = new Dep();
  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
      }
      return value
    }
  });
}

3、vue.js数据绑定原理

这张图比较清晰地展示了整个流程,首先通过一次渲染操作触发Data的getter(这里保证只有视图中需要被用到的data才会触发getter)进行依赖收集,这时候其实Watcher与data可以看成一种被绑定的状态(实际上是data的闭包中有一个Deps订阅者,在修改的时候会通知所有的Watcher观察者),在data发生变化的时候会触发它的setter,setter通知Watcher,Watcher进行回调通知组件重新渲染的函数,之后根据diff算法来决定是否发生视图的更新。

initData主要是初始化data中的数据,将数据进行Observer,监听数据的变化。下面这段代码主要做了两件事,一是将_data上面的数据代理到vm上,另一件事通过observe将所有数据变成observable。

function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === ‘function‘
    ? getData(data, vm)
    : data || {};
  // proxy data on instance
  var keys = Object.keys(data);
  var i = keys.length;
  while (i--) { // 遍历data中的数据
    var key = keys[i];
    proxy(vm, "_data", key); // 将data上面的属性代理到了vm实例上
  }
  // observe data
  /*从这里开始我们要observe了,开始对数据进行绑定,这里有尤大大的注释asRootData,这步作为根数据,下面会进行递归observe进行对深层对象的绑定。*/
  observe(data, true /* asRootData */);
}

observe

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
// 尝试创建一个Observer实例(__ob__),如果成功创建Observer实例则返回新的Observer实例,如果已有Observer实例则返回现有的Observer实例。
function observe (value, asRootData) {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
var ob;
  /*这里用__ob__这个属性来判断是否已经有Observer实例,如果没有Observer实例则会新建一个Observer实例并赋值给__ob__这个属性,如果已有Observer实例则直接返回该Observer实例*/
  if (hasOwn(value, ‘__ob__‘) && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
     /*如果是根数据则计数,后面Observer中的observe的asRootData非true*/
    ob.vmCount++;
  }
return ob
}

Vue的响应式数据都会有一个__ob__的属性作为标记,里面存放了该属性的观察器,也就是Observer的实例,防止重复绑定。

Observer的作用就是遍历对象的所有属性将其进行双向绑定。

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object‘s property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, ‘__ob__‘, this); // 将Observer实例绑定到value的__ob__属性上面去
  if (Array.isArray(value)) {
    /*
      如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。
      这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。
    */
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value); // 如果是数组则需要遍历数组的每一个成员进行observe
  } else {
    this.walk(value); // 如果是对象则直接walk进行绑定
  }
};
/**
 * Walk through all properties and convert them into
 * getter/setters. This method should only be called when
 * value type is Object.
 */
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  // 遍历对象的每一个属性进行defineReactive绑定
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i]);
  }
};
/**
 * Observe a list of Array items.
 */
Observer.prototype.observeArray = function observeArray (items) {
  // 数组需要遍历每一个成员进行observe
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

Observer为数据加上响应式属性进行双向绑定。如果是对象则进行深度遍历,为每一个子对象都绑定上方法,如果是数组则为每一个成员都绑定上方法。如果是修改一个数组的成员,该成员是一个对象,那只需要递归对数组的成员进行双向绑定即可。但这时候出现了一个问题:如果我们进行pop、push等操作的时候,push进去的对象根本没有进行过双向绑定,更别说pop了,那么我们如何监听数组的这些变化呢? Vue.js提供的方法是重写push、pop、shift、unshift、splice、sort、reverse这七个数组方法。

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

变异方法,顾名思义,会改变调用了这些方法的原始数组。相比之下,也有非变异方法,例如 filter()concat() 和 slice() 。它们不会改变原始数组,而总是返回一个新数组。当使用非变异方法时,可以用新数组替换旧数组。你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [ ‘push‘, ‘pop‘, ‘shift‘, ‘unshift‘, ‘splice‘, ‘sort‘, ‘reverse‘ ];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  var original = arrayProto[method];
  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__;
    var inserted;
    switch (method) {
      case ‘push‘:
      case ‘unshift‘:
        inserted = args;
        break
      case ‘splice‘:
        inserted = args.slice(2);
        break
    }
    if (inserted) { ob.observeArray(inserted); } // 数组新插入的元素需要重新进行observe才能响应式
    // notify change
    // dep通知所有注册的观察者进行响应式处理
    ob.dep.notify();
    return result
  });
});

从数组的原型新建一个Object.create(arrayProto)对象,通过修改此原型可以保证原生数组方法不被污染。在保证不污染不覆盖数组原生方法添加监听,主要做了两个操作,第一是通知所有注册的观察者进行响应式处理,第二是如果是添加成员的操作,需要对新成员进行observe。

Dep就是一个发布者,可以订阅多个观察者,依赖收集之后Deps中会存在一个或多个Watcher对象,在数据变更的时候通知所有的Watcher。

// 通知所有订阅者
Dep.prototype.notify = function notify () {
  // stabilize the subscriber list first
  var subs = this.subs.slice();for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};
/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
// 调度者接口,当依赖发生改变的时候进行回调
Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) { // 同步则执行run直接渲染视图
    this.run();
  } else { // 异步推送到观察者队列中,由调度者调用
    queueWatcher(this);
  }
};
/**
 * Scheduler job interface.
 * Will be called by the scheduler.
 */
// 调度者工作接口,将被调度者回调
Watcher.prototype.run = function run () {
  if (this.active) {
    var value = this.get();
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      // 即便值相同,拥有Deep属性的观察者以及在对象/数组上的观察者应该被触发更新,因为它们的值可能发生改变。
      isObject(value) ||
      this.deep
    ) {
      // set new value
      var oldValue = this.value;
      this.value = value;
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
        }
      } else {
        this.cb.call(this.vm, value, oldValue);
      }
    }
  }
};
/**
 * Define a reactive property on an Object.
 */
function defineReactive$$1 (obj, key, val, customSetter, shallow) {
  var dep = new Dep();// cater for pre-defined getter/setters
  // 如果之前该对象已经预设了getter以及setter函数则将其取出来,新定义的getter/setter中会将其执行,保证不会覆盖之前已经定义的getter/setter。
  var getter = property && property.get;
  var setter = property && property.set;// 对象的子对象递归进行observe并返回子节点的Observer对象
  var childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend(); // 进行依赖收集
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) { // 是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归
            dependArray(value);
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
    }
  });
}

4、vue.js事件机制

为什么在HTML中监听事件(参考:https://cn.vuejs.org/v2/guide/events.html#为什么在-HTML-中监听事件

  你可能注意到这种事件监听的方式违背了关注点分离这个长期以来的优良传统。但不必担心,因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护上的困难。实际上,使用 v-on 有几个好处:扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法;因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试;当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何清理它们。

原文地址:https://www.cnblogs.com/colorful-coco/p/11719077.html

时间: 2024-10-08 21:51:39

vue源码学习的相关文章

Vue源码学习(一)

Vue构建 在scripts/build.js 中,从配置文件scripts/config.js读取配置,再通过命令行参数对构建配置做过滤,这样就可以构建出不同用途(平台:web/weex)的 Vue.js 了. 在scripts/config.js通过resolve查找相关的打包文件. 把自定义的build对象专程rollup所规定的格式,用rollup进行打包. 大专栏  Vue源码学习(一)"headerlink" title="Vue在源码是什么?">

vue 源码学习(一) 目录结构和构建过程简介

Flow vue框架使用了Flow作为类型检查,来保证项目的可读性和维护性.vue.js的主目录下有Flow的配置.flowconfig文件,还有flow目录,指定了各种自定义类型. 在学习源码前可以先看下Flow的语法 官方文档 目录结构 vue.js源码主要在src下 src ├── compiler # 编译相关 ├── core # 核心代码 ├── platforms # 不同平台的支持 ├── server # 服务端渲染 ├── sfc # .vue 文件解析 ├── shared

Vue源码学习(六)之虚拟DOM——Vue中的DOM-Diff (上)

1. 前言 在上一篇文章介绍VNode的时候我们说了,VNode最大的用途就是在数据变化前后生成真实DOM对应的虚拟DOM节点,然后就可以对比新旧两份VNode,找出差异所在,然后更新有差异的DOM节点,最终达到以最少操作真实DOM更新视图的目的.而对比新旧两份VNode并找出差异的过程就是所谓的DOM-Diff过程.DOM-Diff算法时整个虚拟DOM的核心所在,那么接下来,我们就以源码出发,深入研究一下Vue中的DOM-Diff过程是怎样的. 2. patch 在Vue中,把 DOM-Dif

Vue源码学习之数据初始化

首发地址:CJWbiu's Blog 在这里思考一个问题,使用Vue的时候需要在创建Vue实例时传入一个option,这里包含了我们定义的props.methods.data等.而在methods的方法中获取data中的key值都是直接通过this.key获取option对象中的methods中的定义的方法如何通过this访问到data中的数据呢? let vue = new Vue({ el: '#app', methods: { say() { console.log(this.msg) }

【译】Vue源码学习(一):Vue对象构造函数

本系列文章详细深入Vue.js的源代码,以此来说明JavaScript的基本概念,尝试将这些概念分解到JavaScript初学者可以理解的水平.有关本系列的一些后续的计划和轨迹的更多信息,请参阅此文章.有关本系列的文章更新进度的信息,请关注我的Tweeter.本系列的文章目录,请查看该链接. Vue对象构造函数 Vue实例是深入了解Vue源代码的一个基本点.正如Vue官方文档所说那样,"每个Vue应用程序都是通过使用Vue函数创建一个新的Vue实例来开始的." 在Vue的源码中,一个新

vue源码解读(一)Observer/Dep/Watcher是如何实现数据绑定的

欢迎star我的github仓库,共同学习~目前vue源码学习系列已经更新了5篇啦~ https://github.com/yisha0307/... 快速跳转: Vue的双向绑定原理(已完成) 说说vue中的Virtual DOM(已完成) React diff和Vue diff实现差别 Vue中的异步更新策略(已完成) Vuex的实现理解 Typescript学习笔记(持续更新ing) Vue源码中闭包的使用(已完成) 介绍 最近在学习vue和vuex的源码,记录自己的一些学习心得.主要借鉴

Vue.js 源码学习之Flag篇

The Progressive JavaScript Framework --vuejs.org 起因 第一次接触 Vue.js 是因为要做一个通讯录的外包项目,这个项目要有前台展示和中后台管理,从轮子做起肯定是不明智的选择,所以当时初步定下的是 Vue.js + Element UI 的技术栈. 项目过程很漫长,因为给的钱实在是可有可无,权当是学习了. 项目的接口是交给了同学. 整个项目采用的是钱后端分离的开发模式,我做我的页面,他做他的接口. 项目出了两个版本,做的时候,中间就强行的看文档.

大白话Vue源码系列(01):万事开头难

阅读目录 Vue 的源码目录结构 预备知识 先捡软的捏 Angular 是 Google 亲儿子,React 是 Facebook 小正太,那咱为啥偏偏选择了 Vue 下手,一句话,Vue 是咱见过的最对脾气的 MVVM 框架.之前也使用过 knockout,angular,react 这些框架,但都没有让咱产生 follow 的冲动.直到见到 Vue,简直是一见钟情啊. Vue 的官方文档已经对 Vue 如何使用提供了最好的教程,建议 Vue 新手直接去官网学习,而不要在网上找些质量参差不齐的

vue源码分析:响应式之依赖收集

今天记录下vue源码的响应式的源码学习,第一步,我们想下,为什么为们写一个data.然后里边的数据变化就能映射到dom的变化上.这里用到了Object.defineProperty(obj,key, {}),如果你还不了解建议去jsmdn上仔细了解下,这个函数的目的就是检测你得赋值和取值的过程用来定义自己的行为,比如obj[key]会拿到配置里边的get函数定义,obj[key] = 'xxx'会调用配置中的set.那么vue也是一样的,在get中做了依赖收集(也就是说哪些dom用到了我定义的这