从最简单的案例,来学习Vue.js源码。

<body>
<div id='app'>
<input type="text" v-model="message">---{{message}}
</div>
</body>
<script src='./vue.js'></script>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
});
</script>

(一)为何可以直接使用 Vue?

(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Vue = factory());
}(this, (function () {
'use strict';
   function Vue$3(options){
this._init(options);
}
   return Vue$3;
})));

   此写法,即兼容了 cmd ,也兼容了ES6,也将Vue对象挂载到window对象。

(二)Vue双向绑定的实现原理

   Vue 采用数据劫持,通过Object.defineProperty的getter和setter,结合观察者模式来实现数据绑定。

   与此相关的对象有四个:

   Observer(数据监听器):对数据对象的所有属性进行监听,如果 Data Change  ===通知==> Watcher;

   Watcher(订阅者): 连接 Observer、Compile 的桥梁。

   Dep(消息订阅器):收集 Watcher,数据变动触发 notify 函数,再调用 Watcher.update()

Compile:(Directive 指令解析器):对元素节点进行扫描和解析,替换、绑定相应回调函数。

   其实这里我有一个疑惑: 为什么Dep 要收集 Watcher?

   看源码的时候我重点关注上面 Observer、Watcher、Dep.

     进入Vue构造函数,实例化对象,进入 Vue.prototype._init() 函数。

  (1) Vue.prototype._init() 函数

 Vue.prototype._init = function (options) {
var vm = this;
// a uid
vm._uid = uid$++;
vm._isVue = true; //代表一个Vue实例,不是组件,不用被监听
if (options && options._isComponent) {
initInternalComponent(vm, options);//注入的参数中是组件处理
} else {
//实例内部初始化,返回 vm.constructor.options ,如果含有 super,内部也会调用 mergeOptions()
//extend(Vue.options,XXX)以及 initGlobalAPI() 中扩展了options 所有实例基础参数
var tempOptions = resolveConstructorOptions(vm.constructor);
vm.$options = mergeOptions(tempOptions, options || {}, vm);
}
//完成后,vm 拥有了 _renderProxy 对象属性
initProxy(vm);
//为什么要保存自身引用呢?
vm._self = vm;
//一步一步往 vm 添加属性,方法
initLifecycle(vm); //
initEvents(vm);//父子组件通信工作
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm);//完成 watch observe 等初始化
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created'); //Vue 传入的参数中有el属性,进行挂载,启动
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}

      2-13行代码,通过整合传入的参数,赋值vm. $options属性中,以便方便的取出。

    如果我们使用了 Vuex或者VueRouter,也会将其方法属性挂载在 $options属性中。

  (2)initProxy()

    为vm新增了一个  _renderProxy 代码属性。 

  initProxy = function initProxy(vm) {
if (hasProxy) {
//是否支持 es6 的代理
var options = vm.$options;
var handlers = options.render && options.render._withStripped
? getHandler
: hasHandler;
vm._renderProxy = new Proxy(vm, handlers);
} else {
vm._renderProxy = vm;
}
};
var hasHandler = {
has: function has(target, key) {
var has = key in target;
//判断key是否跟内置全局变量冲突
var isAllowed = allowedGlobals(key) || key.charAt() === '_';
if (!has && !isAllowed) {
warnNonPresent(target, key);
}
return has || !isAllowed
}
}; var getHandler = {
get: function get(target, key) {
if (typeof key === 'string' && !(key in target)) {
warnNonPresent(target, key);
}
return target[key]
}
};

     vm. _renderProxy  会在 Vue.prototype._render() 中如下使用。

     vnode = render.call(vm._renderProxy, vm.$createElement);

  (三) Observer 的初始化

    (1) initData()函数

     initData完成了对model元素Data数据格式化、元素代理初始化、监听初始化。

  function initData(vm) {
var data = vm.$options.data;
/*获取自定义 data 数据,这里为什么使用闭包,而不是直接获取 options.data*/
data = vm._data = typeof data === 'function' ?
getData(data, vm) : data || {}; var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
//循环为每个 data属性加入代理
var key = keys[i];
if (props && hasOwn(props, key)) {
} else if (!isReserved(key)) {
//如果key以 $ _ 开头,不作处理,是Vue的关键字
//复制创建新的属性。直接挂在在vm 下,且自定义了 get/set 方法,
//其真实的值在 vm._data 下
proxy(vm, "_data", key);
}
}
//处理完成后,vm 以及 vm._data 都含有用户定义的model数据
//监听
observe(data, true /* asRootData */);
}

  (2)observe(data,true)  

    这个函数会返回一个Observer() 实例对象, 如果model还未添加监听属性,则添加。

  function observe(value, asRootData) {
if (!isObject(value)) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
//有 __ob__ 属性,即已经被监听
ob = value.__ob__;
} else if (
//是否应该被监听 是否是服务器渲染 必须为数组或对象 是否可扩展 是否实例才有的属性
observerState.shouldConvert && !isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && !value._isVue) {//生成一个观察者
ob = new Observer(value);
}
//根数据计数 属性 ++
if (asRootData && ob) {
ob.vmCount++;
}
//ob 包含了 dep,value,__ob__属性引用自身,自定义 get\set 方法,
//计数vmCount,原型方法haiyou observeArray,监听对象的walk方法
return ob
}

    observe()函数的关键点在于 ob=new Observer(value) 。

    (3)Observer 对象

    Observer初始化时,会将当前Observer自身引用挂载在 model 中,

    同时循环为每个model属性重写get/set 方法,这样就实现了数据劫持。        

 /**
* 创建Dep对象实例
* 将自身this添加到value的ob属性上
*/
var Observer = function Observer(value) {
this.value = value;
this.dep = new Dep(); //Dep 构造函数: uid--id,subs[] --依赖收集
this.vmCount = ;
def(value, '__ob__', this);//为model属性添加 __ob__属性
if (Array.isArray(value)) {
//递归调用 Observer(value) 最后仍然走 walk()
var augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);//原型扩展
this.observeArray(value);
} else {
this.walk(value);
}
}; //Walk 为每个属性对象添加get/set
Observer.prototype.walk = function walk(obj) {
var keys = Object.keys(obj);
for (var i = ; i < keys.length; i++) {
defineReactive$$(obj, keys[i], obj[keys[i]]);
}
};

   (4)defineReactive$$1()

    这个函数主要是对Object某个属性值设置了数据劫持,也就是通过重写对象属性中的 get/set 方法,

    一旦改变,就会立刻触发相关函数。

   /**
* 对象属性被劫持 通过调用Object.defineProperty 给data的每个属性添加 getter setter方法,
* 当data某个属性被访问时,调用getter方法,判断当 Dep.target 不为空时调用 dep.denpend 和 childObj.dep.denpend方法
* 当改变data的属性时,调用setter方法,这时调用 dep.notify方法进行通知
*
*/
function defineReactive$$(obj, key, val, customSetter, shallow) {
var dep = new Dep();//依赖管理 var property = Object.getOwnPropertyDescriptor(obj, key);//返回键描述信息
if (property && property.configurable === false) {
//不可以修改直接返回
return
} var getter = property && property.get;
var setter = property && property.set; 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) {
var value = getter ? getter.call(obj) : val; //获取当前值,是前一个值
if (newVal === value || (newVal !== newVal && value !== value)) {
//值没有发生变化,不再做任何处理
return
}
/* eslint-enable no-self-compare */
if ("development" !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);//调用默认setter方法或将新值赋给当前值
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();//赋值后通知依赖变化
}
});
}

但是我有一点疑惑的是:model值改变,会按照  hasHandler ==> proxySetter ==> reactiveSetter 这样一种顺序,

 直至最后 触发 Dep.notify()  为什么?

Vue源码学习(一)———数据双向绑定 Observer的更多相关文章

  1. Vue源码学习之数据初始化

    首发地址:CJWbiu's Blog 在这里思考一个问题,使用Vue的时候需要在创建Vue实例时传入一个option,这里包含了我们定义的props.methods.data等.而在methods的方 ...

  2. Vue源码学习1——Vue构造函数

    Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ...

  3. 【Vue源码学习】依赖收集

    前面我们学习了vue的响应式原理,我们知道了vue2底层是通过Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些 ...

  4. Vue源码学习三 ———— Vue构造函数包装

    Vue源码学习二 是对Vue的原型对象的包装,最后从Vue的出生文件导出了 Vue这个构造函数 来到 src/core/index.js 代码是: import Vue from './instanc ...

  5. Vue源码学习二 ———— Vue原型对象包装

    Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ...

  6. 最新 Vue 源码学习笔记

    最新 Vue 源码学习笔记 v2.x.x & v3.x.x 框架架构 核心算法 设计模式 编码风格 项目结构 为什么出现 解决了什么问题 有哪些应用场景 v2.x.x & v3.x.x ...

  7. Vue 源码学习(1)

    概述 我在闲暇时间学习了一下 Vue 的源码,有一些心得,现在把它们分享给大家. 这个分享只是 Vue源码系列 的第一篇,主要讲述了如下内容: 寻找入口文件 在打包的过程中 Vue 发生了什么变化 在 ...

  8. 【Vue源码学习】响应式原理探秘

    最近准备开启Vue的源码学习,并且每一个Vue的重要知识点都会记录下来.我们知道Vue的核心理念是数据驱动视图,所有操作都只需要在数据层做处理,不必关心视图层的操作.这里先来学习Vue的响应式原理,V ...

  9. VUE 源码学习01 源码入口

    VUE[version:2.4.1] Vue项目做了不少,最近在学习设计模式与Vue源码,记录一下自己的脚印!共勉!注:此处源码学习方式为先了解其大模块,从宏观再去到微观学习,以免一开始就研究细节然后 ...

随机推荐

  1. DCDC电源 TPS54525

    电路图: 输入电压12V,输出电压5V.不接负载可以获得5V输出,接入负载后只有4mV,且PG为低. 预计原因:可能是上电瞬间电流过大,芯片过流保护. 解决方案:SS软启动引脚的电容C331加大至1u ...

  2. 服务器后台代码生成TreeView的json字符串

    1.根据treeView控件的属性建立vo类 package cn.allen.tree.vo; import java.util.List; import java.util.Map; public ...

  3. 实验七:Xen环境下cirrOS的安装配置

    实验名称: Xen环境下cirrOS的安装配置 实验环境: 这里的cirrOS和实验六中的busybox的启动方式相同,唯一的区别就是我们使用的cirrOS镜像中,已经包含了根文件系统.内核文件以及r ...

  4. 关于RandomAccessFile一个坑!!!!

    最近正好遇到了使用RandomAccessFile做断点下载的情况,被一个问题坑了好多次 本来的代码: RandomAccessFile randomAccessFile = new RandomAc ...

  5. pandas数据结构之DataFrame操作

    这一次我的学习笔记就不直接用官方文档的形式来写了了,而是写成类似于“知识图谱”的形式,以供日后参考. 下面是所谓“知识图谱”,有什么用呢? 1.知道有什么操作(英文可以不看) 2.展示本篇笔记的结构 ...

  6. 学习Flask框架

      # -*- encoding: utf-8 -*- #导包 from flask import Flask #建立flask对象 app = Flask(__name__) #使用flask路由器 ...

  7. MySQL(索引)

    索引 索引,是数据库中专门用于帮助用户快速查询数据的一种数据结构.类似于字典中的目录,查找字典内容时可以根据目录查找到数据的存放位置,然后直接获取即可. MySQL中常见索引有: 普通索引 唯一索引 ...

  8. 7款不错的 CI/CD工具

    时至今日,越来越多的工程团队开始实行敏捷开发,借以推动更短.更快的发布周期.而代码库的增长与更高的生产构建频率,也带动持续集成与持续部署/交付工具快速兴起.如果您有意提升发布频率,或者是不太清楚哪些工 ...

  9. python大法好——操作mysql

    python操作mysql数据库 Python 标准数据库接口为 Python DB-API,Python DB-API为开发人员提供了数据库应用编程接口. Python 数据库接口支持非常多的数据库 ...

  10. TIMESTAMP(6)类型的时间差

    TIMESTAMP 数据类型 它包括了所有DATE数据类型的年月日时分秒的信息,而且包括了小数秒的信息. 以分钟为单位查询时间差 select ROUND(TO_NUMBER(to_date(to_c ...