当前篇:vue源码分析【3】-vue响应式
以下代码和分析过程需要结合vue.js源码查看,通过打断点逐一比对。
模板代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="./../../oldVue.js"></script>
</head>
<body>
<div id="app">
<h2>开始存钱</h2>
<div>每月存 :¥{{ money }}</div>
<div>存:{{ num }}个月</div>
<div>总共存款: ¥{{ total }}</div>
<button @click="getMoreMoney">{{arryList[0].name}}多存一点</button>
<msg-tip :msginfo='msgText' :totalnum='total'></msg-tip>
</div>
<script>
debugger;
// 定义一个新组件
var a = {
props:['msginfo', 'totalnum'],
data: function () {
return {
count: 0
}
},
template: '<div>{{ msginfo }}存了¥{{ totalnum }}</div>'
}
var app = new Vue({
el: '#app',
components: { msgTip: a},
beforeCreate() { },
created() { },
beforeMount() { },
mounted: () => { },
beforeUpdate() { },
updated() { },
beforeDestroy() { },
destroyed() { },
data: function () {
return {
money: 100,
num: 12,
arryList: [{name:'子树'}],
msgText: "优秀的乃古:"
}
},
computed: {
total() {
return this.money * this.num;
}
},
watch:{ // 这里一开始没有写,在watch章节才加的
money:{
handler(newVal, oldVal){
this.msgText = newVal+this.msgText
},
deep:true,
immediate:true
}
},
methods: {
getMoreMoney() {
this.money = this.money * 2
this.arryList.unshift({name: '大树'})
}
}
})
</script>
</body>
</html>
1. 前言
本文的结构依据点,线,面来展开。
- 点即函数的作用
- 线即函数的执行流程
- 面即源码的详细解读
十分不建议直接看源码,很多函数非常长,并且链路很长,在没有对函数有大概的了解情况,大概率下,你读了一遍源码后会发现,wc 我刚看了源码了吗?可是咋记不清它们做了啥操作。因此,先看作用,再看流程,再展开看源码。
2. 概念
2-1. 什么是Vue响应式呢
数据发生变化后,会重新对页面渲染,这就是Vue响应式
2-2. 想完成这个过程,我们需要做些什么
- 侦测数据的变化
- 收集视图依赖了哪些数据
- 数据变化时,自动“通知”需要更新的视图部分,并进行更新
它们对应专业俗语分别是:
- 数据劫持 / 数据代理
- 依赖收集
- 发布订阅模式
3-3. 翻译翻译
张麻子:数据发生变化,会重新渲染,你给翻译翻译
师爷:我们肯定要知道哪些数据发生了变化吧(数据劫持),光知道哪些数据变化了还没用,还要知道视图用到了哪些变化的数据吧(依赖收集),最后,还得告诉页面,你依赖的数据变化了,赶紧更新吧(发布订阅模式)
4-4. 名词解析
为了让大家能够更好的阅读,介绍几个基本的概念。
- Watcher: 观察者
- 我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
- Dep: 订阅者
- 收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。Dep用于解耦属性的依赖收集和派发更新操作,「说得具体点」:它的主要作用是用来存放 Watcher 观察者对象。
3. 整体过程
Vue实例化一个对象的具体过程如下:
- 新创建一个实例后,Vue调用compile将el转换成vnode。
- 调用initState, 创建props, data的钩子以及其对象成员的Observer(添加getter和setter)。
- 执行mount挂载操作,在挂载时建立一个直接对应render的Watcher,并且编译模板生成render函数,执行vm._update来更新DOM。
- 每当有数据改变,都将通知相应的Watcher执行回调函数,更新视图。
- 当给这个对象的某个属性赋值时,就会触发set方法。
- set函数调用,触发Dep的notify()向对应的Watcher通知变化。
- Watcher调用update方法。
在这个过程中:
- Observer是用来给数据添加Dep依赖。
- Dep是data每个对象包括子对象都拥有一个该对象, 当所绑定的数据有变更时, 通过dep.notify()通知Watcher。
- Compile是HTML指令解析器,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
- Watcher是连接Observer和Compile的桥梁,Compile解析指令时会创建一个对应的Watcher并绑定update方法 , 添加到Dep对象上。
data:{
Dep:{
watch1,
watch2,
}
}
3. initState
3-1. 基本信息
初始化状态
new Vue 后会开始,执行function initMixin,来初始化各个状态。
作用:
初始化状态: 分别初始化props、methods、data、computed、watch,它也是数据响应式的入口优先级:props、methods、data、computed对象中的属性不能出现重复,优先级和列出顺序一致,其中 computed 中的 key 不能和 props、data 中的 key 重复,methods 不影响
源码:
function initState(vm) {
vm._watchers = []; // 初始化观察者队列
var opts = vm.$options; // 初始化参数
// 判断是否有props属性,如果有则添加观察者,此例我们没有在加props属性
// 那么opts.props是在哪里挂载的,实际是在initMixin的时候,vm.$options = mergeOptions()
// 这里挂载的
if (opts.props) {
//初始化props 检验props 数据格式是否是规范的如果是规范的则添加到观察者队列中
initProps(vm, opts.props);
}
if (opts.methods) {
// 初始化事件Methods 把事件 冒泡到 vm[key] 虚拟dom 最外层中
initMethods(vm, opts.methods);
}
if (opts.data) {
// 初始化数据 获取options.data 的数据 将他们添加到 监听者中
initData(vm);
} else {
// 判断value 是否有__ob__ 实例化 dep对象,获取dep对象 为 value添加__ob__ 属性,
// 把vm._data添加到观察者中 返回 new Observer 实例化的对象
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { //计算属性
//初始化计算属性 并且判断属性的key 是否 在 data ,将 计算属性的key 添加入监听者中
initComputed(vm, opts.computed);
}
//options 中的 watch
if (opts.watch && opts.watch !== nativeWatch) {
//初始化Watch
initWatch(vm, opts.watch);
}
}
3-2. initProps
作用:
初始化数据 获取options.props 的数据, 为 props 对象的每个属性设置响应式,并将其代理到 vm 实例上
源码:
if (opts.props) {
//初始化props 检验props 数据格式是否是规范的如果是规范的则添加到观察者队列中
initProps(vm, opts.props);
}
//初始化props 检验props 数据格式是否是规范的如果是规范的则添加到观察者队列中
function initProps(
vm,
propsOptions //props对象 例:{ msginfo: {type: null}, totalnum: {type: null} }
// 变成这种格式,是normalizeProps函数处理得到的
) {
debugger
// 例:{ msginfo: "优秀的乃古:",totalnum: 1200 }
var propsData = vm.$options.propsData || {};
var props = vm._props = {}; // 挂载_props
var keys = vm.$options._propKeys = []; // 挂载_propKeys
var isRoot = !vm.$parent; // $parent属性存在,此节点就不是根节点
// 设置不监听 观察者
if (!isRoot) {
toggleObserving(false); // shouldObserve = false;
}
// 遍历函数,传入props的key
var loop = function (key) {
debugger
...
};
//循环校验 props 是否 是合格数据 并且添加观察者
for (var key in propsOptions) loop(key);
toggleObserving(true);
}
loop:
获取 props[key] 的默认值,并对prop进行校验
var loop = function (key) {
debugger
keys.push(key);
// 获取 props[key] 的默认值
var value = validateProp(
key, //props 对象的key 例:"totalnum"
propsOptions, //例:{ msginfo: {type: null}, totalnum: {type: null} }
propsData, // 例:{ msginfo: "优秀的乃古:",totalnum: 1200 }
vm
);
/* istanbul ignore else 伊斯坦布尔忽略其他 */
{
//大写字母,加完减号又转成小写了 比如把驼峰 aBc 变成了 a-bc
//匹配大写字母并且两面不是空白的 替换成 - 在转换成小写
var hyphenatedKey = hyphenate(key);
// 检查属性是否为保留属性。
//var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
//输出警告
warn(
("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
vm
);
}
//通过defineProperty的set方法去通知notify()订阅者subscribers有新的值修改
defineReactive(props, key, value, function () {
if (vm.$parent && !isUpdatingChildComponent) {
warn(
"Avoid mutating a prop directly since the value will be " +
"overwritten whenever the parent component re-renders. " +
"Instead, use a data or computed property based on the prop's " +
"value. Prop being mutated: \"" + key + "\"",
vm
);
}
});
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) { //如果vm中没有props属性,则把他添加到vm中,这样组件this.[propsKey] 就可以获取到值了
proxy(vm, "_props", key);
}
};
3-2-1. validateProp
执行流程:
- 判断prop的type是否是布尔值
- 定义一个value为props对象中对应key的值,对其进行一系列操作,最终返回它
- 如果type是布尔值
- 如果key不是propsData自有属性,并且没有定义default 默认值的时候,设置value 为false
- 如果value为空,或者value和key一样,且(type不是字符串,或者布尔值索引小于字符串的时候,说明先匹配的是布尔类型),则设置value为true
- 如果value未定义,尝试获取默认值,对value添加观察
- 检查prop 是否合格
- 返回最终的value
源码:
function validateProp(
key, //props 对象的key 例:"totalnum"
propOptions, //例:{ msginfo: {type: null}, totalnum: {type: null} }
propsData, // 例:{ msginfo: "优秀的乃古:",totalnum: 1200 }
vm
) { //vm this属性
var prop = propOptions[key]; //获取组件定义的props 属性 例:{type: null}
var absent = !hasOwn(propsData, key); // key是否不为自身属性
var value = propsData[key]; // 获取值 例:"优秀的乃古:"
/**
* prop.type:
* 传入prop的类型,props如果是对象形式的声明,我们一般会写它的类型type,如果是数组格式,
* 我们就只写了各个props的key,所以数组格式的type为null
*
* getTypeIndex:
* 判断props中的type是否符合期望类型,如果符合就返回0或者下标,否则返回-1
*/
var booleanIndex = getTypeIndex(Boolean, prop.type);
if (booleanIndex > -1) { // 如果是boolean值
// 如果key 不是propsData 实例化,或者 没有定义default 默认值的时候 设置value 为false
if (absent && !hasOwn(prop, 'default')) {
value = false;
} else if (
value === '' // 如果value 是空
|| value === hyphenate(key) //或者key转出 - 形式和value 相等的时候
) {
// 判断prop.type 的类型是否是string字符串类型
var stringIndex = getTypeIndex(String, prop.type);
// 如果不是字符串类型,
// 或者布尔值索引小于字符串的时候,说明先匹配的是布尔类型,
// 此时将值强制转换为true
if (
stringIndex < 0 ||
booleanIndex < stringIndex) {
value = true;
}
}
}
debugger
// 如果value未定义
if (value === undefined) {
// 尝试获取默认值
value = getPropDefaultValue(vm, prop, key);
var prevShouldObserve = shouldObserve;
toggleObserving(true);
observe(value);
toggleObserving(prevShouldObserve);
}
{
//检查prop 是否合格
assertProp(
prop, //属性的type值
key, //props属性中的key
value, //view 属性的值
vm, // VueComponent 组件构造函数
absent //false
);
}
return value
}
3-2-1-1. getPropDefaultValue
作用:
返回prop属性默认的default值。
执行流程:
- 如果prop上不存在default,那么直接返回undefined
- prop的default是函数,并且type不是函数声明,则返回执行defalut函数,否则返回defalut
关联文档:
我们在写props的时候,如果我们接收的是布尔值,直接在default写true或者false没问题,如果我们接收的是数组或者对象,默认值直接写[]或者{}就会报错,平常业务中我们可能会经常不注意导致报错,例如:
props: {
visible: {
type: Boolean,
default: false //布尔值可以不用工厂函数
},
selectLogisticsNos: {
type: Array,
default: () => [] //Object/Array必须用工厂函数
// default: [] //会报错
},
}
那么为什么要这么做呢? 因为我们的对象操作的时候,需要把它们的this指向vm实例。
源码:
function getPropDefaultValue(vm, prop, key) {
// 判断该对象prop 中的default 是否是prop 实例化的
if (!hasOwn(prop, 'default')) {
return undefined
}
var def = prop.default;
/**
* 属性键的默认值无效,
类型为Object/Array的属性必须使用工厂函数,返回默认值
例: props: {
visible: {
type: Boolean,
default: false //布尔值可以不用工厂函数
},
selectLogisticsNos: {
type: Array,
default: () => [] //Object/Array必须用工厂函数
},
}
*/
if ("development" !== 'production' && isObject(def)) {
warn(
'Invalid default value for prop "' + key + '": ' +
'Props with type Object/Array must use a factory function ' +
'to return the default value.',
vm
);
}
// 原始PROP值也未在先前的渲染中定义,
// 返回先前的默认值以避免不必要的监视触发器
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key]
}
// prop的default是函数,并且type不是函数,则执行defalut函数,否则返回defalut
return typeof def === 'function' && getType(prop.type) !== 'Function'
? def.call(vm)
: def
}
3-3. initMethods
作用:
对函数进行一系列的约束,修正methods对象,并且直接把各个函数直接挂载到vue上
执行流程:
- 遍历methods对象
- 对函数进行约束
- 如果函数对象对应key的事件不存在则发出警告
- 如果属性中定义了key,则在methods中不能定义同样的key
- 检查一个字符串是否以
$或者_开头的字母,事件不能以$或者_开头的字母
- 把事件直接挂载到vue上,如果是函数为空则给一个空函数,如果是有函数则返回函数函数
关联文档:
我们在vue中使用事件时,直接用this.fn(),即Vue.fn()。那么为什么能直接这么用呢?
就是因为initMethods最后一步,将每个函数直接挂载到了vm上了。
源码:
if (opts.methods) {
// 初始化事件Methods 把事件 冒泡到 vm[key] 虚拟dom 最外层中
initMethods(vm, opts.methods);
}
function initMethods(
vm,
methods //例:{ getMoreMoney: ƒ getMoreMoney() }
) {
debugger
var props = vm.$options.props;
//循环 methods 事件对象
for (var key in methods) {
{
// 如果对应key的事件不存在则发出警告
if (methods[key] == null) {
warn(
"Method \"" + key + "\" has an undefined value in the component definition. " +
"Did you reference the function correctly?",
vm
);
}
// 如果属性中定义了key,则在methods中不能定义同样的key
if (props && hasOwn(props, key)) {
warn(
("Method \"" + key + "\" has already been defined as a prop."),
vm
);
}
// isReserved 检查一个字符串是否以$或者_开头的字母
// 事件不能以$或者_开头的字母,因为$ 和 _ 开头的方法一般是内置方法,会重复
if ((key in vm) && isReserved(key)) {
warn(
"Method \"" + key + "\" conflicts with an existing Vue instance method. " +
"Avoid defining component methods that start with _ or $."
);
}
}
// 把事件放在最外层对象中,如果是函数为空则给一个空函数,如果是有函数则返回函数函数
vm[key] = methods[key] == null ? noop : bind(methods[key], vm);
}
}
3-4. initData
先看下initState(vm)此时的入参Vue实例的内容:
$attrs: (...) // 属性集合
$children: [] // 子节点
$createElement: ƒ (a, b, c, d) //创建节点
$listeners: (...) //属性事件
$options: {components: {…}, methods: {…}, el: "#app", …} //var app=new Vue传进来的data,method等
$parent: undefined
$refs: {}
$root: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
$scopedSlots: {}
$slots: {}
$vnode: undefined
_c: ƒ (a, b, c, d)
arguments: (...)
caller: (...)
length: 4
name: ""
prototype: {constructor: ƒ}
__proto__: ƒ ()
[[FunctionLocation]]: oldVue.js:6695
[[Scopes]]: Scopes[3]
_directInactive: false
_events: {}
_hasHookEvent: false
_inactive: null
_isBeingDestroyed: false
_isDestroyed: false
_isMounted: false
_isVue: true
_renderProxy: Proxy {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
_self: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
_staticTrees: null
_uid: 0
_vnode: null
_watcher: null
$data: (...)
$isServer: (...)
$props: (...)
$ssrContext: (...)
get $attrs: ƒ reactiveGetter()
set $attrs: ƒ reactiveSetter(newVal)
get $listeners: ƒ reactiveGetter()
set $listeners: ƒ reactiveSetter(newVal)
__proto__: Object
可以看到,此时的vm已经挂载了常用的属性和函数。
作用:
初始化数据 获取options.data 的数据,对data,methods,props进行判重,将data各个值代理到vm上,并 将他们添加到 监听者中。
执行流程:
- 重置data,如果data是函数,则取data函数的返回,否取自身的值,同时给Vue挂载值为data的_data属性(对象)
- 经过重置后,data不是一个对象,就重置data为{},并且给出警告
- 遍历data对象中的key,如果key和methods或者props中的属性名重复了,给出警告
- 如果key不是 以$或者_开头,代理 data 对象上的属性到 vm 实例
- 为 data 对象上的数据设置响应式
关联文档:
我们在vue中使用data属性时,直接用this.xxx,即Vue.xxx。那么为什么能直接这么用呢?
就是因为这里做了代理proxy(vm, "_data", key);,将每个函数直接挂载到了vm上了。
源码:
if (opts.data) {
// 初始化数据 获取options.data 的数据 将他们添加到 监听者中
initData(vm);
} else {
// 判断value 是否有__ob__ 实例化 dep对象,获取dep对象 为 value添加__ob__ 属性,
// 把vm._data添加到观察者中 返回 new Observer 实例化的对象
observe(vm._data = {}, true /* asRootData */);
}
function initData(vm) {
var data = vm.$options.data;
// 如果data是函数,则取data函数的返回,否取自身的值,同时给Vue挂载值为data的_data(对象)
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
// 此时拿到的data为: { money: 100, num: 12 }
if (!isPlainObject(data)) { //如果不是对象 则重置data为{},并发出警告日志
data = {};
"development" !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
// 例:["money", "num", "arryList", "msgText"]
var keys = Object.keys(data);
var props = vm.$options.props; //获取props 属性
// 例:{ getMoreMoney: ƒ getMoreMoney() }
var methods = vm.$options.methods; //获取事件
var i = keys.length; //获取数据key的长度
while (i--) { //循环data
var key = keys[i];
{
if (methods && hasOwn(methods, key)) { //如果数据中的 key 与事件 中的定义的key 一样 则发出警告
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
if (props && hasOwn(props, key)) { //如果数据中的 key 与props属性 中的定义的key 一样 则发出警告
"development" !== 'production' && warn(
"The data property \"" + key + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
} else if (!isReserved(key)) { //如果key不是 以$或者_开头
proxy(vm, "_data", key); // 给target即Vue实例上的每个sourceKey上的key设置监听
}
}
// 为 data 对象上的数据设置响应式
observe(data, true);
}
3-4-1. proxy
作用:
设置代理,将 data和props上的属性 直接代理到 Vue上,设置get和set方法
源码:
proxy(vm, "_props", key);
proxy(vm, "_data", key);
function proxy(
target, // Vue实例
sourceKey, // 监听Vue上的key。 例:sourceKey = '_data'
key // 例:msgText
) {
debugger
// 分别挂载get和set方法
sharedPropertyDefinition.get = function proxyGetter() { //设置get函数
return this[sourceKey][key] // sourceKey = "_data", key = "num"
};
sharedPropertyDefinition.set = function proxySetter(val) {//设置set函数
this[sourceKey][key] = val;
};
// 给target即Vue实例上的每个sourceKey上的key设置监听
/**
* target 例:
* {
$attrs: (...),
$children: [],
$createElement: ƒ (a, b, c, d),
$listeners: (...),
...,
msgText: "优秀的乃古:",
get msgText: ƒ proxyGetter(), //代理了这2个方法
set msgText: ƒ proxySetter(val) //代理了这2个方法
}
*/
Object.defineProperty(target, key, sharedPropertyDefinition);
}
var sharedPropertyDefinition = { //共享属性定义
enumerable: true,
configurable: true,
get: noop,
set: noop
};
执行完以后,会把data中的money和num, arryList直接挂载到Vue实例上去。
3-5 observe
function initData(vm)最后一步执行了observe(data, true);
作用:
为对象创建观察者实例,如果对象已经被观察过,则返回已有的观察者实例,否则创建新的观察者实例
执行流程:
- 如果data 不是一个对象 或者 是实例化的VNode,则什么都不做(非对象和 VNode 实例不做响应式处理),退出函数
- 第一次进来的时候,value中没有
__ob__的,如果存在__ob__属性,则表示已经做过观察了,直接返回__ob__属性,ob = value.__ob__ - 如果没有没有
__ob__,如果value是对象和数组,则创建观察者实例,ob = new Observer(value); - 如果ob存在,ob上的计数器递增
- 最后返回ob
源码:
observe(data, true);
function observe(
value, // 第一次传进来data对象。
// 例:{ arryList: [{…}],money: 100,msgText: "优秀的乃古:"num: 12 }
asRootData //asRootData = true
) {
debugger
//data 不是一个对象 或者 是实例化的VNode
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
// 第一次进来的时候,value中没有__ob__的
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve && //shouldObserve 为真
!isServerRendering() && //并且不是在服务器node环境下
(Array.isArray(value) || isPlainObject(value)) && //是数组或者是对象
/**
* Object.isExtensible用于判断对象是否可以被拓展
*/
Object.isExtensible(value) &&
!value._isVue //_isVue为假
) {
//实例化 dep对象 为 value添加__ob__ 属性
ob = new Observer(value);
}
// 如果是RootData,即咱们在新建Vue实例时,传到data里的值,只有RootData在每次observe的时候,会进行计数。
// vmCount是用来记录此Vue实例被使用的次数的, 比如,我们有一个组件logo,页面头部和尾部都需要展示logo,
// 都用了这个组件,那么这个时候vmCount就会计数,值为2
if (asRootData && ob) { //是根节点数据的话 并且 ob 存在
ob.vmCount++; //统计有几个vm
}
// 实例化 dep对象,获取dep对象 为 value添加__ob__ 属性
//例: { dep: Dep {id: 7, subs: Array(0)}, value: {__ob__: Observer}, vmCount: 0 }
return ob
}
3-6 Observer
作用:
Observer构造函数,为observe服务的。
观察者类,会被附加到每个被观察的对象上, 为 value添加__ob__ 属性, 而对象的各个属性则会被转换成 getter/setter,并收集依赖和通知更新
执行流程:
- 将当前Observer挂载到data的__ob__上
- 如果value是数组,则遍历数组的每一项,执行observe观察
- 如果是对象,遍历每个属性并将其转换为getter / setter
源码:
ob = new Observer(value);
var Observer = function Observer(value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
//设置监听 value 必须是对象
debugger
// 给value添加__ob__属性,值就是本Observer对象,value.__ob__ = this;
// Vue.$data 中每个对象都有 __ob__ 属性,包括 Vue.$data对象本身
def(value, '__ob__', this);
debugger
/**
* value 为数组
* hasProto = '__proto__' in {}
* 用于判断对象是否存在 __proto__ 属性,
通过 obj.__proto__ 可以访问对象的原型链
* 但由于 __proto__ 不是标准属性,所以有些浏览器不支
持,比如 IE6-10,Opera10.1
* 为什么要判断,是因为一会儿要通过 __proto__ 操作数据的原型链
覆盖数组默认的七个原型方法,以实现数组响应式
*/
if (Array.isArray(value)) { //判断是不是数组
var augment = hasProto //__proto__ 存在么 高级浏览器都会有这个
? protoAugment
: copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value); //遍历value,执行 observe(items);最终还是会执行 walk 方法
} else {
// 为对象的每个属性(包括嵌套对象)设置响应式
this.walk(value);
}
};
// 当前浏览器支持`__proto__`时,将数组的api都挂载到value的原型上
function protoAugment(
target, // 就是上面的value(当为数组格式时)
src, // 数组api集合
keys //api的key的集合 这里不会用上
) {
target.__proto__ = src;
}
// 当前浏览器不支持`__proto__`时,给value设置代理,挂载数组方法
function copyAugment(
target, // 就是上面的value(当为数组格式时)
src, // 数组api集合
keys //api的key的集合
) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
def(target, key, src[key]);
}
}
3-6-1 def
作用:
用defineProperty 定义属性,给传入的对象上的key重写为传入的函数
源码:
/**用defineProperty 定义属性
第一个参数是对象
第二个是key
第三个是函数
第四个是 是否可以枚举。
*/
function def(
obj, // obj = Array {} ,数组原型对象
key, // key = "push
val, // val = ƒ mutator()
enumerable // enumerable = undefined
) {
// obj即传入的数组的原型方法,这里如果传的key为push,则是给数组原型挂载了传入的方法ƒ mutator()
// 如: obj = { push: ƒ mutator(), __proto__: Array(0) }
Object.defineProperty(obj, key, {
value: val, //值
enumerable: !!enumerable, //定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。
writable: true, //可以 改写 value
configurable: true //configurable特性表示对象的属性是否可以被删除,以及除writable特性
//外的其他特性是否可以被修改。
});
}
def函数的另外一个作用:
def函数在多个地方有用到,我们知道Vue能对数组响应式,因为对数组方法进行了重写,其代码如下:
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
// 重写了数组的这些方法
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/**
* 更新数据时候如果是数组拦截方法,如果在数据中更新用的
是'push','pop','shift','unshift','splice','sort','reverse' 方法则会调用这里
*/
methodsToPatch.forEach(function (
method //数组的方法名
) {
// 获取数组的各个原生方法
var original = arrayProto[method];
debugger
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);
}
//更新通知
ob.dep.notify();
return result
});
});
/**依次给传的数组原型对象上的方法设置重写,重写为传入的函数
用defineProperty 定义属性
第一个参数是对象
第二个是key
第三个是vue
第四个是 是否可以枚举。
*/
function def(
obj, // obj = Array {} ,数组原型对象
key, // key = "push
val, // val = ƒ mutator()
enumerable // enumerable = undefined
) {
debugger
// obj即传入的数组的原型方法,这里如果传的key为push,则是给数组原型挂载了传入的方法ƒ mutator()
// 如: obj = { push: ƒ mutator(), __proto__: Array(0) }
Object.defineProperty(obj, key, {
value: val, //值
enumerable: !!enumerable, //定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。
writable: true, //可以 改写 value
configurable: true //configurable特性表示对象的属性是否可以被删除,以及除writable特性外的其他特性是否可以被修改。
});
}
3-6-2 walk
作用:
遍历每个属性并将它们转换为getter/setter。
源码:
Observer.prototype.walk = function walk(obj) {
debugger
var keys = Object.keys(obj); //此时 keys = ["money", "num", "arryList"]
for (var i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]); // 传入对象各个key
}
};
我们看下defineReactive函数,它非常的长,我们将在【章节4】进行讲解。
defineReactive的作用是:
- 在object上定义一个响应式的属性
- 把对象obj里的属性key变成一个getter/setter形式的响应式的属性
- 同时在getter的时候收集依赖,并在setter的时候触发依赖。
3-6-3 总结
observe这个函数传入一个 obj(需要被追踪变化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理,给每个属性加上set和get方法,以此来达到实现侦测对象变化。值得注意的是,observe 会进行递归调用。
observe执行完后返回的ob为:
{
dep: {id: 2, subs: Array(0)},
value: {
"money": 100,
"num": 12,
"arryList": [
{
"name": "子树"
}
]
},
vmCount: 1,
__proto__: Object
}
再回到initData函数observe(data, true),
这时的data已经变成下面这样的格式了:
{
arryList: [{ name: "子树" }],
money: 100,
num: 12,
__ob__: Observer {value: {…}, dep: Dep, vmCount: 1},
get arryList: ƒ reactiveGetter(),
set arryList: ƒ reactiveSetter(newVal),
get money: ƒ reactiveGetter(),
set money: ƒ reactiveSetter(newVal),
get num: ƒ reactiveGetter(),
set num: ƒ reactiveSetter(newVal),
__proto__: Object
}
每个属性都已经挂载了相应的get和set方法。
3-7. initComputed
作用:
执行流程:
- 传入计算属性集合
- 创建一个新的监听者对象watchers,同时挂载到vm的_computedWatchers上(由于是引用类型,后续修改watchers就意味着修改vm的_computedWatche)
- 定义getter, 如果计算属性值为函数,则取它本身,否则取它的get属性
- 如果getter是空 警告: 计算属性缺少Getter
- 如果不是node ssr渲染,给watchers添加计算属性的观察者
- 判断computed 的属性key
- 如果不在vm中,定义计算属性 并且 把计算属性的key和计算函数 直接添加到vm上(所以我们能直接用this.propsxx)
- 如果在vm中,而且在$data或者props中,警告:计算属性在data或者prop已经定义,不能重复重复定义 源码:
if (opts.computed) {
initComputed(vm, opts.computed);
}
function initComputed(
vm,
computed // 例:{ total: ƒ total() }
) {
debugger
//创建一个新的监听者对象空对象,同时挂载到vm的_computedWatchers上
var watchers = vm._computedWatchers = Object.create(null);
// 服务器呈现 判断是不是node 服务器环境
var isSSR = isServerRendering();
for (var key in computed) {
var userDef = computed[key]; // 获取计算函数
// 定义getter, 如果计算属性值为函数,则取它本身,否则取它的get属性
var getter = typeof userDef === 'function' ? userDef : userDef.get;
//如果getter是空 警告 计算属性缺少Getter
if ("development" !== 'production' && getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}
// 如果不是node ssr渲染,给watchers添加计算属性的观察者
// 这一步可以看出computed也是一个watcher
if (!isSSR) {
watchers[key] = new Watcher(
vm, //vm vode
getter || noop, //noop为空函数
noop, // 回调函数
computedWatcherOptions //参数 lazy = true
);
/**
* watchers,例:
* {
total:{
vm: Vue, deep: false, user: false, lazy: true, sync: false, …
}
}
*/
}
if (!(key in vm)) {
// 如果computed 属性key,不在vm中,
// 定义计算属性 并且 把计算属性的key和计算函数 添加到vm上
defineComputed(vm, key, userDef);
} else {
// 警告:计算属性在data或者prop已经定义,不能重复重复定义
if (key in vm.$data) {
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
}
}
}
}
3-7-1. defineComputed
作用:
定义计算属性 并且 把属性的数据 添加到对象监听中
源码:
function initComputed(vm, computed) {
for (var key in computed) {
var userDef = computed[key]; // 获取计算函数
if (!(key in vm)) {
// 如果computed 属性key,不在vm中,
// 定义计算属性 并且 把计算属性的key和计算函数 添加到vm上
defineComputed(vm, key, userDef);
}
}
}
function defineComputed(
target, //目标
key, //key
userDef //值
) {
var shouldCache = !isServerRendering(); //如果不是node服务器 是浏览器
if (typeof userDef === 'function') { //属性的值如果是个函数
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key) //如果不是node服务器 是浏览器 创建计算属性 获取值 收集 dep 依赖
: userDef; //node 服务器取值 直接调用该函数
sharedPropertyDefinition.set = noop; //赋值一个空函数
} else {
sharedPropertyDefinition.get = userDef.get ?//如果userDef.get 存在
(shouldCache && userDef.cache !== false ? //缓存
createComputedGetter(key) : //创建计算属性 获取值 收集 dep 依赖
userDef.get
) :
noop; //如果userDef.get 不存在给一个空的函数
sharedPropertyDefinition.set = userDef.set //如果userDef.set 存在
? userDef.set
: noop;
}
if ("development" !== 'production' &&
sharedPropertyDefinition.set === noop) { //如果设置值等于一个空函数则警告
sharedPropertyDefinition.set = function () {
warn(
("Computed property \"" + key + "\" was assigned to but it has no setter."),
this
);
};
}
//添加对象监听
Object.defineProperty(target, key, sharedPropertyDefinition);
}
var sharedPropertyDefinition = { //共享属性定义
enumerable: true,
configurable: true,
get: noop,
set: noop
};
3-7-2. createComputedGetter
作用:
创建计算属性 获取值 收集 dep 依赖
源码:
function createComputedGetter(key) {
// 用户取值的时候会调用此方法
return function computedGetter() {
// Watcher 实例化之后的对象
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
//dirty为true会去进行求值,这儿的dirty起到了缓存的作用
//this.value 获取值 this.getter
watcher.evaluate();
}
if (Dep.target) {
//为Watcher 添加 为Watcher.newDeps.push(dep); 一个dep对象
//循环deps 收集 newDeps dep 当newDeps 数据被清空的时候重新收集依赖
watcher.depend();
}
//返回值
return watcher.value
}
}
}
Watcher.prototype.evaluate = function evaluate() {
this.value = this.get(); //获取值
this.dirty = false; // 懒惰者标志 标志已经获取过一次值
};
3-8. initWatch
作用:
遍历watch,如果属性值是数组,则遍历属性值(watch配置项),创建watch
源码:
if (opts.watch && opts.watch !== nativeWatch) {
//初始化Watch
initWatch(vm, opts.watch);
}
//初始化Watch监听
function initWatch(
vm,
watch // 例:{ money: {deep: true, immediate: true, handler: ƒ} }
) {
debugger
//循环watch对象
for (var key in watch) {
var handler = watch[key]; //获取单个watch配置项
//如果他是数组handler
if (Array.isArray(handler)) {
//循环数组 创建 监听
for (var i = 0; i < handler.length; i++) {
createWatcher(
vm, //vm 是 vue对象
key, //key
handler[i]//函数或者对象
);
}
} else {
//循环数组 创建 监听
createWatcher(
vm, // vm 是 vue对象
key, // 例:"money"
handler // 例:{deep: true, immediate: true, handler: ƒ}
);
}
}
}
3-8-1. createWatcher
作用:
遍历watch,如果属性值是数组,则遍历属性值(watch配置项),创建watch
执行流程:
- watch的配置项handler如果是个对象(常用格式下),获取配置项的执行函数handler(注意区别前面的hangler)
- watch的配置项handler如果是字符串,直接在vm获取对应执行函数,
- 最后返回vm.$watch,兼容处理cb,根据immediate来是否立即执行watch函数,卸载观察者,解除监听
源码:
// 转义handler 并且为数据 创建 Watcher 观察者
function createWatcher(
vm, // vm对象
expOrFn, // 例:"money"
handler, // 例:vue组件中watch中money对应的对象,{deep: true, immediate: true, handler: ƒ}
options // 参数
) {
if (isPlainObject(handler)) { //判断是否是对象
options = handler;
handler = handler.handler; // handler.handler是函数
}
if (typeof handler === 'string') { //判断handler 是否是字符串 如果是 则是key
handler = vm[handler]; //取值 vm 就是Vue 最外层 中的函数
}
//转义handler 并且为数据 创建 Watcher 观察者
return vm.$watch(
expOrFn,// key 值 或者函数
handler, //函数
options //参数
)
/**
* vm.$watch执行后,会推入到vm.$watchs数组里面,例:
* [{
active: true,
cb: ƒ handler(newVal, oldVal),
deep: true,
depIds: Set(1) {3},
deps: [Dep],
dirty: false,
expression: "money",
getter: ƒ (obj),
id: 2,
lazy: false,
newDepIds: Set(0) {},
newDeps: [],
sync: false,
user: true,
value: 100,
vm: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
* }]
*/
}
3-8-2. $watch
作用:
兼容处理cb,根据immediate来是否立即执行watch函数,卸载观察者,解除监听
执行流程:
- 如果cb是一个对象,需要递归执行createWatcher(它又返回vm.$watch)
- 设置user标志为true,用户手动监听, 就是在 options 自定义的 watch
- 创建一个新的watcher
- 如果immediate属性值为true,立即回调触发函数
- 最后执行watcher.teardown(),卸载监听
关联文档:
我们写watch的时候,如果设置了 immediate:true,则watch一开始就会触发,那是因为下面的这段代码:if (options.immediate) { cb.call(vm, watcher.value); }
源码:
// new Vue之前执行
function stateMixin(Vue) {
...
Vue.prototype.$watch = function (
expOrFn, //watch的name,例:"money"
cb, // watch的执行函数,例:ƒ handler(newVal, oldVal)
options //watch的参数:例:{deep: true, immediate: true, handler: ƒ}
) {
debugger
var vm = this;
//判断是否是对象 如果是对象则递归 深层 监听 直到它不是一个对象的时候才会跳出递归
if (isPlainObject(cb)) {
// 转义handler 并且为数据 创建 Watcher 观察者
return createWatcher(
vm,
expOrFn,
cb,
options
)
}
options = options || {};
options.user = true; //用户手动监听, 就是在 options 自定义的 watch
var watcher = new Watcher(
vm,
expOrFn,
cb,
options
);
/**
* watch,例:
{
active: true,
cb: ƒ handler(newVal, oldVal),
deep: true,
depIds: Set(1) {3},
deps: [Dep],
dirty: false,
expression: "money",
getter: ƒ (obj),
id: 2,
lazy: false,
newDepIds: Set(0) {},
newDeps: [],
sync: false,
user: true,
value: 100,
vm: Vue {_uid: 0, _isVue: true, $options: {…},...},
__proto__: Object
}
*/
// immediate属性值为true,立即回调触发函数
if (options.immediate) {
cb.call(vm, watcher.value);
}
return function unwatchFn() { //卸载观察者,解除监听
//从所有依赖项的订阅方列表中删除self。
watcher.teardown();
}
};
}
3-8-3. Watcher
作用:
Watcher构造函数
执行流程:
源码:
var Watcher = function Watcher(
vm, //vm dom
expOrFn, //获取值的函数,或者是更新viwe试图函数
cb, //回调函数,回调值给回调函数
options, //参数
isRenderWatcher//是否渲染过得观察者
) {
this.vm = vm;
//是否是已经渲染过得观察者
if (isRenderWatcher) { //把当前 Watcher 对象赋值给 vm._watcher上
vm._watcher = this;
}
//把观察者添加到队列里面 当前Watcher添加到vue实例上
vm._watchers.push(this);
// options
if (options) { //如果有参数
this.deep = !!options.deep; //实际
this.user = !!options.user; //用户
this.lazy = !!options.lazy; //懒惰 ssr 渲染
this.sync = !!options.sync; //如果是同步
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb; //回调函数
this.id = ++uid$1; // uid for batching uid为批处理 监听者id
this.active = true; //激活
this.dirty = this.lazy; // for lazy watchers 对于懒惰的观察者
this.deps = []; // 观察者队列
this.newDeps = []; // 新的观察者队列
// 内容不可重复的数组对象
this.depIds = new _Set();
this.newDepIds = new _Set();
// 把函数变成字符串形式
this.expression = expOrFn.toString();
//getter的解析表达式
if (typeof expOrFn === 'function') {
//获取值的函数
this.getter = expOrFn;
} else {
//如果是keepAlive 组件则会走这里
//path 因该是路由地址
// if (bailRE.test(path)) { // 匹配上 返回 true var bailRE = /[^\w.$]/; //匹配不是 数字字母下划线 $符号 开头的为true
// return
// }
// //匹配不上 path在已点分割
// var segments = path.split('.');
// return function (obj) {
//
// for (var i = 0; i < segments.length; i++) {
// //如果有参数则返回真
// if (!obj) {
// return
// }
// //将对象中的一个key值 赋值给该对象 相当于 segments 以点拆分的数组做obj 的key
// obj = obj[segments[i]];
// }
// //否则返回一个对象
// return obj
// }
//匹配不是 数字字母下划线 $符号 开头的为true
this.getter = parsePath(expOrFn);
if (!this.getter) { //如果不存在 则给一个空的数组
this.getter = function () {
};
"development" !== 'production' && warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
this.value = this.lazy ? // lazy为真的的时候才能获取值 这个有是组件才为真
undefined :
this.get(); //计算getter,并重新收集依赖项。 获取值
};
3-8-4. Watcher.prototype.get
作用:
计算getter,并重新收集依赖项。 获取value值
执行流程:
源码:
Watcher.prototype.get = function get() {
//添加一个dep target
pushTarget(this);
var value;
var vm = this.vm;
try {
//获取值 如果报错 则执行catch
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
//“触摸”每个属性,以便它们都被跟踪为
//依赖深度观察
if (this.deep) {
// //如果val 有__ob__ 属性
// if (val.__ob__) {
// var depId = val.__ob__.dep.id;
// // seen 中是否含有depId 属性或者方法
// if (seen.has(depId)) {
// return
// }
// //如果没有则添加进去
// seen.add(depId);
// }
//为 seenObjects 深度收集val 中的key
traverse(value);
}
// 出盏一个pushTarget
popTarget();
//清理依赖项集合。
this.cleanupDeps();
}
//返回值
return value
};
3-8-5. traverse
源码:
function traverse(val) {
// 搜索seen 为seen添加depId
//seenObjects set对象
// 为 seenObjects 深度收集val 中的key
_traverse(val, seenObjects);
//清除对象 给对象置空
seenObjects.clear();
}
function _traverse(val, seen) {
// de_traverse
var i, keys;
//判断是否是数组
var isA = Array.isArray(val);
//isFrozen 方法判断一个对象是否被冻结。
//val 是否是被VNode 实例化
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
//如果val 有__ob__ 属性
if (val.__ob__) {
var depId = val.__ob__.dep.id;
// seen 中是否含有depId 属性或者方法
if (seen.has(depId)) {
return
}
// seen 是 seenObjects = new _Set(); add 就是set对象中的add方法,添加为一的值得key
//如果没有则添加进去
seen.add(depId);
}
//如果是数组
if (isA) {
i = val.length;
//则循环检查 回调递归
while (i--) {
_traverse(val[i], seen);
}
} else {
keys = Object.keys(val);
i = keys.length;
//如果是对象也循环递归检查
while (i--) {
_traverse(val[keys[i]], seen);
}
}
}
4. defineReactive
作用: 在object上定义一个响应式的属性,这个方法,就是把对象obj里的属性key变成一个getter/setter形式的响应式的属性 同时在getter的时候收集依赖,并在setter的时候触发依赖。
执行流程:
- 初始化了一个依赖的对象dep(例:{id: 3, subs: Array(0)}),对象中有空的观察者列表,用来把用到该属性的依赖都放在这里面。
- 如果传入传入对象的对应的key不能修改,就退出函数
- 如果该属性没有get或者有set,而且只传了2个参数,那就设置传入的val为传入的属性值
- 判断value 是否有__ob__ 实例化 dep对象,获取dep对象 为 value添加__ob__ 属性
- 通过Object.defineProperty设置属性变成了getter和setter的形式
源码:
Observer.prototype.walk = function walk(obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
};
function defineReactive(
obj, //对象 obj = {money: 100, num: 12, arryList: Array(1), __ob__: Observer}
key,//对象的key key = "money"
val, //监听的数据 返回的数据 val = undefined
customSetter, // 日志函数 shallow = undefined
shallow //是否要添加__ob__ 属性 shallow = undefined
) {
//初始化了一个依赖的对象,对象中有空的观察者列表,用来把用到该属性的依赖都放在这里面。
var dep = new Dep(); // dep = Dep {id: 3, subs: Array(0)}
/**
* getOwnPropertyDescriptor方法:
* 判断该属性key是否是自有属性,obj为传入的对象,例如:
* Object.getOwnPropertyDescriptor(obj, 'money'),结果如下:
* {value: 100, writable: true, enumerable: true, configurable: true}
* 通过configurable判断该属性能否修改,不能修改就不需要做响应式处理,
* 直接退出函数
*/
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// 获取已经实现的 getter /setter 方法
var getter = property && property.get;
var setter = property && property.set;
// 如果该属性没有get或者有set,而且只传了2个参数,那就设置val为传入的属性值
// arguments即defineReactive的入参,由例子可以看到,此时其它值为undefined
// 只有2个参数是有值的,因此arguments为2
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
debugger
/**
* 判断value 是否有__ob__ 实例化 dep对象,获取dep对象 为 value添加__ob__ 属性
* 递归把val添加到观察者中 返回 new Observer 实例化的对象
*/
var childOb = !shallow && observe(val);
debugger
console.log(Dep.target)
//属性变成了getter和setter的形式
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 该属性的值返回去,同时收集依赖,记录哪些地方用到了该属性。
/**
* getter即上面的property.get
* 判断属性中是否有get方法(如通过getOwnPropertyDescriptor判断'money'是否有get方法)
* 如有,则调用这个get方法
* 如果没有,直接取obj上对应的key的值
*/
//
var value = getter ? getter.call(obj) : val;
//Dep.target 全局变量指向的就是当前正在解析指令的Complie生成的 Watcher
// 会执行到 dep.addSub(Dep.target), 将 Watcher 添加到 Dep 对象的 Watcher 列表中
if (Dep.target) { //Dep.target 静态标志 标志了Dep添加了Watcher 实例化的对象
//添加一个dep
dep.depend(); // 实际执行function depend(),然后执行function addDep(dep)
if (childOb) { //如果子节点存在也添加一个dep
childOb.dep.depend();
if (Array.isArray(value)) { //判断是否是数组 如果是数组
dependArray(value); //则数组也添加dep
}
}
}
return value
},
set: function reactiveSetter(newVal) {
/**
* 新的值newVal,赋值给旧的val,如果有以前的setter,就用以前的setter,如果没有setter,就返回。
* 最后新的值添加__ob__属性,然后触发该属性的依赖,通知他们去变更
*/
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare 新旧值比较 如果是一样则不执行了*/
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare
* 不是生产环境的情况下
* */
if ("development" !== 'production' && customSetter) {
customSetter();
}
if (setter) {
//set 方法 设置新的值
setter.call(obj, newVal);
} else {
//新的值直接给他
val = newVal;
}
//observe 添加 观察者
childOb = !shallow && observe(newVal);
// 如果数据被重新赋值了, 调用 Dep 的 notify 方法, 通知所有的 Watcher
dep.notify();
}
});
}
实际上,在new Vue中执行初始化过程中,就已经执行了几次defineReactive了,如:initRender,defineReactive
defineReactive(
vm, // Vue实例
'$attrs',
parentData && parentData.attrs || emptyObject, // {}
function () {
!isUpdatingChildComponent && warn("$attrs is readonly.", vm);
},
true
);
defineReactive(
vm, '$listeners',
options._parentListeners || emptyObject,
function () {
!isUpdatingChildComponent && warn("$listeners is readonly.", vm);
},
true
);
它给Vue上挂载了$attrs,$listeners等属性。
我们分析下从observe走到defineReactive的流程:
observe()=>new Observer=>function Observer()=>this.walk()=> defineReactive(obj, keys[i])=>function defineReactive()
此时,执行defineReactive传入的参数为
function defineReactive(
obj, //对象 obj = {money: 100, num: 12, arryList: Array(1), __ob__: Observer}
key,//对象的key key = "money"
val, //监听的数据 返回的数据 val = undefined
customSetter, // 日志函数 shallow = undefined
shallow //是否要添加__ob__ 属性 shallow = undefined
)
可以看到shallow的值为空,所以执行到var childOb = !shallow && observe(val);,就会取后面observe(val)去执行。它又走进observe函数了,这个函数在上一节有说明。注意,面执行了val = obj[key],已经将val重置为100了。
function observe(
value, // 100
asRootData // undefined
){
// value 不是一个对象 或者 是实例化的VNode
if (!isObject(value) || value instanceof VNode) {
return
}
}
此时value不满足条件,直接退出,childOb还是undefined。
我们再回到defineReactive函数中,他会给传入的object对应的key设置get和set方法,属性变成了getter和setter的形式。
4-1. Dep
作用: 一个 dep 对应一个 obj.key
在读取响应式数据时,负责收集依赖,每个 dep(或者说 obj.key)依赖的 watcher 有哪些
在响应式数据更新时,负责通知 dep 中那些 watcher 去执行 update 方法
执行流程:
- Dep默认有id(确保每个Dep都有唯一的ID)和subs(用来存放Watcher对象的数组)
- 并向原型挂载了几个方法:
- addSub:向subs数组添加依赖Watcher
- removeSub:移除依赖
- depend:设置某个Watcher的依赖
- notify:通知所有Watcher对象更新视图
- addSub:向subs数组添加依赖Watcher
源码:
var uid = 0;
// Dep是订阅者Watcher对应的数据依赖,它将watch全部装入自身的subs数组中
var Dep = function Dep() {
//每个Dep都有唯一的ID
this.id = uid++;
/* 用来存放Watcher对象的数组 */
this.subs = [];
};
//向subs数组添加依赖Watcher
Dep.prototype.addSub = function addSub(sub) {
this.subs.push(sub);
};
//移除依赖
Dep.prototype.removeSub = function removeSub(sub) {
remove(this.subs, sub);
};
//设置某个Watcher的依赖
//这里添加了Dep.target是否存在的判断,目的是判断是不是Watcher的构造函数调用
//也就是说判断他是Watcher的this.get调用的,而不是普通调用
Dep.prototype.depend = function depend() {
//添加一个dep target 是Watcher dep就是dep对象
if (Dep.target) {
//像指令添加依赖项
Dep.target.addDep(this);
}
};
/* 通知所有Watcher对象更新视图 */
Dep.prototype.notify = function notify() {
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
Dep.target = null;
// 目标堆栈
var targetStack = [];
// 将dep存在的观察者放入targetStack统一管理,然后更新Dep.target为传入的观察者
function pushTarget(_target) {
//target 是Watcher dep就是dep对象
if (Dep.target) {
targetStack.push(Dep.target);
}
Dep.target = _target;
}
4-2. queueWatcher
将观察者推进 queue 队列中 过滤重复的 id 除非是*刷新队列时推送
Watcher.prototype.update = function update() {
// Watcher的lazy默认是false
if (this.lazy) {
this.dirty = true; // 懒惰者标志
} else if (this.sync) { //如果是同步
//更新数据
this.run();
} else {
//如果是多个观察者
queueWatcher(this); //队列中的观察者
}
};
function queueWatcher(watcher) {
debugger
var id = watcher.id;
//has是记录观察者的id的对象 例:{ 2: true }
// 如果传进来的watcher的id在has中不存在,就需要在has上加上
// 如果已经存在了,就说明当前的watch是重复的,直接退出函数
if (has[id] == null) {
has[id] = true;
// 默认是false,进入flushSchedulerQueue 函数等待标志
if (!flushing) {
queue.push(watcher); //把观察者添加到队列中,queue为记录观察者队列的数组
} else {
// 如果已经刷新,则根据监视程序的id拼接它
// 如果已经通过了它的id,那么将立即运行next。
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
//根据id大小拼接插入在数组的哪个位置
queue.splice(i + 1, 0, watcher);
}
// queue the flush
if (!waiting) {
waiting = true;
//为callbacks 收集队列cb 函数 并且根据 pending 状态是否要触发callbacks 队列函数
debugger
nextTick(
flushSchedulerQueue//更新观察者 运行观察者watcher.run() 函数 并且 调用组件更新和激活的钩子
);
}
}
}
4-3. nextTick
为callbacks 收集队列cb 函数 并且根据 pending 状态是否要触发callbacks 队列函数
function nextTick(cb, ctx) {
debugger
//cb 回调函数
//ctx this的指向
var _resolve;
//添加一个回调函数到队列里面去
callbacks.push(function () {
if (cb) {
//如果cb存在 并且是一个函数就执行
try {
cb.call(ctx);
} catch (e) {
//如果不是函数则报错
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
//_resolve 如果存在则执行
_resolve(ctx);
}
});
if (!pending) {
pending = true;
//执行异步宏任务
if (useMacroTask) {
macroTimerFunc(); //异步触发 或者 实现观察者 触发 callbacks 队列中的函数
} else {
microTimerFunc(); //异步触发 或者 实现观察者 触发 callbacks 队列中的函数
}
}
if (!cb && typeof Promise !== 'undefined') {
//如果回调函数不存在 则声明一个Promise 函数
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
4-4. macroTimerFunc
在浏览器环境中,常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate。 而常见的 micro task 有 MutationObsever 和 Promise.then。 Vue中对于 macro task 的实现:
- 优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge 才支持的特性
- 不支持的话再去检测是否支持原生的MessageChannel,
- 如果也不支持的话就会降级为 setTimeout 0。
var microTimerFunc; //微计时器功能
var macroTimerFunc; //宏计时器功能
var useMacroTask = false; //使用宏任务
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
//函数表达式赋值给macroTimerFunc
macroTimerFunc = function () {
debugger
setImmediate(flushCallbacks);
};
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
/**
* 创建了一个通信的管道,这个管道有两个端口,每个端口都可以通过postMessage发送数据,
* 而一个端口只要绑定了onmessage回调方法,就可以接收从另一个端口传过来的数据。
*/
var channel = new MessageChannel();
var port = channel.port2;
//设置端口1 的接受函数为flushCallbacks
channel.port1.onmessage = flushCallbacks;
//端口2推送信息给端口1,发送的内容为1
macroTimerFunc = function () {
debugger
port.postMessage(1);
};
} else {
macroTimerFunc = function () {
debugger
setTimeout(flushCallbacks, 0);
};
}
源码提问
1. 请说一下响应式数据的原理
vue2——核心点:Object.defineProperty —— 修改每一个属性
- 默认Vue在初始化数据时,会给data中的属性使用Object.defineProperty,在获取和设置的进行拦截,重新定义所有属性。
- 当页面取到对应属性时,会进行依赖收集(收集当前组件的watcher)。
- 如果属性发生变化会通知相关依赖进行更新操作。
依赖收集、派发更新的作用:
如果没有这项操作,每个数据更新就会去渲染页面,极大的消耗性能。加了这项操作,去监听相关数据的改变,添加到队列里,当所有改变完事儿之后,一起进行渲染。
vue3——核心点:proxy(代理)—— 直接处理对象
解决了vue2中的处理对象递归、处理数组麻烦的问题
原理:
响应式原理图
2.vue中是如何检测数组变化的?
使用了函数劫持的方式,重写了数组方法。
Vue将data中的数组,进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次进行监控。
Object.create(Array.prototype)复制Array原型链为新的对象;- 拦截了数组的 7 个方法的执行,并使其可响应,7 个方法分别为:
push,pop,shift,unshift,splice,sort,reverse; - 当数组调用到这 7 个方法的时候,执行
ob.dep.notify()进行派发通知Watcher更新;
原理:
具体请看【3-6-1 def】
3.为何vue采用异步渲染?
vue是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件重新渲染。为了性能考虑,vue会在本轮数据更新后,再去异步更新视图。
原理:
- dep.notify: 【3-6-1 def】
- subs[i].update(): 【4-1 Dep】
- queueWatcher: 【4-2 queueWatcher】
- nextTick: 【4-3 nextTick】
4. nextTick实现原理?
nextTick主要是使用了宏任务和微任务,定义了一个异步方法。多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列,所以nextTick就是异步方法。
5. vue中computed的特点
默认computed也是一个watcher,具备缓存,只有当依赖的属性发生变化才会更新视图。
- initComputed: 【3-7 initComputed】
- defineComputed: 【3-7-1. defineComputed】
- createComputedGetter: 【3-7-2 createComputedGetter】
- nextTick: 【4-3 nextTick】
6. watch中的deep:true是如何实现的?
当用户指定了watch中的deep属性为true时,如果当时监控的属性是数组类型,会对对象中的每一项进行求值,此时会将当前watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新。 内部原理就是递归,耗费性能
- initWatch
- createWatcher
- Vue.prototype.$watch
- new Watcher: this.get();
- Watcher.prototype.get:if (this.deep) {//深度监听 traverse(value) }
- traverse
7. 组件中的data为什么是个函数?
同一个组件被复用多次,会创建多个实例。
这些实例用的是同一个构造函数,如果data是一个对象的话,所有组件共享了同一个对象。
为了保证组件的数据独立性,要求每个组件都必须通过data函数返回一个对象作为组件的状态。