1、内容及形式
- 1.介绍目录结构、找到核心入口
- 2.介绍全局api
- 3.以提问互动方式阅读核心源码
2、目录结构
- .circleci 持续集成
- benchmarks 性能评测
- dist 输出目录
- examples 案例
- flow flow声明文件
- packages vue中的包
- scripts 工程化
- src 源码目录
- test 测试相关
- types ts声明文件
3、核心源码目录
├─compiler # 编译的相关逻辑
│ ├─codegen
│ ├─directives
│ └─parser
├─core # vue核心代码
│ ├─components # vue中的内置组件 keep-alive
│ ├─global-api # vue中的全局api
│ ├─instance # vue中的核心逻辑
│ ├─observer # vue中的响应式原理
│ ├─util
│ └─vdom # vue中的虚拟dom模块
├─platforms # 平台代码
│ ├─web # web逻辑 - vue
│ │ ├─compiler
│ │ ├─runtime
│ │ ├─server
│ │ └─util
│ └─weex # weex逻辑 - app
│ ├─compiler
│ ├─runtime
│ └─util
├─server # 服务端渲染模块
├─sfc # 用于编译.vue文件
└─shared # 共享的方法和常量
4、打包流程及入口分析
1.package.json
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex",
2.scripts/build.js
// 1.获取不同的打包的配置
let builds = require('./config').getAllBuilds()
// 2.进行打包
build(builds)
3.scripts/config.js
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
//找到打包入口
src/platforms/web/entry-runtime.js
src/platforms/web/entry-runtime-with-compiler.js
4.核心代码、全局API initGlobalAPI()
- 1.import Vue from './runtime/index'
- 2.import Vue from 'core/index'
- 3.import Vue from './instance/index'
//global-api/index.js
Vue.set
Vue.delete
Vue.nextTick
initUse(Vue) Vue.use
initMixin(Vue) Vue.mixin
initExtend(Vue) Vue.extend
5.Vue.set / Vue.delete
export function set (target: Array | Object, key: any, val: any): any {
//1.数组 使用splice触发视图更新 vue.set(arr,0,99)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = M .max(target.length, key)
target.splice(key, 1, val)
return val
}
//2.对象 是对象本身的属性,直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
//3.不是响应式的 不需要将其定义成响应式属性
if (!ob) {
target[key] = val
return val
}
// 4.将属性定义成响应式的
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
export function del (target: Array | Object, key: any) {
// 1.数组 依旧调用splice方法
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
// 2.本身没有这个属性
if (!hasOwn(target, key)) {
return
}
// 3.删除这个属性
delete target[key]
if (!ob) {
return
}
// 4.通知更新
ob.dep.notify()
}
对象只会拦截已经存在的属性 更改数组索引也不会引发视图更新
6.Vue.nextTick
const callbacks = []; // 存放nextTick回调
let pending = false;
function flushCallbacks () { // 清空队列
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx) // 1.将回调函数存入到callbacks中
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc(); // 2.异步刷新队列
}
// 3.支持promise写法
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
将回调函数存入到一个队列中,最后异步的清空这个队列
timerFunc
// 1.默认先使用Promise 因为mutationObserver有bug可能不工作
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// 解决队列不刷新问题
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
// 2.使用MutationObserver
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
// 3.使用 setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
// 4.使用setTimeout
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
EventLoop 微任务和宏任务,先微任务优先级 降级处理 promise => MutationObserver => setImmediate => setTimeout
5.初始化过程
initMixin(Vue) //_init
stateMixin(Vue) //$set $delete $watch
eventsMixin(Vue) // $on $emit $once $off
lifecycleMixin(Vue) //_update
renderMixin(Vue) //_render
//初始化 init.js
initLifecycle(vm) // 初始化组件间的父子关系
initEvents(vm) // 更新组件的事件
initRender(vm) //初始化_c方法创建虚拟节点
initInjections(vm) // resolve injections before data/props // 初始化inject
initState(vm) // 初始化状态
initProvide(vm) // resolve provide after data/props / 初始化provide
// 1.如果有el就开始挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
// 2.组件的挂载
Vue.prototype.$mount = function (el,hydrating){
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating);
}
// 3.创建渲染watcher进行渲染
export function mountComponent (vm,el,hydrating) {
vm.$el = el
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, { // 创建渲染Watcher
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
return vm
}
6、数据劫持 + 提问?
1.this如何访问data和methods?
// 初始化data数据
function initData(vm) {
let data = vm.$options.data;
// 实例的_data属性就是传入的data
// vue组件data推荐使用函数 防止数据在组件之间共享
data = vm._data = typeof data === "function" ? data.call(vm) : data || {};
// 把data数据代理到vm 也就是Vue实例上面 我们可以使用this.a来访问this._data.a
for (let key in data) {
proxy(vm, `_data`, key);
}
}
// 数据代理
function proxy(object, sourceKey, key) {
Object.defineProperty(object, key, {
get() {
return object[sourceKey][key];
},
set(newValue) {
object[sourceKey][key] = newValue;
},
});
}
通过 this 直接访问到 data 里面的数据的原因是:data里的属性最终会存储到new Vue的实例(vm)上的 _data对象中,访问 this.xxx,是访问Object.defineProperty代理后的 this._data.xxx
// initMethods
function initMethods (vm, methods) {
var props = vm.$options.props;
for (var key in methods) {
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
}
}
function nativeBind (fn, ctx) {
return fn.bind(ctx)
}
var bind = Function.prototype.bind
? nativeBind
: polyfillBind
通过this直接访问到methods里面的函数的原因是:因为methods里的方法通过 bind 指定了this为 new Vue的实例(vm)
2.数组的响应式?
1.因为对数组下标的拦截太浪费性能 对 Observer 构造函数传入的数据参数增加了数组的判断
// src/obserber/index.js import { arrayMethods } from "./array"; class Observer { constructor(value) { if (Array.isArray(value)) { // 这里对数组做了额外判断 // 通过重写数组原型方法来对数组的七种方法进行拦截 value.__proto__ = arrayMethods; // 如果数组里面还包含数组 需要递归判断 this.observeArray(value); } else { this.walk(value); } } observeArray(items) { for (let i = 0; i < items.length; i++) { observe(items[i]); } } }
2.每个响应式数据增加了一个不可枚举的__ob__属性 并且指向了 Observer 实例
- 1.可以根据这个属性来防止已经被响应式观察的数据反复被观测
- 2.响应式数据可以使用__ob__来获取 Observer 实例的相关方法
// src/obserber/index.js
class Observer {
// 观测值 给每个value添加__ob__属性
constructor(value) {
Object.defineProperty(value, "__ob__", {
// 值指代的就是Observer的实例
value: this,
// 不可枚举
enumerable: false,
writable: true,
configurable: true,
});
}
}
3.对数组原型重写
// src/obserber/array.js // 先保留数组原型 const arrayProto = Array.prototype; // 然后将arrayMethods继承自数组原型 // 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能 export const arrayMethods = Object.create(arrayProto); // 为什么重写七个方法? 会改变原数组 // 其它方法如何?join concat map filter forEach reduce every somesome flat slice // value.__proto__ => arrayMethods => arrayMethods.__proto__ => Array.prototype let methodsToPatch = [ "push", "pop", "shift", "unshift", "splice", "reverse", "sort", ]; methodsToPatch.forEach((method) => { arrayMethods[method] = function (...args) { // 这里保留原型方法的执行结果 const result = arrayProto[method].apply(this, args); // 这句话是关键 // this就是被检测的数据,比如数据是{msg:[1,2,3]},msg.push(4) this就是msg ob就是msg.__ob__ (上段代码增加,该数据已经被响应式观察)所以下面就可以使用ob.observeArray const ob = this.__ob__; // 这里的标志就是代表数组有新增操作 let inserted; switch (method) { case "push": case "unshift": inserted = args; break; case "splice": inserted = args.slice(2); default: break; } // 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测 if (inserted) ob.observeArray(inserted); // 之后咱们还可以在这里检测到数组改变了之后从而触发视图更新的操作--后续源码会揭晓 return result; }; });
7、简易版Vue
8、总结
源码并非遥不可及,从源码中学习思想,千里之行始于足下!