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
/** * 做了以下三件事,其实最关键的就是第三件事情 * 1、校验 methoss[key],必须是一个函数 * 2、判重 * methods 中的 key 不能和 props 中的 key 相同 * methos 中的 key 与 Vue 实例上已有的方法重叠,一般是一些内置方法,比如以 $ 和 _ 开头的方法 * 3、将 methods[key] 放到 vm 实例上,得到 vm[key] = methods[key],可以直接通过 this.methodName 访问这个 method */functioninitMethods(vm: Component,methods: Object){constprops=vm.$options.props// 判重处理 methods中的key不能和props中的key重复。props中key的优先级更高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 $.`)}}// 将method中所有方法赋值到vue实例中,支持通过this.menthodKeys的方式访问定义的方法vm[key]=typeofmethods[key]!=='function' ? noop : bind(methods[key],vm)}}
initData
/** * 做了三件事 * 1、判重处理,data 对象上的属性不能和 props、methods 对象上的属性相同 * 2、代理 data 对象上的属性到 vm 实例 * 3、为 data 对象的上数据设置响应式 * -> observe(data, true) */functioninitData(vm: Component){letdata=vm.$options.data// 保证后续处理的data是一个对象// 其实 vm.$options.data 在mergeOptions的时候已经被处理成了一个函数了,但是这里为什么还要判断一次呢?// 因为在mergeOption和这一步initData之间还存在beforeCreated这一步,这一步有可能对vm.$options.data进行修改,因此这里还是要加一下类型判断的~data=vm._data=typeofdata==='function'
? getData(data,vm)
: data||{}// 如果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)}// proxy data on instanceconstkeys=Object.keys(data)constprops=vm.$options.propsconstmethods=vm.$options.methodsleti=keys.lengthwhile(i--){constkey=keys[i]// 判重处理 data中的属性不能和props和methods中的key重复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)){// 如果与props 和 methods中的key都没有重复 并且这个key也不是保留字// 代理data的属性到vue实例 支持通过this.key来进行访问proxy(vm,`_data`,key)}}// observe dataobserve(data,true/* asRootData */)}
// dep.jsdepend(){// 依赖收集 dep.target实际上是一个watcherif(Dep.target){Dep.target.addDep(this)}}// watcher.js/** * Add a dependency to this directive. * 两件事: * 1、添加 dep 给自己(watcher) * 2、添加自己(watcher)到 dep */// 说白了 newDepIds(第一个if)是为了避免一次请求中重复多次收集依赖// depIds(第二个if)是为了避免多次求值中重复收集依赖addDep(dep: Dep){constid=dep.id// 将dep放到watcher中 如果dep已经存在了则不重复添加if(!this.newDepIds.has(id)){this.newDepIds.add(id)this.newDeps.push(dep)// 避免在 dep 中重复添加 watcher,this.depIds 的设置在 cleanupDeps 方法中if(!this.depIds.has(id)){// 将Dep.target这个watcher自己放到dep.subs中 实现了一个双向收集dep.addSub(this)}}}
当我们触发了一个属性的 getter 的时候,就会自动调用依赖收集函数。可以简单的理解成我们当前页面的渲染依赖于这个属性的值。(那么显然,如果后续这个属性的值变化了,我们的页面也需要重新渲染)
Dep.target 是一个全局的 Watcher 对象,他会维护一个叫做 deps 的 Dep Object 数组,记录自己监听了哪些属性。
同时,对于每一个 Dep 对象,他也会维护一个叫做 subs 的 Watcher Object 数组,这样的话,每一个 Dep 对象都知道有哪些 watcher 监听了自己的更新。
dep.notify()
/** * 通知 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()}}
Vue2 源码解析 —— 响应式原理
什么是响应式?
我们知道 Vue 的一个核心就是响应式系统。
例如我们的页面中有这么一部分代码
<div>{{ name }}</div>
,当我们的 name 的值改变的时候,视图会自动的进行更新。这就是所谓的响应式系统。Vue 2 和 Vue 3 对响应式的实现略有不同,这篇文章将会从源代码的角度开始讨论在 Vue 2 中,我们是怎么实现响应式的。
响应式入口
initState
在 Vue 实例初始化的过程中,会执行一个
initState
函数,这个函数位于/src/core/instance/state.js
中。这个函数是数据响应式的入口,依次调用
initProps
initMethods
initData
initComputed
initWatch
方法,完成 props methods data computed watch 属性的响应式处理接下来,我们就看看这些函数分别做了什么吧~
initProps
在这里,我们去掉了一部分不重要的代码,整个 initProps 方法的核心其实就是两个函数,
defineReactive
和proxy
,这两个函数我们会在后文讲解,在这里我们可以对其有一个大致的印象~defineReactive
: 响应式设置的真正核心。Object.defineProperty
都在这里这个方法中定义。proxy
: 将当前的 key 代理到 Vue 实例上,例如原本我们需要通过this._props.keyName
访问的数据,现在可以直接通过this.keyName
访问了initMethods
initData
在这里我们又看到了第三个关键的函数
observe
(前面两个是proxy
和defineReactive
),这个函数也是为数据设置响应式的,那么它和前文的defineReactive
又是什么关系呢?proxy
proxy 其实就是一个非常直观的代理函数,通过使用
Obejct.defineProperty
来重写getter
和setter
,让我们可以通过this[keyName]
直接访问到我们需要的属性observe
observe 方法的核心目的是创建一个
Observer
对象,每一个Observer
对象都会包含一个Dep
对象,然后执行walk
方法或者observeArray
方法,这些方法的尽头都是调用defineReactive
方法defineReactive
在
defineReactive
方法中,我们会为所有的obj[key]
都创建一个 Dep 对象,然后使用Object.defineProperty
来重写对象的 getter 和 setterdep.depend()
当我们触发了一个属性的 getter 的时候,就会自动调用依赖收集函数。可以简单的理解成我们当前页面的渲染依赖于这个属性的值。(那么显然,如果后续这个属性的值变化了,我们的页面也需要重新渲染)
Dep.target 是一个全局的 Watcher 对象,他会维护一个叫做
deps
的 Dep Object 数组,记录自己监听了哪些属性。同时,对于每一个 Dep 对象,他也会维护一个叫做
subs
的 Watcher Object 数组,这样的话,每一个 Dep 对象都知道有哪些 watcher 监听了自己的更新。dep.notify()
当我们触发了一个属性的 setter 方法的时候,就会触发依赖更新函数。由于在依赖收集的过程中,我们的 Dep 属性已经知道了哪些 Watcher 监听自己了,因此我们此时就可以精准的告诉那些 Watcher 你需要触发自己的
update
方法,来对于属性的变更作出一定的响应了 —— 例如 render watcher,它做出的响应就是会重新生成 vDOM 然后重新渲染页面,也就是我们所说的页面响应式。总结
Question 1. Vue2 是怎样实现响应式的?
Vue2 的响应式本质上就是通过
Object.defineProperty
来拦截并且重写 getter 和 setter 的。然而,这个方案有很多不可避免的缺陷,因此在 Vue3 中被Proxy
给替代了。{name: 'kk'}
,现在我直接增加一个属性obj.age = 18
,不幸的是,这个 age 属性是无法被监听的!(必须通过this.$set(obj, age, 18)
才能监听)arr
,我直接使用索引修改数组的值arr[0] = 1
,也是无法被监听的,仍然需要使用$set
方法let childOb = !shallow && observe(val)
,这是一个递归操作,当你的 val 是一个对象的时候,会不断的进行依赖收集。因此,当你的 val 嵌套严重的时候,会有一个较大的性能损失。Question 2. Dep 和 Watcher?
对于每一个属性,它都有一个属于自己的 Dep 对象,Dep 是英文 dependency 依赖的缩写,一个 Dep 可能对应多个 Watchers。也就是说有多个不同的 Watchers 依赖于某个属性 Dep。当这个属性改变的时候,这些 Watchers 也会随之改变。
常见的 Watchers 有三种,一种是程序员声明的 watcher,一种是 computed 计算属性生成的 watcher,还有一种就是 Vue 最一开始的渲染阶段就会生成的 Render Watcher
Question 3. Render Watcher?
首先,我们的代码会执行 initState。在这个阶段我们只是声明了每一个属性的 getter 和 setter。但是我们没有触发任何一个 getter!
然后,当源代码执行到 $mount 的时候,会创建一个 Render Watcher,在创建这个 Render Watcher 的时候,它的 get 函数会被触发,此时会尝试渲染页面。
当我们的代码尝试渲染页面的时候,显然就会试图获取页面上的属性的值,因此前面声明的这些 getter 才会被触发,于是每一个属性对应的 Dep 对象就记录了有一个 Render Watcher 监听了它。
当任意一个属性改变的时候,就会调用
Dep.notify()
,进而调用 Render Watcher 的更新函数,导致页面的重渲染 —— 也就是页面响应式。The text was updated successfully, but these errors were encountered: