You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// 处理 props 对象,为 props 对象的每个属性设置响应式,并将其代理到 vm 实例上functioninitProps(vm: Component,propsOptions: Object){constpropsData=vm.$options.propsData||{}constprops=vm._props={}// 缓存 props 的每个 key,性能优化// cache prop keys so that future props updates can iterate using Array// instead of dynamic object key enumeration.constkeys=vm.$options._propKeys=[]constisRoot=!vm.$parent// root instance props should be convertedif(!isRoot){toggleObserving(false)}// 遍历 props 对象for(constkeyinpropsOptions){// 缓存 keykeys.push(key)// 获取 props[key] 的默认值constvalue=validateProp(key,propsOptions,propsData,vm)// 为 props 的每个 key 是设置数据响应式defineReactive(props,key,value)// static props are already proxied on the component's prototype// during Vue.extend(). We only need to proxy props defined at// instantiation here.if(!(keyinvm)){// 代理 key 到 vm 对象上proxy(vm,`_props`,key)}}toggleObserving(true)}
/** * 做了以下三件事,其实最关键的就是第三件事情 * 1、校验 methoss[key],必须是一个函数 * 2、判重 * methods 中的 key 不能和 props 中的 key 相同 * methos 中的 key 与 Vue 实例上已有的方法重叠,一般是一些内置方法,比如以 $ 和 _ 开头的方法 * 3、将 methods[key] 放到 vm 实例上,得到 vm[key] = methods[key] */functioninitMethods(vm: Component,methods: Object){// 获取 props 配置项constprops=vm.$options.props// 遍历 methods 对象for(constkeyinmethods){if(process.env.NODE_ENV!=='production'){if(typeofmethods[key]!=='function'){warn(`Method "${key}" has type "${typeofmethods[key]}" in the component definition. `+`Did you reference the function correctly?`,vm)}if(props&&hasOwn(props,key)){warn(`Method "${key}" has already been defined as a prop.`,vm)}if((keyinvm)&&isReserved(key)){warn(`Method "${key}" conflicts with an existing Vue instance method. `+`Avoid defining component methods that start with _ or $.`)}}vm[key]=typeofmethods[key]!=='function' ? noop : bind(methods[key],vm)}}
initData
src/core/instance/state.js
/** * 做了三件事 * 1、判重处理,data 对象上的属性不能和 props、methods 对象上的属性相同 * 2、代理 data 对象上的属性到 vm 实例 * 3、为 data 对象的上数据设置响应式 */functioninitData(vm: Component){// 得到 data 对象letdata=vm.$options.datadata=vm._data=typeofdata==='function'
? getData(data,vm)
: data||{}if(!isPlainObject(data)){data={}process.env.NODE_ENV!=='production'&&warn('data functions should return an object:\n'+'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',vm)}/** * 两件事 * 1、判重处理,data 对象上的属性不能和 props、methods 对象上的属性相同 * 2、代理 data 对象上的属性到 vm 实例 */constkeys=Object.keys(data)constprops=vm.$options.propsconstmethods=vm.$options.methodsleti=keys.lengthwhile(i--){constkey=keys[i]if(process.env.NODE_ENV!=='production'){if(methods&&hasOwn(methods,key)){warn(`Method "${key}" has already been defined as a data property.`,vm)}}if(props&&hasOwn(props,key)){process.env.NODE_ENV!=='production'&&warn(`The data property "${key}" is already declared as a prop. `+`Use prop default value instead.`,vm)}elseif(!isReserved(key)){proxy(vm,`_data`,key)}}// 为 data 对象上的数据设置响应式observe(data,true/* asRootData */)}exportfunctiongetData(data: Function,vm: Component): any{// #7573 disable dep collection when invoking data getterspushTarget()try{returndata.call(vm,vm)}catch(e){handleError(e,vm,`data()`)return{}}finally{popTarget()}}
initComputed
/src/core/instance/state.js
constcomputedWatcherOptions={lazy: true}/** * 三件事: * 1、为 computed[key] 创建 watcher 实例,默认是懒执行 * 2、代理 computed[key] 到 vm 实例 * 3、判重,computed 中的 key 不能和 data、props 中的属性重复 * @param {*} computed = { * key1: function() { return xx }, * key2: { * get: function() { return xx }, * set: function(val) {} * } * } */functioninitComputed(vm: Component,computed: Object){// $flow-disable-lineconstwatchers=vm._computedWatchers=Object.create(null)// computed properties are just getters during SSRconstisSSR=isServerRendering()// 遍历 computed 对象for(constkeyincomputed){// 获取 key 对应的值,即 getter 函数constuserDef=computed[key]constgetter=typeofuserDef==='function' ? userDef : userDef.getif(process.env.NODE_ENV!=='production'&&getter==null){warn(`Getter is missing for computed property "${key}".`,vm)}if(!isSSR){// 为 computed 属性创建 watcher 实例watchers[key]=newWatcher(vm,getter||noop,noop,// 配置项,computed 默认是懒执行computedWatcherOptions)}if(!(keyinvm)){// 代理 computed 对象中的属性到 vm 实例// 这样就可以使用 vm.computedKey 访问计算属性了defineComputed(vm,key,userDef)}elseif(process.env.NODE_ENV!=='production'){// 非生产环境有一个判重处理,computed 对象中的属性不能和 data、props 中的属性相同if(keyinvm.$data){warn(`The computed property "${key}" is already defined in data.`,vm)}elseif(vm.$options.props&&keyinvm.$options.props){warn(`The computed property "${key}" is already defined as a prop.`,vm)}}}}/** * 代理 computed 对象中的 key 到 target(vm)上 */exportfunctiondefineComputed(target: any,key: string,userDef: Object|Function){constshouldCache=!isServerRendering()// 构造属性描述符(get、set)if(typeofuserDef==='function'){sharedPropertyDefinition.get=shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)sharedPropertyDefinition.set=noop}else{sharedPropertyDefinition.get=userDef.get
? shouldCache&&userDef.cache!==false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noopsharedPropertyDefinition.set=userDef.set||noop}if(process.env.NODE_ENV!=='production'&&sharedPropertyDefinition.set===noop){sharedPropertyDefinition.set=function(){warn(`Computed property "${key}" was assigned to but it has no setter.`,this)}}// 拦截对 target.key 的访问和设置Object.defineProperty(target,key,sharedPropertyDefinition)}/** * @returns 返回一个函数,这个函数在访问 vm.computedProperty 时会被执行,然后返回执行结果 */functioncreateComputedGetter(key){// computed 属性值会缓存的原理也是在这里结合 watcher.dirty、watcher.evalaute、watcher.update 实现的returnfunctioncomputedGetter(){// 得到当前 key 对应的 watcherconstwatcher=this._computedWatchers&&this._computedWatchers[key]if(watcher){// 计算 key 对应的值,通过执行 computed.key 的回调函数来得到// watcher.dirty 属性就是大家常说的 computed 计算结果会缓存的原理// <template>// <div>{{ computedProperty }}</div>// <div>{{ computedProperty }}</div>// </template>// 像这种情况下,在页面的一次渲染中,两个 dom 中的 computedProperty 只有第一个// 会执行 computed.computedProperty 的回调函数计算实际的值,// 即执行 watcher.evalaute,而第二个就不走计算过程了,// 因为上一次执行 watcher.evalute 时把 watcher.dirty 置为了 false,// 待页面更新后,wathcer.update 方法会将 watcher.dirty 重新置为 true,// 供下次页面更新时重新计算 computed.key 的结果if(watcher.dirty){watcher.evaluate()}if(Dep.target){watcher.depend()}returnwatcher.value}}}/** * 功能同 createComputedGetter 一样 */functioncreateGetterInvoker(fn){returnfunctioncomputedGetter(){returnfn.call(this,this)}}
importtypeWatcherfrom'./watcher'import{remove}from'../util/index'importconfigfrom'../config'letuid=0/** * 一个 dep 对应一个 obj.key * 在读取响应式数据时,负责收集依赖,每个 dep(或者说 obj.key)依赖的 watcher 有哪些 * 在响应式数据更新时,负责通知 dep 中那些 watcher 去执行 update 方法 */exportdefaultclassDep{statictarget: ?Watcher;id: number;subs: Array<Watcher>;constructor(){this.id=uid++this.subs=[]}// 在 dep 中添加 watcheraddSub(sub: Watcher){this.subs.push(sub)}removeSub(sub: Watcher){remove(this.subs,sub)}// 像 watcher 中添加 depdepend(){if(Dep.target){Dep.target.addDep(this)}}/** * 通知 dep 中的所有 watcher,执行 watcher.update() 方法 */notify(){// stabilize the subscriber list firstconstsubs=this.subs.slice()if(process.env.NODE_ENV!=='production'&&!config.async){// subs aren't sorted in scheduler if not running async// we need to sort them now to make sure they fire in correct// ordersubs.sort((a,b)=>a.id-b.id)}// 遍历 dep 中存储的 watcher,执行 watcher.update()for(leti=0,l=subs.length;i<l;i++){subs[i].update()}}}/** * 当前正在执行的 watcher,同一时间只会有一个 watcher 在执行 * Dep.target = 当前正在执行的 watcher * 通过调用 pushTarget 方法完成赋值,调用 popTarget 方法完成重置(null) */Dep.target=nullconsttargetStack=[]// 在需要进行依赖收集的时候调用,设置 Dep.target = watcherexportfunctionpushTarget(target: ?Watcher){targetStack.push(target)Dep.target=target}// 依赖收集结束调用,设置 Dep.target = nullexportfunctionpopTarget(){targetStack.pop()Dep.target=targetStack[targetStack.length-1]}
Watcher
/src/core/observer/watcher.js
/** * 一个组件一个 watcher(渲染 watcher)或者一个表达式一个 watcher(用户watcher) * 当数据更新时 watcher 会被触发,访问 this.computedProperty 时也会触发 watcher */exportdefaultclassWatcher{vm: Component;expression: string;cb: Function;id: number;deep: boolean;user: boolean;lazy: boolean;sync: boolean;dirty: boolean;active: boolean;deps: Array<Dep>;newDeps: Array<Dep>;depIds: SimpleSet;newDepIds: SimpleSet;before: ?Function;getter: Function;value: any;constructor(vm: Component,expOrFn: string|Function,cb: Function,options?: ?Object,isRenderWatcher?: boolean){this.vm=vmif(isRenderWatcher){vm._watcher=this}vm._watchers.push(this)// optionsif(options){this.deep=!!options.deepthis.user=!!options.userthis.lazy=!!options.lazythis.sync=!!options.syncthis.before=options.before}else{this.deep=this.user=this.lazy=this.sync=false}this.cb=cbthis.id=++uid// uid for batchingthis.active=truethis.dirty=this.lazy// for lazy watchersthis.deps=[]this.newDeps=[]this.depIds=newSet()this.newDepIds=newSet()this.expression=process.env.NODE_ENV!=='production'
? expOrFn.toString()
: ''// parse expression for getterif(typeofexpOrFn==='function'){this.getter=expOrFn}else{// this.getter = function() { return this.xx }// 在 this.get 中执行 this.getter 时会触发依赖收集// 待后续 this.xx 更新时就会触发响应式this.getter=parsePath(expOrFn)if(!this.getter){this.getter=noopprocess.env.NODE_ENV!=='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
? undefined
: this.get()}/** * 执行 this.getter,并重新收集依赖 * this.getter 是实例化 watcher 时传递的第二个参数,一个函数或者字符串,比如:updateComponent 或者 parsePath 返回的读取 this.xx 属性值的函数 * 为什么要重新收集依赖? * 因为触发更新说明有响应式数据被更新了,但是被更新的数据虽然已经经过 observe 观察了,但是却没有进行依赖收集, * 所以,在更新页面时,会重新执行一次 render 函数,执行期间会触发读取操作,这时候进行依赖收集 */get(){// 打开 Dep.target,Dep.target = thispushTarget(this)// value 为回调函数执行的结果letvalueconstvm=this.vmtry{// 执行回调函数,比如 updateComponent,进入 patch 阶段value=this.getter.call(vm,vm)}catch(e){if(this.user){handleError(e,vm,`getter for watcher "${this.expression}"`)}else{throwe}}finally{// "touch" every property so they are all tracked as// dependencies for deep watchingif(this.deep){traverse(value)}// 关闭 Dep.target,Dep.target = nullpopTarget()this.cleanupDeps()}returnvalue}/** * Add a dependency to this directive. * 两件事: * 1、添加 dep 给自己(watcher) * 2、添加自己(watcher)到 dep */addDep(dep: Dep){// 判重,如果 dep 已经存在则不重复添加constid=dep.idif(!this.newDepIds.has(id)){// 缓存 dep.id,用于判重this.newDepIds.add(id)// 添加 depthis.newDeps.push(dep)// 避免在 dep 中重复添加 watcher,this.depIds 的设置在 cleanupDeps 方法中if(!this.depIds.has(id)){// 添加 watcher 自己到 depdep.addSub(this)}}}/** * Clean up for dependency collection. */cleanupDeps(){leti=this.deps.lengthwhile(i--){constdep=this.deps[i]if(!this.newDepIds.has(dep.id)){dep.removeSub(this)}}lettmp=this.depIdsthis.depIds=this.newDepIdsthis.newDepIds=tmpthis.newDepIds.clear()tmp=this.depsthis.deps=this.newDepsthis.newDeps=tmpthis.newDeps.length=0}/** * 根据 watcher 配置项,决定接下来怎么走,一般是 queueWatcher */update(){/* istanbul ignore else */if(this.lazy){// 懒执行时走这里,比如 computed// 将 dirty 置为 true,可以让 computedGetter 执行时重新计算 computed 回调函数的执行结果this.dirty=true}elseif(this.sync){// 同步执行,在使用 vm.$watch 或者 watch 选项时可以传一个 sync 选项,// 当为 true 时在数据更新时该 watcher 就不走异步更新队列,直接执行 this.run // 方法进行更新// 这个属性在官方文档中没有出现this.run()}else{// 更新时一般都这里,将 watcher 放入 watcher 队列queueWatcher(this)}}/** * 由 刷新队列函数 flushSchedulerQueue 调用,完成如下几件事: * 1、执行实例化 watcher 传递的第二个参数,updateComponent 或者 获取 this.xx 的一个函数(parsePath 返回的函数) * 2、更新旧值为新值 * 3、执行实例化 watcher 时传递的第三个参数,比如用户 watcher 的回调函数 */run(){if(this.active){// 调用 this.get 方法constvalue=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.isObject(value)||this.deep){// 更新旧值为新值constoldValue=this.valuethis.value=valueif(this.user){// 如果是用户 watcher,则执行用户传递的第三个参数 —— 回调函数,参数为 val 和 oldValtry{this.cb.call(this.vm,value,oldValue)}catch(e){handleError(e,this.vm,`callback for watcher "${this.expression}"`)}}else{// 渲染 watcher,this.cb = noop,一个空函数this.cb.call(this.vm,value,oldValue)}}}}/** * 懒执行的 watcher 会调用该方法 * 比如:computed,在获取 vm.computedProperty 的值时会调用该方法 * 然后执行 this.get,即 watcher 的回调函数,得到返回值 * this.dirty 被置为 false,作用是页面在本次渲染中只会一次 computed.key 的回调函数, * 这也是大家常说的 computed 和 methods 区别之一是 computed 有缓存的原理所在 * 而页面更新后会 this.dirty 会被重新置为 true,这一步是在 this.update 方法中完成的 */evaluate(){this.value=this.get()this.dirty=false}/** * Depend on all deps collected by this watcher. */depend(){leti=this.deps.lengthwhile(i--){this.deps[i].depend()}}/** * Remove self from all dependencies' subscriber list. */teardown(){if(this.active){// remove self from vm's watcher list// this is a somewhat expensive operation so we skip it// if the vm is being destroyed.if(!this.vm._isBeingDestroyed){remove(this.vm._watchers,this)}leti=this.deps.lengthwhile(i--){this.deps[i].removeSub(this)}this.active=false}}}
Vue 源码解读(3)—— 响应式原理
当学习成为了习惯,知识也就变成了常识。 感谢各位的 点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。
封面
前言
上一篇文章 Vue 源码解读(2)—— Vue 初始化过程 详细讲解了 Vue 的初始化过程,明白了
new Vue(options)
都做了什么,其中关于数据响应式
的实现用一句话简单的带过,而这篇文章则会详细讲解 Vue 数据响应式的实现原理。目标
深入理解 Vue 数据响应式原理。
methods、computed 和 watch 有什么区别?
源码解读
经过上一篇文章的学习,相信关于
响应式原理
源码阅读的入口位置大家都已经知道了,就是初始化过程中处理数据响应式这一步,即调用initState
方法,在/src/core/instance/init.js
文件中。initState
initProps
proxy
initMethods
initData
initComputed
initWatch
observe
Observer
defineReactive
dependArray
数组响应式
def
protoAugment
copyAugment
Dep
Watcher
总结
面试官 问:Vue 响应式原理是怎么实现的?
答:
响应式的核心是通过
Object.defineProperty
拦截对数据的访问和设置响应式的数据分为两类:
对象,循环遍历对象的所有属性,为每个属性设置 getter、setter,以达到拦截访问和设置的目的,如果属性值依旧为对象,则递归为属性值上的每个 key 设置 getter、setter
访问数据时(obj.key)进行依赖收集,在 dep 中存储相关的 watcher
设置数据时由 dep 通知相关的 watcher 去更新
数组,增强数组的那 7 个可以更改自身的原型方法,然后拦截对这些方法的操作
添加新数据时进行响应式处理,然后由 dep 通知 watcher 去更新
删除数据时,也要由 dep 通知 watcher 去更新
面试官 问:methods、computed 和 watch 有什么区别?
答:
点击查看动图演示,动图地址:https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9c957654bb484ae7ba4ace1b912cff03~tplv-k3u1fbpfcp-watermark.awebp
示例其实就是答案了
使用场景
methods 一般用于封装一些较为复杂的处理逻辑(同步、异步)
computed 一般用于封装一些简单的同步逻辑,将经过处理的数据返回,然后显示在模版中,以减轻模版的重量
watch 一般用于当需要在数据变化时执行异步或开销较大的操作
区别
methods VS computed
computed VS watch
methods VS watch
链接
感谢各位的:点赞、收藏和评论,我们下期见。
当学习成为了习惯,知识也就变成了常识。 感谢各位的 点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。
The text was updated successfully, but these errors were encountered: