从这一大节开始,正式进入Vue
源码的外围,也是难点之一,响应式零碎的构建。这一节将作为剖析响应式构建过程源码的入门,次要分为两大块,第一块是针对响应式数据props,methods,data,computed,wather
初始化过程的剖析,另一块则是在保留源码设计理念的前提下,尝试手动构建一个根底的响应式零碎。有了这两个根底内容的铺垫,下一篇进行源码具体细节的剖析会更加得心应手。
7.1 数据初始化
回顾一下之前的内容,咱们对Vue
源码的剖析是从初始化开始,初始化_init
会执行一系列的过程,这个过程包含了配置选项的合并,数据的监测代理,最初才是实例的挂载。而在实例挂载前还有意疏忽了一个重要的过程,数据的初始化(即initState(vm)
)。initState
的过程,是对数据进行响应式设计的过程,过程会针对props,methods,data,computed
和watch
做数据的初始化解决,并将他们转换为响应式对象,接下来咱们会逐渐剖析每一个过程。
function initState (vm) { vm._watchers = []; var opts = vm.$options; // 初始化props if (opts.props) { initProps(vm, opts.props); } // 初始化methods if (opts.methods) { initMethods(vm, opts.methods); } // 初始化data if (opts.data) { initData(vm); } else { // 如果没有定义data,则创立一个空对象,并设置为响应式 observe(vm._data = {}, true /* asRootData */); } // 初始化computed if (opts.computed) { initComputed(vm, opts.computed); } // 初始化watch if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); }}
7.2 initProps
简略回顾一下props
的用法,父组件通过属性的模式将数据传递给子组件,子组件通过props
属性接管父组件传递的值。
// 父组件<child :test="test"></child>var vm = new Vue({ el: '#app', data() { return { test: 'child' } }})// 子组件Vue.component('child', { template: '<div>{{test}}</div>', props: ['test']})
因而剖析props
须要剖析父组件和子组件的两个过程,咱们先看父组件对传递值的解决。依照以往文章介绍的那样,父组件优先进行模板编译失去一个render
函数,在解析过程中遇到子组件的属性,:test=test
会被解析成{ attrs: {test: test}}
并作为子组件的render
函数存在,如下所示:
with(){..._c('child',{attrs:{"test":test}})}
render
解析Vnode
的过程遇到child
这个子占位符节点,因而会进入创立子组件Vnode
的过程,创立子Vnode
过程是调用createComponent
,这个阶段咱们在组件章节有剖析过,在组件的高级用法也有剖析过,最终会调用new Vnode
去创立子Vnode
。而对于props
的解决,extractPropsFromVNodeData
会对attrs
属性进行标准校验后,最初会把校验后的后果以propsData
属性的模式传入Vnode
结构器中。总结来说,props
传递给占位符组件的写法,会以propsData
的模式作为子组件Vnode
的属性存在。上面会剖析具体的细节。
// 创立子组件过程function createComponent() { // props校验 var propsData = extractPropsFromVNodeData(data, Ctor, tag); ··· // 创立子组件vnode var vnode = new VNode( ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')), data, undefined, undefined, undefined, context, { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }, asyncFactory );}
7.2.1 props的命名标准
先看检测props
规范性的过程。props
编译后的后果有两种,其中attrs
后面剖析过,是编译生成render
函数针对属性的解决,而props
是针对用户自写render
函数的属性值。因而须要同时对这两种形式进行校验。
function extractPropsFromVNodeData (data,Ctor,tag) { // Ctor为子类结构器 ··· var res = {}; // 子组件props选项 var propOptions = Ctor.options.props; // data.attrs针对编译生成的render函数,data.props针对用户自定义的render函数 var attrs = data.attrs; var props = data.props; if (isDef(attrs) || isDef(props)) { for (var key in propOptions) { // aB 模式转成 a-b var altKey = hyphenate(key); { var keyInLowerCase = key.toLowerCase(); if ( key !== keyInLowerCase && attrs && hasOwn(attrs, keyInLowerCase) ) { // 正告 } } } }}
重点说一下源码在这一部分的解决,HTML对大小写是不敏感的,所有的浏览器会把大写字符解释为小写字符,因而咱们在应用DOM
中的模板时,cameCase(驼峰命名法)的props
名须要应用其等价的 kebab-case
(短横线分隔命名) 命代替。 即: <child :aB="test"></child>
须要写成<child :a-b="test"></child>
7.2.2 响应式数据props
方才说到剖析props
须要两个过程,后面曾经针对父组件对props
的解决做了形容,而对于子组件而言,咱们是通过props
选项去接管父组件传递的值。咱们再看看子组件对props
的解决:
子组件解决props
的过程,是产生在父组件_update
阶段,这个阶段是Vnode
生成实在节点的过程,期间会遇到子Vnode
,这时会调用createComponent
去实例化子组件。而实例化子组件的过程又回到了_init
初始化,此时又会经验选项的合并,针对props
选项,最终会对立成{props: { test: { type: null }}}
的写法。接着会调用initProps
, initProps
做的事件,简略概括一句话就是,将组件的props
数据设置为响应式数据。
function initProps (vm, propsOptions) { var propsData = vm.$options.propsData || {}; var loop = function(key) { ··· defineReactive(props,key,value,cb); if (!(key in vm)) { proxy(vm, "_props", key); } } // 遍历props,执行loop设置为响应式数据。 for (var key in propsOptions) loop( key );}
其中proxy(vm, "_props", key);
为props
做了一层代理,用户通过vm.XXX
能够代理拜访到vm._props
上的值。针对defineReactive
,实质上是利用Object.defineProperty
对数据的getter,setter
办法进行重写,具体的原理能够参考数据代理章节的内容,在这大节后半段也会有一个根本的实现。
7.3 initMethods
initMethod
办法和这一节介绍的响应式没有任何的关系,他的实现也绝对简略,次要是保障methods
办法定义必须是函数,且命名不能和props
反复,最终会将定义的办法都挂载到根实例上。
function initMethods (vm, methods) { var props = vm.$options.props; for (var key in methods) { { // method必须为函数模式 if (typeof methods[key] !== 'function') { warn( "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " + "Did you reference the function correctly?", vm ); } // methods办法名不能和props反复 if (props && hasOwn(props, key)) { warn( ("Method \"" + key + "\" has already been defined as a prop."), vm ); } // 不能以_ or $.这些Vue保留标记结尾 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[method]拜访。 vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm); } }
参考 前端进阶面试题具体解答
7.4 initData
data
在初始化选项合并时会生成一个函数,只有在执行函数时才会返回真正的数据,所以initData
办法会先执行拿到组件的data
数据,并且会对对象每个属性的命名进行校验,保障不能和props,methods
反复。最初的外围办法是observe
,observe
办法是将数据对象标记为响应式对象,并对对象的每个属性进行响应式解决。与此同时,和props
的代理解决形式一样,proxy
会对data
做一层代理,间接通过vm.XXX
能够代理拜访到vm._data
上挂载的对象属性。
function initData(vm) { var data = vm.$options.data; // 根实例时,data是一个对象,子组件的data是一个函数,其中getData会调用函数返回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--) { var key = keys[i]; { // 命名不能和办法反复 if (methods && hasOwn(methods, key)) { warn(("Method \"" + key + "\" has already been defined as a data property."),vm); } } // 命名不能和props反复 if (props && hasOwn(props, key)) { warn("The data property \"" + key + "\" is already declared as a prop. " + "Use prop default value instead.",vm); } else if (!isReserved(key)) { // 数据代理,用户可间接通过vm实例返回data数据 proxy(vm, "_data", key); } } // observe data observe(data, true /* asRootData */);}
最初讲讲observe
,observe
具体的行为是将数据对象增加一个不可枚举的属性__ob__
,标记对象是一个响应式对象,并且拿到每个对象的属性值,重写getter,setter
办法,使得每个属性值都是响应式数据。具体的代码咱们前面剖析。
7.5 initComputed
和下面的分析方法一样,initComputed
是computed
数据的初始化,不同之处在于以下几点:
computed
能够是对象,也能够是函数,然而对象必须有getter
办法,因而如果computed
中的属性值是对象时须要进行验证。- 针对
computed
的每个属性,要创立一个监听的依赖,也就是实例化一个watcher
,watcher
的定义,能够临时了解为数据应用的依赖自身,一个watcher
实例代表多了一个须要被监听的数据依赖。
除了不同点,initComputed
也会将每个属性设置成响应式的数据,同样的,也会对computed
的命名做检测,避免与props,data
抵触。
function initComputed (vm, computed) { ··· for (var key in computed) { var userDef = computed[key]; var getter = typeof userDef === 'function' ? userDef : userDef.get; // computed属性为对象时,要保障有getter办法 if (getter == null) { warn(("Getter is missing for computed property \"" + key + "\"."),vm); } if (!isSSR) { // 创立computed watcher watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions); } if (!(key in vm)) { // 设置为响应式数据 defineComputed(vm, key, userDef); } else { // 不能和props,data命名抵触 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); } } }}
显然Vue
提供了很多种数据供开发者应用,然而剖析完后发现每个解决的外围都是将数据转化成响应式数据,有了响应式数据,如何构建一个响应式零碎呢?后面提到的watcher
又是什么货色?构建响应式零碎还须要其余的货色吗?接下来咱们尝试着去实现一个极简风的响应式零碎。
7.6 极简风的响应式零碎
Vue
的响应式零碎构建是比较复杂的,间接进入源码剖析构建的每一个流程会让了解变得艰难,因而我感觉在尽可能保留源码的设计逻辑下,用最小的代码构建一个最根底的响应式零碎是有必要的。对Dep,Watcher,Observer
概念的初步意识,也有助于下一篇对响应式零碎设计细节的剖析。
7.6.1 框架搭建
咱们以MyVue
作为类响应式框架,框架的搭建不做赘述。咱们模仿Vue
源码的实现思路,实例化MyVue
时会传递一个选项配置,精简的代码只有一个id
挂载元素和一个数据对象data
。模仿源码的思路,咱们在实例化时会先进行数据的初始化,这一步就是响应式的构建,咱们稍后剖析。数据初始化后开始进行实在DOM
的挂载。
var vm = new MyVue({ id: '#app', data: { test: 12 }})// myVue.js(function(global) { class MyVue { constructor(options) { this.options = options; // 数据的初始化 this.initData(options); let el = this.options.id; // 实例的挂载 this.$mount(el); } initData(options) { } $mount(el) { } }}(window))
7.6.2 设置响应式对象 - Observer
首先引入一个类Observer
,这个类的目标是将数据变成响应式对象,利用Object.defineProperty
对数据的getter,setter
办法进行改写。在数据读取getter
阶段咱们会进行依赖的收集,在数据的批改setter
阶段,咱们会进行依赖的更新(这两个概念的介绍放在前面)。因而在数据初始化阶段,咱们会利用Observer
这个类将数据对象批改为相应式对象,而这是所有流程的根底。
class MyVue { initData(options) { if(!options.data) return; this.data = options.data; // 将数据重置getter,setter办法 new Observer(options.data); }}// Observer类的定义class Observer { constructor(data) { // 实例化时执行walk办法对每个数据属性重写getter,setter办法 this.walk(data) } walk(obj) { const keys = Object.keys(obj); for(let i = 0;i< keys.length; i++) { // Object.defineProperty的解决逻辑 defineReactive(obj, keys[i]) } }}
7.6.3 依赖自身 - Watcher
咱们能够这样了解,一个Watcher
实例就是一个依赖,数据不论是在渲染模板时应用还是在用户计算时应用,都能够算做一个须要监听的依赖,watcher
中记录着这个依赖监听的状态,以及如何更新操作的办法。
// 监听的依赖class Watcher { constructor(expOrFn, isRenderWatcher) { this.getter = expOrFn; // Watcher.prototype.get的调用会进行状态的更新。 this.get(); } get() {}}
那么哪个工夫点会实例化watcher
并更新数据状态呢?显然在渲染数据到实在DOM
时能够创立watcher
。$mount
流程后面章节介绍过,会经验模板生成render
函数和render
函数渲染实在DOM
的过程。咱们对代码做了精简,updateView
稀释了这一过程。
class MyVue { $mount(el) { // 间接改写innerHTML const updateView = _ => { let innerHtml = document.querySelector(el).innerHTML; let key = innerHtml.match(/{(\w+)}/)[1]; document.querySelector(el).innerHTML = this.options.data[key] } // 创立一个渲染的依赖。 new Watcher(updateView, true) }}
7.6.4 依赖治理 - Dep
watcher
如果了解为每个数据须要监听的依赖,那么Dep
能够了解为对依赖的一种治理。数据能够在渲染中应用,也能够在计算属性中应用。相应的每个数据对应的watcher
也有很多。而咱们在更新数据时,如何告诉到数据相干的每一个依赖,这就须要Dep
进行告诉治理了。并且浏览器同一时间只能更新一个watcher
,所以也须要一个属性去记录以后更新的watcher
。而Dep
这个类只须要做两件事件,将依赖进行收集,派发依赖进行更新。
let uid = 0;class Dep { constructor() { this.id = uid++; this.subs = [] } // 依赖收集 depend() { if(Dep.target) { // Dep.target是以后的watcher,将以后的依赖推到subs中 this.subs.push(Dep.target) } } // 派发更新 notify() { const subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) { // 遍历dep中的依赖,对每个依赖执行更新操作 subs[i].update(); } }}Dep.target = null;
7.6.5 依赖治理过程 - defineReactive
咱们看看数据拦挡的过程。后面的Observer
实例化最终会调用defineReactive
重写getter,setter
办法。这个办法开始会实例化一个Dep
,也就是创立一个数据的依赖治理。在重写的getter
办法中会进行依赖的收集,也就是调用dep.depend
的办法。在setter
阶段,比拟两个数不同后,会调用依赖的派发更新。即dep.notify
const defineReactive = (obj, key) => { const dep = new Dep(); const property = Object.getOwnPropertyDescriptor(obj); let val = obj[key] if(property && property.configurable === false) return; Object.defineProperty(obj, key, { configurable: true, enumerable: true, get() { // 做依赖的收集 if(Dep.target) { dep.depend() } return val }, set(nval) { if(nval === val) return // 派发更新 val = nval dep.notify(); } })}
回过头来看watcher
,实例化watcher
时会将Dep.target
设置为以后的watcher
,执行完状态更新函数之后,再将Dep.target
置空。这样在收集依赖时只有将Dep.target
以后的watcher push
到Dep
的subs
数组即可。而在派发更新阶段也只须要从新更新状态即可。
class Watcher { constructor(expOrFn, isRenderWatcher) { this.getter = expOrFn; // Watcher.prototype.get的调用会进行状态的更新。 this.get(); } get() { // 以后执行的watcher Dep.target = this this.getter() Dep.target = null; } update() { this.get() }}
7.6.6 后果
一个极简的响应式零碎搭建实现。在精简代码的同时,放弃了源码设计的思维和逻辑。有了这一步的根底,接下来深入分析源码中每个环节的实现细节会更加简略。
7.7 小结
这一节内容,咱们正式进入响应式零碎的介绍,后面在数据代理章节,咱们学过Object.defineProperty
,这是一个用来进行数据拦挡的办法,而响应式零碎构建的根底就是数据的拦挡。咱们先介绍了Vue
外部在初始化数据的过程,最终得出的论断是,不论是data,computed
,还是其余的用户定义数据,最终都是调用Object.defineProperty
进行数据拦挡。而文章的最初,咱们在保留源码设计思维和逻辑的前提下,构建出了一个简化版的响应式零碎。残缺的性能有助于咱们下一节对源码具体实现细节的剖析和思考。