准备
Flow
为什么是flow
不仅是因为它能够很好的实现代码静态检测,主要是Babel和ESLint提供了flow相关插件,对它的支持很友好,可以以非常小的成本实现静态类型检查能力。
工作方式
flow的工作方式主要有2种:
- 类型推断
/*@flow*/
function f1(str) {
return str.split(',')
}
这样,就能推测出str是需要传字符串。
- 类型注释
/*@flow*/
function add(x, y) {
return x + y
}
如果是这样x+y,类型推测就不能确定了,因为+可能是字符串也可能是数字,如果你想确定只能是数字,则需要用到类型注释,如下:
/*@flow*/
function add(x: number, y: number): number {
return x + y
}
自定义类型
flow所能识别的类型有限,如果想要自定义的话,需要在flow提供的配置文件.flowconfig中配置,如下图:可以看出,这里配置的是个路径,具体类型在主目录下flow文件夹中,如果在阅读源码的时候遇到了,可以打开详细看下。
数据驱动
数据驱动是Vue的一个核心思想,它的意思是我们不会对DOM进行直接操作,而是通过定义数据从而来完成对视图的更新,这样做好处非常明显:使代码结构非常清晰并且易于维护。new Vue()发生了什么
在上文已经找到,Vue构造函数是定义在src/core/instance/index.js中:function Vue (options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
// 执行initMixin函数中定义的_init()初始化
// 若不定义,则不会执行
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
其中,Mixin类函数如initMixin(Vue)等定义的是实例全局方法。new Vue()执行的
this._init(options)
定义在同级下init.js文件initMixins(Vue)方法内,实现了一系列值初始化:
initLifecycle(vm)
initEvents(vm)
initRender(vm) // $attrs、$listeners
callHook(vm, 'beforeCreate') // 暴露beforeCreate钩子
/**
* injection之所以在provide之前初始化,是因为在同一个组件内,必须先初始化injecti* on获取到父传递的值,再初始化provide给子传值。就是必须先拿到父值才能传给子
*/
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
从这里,我们也就能知道了,在组件开发中,beforeCreate和created钩子函数分别能获取到哪些值。
在本节,我们要实现的是数据通过模板生成DOM展示出来,现在数据一系列初始化完成了,下面来看下模板。
挂载
记得在入口文件分析的时候,就提到了它的主要功能是扩展了$mount方法,既然是扩展,那就说明已经定义了原方法,实际上,在src/platform/web/runtime/index.js和src/platform/weex/runtime/index.js文件中都分别定义了$mount方法,说明挂载与平台有关。我们应该也很熟悉,在很多实际项目开发中,是用的webpack构建,其中有用到了vue-loader插件,它的功能就是对vue模板进行编译,正因为此,我们引入的vue则不需要选择带编译器的版本。如果是不带编译器的vue版本,则$mount方法就是src/platform/web/runtime/index.js中定义的。在$mount原方法中,实现非常简单,就返回了mountComponent方法:Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
mountComponent方法定义在了core/instance/lifecycle文件下。核心代码:
const vnode = vnode || vm._render()
vm._update(vnode, hydrating) // 核心代码
内部_render()方法作用是生成虚拟DOM,_update()方法是通过虚拟DOM生成真实DOM。下面,继续研究下这2个核心方法。
render
_render()方法被定义在src/core/instance/render.js中:vnode = render.call(vm._renderProxy, vm.$createElement)
可以看出,render函数的参数是vm.$createElement,根据我们平时手写render函数的经验,可以推测createElement就是传说中的h函数,它定义在当前文件initRender()函数中:
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
继续深入看看createElement方法,它是生成VDOM的核心。定义在src/core/vdom/create-element.js,返回VDOM。createElement方法返回内部函数_createElement,在方法中,根据tag类型不同,会存在不同生成VDOM的方式,如果是标签是组件,则是通过createComponent方法生成,其他是通过new VNode()方式产生。
if (config.isReservedTag(tag)) {
// platform built-in elements
if (process.env.NODE_ENV !== 'production' && isDef(data) &&
isDef(data.nativeOn)) {
warn(`The .native modifier for v-on is only valid on components but it
was used on <${tag}>.`, context)
}
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor =
resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
update
_render方法定义在src\core\instance\lifecycle.js中lifecycleMixin函数中:vm.$el = vm.__patch__(prevVnode, vnode)
核心方法是__patch__,在src\platforms\web\runtime\index.js中在Vue原型上定义:
Vue.prototype.__patch__ = inBrowser ? patch : noop
继续往下找到src\platforms\web\runtime\patch.js文件
export const patch: Function = createPatchFunction({ nodeOps, modules })
createPatchFunction方法定义在src\core\vdom\patch.js中,里面函数特别多,功能主要有2个:
- VDOM对比(如果是首次渲染,则不必要;如果非首次,则新生成的VDOM与现真实DOM映射的VDOM进行对比)
- 更新渲染到真实DOM
响应式原理
在上文中,我们大体走了一遍Vue的运行过程,在本节中,一起来看下他的响应式,主要有2个部分:依赖收集和派发更新。依赖收集是让定义的数据响应式,而派发更新则需要根据监听数据的变化而更新页面。Vue中可响应式的数据类型很多,如props、data等,以下我们以定义更多的data为例来说。data的初始化在src\core\instance\state.js中方法initData(),核心代码就一行
observe(data, true /* asRootData */)
observe方法定义在src\core\observer\index.js中,里面进行了判断,如果传入的对象是响应式的,则直接返回;若不是,则执行:
ob = new Observer(value)
Observer类也在相同文件中,根据是否是数组,进行了判断,数组的响应式处理后面再说。在构造函数中执行了内部walk(),而walk方法则定义了对象响应式的核心方法:
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
definedReactive方法也在当前文件:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// Watcher
// 执行依赖收集
if (Dep.target) {
dep.depend()
// 针对深层对象依赖收集
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) { // 负责通知更新
const 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 (process.env.NODE_ENV !== 'production' && 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()
}
})
可以看到Vue数据响应式原理的核心是采用了对象的defineProperty方法里面定义的getter/setter,在get方法中:
dep.depend() // 执行依赖收集
在set方法中:
dep.notify() // 通知更新
那你一定好奇,dep到底是什么呢?Dep类就是管理watcher的工具。内部有4个方法:
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 对象响应式依赖收集时执行
depend () {
if (Dep.target) {
// 在Watcher中定义
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = 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
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
在构造函数中定义了subs数组用于管理watcher,addSub()和removeSub()就是对subs数组的增删操作,而下面depend()和notify()方法在defineProperty的get/set方法都调用了,分别用于数据的依赖收集和通知更新。既然Sub是对watcher的管理,那watcher监听的是什么?整个应用或是单个组件,或是组件内的某个属性呢?来看看Watcher类:src\core\observer\watcher.js,它的第一个入参vm: Component,是一个组件,上文说的Dep类,它其实存在一个静态属性target,类型就是Watcher,指向的就是当前正在处理的组件。所以说,watcher监听的是单个组件内响应式数据。
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
...
}