SegmentFault vue解析最新的文章
2020-04-19T19:36:51+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
vue-数据监听与依赖收集
https://segmentfault.com/a/1190000022422120
2020-04-19T19:36:51+08:00
2020-04-19T19:36:51+08:00
hfhan
https://segmentfault.com/u/hfhan
8
<p>文章围绕下面demo进行分析</p>
<pre><code><div id="app">
<span>{{a.b}} {{c}} {{d}}</span>
</div>
<script>
var app = new Vue({
el: "#app",
data: function(){
return {
a: {
b: 1
},
c: 1
}
},
watch:{
'a.b': function(){
console.log(22)
}
},
computed:{
d: function (){
return this.c
}
}
});
</script></code></pre>
<h5>数据监听:从initData开始</h5>
<p>vue监听data数据的步骤可以概括为下面几步:</p>
<pre><code>1、使用initData初始化data数据
2、将data挂载到vm._data中(后面会将_data赋值给$data,这里的data如果是函数,就是返回的数据)
3、通过observe监听数据
4、如果数据是一个非object类型的数据(typeof == object,且不为null),终止程序
5、observe中使用new Observer生成ob
6、Observer中使用`this.dep = new Dep()`挂载dep
7、Observer中将__ob__指向this,所以可以使用`__ob__.dep`找到dep
8、遍历属性,使用defineReactive监听属性
9、defineReactive中生成`var dep = new Dep();`,这个dep是一个闭包
10、defineReactive中使用observe监听属性所代表的值,也就是步骤3,至此循环递归
</code></pre>
<h5>依赖收集</h5>
<p>三种watcher</p>
<pre><code>1、normal-watcher(watch中的数据,通过initWatch生成) 记录在_watchers中(所有的watch都会存放在这里)
2、computed-watcher(computed) 记录在_computedWatchers中
3、render-watcher 就是vm._watcher
</code></pre>
<h6>normal-watcher:运行initWatch</h6>
<p>我们在组件钩子函数watch 中定义的,都属于这种类型,即只要监听的属性改变了,都会触发定义好的回调函数,这类watch的expression是计算属性中的属性名。</p>
<p>在初始化watch的时候(initWatch),会调用vm.$watch函数,vm.$watch函数会直接使用Watcher构建观察者对象。watch中属性的值作为watcher.cb存在,在观察者update的时候,在watcher.run函数中执行。watch中属性的key会进行parsePath处理,并用parsePath返回的函数,获取watch的初始值value。</p>
<p>比如把a.b解析为监听a中的b属性,这样会先寻找a,也就是触发a的get。</p>
<pre><code>1、赋值cb为a.b的值,赋值expression为a.b
2、使用parsePath解析expOrFn,即a.b,并将返回的函数赋值给该watcher实例的getter即this.getter
3、运行this.get获取a.b的值,进行依赖收集,this指向a.b的 watcher实例
4、运行pushTarget将Dep.target指向该watcher实例
5、运行this.getter,会先获取a,运行defineReactive中的get
6、运行dep.depend(此时的dep指的是data.a的dep,在闭包中),进而运行Dep.target.addDep,将data.a的dep追加进该watcher实例中,并将该watcher实例追加进data.a的dep.subs中,因为a具有__ob__,所以会运行a.__ob__.dep.depend,将a的dep追加进该watcher实例中,并将该watcher实例追加进a的dep.subs中
7、利用获取到的a去获取属性b
8、运行dep.depend(此时的dep指的是a.b的dep,在闭包中),进而运行Dep.target.addDep,将a.b的dep追加进该watcher实例中,并将该watcher实例追加进a.b的dep.subs中,因为b不具有__ob__,所以不会继续追加
9、到这里就获取到了a.b的值1,并将这个值赋值给该watcher实例的value
</code></pre>
<h6>computed-watcher:运行initComputed</h6>
<p>我们在组件钩子函数computed中定义的,都属于这种类型,每一个 computed 属性,最后都会生成一个对应的 watcher 对象,但是这类 watcher 有个特点:当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。这类watch的expression是计算属性中的属性名。</p>
<p>在初始化computed的时候(initComputed),会先生成watch实例,然后监测数据是否已经存在data或props上,如果存在则抛出警告,否则调用defineComputed函数,监听数据,为组件中的属性绑定getter及setter。</p>
<p><strong>注意:</strong>computed中的属性是直接绑定在vm上的,所以如果写a.d,那就是属性名是a.d,而不是a对象的属性d。</p>
<pre><code>1、执行initComputed,遍历computed生成watch实例,并挂载到vm._computedWatchers上
(1)赋值cb为空函数,赋值expression为expOrFn(d的值,函数或对象的get)的字符串形式,赋值this.getter为expOrFn
(2)默认的computed设置lazy为true,不运行this.get获取值,所以到这里watch实例就生成了。
2、执行defineComputed函数,如果d的值是函数,或者d的cache属性不是false,那么会使用createComputedGetter函数生成computedGetter函数,作为d的getter函数,如果cache设置为false,不经过createComputedGetter封装,每次获取都会运行get,而d的setter就是他的set或者空函数(默认)
3、当获取d的值时(比如渲染,此时Dep.target为渲染watcher),会运行computedGetter函数
4、根据watcher.dirty的值决定是否运行watcher.evaluate重新获取属性值,这是懒计算的关键。dirty的值默认为true,在依赖改变时或update时变为true,在evaluate后变为false
(1)watcher.evaluate中运行this.get获取d的值,进行依赖收集,this指向d的 watcher实例
(2)运行pushTarget将Dep.target指向d的watcher实例
(3)运行this.getter,会先获取this.c的值,运行defineReactive中的get
(4)运行dep.depend(此时的dep指的是data.c的dep,在闭包中),进而运行Dep.target.addDep,将data.c的dep追加进d的watcher实例中,并将d的watcher实例追加进data.c的dep.subs中
(5)d的watcher出栈,将Dep.target重新设置为渲染watcher
5、运行watcher.depend,遍历watcher.deps(这里主要是data.c的dep),将他们与渲染watcher互相关联
</code></pre>
<p><strong>注意:</strong>computed中的数据不经过Observer监听,所以不存在dep</p>
<h6>render-watcher:运行mountComponent挂载组件</h6>
<p>每一个组件都会有一个 render-watcher, 当 data/computed 中的属性改变的时候,会调用该 render-watcher 来更新组件的视图。这类watch的expression是 <code>function () {vm._update(vm._render(), hydrating);}</code>。</p>
<pre><code>1、生成updateComponent函数,
2、实例化一个渲染watcher,把updateComponent当作expOrFn参数传入
3、赋值cb为空函数,赋值expression为updateComponent的字符串形式,赋值this.getter为expOrFn
4、运行this.get,进行依赖收集
5、运行pushTarget将Dep.target指向该渲染watcher实例
6、运行this.getter,即updateComponent函数
7、用render函数生成vnode,并将其作为第一个参数,传入_update
8、render函数中会对用到的变量进行getter操作,并完成依赖收集
(1)获取a,将data.a的dep追加进该渲染watcher实例中,并将该渲染watcher实例追加进data.a的dep.subs中
(2)获取a.b,将a.b的dep追加进该渲染watcher实例中,并将该渲染watcher实例追加进a.b的dep.subs中
(3)获取c,将data.c的dep追加进该渲染watcher实例中,并将该渲染watcher实例追加进data.c的dep.subs中
(4)获取d,运行d的getter函数computedGetter(详情看上面computed-watcher中的步骤3-5)
9、完成依赖收集后,变量修改,会触发dep.notify,通知渲染watcher实例的update操作,重新进行渲染</code></pre>
vue数据初始化--initState
https://segmentfault.com/a/1190000018821652
2019-04-10T16:38:47+08:00
2019-04-10T16:38:47+08:00
hfhan
https://segmentfault.com/u/hfhan
14
<h2>数据初始化</h2>
<p>Vue 实例在建立的时候会运行一系列的初始化操作,而在这些初始化操作里面,和数据绑定关联最大的是 initState。</p>
<p>首先,来看一下他的代码:</p>
<pre><code>function initState(vm) {
vm._watchers = [];
var opts = vm.$options;
if(opts.props) {
initProps(vm, opts.props); //初始化props
}
if(opts.methods) {
initMethods(vm, opts.methods); //初始化methods
}
if(opts.data) {
initData(vm); //初始化data
} else {
observe(vm._data = {}, true /* asRootData */ );
}
if(opts.computed) {
initComputed(vm, opts.computed); //初始化computed
}
if(opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch); //初始化watch
}
}</code></pre>
<p>在这么多的数据的初始化中,props、methods和data是比较简单的(所以我就不详细介绍了☺),而computed 和 watch则相对较难,逻辑较复杂,所以我下面主要讲下computed 和 watch(以下代码部分为简化后的)。</p>
<p>initState里面主要是对vue实例中的 props, methods, data, computed 和 watch 数据进行初始化。</p>
<p>在初始化props的时候(initProps),会遍历props中的每个属性,然后进行类型验证,数据监测等(提供为props属性赋值就抛出警告的钩子函数)。</p>
<p>在初始化methods的时候(initMethods),主要是监测methods中的方法名是否合法。</p>
<p>在初始化data的时候(initData),会运行 observe 函数深度遍历数据中的每一个属性,进行数据劫持。</p>
<p>在初始化computed的时候(initComputed),会监测数据是否已经存在data或props上,如果存在则抛出警告,否则调用defineComputed函数,监听数据,为组件中的属性绑定getter及setter。如果computed中属性的值是一个函数,则默认为属性的getter函数。此外属性的值还可以是一个对象,他只有三个有效字段set、get和cache,分别表示属性的setter、getter和是否启用缓存,其中get是必须的,cache默认为true。</p>
<pre><code>function initComputed(vm, computed) {
var watchers = vm._computedWatchers = Object.create(null);
for(var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
//创建一个计算属性 watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
if(!(key in vm)) {
//如果定义的计算属性不在组件实例上,对属性进行数据劫持
//defineComputed 很重要,下面我们再说
defineComputed(vm, key, userDef);
} else {
//如果定义的计算属性在data和props有,抛出警告
}
}
}</code></pre>
<p>在初始化watch的时候(initWatch),会调用vm.$watch函数为watch中的属性绑定setter回调(如果组件中没有该属性则不能成功监听,属性必须存在于props、data或computed中)。如果watch中属性的值是一个函数,则默认为属性的setter回调函数,如果属性的值是一个数组,则遍历数组中的内容,分别为属性绑定回调,此外属性的值还可以是一个对象,此时,对象中的handler字段代表setter回调函数,immediate代表是否立即先去执行里面的handler方法,deep代表是否深度监听。</p>
<p>vm.$watch函数会直接使用Watcher构建观察者对象。watch中属性的值作为watcher.cb存在,在观察者update的时候,在watcher.run函数中执行。想了解这一过程可以看我上一篇的<a href="https://segmentfault.com/a/1190000018795293">vue响应式系统--observe、watcher、dep</a> 中关于Watcher的介绍。</p>
<pre><code>function initWatch(vm, watch) {
//遍历watch,为每一个属性创建侦听器
for(var key in watch) {
var handler = watch[key];
//如果属性值是一个数组,则遍历数组,为属性创建多个侦听器
//createWatcher函数中封装了vm.$watch,会在vm.$watch中创建侦听器
if(Array.isArray(handler)) {
for(var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
//为属性创建侦听器
createWatcher(vm, key, handler);
}
}
}
function createWatcher(vm, expOrFn, handler, options) {
//如果属性值是一个对象,则取对象的handler属性作为回调
if(isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
//如果属性值是一个字符串,则从组件实例上寻找
if(typeof handler === 'string') {
handler = vm[handler];
}
//为属性创建侦听器
return vm.$watch(expOrFn, handler, options)
} </code></pre>
<h2>computed</h2>
<p>computed本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值</p>
<p>下面将围绕这一句话来做解释。</p>
<p>上面代码中提到过,当计算属性中的数据存在与data和props中时,会被警告,也就是这种做法是错误的。所以一般的,我们都会直接在计算属性中声明数据。还是那个代码片段中,如果定义的计算属性不在组件实例上,会运行defineComputed函数对数据进行数据劫持。下面我们来看下defineComputed函数中做了什么。</p>
<pre><code>function defineComputed(target, key, userDef) {
//是不是服务端渲染
var shouldCache = !isServerRendering();
//如果我们把计算属性的值写成一个函数,这时函数默认为计算属性的get
if(typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache ?
//如果不是服务端渲染,则默认使用缓存,设置get为createComputedGetter创建的缓存函数
createComputedGetter(key) :
//否则不使用缓存,直接设置get为userDef这个我们定义的函数
userDef;
//设置set为空函数
sharedPropertyDefinition.set = noop;
} else {
//如果我们把计算属性的值写成一个对象,对象中可能包含set、get和cache三个字段
sharedPropertyDefinition.get = userDef.get ?
shouldCache && userDef.cache !== false ?
//如果我们传入了get字段,且不是服务端渲染,且cache不为false,设置get为createComputedGetter创建的缓存函数
createComputedGetter(key) :
//如果我们传入了get字段,但是是服务端渲染或者cache设为了false,设置get为userDef这个我们定义的函数
userDef.get :
//如果没有传入get字段,设置get为空函数
noop;
//设置set为我们传入的传入set字段或空函数
sharedPropertyDefinition.set = userDef.set ?
userDef.set :
noop;
}
//虽然这里可以get、set都可以设置为空函数
//但是在项目中,get为空函数对数据取值会报错,set为空函数对数据赋值会报错
//而computed主要作用就是计算取值的,所以get字段是必须的
//数据劫持
Object.defineProperty(target, key, sharedPropertyDefinition);
}</code></pre>
<p>在上一篇的 <a href="https://segmentfault.com/a/1190000018795293">vue响应式系统--observe、watcher、dep</a> 中,我有关于Watcher的介绍中提到,计算属性 watcher实例化的时候,会把options.lazy设置为true,这里是计算属性惰性求值,且可缓存的关键,当然前提是cache不为false。</p>
<p>cache不为false,会调用createComputedGetter函数创建计算属性的getter函数computedGetter,</p>
<p>先来看一段代码</p>
<pre><code>function createComputedGetter(key) {
return function computedGetter() {
var watcher = this._computedWatchers && this._computedWatchers[key];
if(watcher) {
if(watcher.dirty) {
//watcher.evaluate中更新watcher的值,并把watcher.dirty设置为false
//这样等下次依赖更新的时候才会把watcher.dirty设置为true,然后进行取值的时候才会再次运行这个函数
watcher.evaluate();
}
//依赖追踪
if(Dep.target) {
watcher.depend();
}
//返回watcher的值
return watcher.value
}
}
}
//对于计算属性,当取值计算属性时,发现计算属性的watcher的dirty是true
//说明数据不是最新的了,需要重新计算,这里就是重新计算计算属性的值。
Watcher.prototype.evaluate = function evaluate() {
this.value = this.get();
this.dirty = false;
};
//当一个依赖改变的时候,通知它update
Watcher.prototype.update = function update() {
//三种watcher,只有计算属性 watcher的lazy设置了true,表示启用惰性求值
if(this.lazy) {
this.dirty = true;
} else if(this.sync) {
//标记为同步计算的直接运行run,三大类型暂无,所以基本会走下面的queueWatcher
this.run();
} else {
//将watcher推入观察者队列中,下一个tick时调用。
//也就是数据变化不是立即就去更新的,而是异步批量去更新的
queueWatcher(this);
}
};</code></pre>
<p>当options.lazy设置为true之后(仅计算属性watcher的options.lazy设置为true),每次依赖更新,都不会主动触发run函数,而是把watcher.dirty设置为true。这样,当对计算属性进行取值时,就会运行computedGetter函数,computedGetter函数中有一个关于watcher.dirty的判断,当watcher.dirty为true时会运行watcher.evaluate进行值的更新,并把watcher.dirty设置为false,这样就完成了惰性求值的过程。后面只要依赖不更新,就不会运行update,就不会把watcher.dirty为true,那么再次取值的时候就不会运行watcher.evaluate进行值的更新,从而达到了缓存的效果。</p>
<p>综上,我们了解到cache不为false的时候,计算属性都是惰性求值且具有缓存性的,而cache默认是true,我们也大多使用这个默认值,所以我们说<code> computed本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值</code>。</p>
vue响应式系统--observe、watcher、dep
https://segmentfault.com/a/1190000018795293
2019-04-08T17:34:31+08:00
2019-04-08T17:34:31+08:00
hfhan
https://segmentfault.com/u/hfhan
32
<h2>Vue的响应式系统</h2>
<p>Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的JavaScript 对象,而当你修改它们时,视图会进行更新,这使得状态管理非常简单直接,我们可以只关注数据本身,而不用手动处理数据到视图的渲染,避免了繁琐的 DOM 操作,提高了开发效率。</p>
<p>vue 的响应式系统依赖于三个重要的类:Dep 类、Watcher 类、Observer 类,然后使用发布订阅模式的思想将他们揉合在一起(不了解发布订阅模式的可以看我之前的文章<a href="https://segmentfault.com/a/1190000018706349">发布订阅模式与观察者模式</a>)。</p>
<p><img src="/img/bVbq1E6" alt="Vue的响应式系统" title="Vue的响应式系统"></p>
<h2>Observer</h2>
<p>Observe扮演的角色是发布者,他的主要作用是调用defineReactive函数,在defineReactive函数中使用Object.defineProperty 方法对对象的每一个子属性进行数据劫持/监听。</p>
<p><strong>部分代码展示</strong></p>
<p>defineReactive函数,Observe的核心,劫持数据,在getter中向Dep(调度中心)添加观察者,在setter中通知观察者更新。</p>
<pre><code>function defineReactive(obj, key, val, customSetter, shallow){
//监听属性key
//关键点:在闭包中声明一个Dep实例,用于保存watcher实例
var dep = new Dep();
var getter = property && property.get;
var setter = property && property.set;
if(!getter && arguments.length === 2) {
val = obj[key];
}
//执行observe,监听属性key所代表的值val的子属性
var childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
//获取值
var value = getter ? getter.call(obj) : val;
//依赖收集:如果当前有活动的Dep.target(观察者--watcher实例)
if(Dep.target) {
//将dep放进当前观察者的deps中,同时,将该观察者放入dep中,等待变更通知
dep.depend();
if(childOb) {
//为子属性进行依赖收集
//其实就是将同一个watcher观察者实例放进了两个dep中
//一个是正在本身闭包中的dep,另一个是子属性的dep
childOb.dep.depend();
}
}
return value
},
set: function reactiveSetter(newVal) {
//获取value
var value = getter ? getter.call(obj) : val;
if(newVal === value || (newVal !== newVal && value !== value)) {
return
}
if(setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
//新的值需要重新进行observe,保证数据响应式
childOb = observe(newVal);
//关键点:遍历dep.subs,通知所有的观察者
dep.notify();
}
});
}</code></pre>
<h2>Dep</h2>
<p>Dep 扮演的角色是调度中心/订阅器,主要的作用就是收集观察者Watcher和通知观察者目标更新。每个属性拥有自己的消息订阅器dep,用于存放所有订阅了该属性的观察者对象,当数据发生改变时,会遍历观察者列表(dep.subs),通知所有的watch,让订阅者执行自己的update逻辑。</p>
<p><strong>部分代码展示</strong></p>
<p>Dep的设计比较简单,就是收集依赖,通知观察者</p>
<pre><code>//Dep构造函数
var Dep = function Dep() {
this.id = uid++;
this.subs = [];
};
//向dep的观察者列表subs添加观察者
Dep.prototype.addSub = function addSub(sub) {
this.subs.push(sub);
};
//从dep的观察者列表subs移除观察者
Dep.prototype.removeSub = function removeSub(sub) {
remove(this.subs, sub);
};
Dep.prototype.depend = function depend() {
//依赖收集:如果当前有观察者,将该dep放进当前观察者的deps中
//同时,将当前观察者放入观察者列表subs中
if(Dep.target) {
Dep.target.addDep(this);
}
};
Dep.prototype.notify = function notify() {
// 循环处理,运行每个观察者的update接口
var subs = this.subs.slice();
for(var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
//Dep.target是观察者,这是全局唯一的,因为在任何时候只有一个观察者被处理。
Dep.target = null;
//待处理的观察者队列
var targetStack = [];
function pushTarget(_target) {
//如果当前有正在处理的观察者,将他压入待处理队列
if(Dep.target) {
targetStack.push(Dep.target);
}
//将Dep.target指向需要处理的观察者
Dep.target = _target;
}
function popTarget() {
//将Dep.target指向栈顶的观察者,并将他移除队列
Dep.target = targetStack.pop();
}</code></pre>
<h2>Watcher</h2>
<p>Watcher扮演的角色是订阅者/观察者,他的主要作用是为观察属性提供回调函数以及收集依赖(如计算属性computed,vue会把该属性所依赖数据的dep添加到自身的deps中),当被观察的值发生变化时,会接收到来自dep的通知,从而触发回调函数。,</p>
<p><strong>部分代码展示</strong></p>
<p>Watcher类的实现比较复杂,因为他的实例分为渲染 watcher(render-watcher)、计算属性 watcher(computed-watcher)、侦听器 watcher(normal-watcher)三种, <br>这三个实例分别是在三个函数中构建的:mountComponent 、initComputed和Vue.prototype.$watch。</p>
<p><strong>normal-watcher:</strong>我们在组件钩子函数watch 中定义的,都属于这种类型,即只要监听的属性改变了,都会触发定义好的回调函数,这类watch的expression是计算属性中的属性名。</p>
<p><strong>computed-watcher:</strong>我们在组件钩子函数computed中定义的,都属于这种类型,每一个 computed 属性,最后都会生成一个对应的 watcher 对象,但是这类 watcher 有个特点:当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。这类watch的expression是我们写的回调函数的字符串形式。</p>
<p><strong>render-watcher:</strong>每一个组件都会有一个 render-watcher, 当 data/computed 中的属性改变的时候,会调用该 render-watcher 来更新组件的视图。这类watch的expression是<code> function () {vm._update(vm._render(), hydrating);}</code>。</p>
<p>除了功能上的区别,这三种 watcher 也有固定的<strong>执行顺序</strong>,分别是:<code>computed-render -> normal-watcher -> render-watcher</code>。</p>
<p>这样安排是有原因的,这样就能尽可能的保证,在更新组件视图的时候,computed 属性已经是最新值了,如果 render-watcher 排在 computed-render 前面,就会导致页面更新的时候 computed 值为旧数据。</p>
<p>这里我们只看其中一部分代码</p>
<pre><code>function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
if(isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if(options) {
this.deep = !!options.deep; //是否启用深度监听
this.user = !!options.user; //主要用于错误处理,侦听器 watcher的 user为true,其他基本为false
this.lazy = !!options.lazy; //惰性求职,当属于计算属性watcher时为true
this.sync = !!options.sync; //标记为同步计算,三大类型暂无
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
//初始化各种属性和option
//观察者的回调
//除了侦听器 watcher外,其他大多为空函数
this.cb = cb;
this.id = ++uid$1; // uid for batching
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();
// 解析expOrFn,赋值给this.getter
// 当是渲染watcher时,expOrFn是updateComponent,即重新渲染执行render(_update)
// 当是计算watcher时,expOrFn是计算属性的计算方法
// 当是侦听器watcher时,expOrFn是watch属性的名字,this.cb就是watch的handler属性
//对于渲染watcher和计算watcher来说,expOrFn的值是一个函数,可以直接设置getter
//对于侦听器watcher来说,expOrFn是watch属性的名字,会使用parsePath函数解析路径,获取组件上该属性的值(运行getter)
//依赖(订阅目标)更新,执行update,会进行取值操作,运行watcher.getter,也就是expOrFn函数
if(typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
this.value = this.lazy ? undefined : this.get();
};
//取值操作
Watcher.prototype.get = function get() {
//Dep.target设置为该观察者
pushTarget(this);
var vm = this.vm;
//取值
var value = this.getter.call(vm, vm);
//移除该观察者
popTarget();
return value
};
Watcher.prototype.addDep = function addDep(dep) {
var id = dep.id;
if(!this.newDepIds.has(id)) {
//为观察者的deps添加依赖dep
this.newDepIds.add(id);
this.newDeps.push(dep);
if(!this.depIds.has(id)) {
//为dep添加该观察者
dep.addSub(this);
}
}
};
//当一个依赖改变的时候,通知它update
Watcher.prototype.update = function update() {
//三种watcher,只有计算属性 watcher的lazy设置了true,表示启用惰性求值
if(this.lazy) {
this.dirty = true;
} else if(this.sync) {
//标记为同步计算的直接运行run,三大类型暂无,所以基本会走下面的queueWatcher
this.run();
} else {
//将watcher推入观察者队列中,下一个tick时调用。
//也就是数据变化不是立即就去更新的,而是异步批量去更新的
queueWatcher(this);
}
};
//update执行后,运行回调cb
Watcher.prototype.run = function run() {
if(this.active) {
var value = this.get();
if(
value !== this.value ||
isObject(value) ||
this.deep
) {
var oldValue = this.value;
this.value = value;
//运行 cb 函数,这个函数就是之前传入的watch中的handler回调函数
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);
}
}
}
};
//对于计算属性,当取值计算属性时,发现计算属性的watcher的dirty是true
//说明数据不是最新的了,需要重新计算,这里就是重新计算计算属性的值。
Watcher.prototype.evaluate = function evaluate() {
this.value = this.get();
this.dirty = false;
};
//收集依赖
Watcher.prototype.depend = function depend() {
var this$1 = this;
var i = this.deps.length;
while(i--) {
this$1.deps[i].depend();
}
};</code></pre>
<h2>总结</h2>
<p>Observe是对数据进行监听,Dep是一个订阅器,每一个被监听的数据都有一个Dep实例,Dep实例里面存放了N多个订阅者(观察者)对象watcher。</p>
<p>被监听的数据进行取值操作时(getter),如果存在Dep.target(某一个观察者),则说明这个观察者是依赖该数据的(如计算属性中,计算某一属性会用到其他已经被监听的数据,就说该属性依赖于其他属性,会对其他属性进行取值),就会把这个观察者添加到该数据的订阅器subs里面,留待后面数据变更时通知(会先通过观察者id判断订阅器中是否已经存在该观察者),同时该观察者也会把该数据的订阅器dep添加到自身deps中,方便其他地方使用。</p>
<p>被监听的数据进行赋值操作时(setter)时,就会触发dep.notify(),循环该数据订阅器中的观察者,进行更新操作。</p>
vue自定义指令--directive
https://segmentfault.com/a/1190000018767046
2019-04-04T17:21:24+08:00
2019-04-04T17:21:24+08:00
hfhan
https://segmentfault.com/u/hfhan
45
<p>Vue中内置了很多的指令,如v-model、v-show、v-html等,但是有时候这些指令并不能满足我们,或者说我们想为元素附加一些特别的功能,这时候,我们就需要用到vue中一个很强大的功能了—自定义指令。</p>
<p>在开始之前,我们需要明确一点,自定义指令解决的问题或者说使用场景是对普通 DOM 元素进行底层操作,所以我们不能盲目的胡乱的使用自定义指令。</p>
<h2>如何声明自定义指令?</h2>
<p>就像vue中有全局组件和局部组件一样,他也分全局自定义指令和局部指令。</p>
<pre><code>let Opt = {
bind:function(el,binding,vnode){ },
inserted:function(el,binding,vnode){ },
update:function(el,binding,vnode){ },
componentUpdated:function(el,binding,vnode){ },
unbind:function(el,binding,vnode){ },
}</code></pre>
<p>对于全局自定义指令的创建,我们需要使用<code> Vue.directive</code>接口</p>
<pre><code>Vue.directive('demo', Opt)</code></pre>
<p>对于局部组件,我们需要在组件的钩子函数directives中进行声明</p>
<pre><code>Directives: {
Demo: Opt
}</code></pre>
<p>Vue中的指令可以简写,上面Opt是一个对象,包含了5个钩子函数,我们可以根据需要只写其中几个函数。如果你想在 bind 和 update 时触发相同行为,而不关心其它的钩子,那么你可以将Opt改为一个函数。</p>
<pre><code>let Opt = function(el,binding,vnode){ }</code></pre>
<h2>如何使用自定义指令?</h2>
<p>对于自定义指令的使用是非常简单的,如果你对vue有一定了解的话。</p>
<p>我们可以像<code>v-text=”’test’”</code>一样,把我们需要传递的值放在‘=’号后面传递过去。</p>
<p>我们可以像<code>v-on:click=”handClick” </code>一样,为指令传递参数’click’。</p>
<p>我们可以像<code>v-on:click.stop=”handClick” </code>一样,为指令添加一个修饰符。</p>
<p>我们也可以像<code>v-once</code>一样,什么都不传递。</p>
<p>每个指令,他的底层封装肯定都不一样,所以我们应该先了解他的功能和用法,再去使用它。</p>
<h2>自定义指令的 钩子函数</h2>
<p>上面我们也介绍了,自定义指令一共有5个钩子函数,他们分别是:bind、inserted、update、componentUpdate和unbind。</p>
<p>对于这几个钩子函数,了解的可以自行跳过,不了解的我也不介绍,自己去官网看,没有比官网上说的更详细的了:<a href="https://link.segmentfault.com/?enc=AiBeFu1SJpWAKhZbpphNoQ%3D%3D.W1j2P2GeaCid6C7UvZf6gMYB7iY3TlI9UD385igyniWe9OjeQiwBErii1KpA2fF3uaqTq9E2QFX2ZhVAu2OEvhtHsp9k2HK4Ohsl7I3VlI6kRUPToyBCs%2B21DmI9dnai" rel="nofollow">钩子函数</a></p>
<h2>项目中的bug</h2>
<p>在项目中,我们自定义一个全局指令<code>my-click</code>:</p>
<pre><code>Vue.directive('my-click',{
bind:function(el, binding, vnode, oldVnode){
el.addEventListener('click',function(){
console.log(el, binding.value)
})
}
})</code></pre>
<p>同时,有一个数组<code>arr:[1,2,3,4,5,6]</code>,我们遍历数组,生成dom元素,并为元素绑定指令:</p>
<pre><code><ul>
<li v-for="(item,index) in arr" :key="index" v-my-click="item">{{item}}</li>
</ul></code></pre>
<p><img src="/img/bVbqUhK?w=304&h=147" alt="click" title="click"></p>
<p>可以看到,当我们点击元素的时候,成功打印了元素,以及传递过去的数据。</p>
<p>可是,当我们把最后一个元素动态的改为8之后(6 --> 8),点击元素,元素是对的,可是打印的数据却仍然是6.</p>
<p><img src="/img/bVbqUhT?w=281&h=146" alt="click" title="click"></p>
<p>或者,当我们删除了第一个元素之后,点击元素</p>
<p><img src="/img/bVbqUhY?w=301&h=127" alt="click" title="click"></p>
<p>这是为什么呢????带着这个疑问,我去看了看源码。在进行下面的源码分析之前,先来说结论:</p>
<p>组件进行初始化的时候,也就是第一次运行指令的时候,会执行bind钩子函数,我们所传入的参数(binding)都进入到了这里,并形成了一个闭包。</p>
<p>当我们进行数据更新的时候,vue虚拟dom不会销毁这个组件(如果说删除某个数据,会从后往前销毁组件,前面的总是最后销毁),而是进行更新(根据数据改变),如果指令有update钩子会运行这个钩子函数,但是对于元素在bind中绑定的事件,在update中没有处理的话,他不会消失(依然引用初始化时形成的闭包中的数据),所以当我们更改数据再次点击元素后,看到的数据还是原数据。</p>
<h2>源码分析</h2>
<p>函数执行顺序:createElm/initComponent/patchVnode --> invokeCreateHooks (cbs.create) --> updateDirectives --> _update</p>
<p>在createElm方法和initComponent方法和更新节点patchVnode时会调用invokeCreateHooks方法,它会去遍历cbs.create中钩子函数进行执行,cbs.create中的钩子函数如下图所示共8个。我们所需要看的就是updateDirectives这个函数,这个函数会继续调用_update函数,vue中的指令操作就都在这个_update函数中了。</p>
<p><img src="/img/bVbqUih?w=296&h=183" alt="_update" title="_update"></p>
<p>下面我们就来详细看下这个_update函数。</p>
<pre><code>function _update(oldVnode, vnode) {
//判断旧节点是不是空节点,是的话表示新建/初始化组件
var isCreate = oldVnode === emptyNode;
//判断新节点是不是空节点,是的话表示销毁组件
var isDestroy = vnode === emptyNode;
//获取旧节点上的所有自定义指令
var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);
//获取新节点上的所有自定义指令
var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);
//保存inserted钩子函数
var dirsWithInsert = [];
//保存componentUpdated钩子函数
var dirsWithPostpatch = [];
var key, oldDir, dir;
//这里先说下callHook$1函数的作用
//callHook$1有五个参数,第一个参数是指令对象,第二个参数是钩子函数名称,第三个参数新节点,
//第四个参数是旧节点,第五个参数是是否为注销组件,默认为undefined,只在组件注销时使用
//在这个函数里,会根据我们传递的钩子函数名称,运行我们自定义组件时,所声明的钩子函数,
//遍历所有新节点上的自定义指令
for(key in newDirs) {
oldDir = oldDirs[key];
dir = newDirs[key];
//如果旧节点中没有对应的指令,一般都是初始化的时候运行
if(!oldDir) {
//对该节点执行指令的bind钩子函数
callHook$1(dir, 'bind', vnode, oldVnode);
//dir.def是我们所定义的指令的五个钩子函数的集合
//如果我们的指令中存在inserted钩子函数
if(dir.def && dir.def.inserted) {
//把该指令存入dirsWithInsert中
dirsWithInsert.push(dir);
}
} else {
//如果旧节点中有对应的指令,一般都是组件更新的时候运行
//那么这里进行更新操作,运行update钩子(如果有的话)
//将旧值保存下来,供其他地方使用(仅在 update 和 componentUpdated 钩子中可用)
dir.oldValue = oldDir.value;
//对该节点执行指令的update钩子函数
callHook$1(dir, 'update', vnode, oldVnode);
//dir.def是我们所定义的指令的五个钩子函数的集合
//如果我们的指令中存在componentUpdated钩子函数
if(dir.def && dir.def.componentUpdated) {
//把该指令存入dirsWithPostpatch中
dirsWithPostpatch.push(dir);
}
}
}
//我们先来简单讲下mergeVNodeHook的作用
//mergeVNodeHook有三个参数,第一个参数是vnode节点,第二个参数是key值,第三个参数是回函数
//mergeVNodeHook会先用一个函数wrappedHook重新封装回调,在这个函数里运行回调函数
//如果该节点没有这个key属性,会新增一个key属性,值为一个数组,数组中包含上面说的函数wrappedHook
//如果该节点有这个key属性,会把函数wrappedHook追加到数组中
//如果dirsWithInsert的长度不为0,也就是在初始化的时候,且至少有一个指令中有inserted钩子函数
if(dirsWithInsert.length) {
//封装回调函数
var callInsert = function() {
//遍历所有指令的inserted钩子
for(var i = 0; i < dirsWithInsert.length; i++) {
//对节点执行指令的inserted钩子函数
callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);
}
};
if(isCreate) {
//如果是新建/初始化组件,使用mergeVNodeHook绑定insert属性,等待后面调用。
mergeVNodeHook(vnode, 'insert', callInsert);
} else {
//如果是更新组件,直接调用函数,遍历inserted钩子
callInsert();
}
}
//如果dirsWithPostpatch的长度不为0,也就是在组件更新的时候,且至少有一个指令中有componentUpdated钩子函数
if(dirsWithPostpatch.length) {
//使用mergeVNodeHook绑定postpatch属性,等待后面子组建全部更新完成调用。
mergeVNodeHook(vnode, 'postpatch', function() {
for(var i = 0; i < dirsWithPostpatch.length; i++) {
//对节点执行指令的componentUpdated钩子函数
callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
}
});
}
//如果不是新建/初始化组件,也就是说是更新组件
if(!isCreate) {
//遍历旧节点中的指令
for(key in oldDirs) {
//如果新节点中没有这个指令(旧节点中有,新节点没有)
if(!newDirs[key]) {
//从旧节点中解绑,isDestroy表示组件是不是注销了
//对旧节点执行指令的unbind钩子函数
callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
}
}
}
}</code></pre>
<p>callHook$1函数</p>
<pre><code>function callHook$1(dir, hook, vnode, oldVnode, isDestroy) {
var fn = dir.def && dir.def[hook];
if(fn) {
try {
fn(vnode.elm, dir, vnode, oldVnode, isDestroy);
} catch(e) {
handleError(e, vnode.context, ("directive " + (dir.name) + " " + hook + " hook"));
}
}
}</code></pre>
<h2>解决</h2>
<p>看过了源码,我们再回到上面的bug,我们应该如何去解决呢?</p>
<p><strong>1、事件解绑,重新绑定</strong></p>
<p>我们在bind钩子中绑定了事件,当数据更新后,会运行update钩子,所以我们可以在update中先解绑再重新进行绑定。因为bind和update中的内容差不多,所以我们可以把bind和update合并为同一个函数,在用自定义指令的简写方法写成下面的代码:</p>
<pre><code>Vue.directive('my-click', function(el, binding, vnode, oldVnode){
//点击事件的回调挂在在元素myClick属性上
el.myClick && el.removeEventListener('click', el.myClick);
el.addEventListener('click', el.myClick = function(){
console.log(el, binding.value)
})
})</code></pre>
<p><img src="/img/bVbqUi2?w=297&h=135" alt="click" title="click"></p>
<p>可以看到,数据已经变成我们想要的数据了。</p>
<p><strong>2、把binding挂在到元素上,更新数据后更新binding</strong></p>
<p>我们已经知道了,造成问题的根本原因是初始化运行bind钩子的时候为元素绑定事件,事件内获取的数据是初始化的时候传递过来的数据,因为形成了闭包,那么我们不使用能引起闭包的数据,把数据存到某一个地方,然后去更新这个数据。</p>
<pre><code>Vue.directive('my-click',{
bind: function(el, binding, vnode, oldVnode){
el.binding = binding
el.addEventListener('click', function(){
var binding = this.binding
console.log(this, binding.value)
})
},
update: function(el, binding, vnode, oldVnode){
el.binding = binding
}
})</code></pre>
<p>这样也能达到我们想要的效果。</p>
<p><strong>3、更新父元素</strong></p>
<p>如果我们为父元素ul绑定一个变化的key值,这样,当数据变更的时候就会更新父元素,从而重新创建子元素,达到重新绑定指令的效果。</p>
<pre><code><ul :key="Date.now()">
<li v-for="(item,index) in arr" :key="index" v-my-click="item">{{item}}</li>
</ul></code></pre>
<p>这样也能达到我们想要的效果。</p>
记一次思否问答的问题思考:Vue为什么不能检测数组变动
https://segmentfault.com/a/1190000015783546
2018-07-26T23:06:53+08:00
2018-07-26T23:06:53+08:00
hfhan
https://segmentfault.com/u/hfhan
237
<p>问题来源:<a href="https://segmentfault.com/q/1010000015780995">https://segmentfault.com/q/10...</a></p>
<p>问题描述:Vue检测数据的变动是通过Object.defineProperty实现的,所以无法监听数组的添加操作是可以理解的,因为是在构造函数中就已经为所有属性做了这个检测绑定操作。</p>
<p>但是官方的原文:由于 JavaScript 的限制, Vue 不能检测以下变动的数组:</p>
<blockquote>当你利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue<br>当你修改数组的长度时,例如: vm.items.length = newLength</blockquote>
<p>这句话是什么意思?我测试了下Object.defineProperty是可以通过索引属性来设置属性的访问器属性的,那为何做不了监听?</p>
<p>有些论坛上的人说因为数组长度是可变的,即使长度为5,但是未必有索引4,我就想问问这个答案哪里来的,修改length,新增的元素会被添加到最后,它的值为undefined,通过索引一样可以获取他们的值,怎么就叫做“未必有索引4”了呢?</p>
<p>既然知道数组的长度为何不能遍历所有元素并通过索引这个属性全部添加set和get不就可以同时更新视图了吗?</p>
<p>如果非要说的话,考虑到性能的问题,假设元素内容只有4个有意义的值,但是长度确实1000,我们不可能为1000个元素做检测操作。但是官方说的由于JS限制,我想知道这个限制是什么内容?各位大大帮我解决下这个问题,感谢万分</p>
<hr>
<hr>
<p>面对这个问题,我想说的是,首先,长度为1000,但只有4个元素的数组并不一定会影响性能,<del>因为js中对数据的遍历除了for循环还有forEach、map、filter、some等,除了for循环外(for,for...of),其他的遍历都是对键值的遍历,也就是除了那四个元素外的空位并不会进行遍历(执行回调),所以也就不会造成性能损耗,</del>因为循环体中没有操作的话,所带来的性能影响可以忽略不计,下面是长度为10000,但只有两个元素的数组分别使用for及forEach遍历的结果:</p>
<pre><code>var arr = [1]; arr[10000] = 1
function a(){
console.time()
for(var i = 0;i<arr.length;i++)console.log(1)
console.timeEnd()
}
a(); //default: 567.1669921875ms
a(); //default: 566.2451171875ms
function b(){
console.time()
arr.forEach(item=>{console.log(2)})
console.timeEnd()
}
b(); //default: 0.81982421875ms
b(); //default: 0.434814453125ms</code></pre>
<p>可以看到结果非常明显,不过,如果for循环中不做操作的话两者速度差不多</p>
<p>其次,我要说的是,我也不知道这个限制是什么 (⇀‸↼‶) ╮( •́ω•̀ )╭</p>
<p>Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。数组的索引也是属性,所以我们是可以监听到数组元素的变化的</p>
<pre><code>var arr = [1,2,3,4]
arr.forEach((item,index)=>{
Object.defineProperty(arr,index,{
set:function(val){
console.log('set')
item = val
},
get:function(val){
console.log('get')
return item
}
})
})
arr[1]; // get 2
arr[1] = 1; // set 1</code></pre>
<p>但是我们新增一个元素,就不会触发监听事件,因为这个新属性我们并没有监听,删除一个属性也是。</p>
<p>再回到题主的问题,既然数组是可以被监听的,那为什么vue不能检测<code>vm.items[indexOfItem] = newValue</code>导致的数组元素改变呢,哪怕这个下标所对应的元素是存在的,且被监听了的?</p>
<p>为了搞清楚这个问题,我用vue的源码测试了下,下面是vue对数据监测的源码:<br><img src="/img/bVbeoaB?w=812&h=621" alt="Observer" title="Observer"></p>
<p>可以看到,当数据是数组时,会停止对数据属性的监测,我们修改一下源码:<br><img src="/img/bVbeoaN?w=853&h=508" alt="修改Observer" title="修改Observer"></p>
<p>使数据为数组时,依然监测其属性,然后在defineReactive函数中的get,set打印一些东西,方便我们知道调用了get以及set。这里加了个简单判断,只看数组元素的get,set<br><img src="/img/bVbeoaU?w=1010&h=634" alt="修改defineReactive" title="修改defineReactive"></p>
<p>然后写了一个简单案例,主要测试使用<code>vm.items[indexOfItem] = newValue</code>改变数组元素能不能被监测到,并响应式的渲染页面<br><img src="/img/bVbeoa2?w=1157&h=453" alt="简单案例" title="简单案例"></p>
<p>运行页面<br><img src="/img/bVbeoa5?w=545&h=177" alt="数组测试" title="数组测试"></p>
<p>可以看到,运行了6次get,我们数组长度为3,也就是说数组被遍历了两遍。两遍不多,页面渲染一次,可能多次触发一个数据的监听事件,哪怕这个数据只用了一次,具体的需要看尤大代码怎么写的。就拿这个来说,当监听的数据为数组时,会运行dependArray函数(代码在上面图中get的实现里),这个函数里对数组进行了遍历取值操作,所以会多3遍get,这里主要是vue对data中arr数组的监听触发了dependArray函数。</p>
<p>当我们点击其中一个元素的时候,比如我点击的是3<br><img src="/img/bVbeobc?w=522&h=228" alt="点击3" title="点击3"></p>
<p>可以看到会先运行一次set,然后数据更新,重新渲染页面,数组又是被遍历了两遍。</p>
<p>但是!!!数组确实变成响应式的了,也就是说js语法功能并不会限制数组的监测。</p>
<p>这里我们是用长度为3的数组测试的,当我把数组长度增加到9时<br><img src="/img/bVbeobe?w=542&h=158" alt="新数组测试" title="新数组测试"></p>
<p>可以看到,运行了18次get,数组还是被遍历了两遍,点击某个元素同理,渲染的时候也是被遍历两次。<br><img src="/img/bVbeobn?w=547&h=244" alt="新数组测试" title="新数组测试"></p>
<p>有了上面的实验,我的结论是数组在vue中是可以实现响应式更新的,但是不明白尤大是出于什么考虑,没有加入这一功能,希望有知道的大佬们不吝赐教</p>
<hr>
<h2>2018-07-27补充</h2>
<p>github上提问了尤大<br><img src="/img/bVbepk3?w=807&h=795" alt="github提问" title="github提问"></p>