前言:使用 vue 也有一段时间了,前段时间看了看 vue 的源码,很多小伙伴看到vue几万行的源码都是望而止步了,当时看源码看得也很费劲,下面分享一下我对vue源码的一些理解和感触。
源码目录
下面会把部分不是很重要的代码进行简化,方便大家观看
1. new Vue初始化流程
新建一个 html 文件引入vue。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./vue-2.6.10/dist/vue.js"></script>
</script>
<script>
new Vue({
el:'#app',
})
</script>
</body>
</html>
vue 初始化就从这里开始了。 我们现在打开vue源码 src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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')
}
// new Vue的时候就执行到了这个_init函数
this._init(options)
}
initMixin(Vue) // 其他的不看,先看这个函数
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
vue 的整个构造函数就调用了 _init 方法 , 那么这个 _init 方法哪儿来的呢?
我们看看 _init 函数做了些什么。
initMixin(Vue)
我们现在打开源码 src/core/instance/init.js
Vue.prototype._init = function (options) {
let vm = this;
// 每个vue都会有一个uid
vm._uid = uid++
// 把一些全局的api方法混入到当前实例的$options上面
vm.$options = mergeOptions(vm.constructor.options, options)
//生命周期钩子beforeCreate
callHook(vm, 'beforeCreate')
//初始化状态,props,methods,data,computed,watch
initState(vm)
//初始化成功后调用created钩子
callHook(vm, 'created')
if (vm.$options.el) {
//开始挂载
vm.$mount(vm.$options.el)
}
}
下面我们找到打开 $mount 看看它做了什么
点开源码 src/platforms/web/entry-runtime-with-compiler.js
把一些多余的 代码简化一下。
Vue.prototype.$mount = function (el) {
//根据用户传入的 el 属性获取节点
el = el && document.querySelector(el);
let vm = this;
//把节点放在 vm.$el 上方便后面使用
vm.$el = el;
let options = vm.$options;
let template
/**
* 编译权重:
* 优先看有没有render函数,如果有直接用
* 如果没有render函数就看有没有template模板
* 如果都没有就直接获取el的outerHTML作为渲染模板
*/
if (!options.render) {
if (!options.template) {
template = el.outerHTML
} else {
template = vm.$options.template
}
}
if (template) {
//用 template 生成 render 函数
let render = compileToFunctions(template)
options.render = render
}
//调用 mount 方法开始渲染页面。
return mount(this, el)
}
上面代码主要实现了 vue 渲染过程中很重要的一步,得到 render 函数。
如果我们使用的 template 进行编写HTML代码,vue 内部会把模板编译成 vue 可识别的 render 函数,如果有写 render 则可以省去编译过程。( 直接写 render 函数对 vue 编译效率会更好 )
下面打开源码 src/core/instance/lifecycle.js 找到 mountComponent 方法
export function mountComponent(vm, el) {
//渲染之前调用 beforeMount 生命周期
callHook(vm, 'beforeMount')
//创建一个更新渲染函数 ( 用来得到 Vnode 渲染真实 dom )
let updateComponent = () => {
vm.update(vm._render())
}
//生成一个渲染 watcher 每次页面依赖的数据更新后会调用 updateComponent 进行渲染
new Watcher(vm, updateComponent, () => {},{
before () {
callHook(vm, 'beforeUpdate')
}
},true)
//渲染真实 dom 结束后调用 mounted 生命周期
callHook(vm, 'mounted')
}
这里就是开始准备挂载真实 dom 了,创建了渲染 watcher ,渲染 watcher 内部调用了 updateComponent 方法。
继续往下看打开 Watcher 所在位置 src/core/observer/watcher.js
export class Watcher {
constructor(vm,expOrFn,cb,options) {
if (typeof expOrFn === 'function') {
// 保留 updateComponent 方法
this.getters = expOrFn
}
this.get();
}
get() {
pushTarget(this)
let value
// 这里调用了 updateComponent 方法
value = this.getters.call(this.vm, this.vm);
popTarget()
return value
}
}
vue 初次渲染时 watcher 内部调用了 updateComponent 方法 ( 数据添加依赖我们后面说 )
updateComponent 整个渲染周期最关键的几行。
let updateComponent = () => {
//获取到虚拟 dom 调用 update 进行渲染
vm.update(vm._render())
}
我们进入 vm._render 函数
打开 src/core/instance/render.js
Vue.prototype._render = function () {
let vm = this
// 拿到 render 函数
let render = vm.$options.render;
// 调用 render 函数得到 Vnode
return render.call(vm)
}
调用 render 函数得到 Vnode
接着我们进入 vm.update 函数
找到 vm.update 函数位置 src/core/instance/lifecycle.js
Vue.prototype.update = function (vnode) {
let vm = this
// 获取到上一次的 Vnode 用于 diff 对比
const prevVnode = vm._vnode
if (!prevVnode) {
//首次渲染走这里
vm.$el = patch(vm.$el, vnode)
} else {
//数据更新驱动视图更新走这里
vm.$el = patch(prevVnode, vnode)
}
//保留 Vnode
vm._vnode = vnode
}
进入 patch 方法 src/core/vdom/patch.js
return function patch(el, vnode, hydrating, removeOnly) {
//首次渲染使用 Vnode 创建真实 dom
createElm(vnode, false, el)
return vnode.elm
}
function createElm (
vnode, //虚拟dom
insertedVnodeQueue,
parentElm, //父节点
) {
// 查看元素 tag 是不是组件,如果是组件就创建组件
if (createComponent(vnode, insertedVnodeQueue, parentElm)) {
return
}
const data = vnode.data //得到 data 数据
const children = vnode.children //得到子元素
const tag = vnode.tag //获取标签名
vnode.elm = document.createElement(tag)
if (isDef(tag)) {
//如果有子节点递归渲染子节点
createChildren(vnode, children, insertedVnodeQueue)
//给父元素插入子元素
parentElm.appendChild(elm)
} else if (isTrue(vnode.isComment)) {
//创建注释节点
vnode.elm = document.createComment(vnode.text)
//给父元素插入注释节点
parentElm.appendChild(elm)
} else {
//创建文本节点
vnode.elm = document.createTextNode(vnode.text)
//给父元素插入文本节点
parentElm.appendChild(elm)
}
}
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; ++i) {
//渲染子节点
createElm(children[i], insertedVnodeQueue, vnode.elm)
}
}
}
以上就是 new Vue 的整个流程。
2. 双向数据绑定原理解析
大家可能都知道,关于Vue的双向绑定,很多人都知道,核心是 Object.defineProperty() 方法,那接下来我们就简单介绍一下! 语法: Object.defineProperty(obj, prop, descriptor) 其中: obj 要在其上定义属性的对象。 prop 要定义或修改的属性的名称。 descriptor 将被定义或修改的属性描述符。
其实,简单点来说,就是通过此方法来定义一个值。 调用,使用到了 get 方法, 赋值,使用到了 set 方法。
vue 双向绑定内部核心就是利用了两个类, Dep 类和 watcher 类
每个在页面上使用了的属性、数组、对象都会有一个 Dep 类,访问属性的时候 get 方法会收集对应的 watcher
同样渲染 watcher 也会收集对应的 Dep
vue 内部实现双向绑定过程:简单来说就是初始化 data 的时候会调用 observe 方法给,data 里的属性重写 get 方法和 set 方法,到渲染真实 dom 的时,渲染 watcher 会去访问页面上使用的属性变量,给属性的 Dep 都加上渲染函数,每次修改数据时通知渲染 watcher 更新视图
打开 src/core/observer/index.js
eexport function observe(data) {
// 不是对象或者数组直接 return
if (typeof data !== 'object' || data == null) {
return
}
// 这里的 data 就是创建 vue 传入的 data 属性
return new Observe(data)
}
class Observe{
constructor(value) {
//添加 Dep
this.dep = new Dep()
//用于数组改变了可以获取到 Dep 进行更新视图
Object.defineProperty(value, '__ob__', {
value: this,
enumerable: false,
})
//vue 对数组做了特别的处理 数组重写了一些方法
if (Array.isArray(value)) {
//如果是数组重写数组方法,再进行尝试监听
value.__proto__ = arrayMethods;
//尝试监听数组内部的属性。
this.observeArray(value)
} else {
//递归处理对象
this.walk(value)
}
}
walk(data) {
Object.keys(data).forEach((key, index) => {
defineProperty(data,key,data[key])
})
}
observeArray(value) {
value.forEach(item => {
observe(item)
})
}
}
Dep 类的实现 src/core/observer/dep.js
let id = 0
export class Dep{
constructor() {
this.subs = [];
this.id = id++
}
//给 watcher 添加 Dep
depend() {
Dep.target.addDep(this)
}
//给 dep 添加对应的 watch
addSub(watch) {
this.subs.push(watch)
}
//调用 watcher 里的渲染函数
notify() {
this.subs.forEach((watcher) => {
if (watcher) {
watcher.upDate()
}
})
}
}
const targetStack = []
// 渲染阶段,访问页面上的属性变量时,给对应的 Dep 添加 watcher
export function pushTarget(watcher) {
targetStack.push(watcher)
Dep.target = watcher
}
// 访问结束后删除
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
每个属性、对象、数组上都有一个 Dep 类型,Dep 类主要就是收集用于渲染的 watcher
watcher 类 src/core/observer/watcher.js
export class Watcher {
constructor(vm,expOrFn,cb,options) {
this.vm = vm
this.expOrFn = expOrFn
this.deps = [];
this.set = {}
this.id = id++
if (typeof expOrFn === 'function') {
this.getters = expOrFn
}
this.value = this.get();
}
addDep(dep) {
let id = dep.id
//去重防止 dep 添加 watch 多次
if (!this.set[id]) {
// watcher 添加 dep
this.deps.push(dep)
//重点!给 dep 添加 watch
dep.addSub(this)
this.set[id] = true;
}
}
get() {
//标记 target
pushTarget(this)
// 重点,这里会去访问我们给属性重写的 get 方法,添加 watcher 依赖
this.getters.call(this.vm, this.vm);
//弹出target防止data上每个属性都产生依赖,只有页面上使用的变量需要依赖
popTarget()
return value
}
upDate() {
this.get(this)
}
}
接着看看 defineProperty 方法
function defineProperty(data, key, value) {
//childDep 这个 dep 只会存在 {} 或 [] 里面
let childDep = observe(value)
//这个 dep 会存在每个属性里面
let dep = new Dep();
Object.defineProperty(data, key, {
get() {
//渲染的期间给每个放在页面上的变量添加 watcher
//只有渲染阶段才会 Dep.target ,有正常访问 target 是没有的
if (Dep.target) {
//给属性 dep 添加 watcher
dep.depend()
if (childDep) {
//给属性是数组或者是对象的添加 watcher
childDep.dep.depend()
if (Array.isArray(value)) {
//如果是数组递归数组给数组里面的数组添加 watcher
dependArray(value)
}
}
}
return value
},
set(newValue) {
if(newValue == value)return
//更新数组或者对象的时候也要创建一个新的 dep 给 childDep
childDep = observe(newValue)
value = newValue
//更新视图
dep.notify()
}
})
}
初始化 data 属性时,递归给 data 的属性,重写 get set,同时会给它们身上都添加一个 Dep 类, 渲染阶段 Dep 类会收集 watcher 。每次修改数据会调用 dep.notify() 更新视图
3. 监听数组变化
点开 src/core/observer/array.js
//获取数组的原型Array.prototype,拿到数组原有的方法
const arrayProto = Array.prototype
//创建一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype
export const arrayMethods = Object.create(arrayProto)
//列出需要重写的方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
//遍历列出的方法
methodsToPatch.forEach((key) => {
//重写数组方法
arrayMethods[key] = function (...args) {
// 数组原有的方法
let result = arrayProto[key].apply(this, args);
let inserted
//该数组是响应式的时候,拿到数组上的 __ob__ 属性
let ob = this.__ob__
//处理如果是数组添加的对象或者是数组
switch (key) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
default:
break;
}
if (inserted) ob.observeArray(inserted)
//重点!每个响应式数组上都会有一个 __ob__ 利用我们保留的 __ob__ 属性获取 notify 方法更新视图
ob.dep.notify()
return result
}
})
4. Watch监听的实现
打开 src/core/instance/state.js 找到 initWatch
function initWatch(vm) {
//获取到传入的 watch
let watch = vm.$options.watch
//遍历 watch 列表
for (let key in watch) {
const handler = watch[key];
//可以传入数组
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
//创建 watch
createWatcher(vm, key, handler)
}
}
}
function createWatcher(vm, expOrFn, handler, options) {
// handler 如果是对象取出里面的 handler 函数
if (typeof handler === "object") {
options = handler
handler = handler.handler
}
//如果监听的方法是 methods 里的方法直接取出来
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
Vue.prototype.$watch = function (expOrFn, cb, options) {
let vm = this;
options = options || {};
//标识这个是用户 watch
options.user = true;
//使用 Watcher 类来创建渲染 Watcher
const watcher = new Watcher(vm, expOrFn, cb, options);
}
监听 Watcher 类的实现
export class Watcher {
constructor(vm,expOrFn,cb,options) {
this.vm = vm
this.expOrFn = expOrFn //监听的属性 如:a.b.c
this.cb = cb // watch 回调
if (options) {
this.user = !!options.user //这是个 watch
this.deep = !!options.deep //深度监听
}
this.deps = [];
this.set = {}
this.id = id++
function parsePath(path) {
path = path.split('.')
return function (obj) {
path.forEach((key) => {
obj = obj[key]
})
return obj
}
}
if (typeof expOrFn === 'function') {
this.getters = expOrFn
} else {
//访问监听的变量 如:a.b.c.d
this.getters = parsePath(this.expOrFn)
}
//留住 value
this.value = this.get();
}
get() {
//深度访问对象内部每一个值
function traverse(val) {
let isA = Array.isArray(val);
if (!isA) {
let key = Object.keys(val);
let i = key.length
while (i--) {
traverse(val[key])
}
}
}
//标记target
pushTarget(this)
//访问监听的属性
let value = this.getters.call(this.vm, this.vm);
if (this.deep) {
traverse(value)
}
//弹出target防止data上每个属性都产生依赖,只有页面上使用的变量需要依赖
popTarget()
return value
}
run() {
let newValue = this.get()
//取出旧值
const oldValue = this.value
//留住新值
this.value = newValue
//用户自己传入的watch
if (this.user) {
//这里的 cb 就是传入的 watch 回调函数
this.cb.call(this.vm, newValue, oldValue)
}
}
addDep(dep) {
let id = dep.id
//去重防止dep添加watch多次
if (!this.set[id]) {
//watcher添加dep
this.deps.push(dep)
//给dep添加watch
dep.addSub(this)
this.set[id] = true;
}
}
upDate() {
this.run()
}
}
核心也是利用 watcher 和 dep 两个类来实现的。区别就是这次 watcher 保留的是用户传入的 watch 回调函数,依赖发生更新时调用函数,传入新值和旧值if (this.user) { this.cb.call(this.vm, newValue, oldValue) }
5. computed实现
打开 src/core/instance/state.js 找到 initComputed
function initComputed(vm) {
//获取传入的 computed
let computed = vm.$options.computed;
//创建一个 watchers 空对象
const watchers = vm._computedWatchers = Object.create(null)
//遍历 computed
for (const key in computed) {
//拿到计算属性方法
const userDef = computed[key];
// 创建 Watcher lazy:true 默认不执行 ,看是否需要重新计算,computed 是有缓存的
watchers[key] = new Watcher(vm, userDef, ()=>{}, {lazy:true});
//监听 computed 方法
defineComputed(vm, key, userDef)
}
}
let sharedPropertyDefinition = {}
function defineComputed(target, key, userDef) {
if (typeof userDef === 'function') {
//传入的是方法走这里
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = ()=>{}
} else {
//也可以传入一个对象,有 set 方法
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = userDef.set
}
//包装计算属性的方法名,给 vm 上添加 computed 方法
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter(key) {
//重点!模板上访问计算属性才走这里
return function () {
// 取出创建的 computedWatchers
let watch = this._computedWatchers[key];
// dirty = lazy 默认是 true
if (watch.dirty) {
//渲染的时候会进入这里
watch.evaluate()
}
//此时渲染还未结束 Dep.target = 渲染 watcher,computed 函数内部的变量收集渲染 watcher
if (Dep.target) {
watch.depend()
}
//计算好的参数返回给用户
return watch.value
}
}
计算属性 watcher 类实现
export class Watcher {
constructor(vm,expOrFn,cb,options) {
this.vm = vm
this.expOrFn = expOrFn //传入的 computed 方法
this.cb = cb
if (options) {
this.lazy = !!options.lazy // true 默认不执行 这是一个 computed
}
this.dirty = this.lazy// computed 看是否需要从新求值
this.deps = [];
this.set = {}
this.id = id++
if (typeof expOrFn === 'function') {
this.getters = expOrFn
}
//留住value
this.value = this.lazy? undefined : this.get();
}
get() {
//标记 computed target
pushTarget(this)
// 调用 computed 函数得到计算的值
let value = this.getters.call(this.vm, this.vm);
//弹出 target 防止data上每个属性都产生依赖,只有页面上使用的变量需要依赖
popTarget()
return value
}
run() {
let newValue = this.get()
}
addDep(dep) {
let id = dep.id
//去重防止dep添加watch多次
if (!this.set[id]) {
//watcher添加dep
this.deps.push(dep)
//给dep添加watch
dep.addSub(this)
this.set[id] = true;
}
}
upDate() {
//修改属性计算属性依赖的变量重置 dirty
if (this.lazy) {
this.dirty = true
}
}
evaluate() {
//当走到这里时,页面正在渲染中 Dep.target 已经有一个渲染 watcher 了
this.value = this.get();
//修改了计算属性里面脏值
this.dirty = false
}
depend () {
//给 computed 函数内部的属性添加渲染 watcher
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
重点:计算属性方法内部变量的 Dep 上会有两个 watcher 分别是是计算属性 wathcer 和渲染 watcher,计算属性 watcher 的作用只需要控制是否需要重新计算,跟着调用依赖的渲染 watcher 重新计算属性
6. nextTick原理
打开 src/core/util/next-tick.js
const callbacks = []//储存 nextTick 回调函数
let pending = false //只开启一个 timerFunc
let timerFunc // 定时函数
/*
vue的降级策略(兼容)promise -> MutationObserver -> setImmediate -> setTimeout
原理:利用异步队列
在每个 macro-task 运行完以后,UI 都会重渲染,那么在 miscro-task (异步事件回调) 中就完成数据更新,当前 次事件循环 结束就可以得到最新的 UI 了。反之如果新建一个 macro-task 来做数据更新,那么渲染就会进行两次。
*/
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//如果支持promise,使用promise实现
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
} else if (!isIE && typeof MutationObserver !== 'undefined') {
//如果Promise不支持,但支持MutationObserver
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
//改变 counter 属性触发 flushCallbacks
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
//上面两种都不支持,用setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
//都不支持使用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick (cb, ctx) {
let _resolve
//压入 nextTick 回调,可存入多个
callbacks.push(() => {
cb && cb.call(ctx)
})
//开起定时函数
if (!pending) {
pending = true
timerFunc()
}
}
function flushCallbacks () {
pending = false
//复制 callback
const copies = callbacks.slice(0)
//清除 callback
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
//以此调用 nextTick 回调
copies[i]()
}
}
vue用异步队列的方式来控制DOM更新和nextTick回调先后执行
micro-task因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
因为兼容性问题,vue不得不做了microtask向macrotask的降级方案
7. Vue.extend原理
打开 src/core/global-api/extend.js
export function initExtend (Vue) {
// 每一个组件实例都有一个唯一的 cid
Vue.cid = 0
let cid = 1
// Vue.extend 方法实体
Vue.extend = function (extendOptions) {
// 用户传进来的参数,包含组件实例的对象
extendOptions = extendOptions || {}
// 将 Super 指向父类 this ,也就是 Vue
const Super = this
// SuperId 使用父类的唯一cid
const SuperId = Super.cid
// 创建缓存,有缓存直接返回缓存
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
// 组件实例 name
const name = extendOptions.name || Super.options.name
//子组件的构造函数
const Sub = function VueComponent (options) {
this._init(options)
}
// 通过create继承父类原型
Sub.prototype = Object.create(Super.prototype)
// 修改子类 constructor 指向自己
Sub.prototype.constructor = Sub
// 创建子类唯一 cid
Sub.cid = cid++
// 合并父类 options 和子类自有 options
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// 子类有props,初始化子类props,并创建监听
if (Sub.options.props) {
initProps(Sub)
}
// 子类有 computed,初始化 computed,并创建监听,流程和上边 computed 实现一样
if (Sub.options.computed) {
initComputed(Sub)
}
//用于递归组件
if (name) {
//给组件的components添加上组件自己
Sub.options.components[name] = Sub
}
// 上边说的缓存
cachedCtors[SuperId] = Sub
// 返回子类 , 实现官方案例那样,new一个实例,通过 $mount 挂载 。
return Sub
}
}
简单来说就是基于 Vue 构造函数,创建一个子类,然后继承父类的参数和方法,最后再返回这个子类,每个子组件都是一个 Sub 构造函数。子组件创建流程和 new Vue初始化流程 区别不大。
子组件创建过程中没有 el ,vue 渲染组件的时候内部自动调用 child.$mount(undefined) 不会挂载到页面上,而是放在 vnode.componentInstance.el 上,通过父组件压入到页面上。