_init() 初始化函数
方法被调用,主要做对初始化操作 以及 对传入 props、methods、data、computed 与 watch 做处理,在 initState 方法内转换为可观察,beforeCreate、created 钩子函数被触发,组件未挂载。
initProps
主要是对 props 进行逐个遍历,验证 value 数据类型,给 props 设置响应式 设置访问代理,不会进行递归遍历。
function initProps (vm, propsOptions) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// ... 省略
for (const key in propsOptions) {
const value = validateProp(key, propsOptions, propsData, vm)
// ... 省略
defineReactive(props, key, value)
// 转接访问,当访问 vm.key 属性,转到访问 vm._props.key 属性
proxy(vm, `_props`, key)
}
}
- props 数据从何来?
数据是直接从 父组件上传过来的,没有进行拷贝等处理,原样传过来。
- props 怎么传过来的?
模版编译成渲染函数,而当渲染函数执行时,作用域指向的是 父组件,变量 数据也就从 父的作用域取,包括 子组件 也是,所以 props 接收的数据是直接 引用父组件的,例如下面渲染函数:
// this 指向父组件,父组件有 treeData 数据
with(this) {
return createElement('ul', {
attrs: {
"id": "app"
}
}, [createElement('item', { // item 是组件模版 (<item class="item" :model="treeData"></item>)
staticClass: "item",
attrs: {
"model": treeData // 直接引用父组件的
}
})], 1)
}
- props 怎么读取?
子组件 初始化时,也会调用 initProps 方法,逐个把数据复制到 当前 子组件实例上(vm._props) 并 转换为 响应式 和 设置访问代理,当访问数据时是从复制过来(vm._props)的数据进行访问,然而当你想修改 props 时是不会影响 父组件 数据变更的,并会给你发出警告。
- props 怎么更新?
子组件 渲染函数执行时,需要从 父组件 读取数据,此时,就会触发 父组件数据的 依赖收集(get)把 子组件 的 watcher 收集到 自己的收集器(dep)中,当 父组件 数据发生变更,则通知自己所有 依赖,重新执行 子组件 渲染函数,重新读取父组件数据。
initMethods
主要对methods 函数遍历 挂载到当前实例(vm),并 使用bind函数,将 this 绑定到 实例(vm),另外会判断跟 props 有重名会 警告。
function initMethods (vm, methods) {
for (const key in methods) {
vm[key] = bind(methods[key], vm)
}
}
initData
主要对 data 数据通过 observe 函数绑定 观察者 保存到 __ ob__ 属性,并设置 访问代理(proxy),另外会判断跟 props methods 函数 是否重名 并且 不能以 $ 开头,否则发出 警告。
function initData (vm) {
let data = vm.$options.data
// 得到 data 数据
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
let i = keys.length
// 遍历 data 中的数据
while (i--) {
const key = keys[i]
proxy(vm, `_data`, key)
}
observe(data, true)
}
- proxy(vm,
_data
, key) 代理作用?
传入的数据(data)实际被保存到 _data 属性,遍历逐个 设置(proxy) ,当 vm.name 访问属性时,此时 触发 get ,则从 _data 找数据返回,更新属性同理 触发 set,代码如下:
function proxy (target, sourceKey, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
function proxySetter (val) {
this[sourceKey][key] = val;
},
function proxyGetter () {
return this[sourceKey][key]
}
});
}
proxy(vm, `_data`, key)
initComputed
初始化计算属性,遍历为个属性创建一个 new Watcher 传入 并 计算属性的值(fn)或者 noop 空的函数 以计算属性的 key,保存到 保存到 vm._computedWatchers 对象内,接着为每个 计算属性 通过 Object.defineProperty 函数,设置 getter/getter 响应式,也会有判断重名。
function initComputed (vm, computed) {
// 存放 wathcer
const watchers = vm._computedWatchers = Object.create(null)
// 遍历计算属性
for (const key in computed) {
const userDef = computed[key]
// 创建 watcher
const getter = typeof userDef === 'function' ? userDef : userDef.get
watchers[key] = new Watcher(vm, getter || noop, noop, { lazy: true })
// 设置响应式
const propertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
if (typeof userDef === 'function') {
propertyDefinition.get = createComputedGetter(key)
} else {
propertyDefinition.get = userDef.get
? (userDef.cache !== false ? createComputedGetter(key) : userDef.get)
: noop
}
Object.defineProperty(vm, key, propertyDefinition)
}
}
- 创建 watcher 作用是什么?
用来在 读取 data 数据时 收集依赖(watcher)
设想:有 data.name 数据,一个计算属性 getName ,计算属性的方法内 读取 data.name,此时 触发 name 的getter 进行收集依赖,其实就是把 计算属性 watcher 收集起来,当 data.name 发生变更时 会通知到这个 watcher。
- computed(计算属性) 如何控制缓存?
通过【脏数据标志位 dirty】,dirty 是 watcher 的一个属性,当 new Watcher 时 传入 lazy:true 赋值给 dirty ,通过 dirty 控制是否需要重新计算结果,当 计算属性的依赖更新时会 设置为 true 表示需要重新计算, 计算属性 再被读取 则 返回计算好的结果。
当 dirty 为 true 时,读取 computed 会重新计算
当 dirty 为 false 时,读取 computed 会使用缓存
- 依赖 data 变化,computed(计算属性) 如何更新?
场景:
A 页面 引用两个 计算属性 computed getName,依赖 data.name
- 当 data.name 变更时,通知 computed getName 对应的 watcher 把 【标志位 dirty】变在 true
- 接着 通知 A 页面 watcher 进行更新渲染,接着会重新读取 computed getName,然后执行 getName 得到新的值保存起来。
为何能通知 A 页面的 watcher?
点击查看【processon】
initWatch
逐个遍历传入 watch 为每个属性创建 new Watcher ,在创建的同时会对 key 进行解析,拿 key 从当前实例 查找 data,找到 则会 触发 data - getter 依赖收集 并把当前 watch - Watcher 收集起来,当 data 更新则会通知 Watcher 执行,执行完结后 执行 watch 定义的回调,伪代码如下:
function initWatch (vm, watch) {
for (const key in watch) {
// 拿到 watch 回调方法
const handler = watch[key]
// handler
options = handler
options.user = true
const watcher = new Watcher(vm, key, handler, options)
if (options.immediate) {
// watcher.value 在 new 时已经取到值了
cb.call(vm, watcher.value)
}
}
}
- 设置 immediate 参数,会如何工作?
设置 immediate 会在初始化时就调用一次你设置的 watch 回调,并传入初始化取到的值,否则只会在 监听的 data 更新时才会触发
- 设置 deep 参数,会如何工作?
设置 deep 时,当监听的 data 属性值是个对象,会递归把所以属性都读取一遍,所以 data 属性嵌套得多深都会收集到 watch - Watcher 的依赖,嵌套的属性发生变更也会触发 watch 回调。
标记位
vm.hasHookEvent 标记 是否有事件钩子,在判断的时候就不用调用 哈希表的方法 _来判断是否 有,以减少不必要开销
vm._isVue 标记 防止 vm 实例自身被观察
工具函数
原生 bind 方法实现
function polyfillBind (fn, ctx) {
function boundFn (a) {
var args = arguments.length;
return args ? args > 1 ? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
boundFn._length = fn.length;
return boundFn
}
属性是否以 $ 开头
function isReserved (str) {
const c = (str + '').charCodeAt(0)
return c === 0x24 || c === 0x5F
}
解析嵌套对象属性 Key (obj.user.name)
const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
function parsePath (path, obj){
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}