前言:
vue3.0马上要来了,于今昔写一篇vue将一个字符串hellowrold渲染于页面的过程,慰藉我这几个月写vue的‘枯燥’。
源码版本是2.6.10。
开始:
我们的模板足够简单:
<div id="app">{{msg}}</div>
vue实例的配置也足够简单:
new Vue({ el:'#app', data:function(){ return{ msg: 'hello,world' } } })
下面带着配置进入vue的构造函数:
function Vue (options) { if (!(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword'); } this._init(options); }
构造函数足够简单,判断了一下是否用new调用,然后进入实例的_init_方法:
Vue.prototype._init = function (options) { var vm = this; // a uid vm._uid = uid$3++; var startTag, endTag; /* istanbul ignore if */ if (config.performance && mark) { startTag = "vue-perf-start:" + (vm._uid); endTag = "vue-perf-end:" + (vm._uid); mark(startTag); } // a flag to avoid this being observed vm._isVue = true; // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options); } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ); } /* istanbul ignore else */ { initProxy(vm); } // expose real self vm._self = vm; initLifecycle(vm); initEvents(vm); initRender(vm); callHook(vm, 'beforeCreate'); initInjections(vm); // resolve injections before data/props initState(vm); initProvide(vm); // resolve provide after data/props callHook(vm, 'created'); /* istanbul ignore if */ if (config.performance && mark) { vm._name = formatComponentName(vm, false); mark(endTag); measure(("vue " + (vm._name) + " init"), startTag, endTag); } if (vm.$options.el) { vm.$mount(vm.$options.el); } }; }
init函数一进来,先使用vm保存了this,然后给组件定义了一个_uid属性,每初始化一个组建这个东西就+1从0开始,下来一个
config.performance && mark 这个属性是用来记录vue的性能的相关参数,因为是根组建,所以下来的if-else走else分支,然后开始mergeOptions, mergeOptions其实就是将vue实例上缺省的属性设置成了默认值,然后merge之后我们的option长这个亚子:
"{"components":{},"directives":{},"filters":{},"beforeCreate":[null],"destroyed":[null],"el":"#app"}"
接着开始proxy:
initProxy = function initProxy (vm) { if (hasProxy) { // determine which proxy handler to use var options = vm.$options; var handlers = options.render && options.render._withStripped ? getHandler : hasHandler; vm._renderProxy = new Proxy(vm, handlers); } else { vm._renderProxy = vm; }
这个hasProxy是用于检测当前环境是否支持Proxy,如果支持之后就进行代理操作,三元运算符表示如果当前实例上已经有render就用getHandler代理操作,否则就用hasHandler,因为我们没有render故而得到了hasHandler代理操作:
var hasHandler = { has: function has (target, key) { var has = key in target; var isAllowed = allowedGlobals(key) || (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)); if (!has && !isAllowed) { if (key in target.$data) { warnReservedPrefix(target, key); } else { warnNonPresent(target, key); } } return has || !isAllowed } };
proxy就不用细讲了,以上的代理的作用就是检测当前vm上有某个key时,也就是 key in vm时,proxy代理了这个操作。具体细节就是 先检测当前vm上有无此属性,然后isAllowed 这个值呢 由allowedGlobals产生,allowedGlobals呢是一个由全局对象或着js语言关键词组成的闭包map函数,长这样:
首先是关键字部分:
var allowedGlobals = makeMap( 'Infinity,undefined,NaN,isFinite,isNaN,' + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + 'require' // for Webpack/Browserify );
makeMap就是用这个字符串产生一个map对象,然后返回一个闭包函数,makeMap长这样:
function makeMap ( str, expectsLowerCase ) { var map = Object.create(null); var list = str.split(','); for (var i = 0; i < list.length; i++) { map[list[i]] = true; } return expectsLowerCase ? function (val) { return map[val.toLowerCase()]; } : function (val) { return map[val]; } }
回到has那个方法里,也就是说如果现在key是一个关键字那么就不用执行后面的东西啦,此时isAllowed就是true,否则,如果key是个字符串而且以_开头,而且vm.$data无法访问,那么此时isAllowed就是false啦。下来那个if就是说如果vm上没有属性key,并且isAllowed是false,就会执行下边的两个警告。最后返回结果。然后代理完成,并在当前vm实例上绑定了一个属性_renderProxy引用当前经过proxy之后的vm。接下来进入各种初始化过程,包含生命周期钩子函数,事件,render,然后在调用了第一个生命周期钩子,beforeCreate,随后初始化injection,state,以及provider。injection和provider是vue的依赖注入机制,这里我们不需要细讲,我们着重进入initState:
function initState (vm) { vm._watchers = []; var opts = vm.$options; if (opts.props) { initProps(vm, opts.props); } if (opts.methods) { initMethods(vm, opts.methods); } if (opts.data) { initData(vm); } else { observe(vm._data = {}, true /* asRootData */); } if (opts.computed) { initComputed(vm, opts.computed); } if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); } }
一进来,先初始化了一个_watchers,然后开始初始化props和methods,因为这里我们没有定义这两个东西所以直接来到initData:
function initData (vm) { var data = vm.$options.data; data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}; if (!isPlainObject(data)) { data = {}; warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ); } // proxy data on instance 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 ); } } 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)) { proxy(vm, "_data", key); } } // observe data observe(data, true /* asRootData */); }
首先拿到data,(敲黑板,这里拿到data之后,还会在当前实例上声明一个_data用于保存当前组建得data数据,这个_data后期会代理vm上访问data里得值)。然后进入while循环,如果data中某个key值与props或者methods上的key重复了,会予以警告,否则进入另一个代理程序,proxy:
function proxy (target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] }; sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); }
这个代理呢实际就是将vm.key 的get和set代理到了 vm._data.key上,往后在vue实例中使用 this.msg = 'balabal'时,实际上是将这个值set到了this,_data.msg上。这是后话。代理完成之后,来到了observe:
/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ function observe (value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob }
入口处的判断表明如果value是原始值或者是VNode的实例时不予以ovserve的。随后如果此对象已经被observe过了的话会有一个'__ob__'属性引用了observe当前对象之后的结果,如果有ob就用以前已经存在的否则经过一系列判断之后进入Observer,这些判断条件包括一个全局变量shouldObserve声明的默认值就是true,一个是否时服务端渲染的函数这里当然是true,然后判断是否为对象或者数组,这里value就是我们data的原始值,所以为true,然后判断当前对象是否可扩展,of course是ture,然后下边这个属性因为现在还没有,取反就是true,然后进入Observer:
var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } };
这里,observe实例先保存了传入的value,然后会有一个dep实例生成,然后下来定义了一个observe 判断时会用到的属性__ob__,接着判断是数组,还是对象,这里我们的data是一个对象走walk:
/** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); } };
walk遍历对象,每一个键值,使用defineReactive$$1监听其get,set方法:
/** * Define a reactive property on an Object. */ function defineReactive$$1 ( obj, key, val, customSetter, shallow ) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; if ((!getter || setter) && arguments.length === 2) { val = obj[key]; } var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { 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 (customSetter) { customSetter(); } // #7981: for accessor properties without setter if (getter && !setter) { return } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); } }); }
defineReactive$$1一进来也是生成了一个dep对象,这里看下dep长什么亚子:
{"id":3,"subs":[]}
基本属性是一个id,还有一个subs,subs里会保存依赖收集时对应组建的watcher,这个我们下面会讲到,然后原型方法上是一组关于subs的操作方法,不再细讲。接着取到属性描述符,判断是否configurable,取得已经事先配置好的getter setter,然后如果是非shallow模式,会继续observe,这里我们的val是一个字符串,非对象,所以observe会直接返回,然后我们继续走到Object.defineProperty这里,对于data上的属性,都成为响应式属性,即get方法收集依赖,(他里边这个Dep.target就是收集依赖的时候的watcher实例),set唤起更新程序。设置完毕,层层返回直到observe函数里。返回ob对象。然后observe完毕。继续返回到initState函数里,下来initState函数会继续初始化computed和watch属性,我们代码里没有设置直接返回到最开始的init函数里,随后在当前vue实例初始化完毕之后,created钩子会触发。