initState
- 进行props,methods,data,computed和watch数据的初始化处理,并将他们转换为响应式对象
export function initMixin (Vue: Class<Component>) {
// 在原型上添加 _init 方法
Vue.prototype._init = function (options?: Object) {
// 保存当前实例
const vm: Component = this
// 合并配置
if (options && options._isComponent) {
// 把子组件依赖父组件的 props、listeners 挂载到 options 上,并指定组件的$options
initInternalComponent(vm, options)
} else {
// 把我们传进来的 options 和当前构造函数和父级的 options 进行合并,并挂载到原型上
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._self = vm
initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
initEvents(vm) // 初始化事件:$on, $off, $emit, $once
initRender(vm) // 初始化渲染: render, mixin
callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
initInjections(vm) // 初始化 inject
initState(vm) // 初始化组件数据:props, data, methods, watch, computed
initProvide(vm) // 初始化 provide
callHook(vm, 'created') // 调用生命周期钩子函数
if (vm.$options.el) {
// 如果传了 el 就会调用 $mount 进入模板编译和挂载阶段
// 如果没有传就需要手动执行 $mount 才会进入下一阶段
vm.$mount(vm.$options.el)
}
}
}
initState
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);
}
}
父组件处理传递给子组件的props
<child :test="test"></child>
- 父组件优先进行模板编译得到一个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构造器中
// 创建子组件过程
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
);
}
extractPropsFromVNodeData
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 :a-b=“test”>
子组件处理props
- 子组件处理props的过程,是发生在父组件_update阶段,这个阶段是Vnode生成真实节点的过程,期间会遇到子Vnode,这时会调用createComponent去实例化子组件。而实例化子组件的过程又回到了_init初始化,此时又会经历选项的合并,针对props选项,最终会统一成{props: { test: { type: null }}}的写法。接着会调用initProps, initProps做的事情,简单概括一句话就是,将组件的props数据设置为响应式数据。
initProps
function initProps (vm, propsOptions) {
var propsData = vm.$options.propsData || {};
var loop = function(key) {
···
defineReactive(props,key,value,cb);
if (!(key in vm)) {
//进行数据代理,vm.key访问vm._props.key
proxy(vm, "_props", key);
}
}
// 遍历props,执行loop设置为响应式数据。
for (var key in propsOptions) loop( key );
}
initMethods
- 保证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);
}
}
initData
- 能和props,methods重复。最后的核心方法是observe,observe方法是将数据对象标记为响应式对象,并对对象的每个属性进行响应式处理
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 */);
}
initComputed
- computed可以是对象,也可以是函数,但是对象必须有getter方法,因此如果computed中的属性值是对象时需要进行验证。
- 针对computed的每个属性,要创建一个监听的依赖,也就是实例化一个watcher,watcher的定义,可以暂时理解为数据使用的依赖本身,一个watcher实例代表多了一个需要被监听的数据依赖
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)) {
// 设置为响应式数据,并生成对应的watcher
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);
}
}
}
}
简易响应式系统实现
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))
设置响应式对象 - 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])
}
}
}
依赖本身 - 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()
}
}
初始化Watcher
- 那么哪个时间点会实例化watcher从而更新数据状态呢?显然在渲染数据到真实DOM时可以创建watcher
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)
}
}
依赖管理 - 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;
响应式 defineReactive
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();
}
})
}