作业
知识点
目标
- 环境搭建
- 理顺学习流程
- vue初始化过程
- 深入数据响应式
获取vue
文件结构
源码目录-src
调试环境搭建
npm i
npm run dev
输出文件
术语解释:
- runtime:仅包含运行时,不包含编译器 — 不能使用template这个配置项去写字符串的模板
- common:cjs规范打包 — cjs commonjs,用于webpack1版本
- esm:ES模块,用于webpack2+
- umd:universal module definition,兼容cjs和amd,用于浏览器 – vue.js
入口
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
dev脚本中 -c scripts/config.js
指明配置文件所在
参数 TARGET:web-full-dev
指明输出文件配置项,line123
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'), //入口
dest: resolve('dist/vue.js'), //目标文件
format: 'umd', //输出规范
env: 'development',
alias: { he: './entity-decoder' },
banner
},
一、初始化流程
src\platforms\web\entry-runtime-with-compiler.js
入口文件,覆盖$mount,执行模板解析和编译工作
测试代码
01-init.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue源码剖析</title>
<script src="../../dist/vue.min.js"></script>
</head>
<body>
<div id="demo">
<h1>初始化流程</h1>
<p>{{foo}}</p>
</div>
<script>
//创建实例
const app = new Vue({
el: "#demo",
template: '<div>template<div>',
render(h) {
return h('div', 'render')
},
data: {
foo: 'foo'
}
});
</script>
</body>
</html>
el: "#demo",
template: '<div>template<div>',
render(h) {
return h('div', 'render')
},
el,template,render谁的优先级最高,谁会起作用
render > remplate > el
el
const app = new Vue({
el: "#demo",
data: {
foo: 'foo'
}
});
或者
new Vue({
}).$mount("#demo")
template
const app = new Vue({
template: '<div>template<div>',
data: {
foo: 'foo'
}
});
是有问题的!
使用template
,必须明文调用 $mount('#demo')
const app = new Vue({
template: '<div>template<div>',
data: {
foo: 'foo'
}
}).$mount('#demo');
源码
//+ 保存原来的$mount
const mount = Vue.prototype.$mount
//+ 覆盖默认$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
//+ 解析option
const options = this.$options
//+ 1.先判断render
if (!options.render) {
let template = options.template
//+ 2. template 模板解析
if (template) {
......
//+ 3. el
} else if (el) {
template = getOuterHTML(el)
}
//+ 如果存在模板,执行编译
if (template) {
//+ 得到渲染函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
//+ 执行挂载
return mount.call(this, el, hydrating)
}
得到render函数
const app = new Vue({
el: "#demo",
data: {
foo: 'foo'
}
});
console.log(app.$options.render); // options.render = render
src\platforms\web\runtime\index.js
定义$mount
源码
//1+ 指定补丁方法:传入的虚拟dom转换为真实dom
//+ 1.初始化,2.更新
Vue.prototype.__patch__ = inBrowser ? patch : noop
//+ 实现$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
//+ 初始化,将首次渲染结果替换el
return mountComponent(this, el, hydrating)
}
src\core\index.js
定义全局API
源码
//+ 定义全局API
initGlobalAPI(Vue)
src\core\instance\index.js
定义构造函数
定义实例方法
源码
//+ 构造函数
function Vue (options) {
......
//+ 初始化
this._init(options)
}
initMixin(Vue) //通过该方法给Vue添加_init方法 实现iinit函数
2.定义实例方法
stateMixin(Vue) //状态相关api $set,$delete,$watch
eventsMixin(Vue) //事件相关api $emit,$on,$off,$once
lifecycleMixin(Vue) //生命周期api _update(),$forceupdate(),$destory()
renderMixin(Vue) //渲染api _render(),$nextTick
src\core\instance\init.js
初始化方法_init定义的地方
源码
Vue.prototype._init = function (options?: Object) {
......
//+ 合并选项
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
......
//+ 重点
initLifecycle(vm) //声明 $parent,$root,$children,$refs
initEvents(vm) //对父组件传入的事件添加监听
initRender(vm) //声明$slots,$scopedSlots,$createElement 插槽,虚拟dom的生成
callHook(vm, 'beforeCreate') //调用beforeCreate钩子
initInjections(vm) //注入数据
initState(vm) //重要:数据的初始化,响应式
initProvide(vm) // 提供数据
callHook(vm, 'created') //
......
}
mountComponent src\core\instance\lifecycle.js
执行挂载,获取vdom并转换为dom
执行创建的时候是:上–>下
执行挂载的时候是:下–>上
render() src\core\instance\render.js
渲染组件,获取dom
update() src\core\instance\lifecycle.js
执行更新,将传入vdom转换为dom,初始化执行的是dom创建操作
整体流程
new Vue()-----------------调用init
this._init(options)--------初始化各种属性
$mount--------------------调用mountComponent
mountComponent()-----声明updateComponent、创建watcher
_render()------------------获取虚拟dom
_update()-----------------把虚拟dom转为真实dom
const app = new Vue({
el: "#demo",
data: {
foo: 'foo'
}
});
写el不需要$mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
vue1+,页面中有一个绑定就有一个watcher–上节课讲的
vue2+中,一个组件只有一个watcher
二、数据响应化
Vue一大特点是数据响应式,数据的变化会作用于UI而不用进行DOM操作。原理上来讲,是利用了JS语言特性Object.defineProperty(),通过定义对象属性setter方法拦截对象属性变更,从而将数值的变化转换为UI的变化。
具体实现是在Vue初始化时,会调用initState,它会初始化data,props等,这里着重关注data初始化
入口:src\core\instance\state.js
initState 初始化数据,包括props、methods、data、computed、watch
export function initState (vm: Component) {
vm._watchers = []
const 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 )
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initData 核心代码是将data数据响应化
function initData (vm: Component) {
let data = vm.$options.data
//+ data是函数还是对象
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && 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
//+ 代理这些数据到实例上
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
//+ 重名判断
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && 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 */)
}
src\core\observer\index.js
observe() 返回一个Observer实例
源码
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
//+ 观察者,已经存在直接返回,不否则创建新实例
let ob: Observer | void
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
}
Observer 对象根据数据类型执行对应的响应化操作
源码
//+ 每一个响应式的对象,都会有一个ob
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
//+ 为什么在Observer里面声明一个dep
//+ object里面新增或者删除属性
//+ array中有变更方法
//+ 只要这些方法被调用,
//+ 都要通过dep去通知
this.dep = new Dep()
this.vmCount = 0
//+ 设置下划线__ob__属性,应用当前Observer实例
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)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
//+ 循环所有的key
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
//+ 遍历所有数组的key,对每个项执行响应化
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
defineReactive 定义对象属性的getter/setter,getter负责添加依赖,setter负责通知更新
源码
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
//+ dep和key一一对应
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
//+ 核心代码 属性拦截,只要是对象类型,均会返回childOb
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
//+ 获取key对应的值
const value = getter ? getter.call(obj) : val
//+ 如果存在依赖
if (Dep.target) {
//+ 收集依赖
dep.depend()
//+ 如果存在子ob,子ob也收集这个依赖
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()
}
})
}
src\core\observer\dep.js
Dep负责管理一组Watcher,包括watcher实例的增删及通知更新
源码
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
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()
}
}
}
src\core\observer\watcher.js
Watcher解析一个表达式并收集依赖,当数值变化时触发回调函数,常用于$watch API和指令中。
每个组件也会有对应Watcher,数值变化会触发其update函数导致重新渲染
源码
export default class Watcher {
......
constructor () {
......
}
get () {
......
}
addDep (dep: Dep) {
......
}
update () {
......
}
......
}
测试代码
相关API:$watch
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue源码剖析</title>
<script src="../../dist/vue.js"></script>
<style>
body {
background: #abcdef;
}
</style>
</head>
<body>
<div id="demo">
<h1>数据响应化</h1>
<p>{{obj.foo}}</p>
</div>
<script>
//创建实例
const app = new Vue({
el: "#demo",
data: {
obj: {
//+ obj是一个对象,有一个ob实例,通过obj上面挂载的dep去通知,只要界面中涉及obj,把和obj相关的组件实例和watcher关联起来
//+ 因为当前组件实例和watcher是一对一对应的,在ob里面将watcher提交更新
//+ dep将订阅的watcher保存下来,便于后面通知更新
//+ 会触发setter方法,告诉watcher有依赖发生了变化,wathcer收到依赖变化的消息,重新渲染虚拟dom,实现页面数据的更新
//+ get
//+ obj和当前组件watcher建立关系
//+ obj.foo 也和当前组件watcher建立关系
foo: 'foo', //删除obj存在的foo,或者动态set一个bar
}
},
mounted(){
setTimeout(()=>{
this.obj.foo = "fooooo"
},1000)
}
});
</script>
</body>
</html>
1.有几个Observer的实例?2个
一个对象会有一个,data最外层{},obj:{}
2.有几个Dep? 2个
有几个key,有几个dep
3.有几个Watcher? 1个
1个组件只有1个Watcher
创建了两个Observer的实例
一个组件一个Watcher
obj = {foo:'foo'}
obj.bar = 'aaa'; //no ok
Vue.set(obj,'bar','aaa');//ok
items = [1,3];
Vue.set(items,0,'abc');//ok
items.length = 0; //no ok
items[0]=='aaa';//no ok
Vue 2.0响应式缺点
1.递归遍历,性能会受影响
2.api不统一
数组响应化
数组数据变化的侦测跟对象不同,我们操作数组通常使用push、pop、splice等方法,此时没有办法得知数据变化。所以vue中采取的策略是拦截这些方法并通知dep。
src\core\observer\array.js
源码
//+ 获取数组原型
const arrayProto = Array.prototype
//+ 备份
export const arrayMethods = Object.create(arrayProto)
//+ 这7个方法才会修改数组
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
//+ 覆盖7个方法
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
//+ 执行原定的任务
const result = original.apply(this, args)
//+ 通知
const ob = this.__ob__
//+ 如果操作是插入操作,还需要额外的响应化处理
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
//+ 通知更新
ob.dep.notify()
return result
})
})
Observer中覆盖数组原型
源码
//+ 判断类型
if (Array.isArray(value)) {
//+ 替换数组对象原型
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
//+ 没有原型,强硬覆盖
copyAugment(value, arrayMethods, arrayKeys)
}
//+ 如果数组里面的元素是对象,还需要做响应化处理
this.observeArray(value)
}
测试代码
相关API:Vue.set()/delete()
在这里插入代码片
作业
1.理出整体流程思维导图
2.自己尝试编写测试案例调试
3.自己研究Vue.set/delete/$watch等API
4.尝试看看vue异步更新队列是如何实现的