Vue源码解读(个人见解 + 网友理解)

【个人总结】Vue源码解读

前言

作为一名前端开发攻城狮来讲,我觉得学习源码是必要的。当时所在的公司主要是使用Vue来做开发的,在那里面呆了两年多,主要是摸鱼过去的,Vue我自认为使用的算是熟练了,但是面试的时候被问到源码有关的问题,我还是回答不出来。以上面的背景未前提,我花了一个多星期的时间去浏览网上各种对Vue源码解析的文章再加上我自己的理解写下了以下文章,可能我的理解不一定是对的,大家可以适当参考下。

目录解说

源码方面我是在github下载的2.6版本
Vue2.6源码传送门
Vue源码根目录下有很多个文件夹,下面我对一些我知道的文件夹做出注释
目录解说

Vue的实例

因为我们在使用Vue的时候通常都是new Vue(),并且通过npm run dev或其他命令启动Vue项目,所以我们根据这两个特点,从package.json文件中找到Vue项目的启动命令
package.json

由命令可见,这里用到了rollup,百度下可知这是个类似webpack的构建工具,但这个不是重点,我们发现它执行了scripts/config.js文件。现在看下scripts/config.js文件:

const builds = {
    ......  
    ......  
    ......  
    // 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
    },  
    ......  
    ......  
    ......  
}

function getConfig(name) {
    const opts = builds[name]
    const config = {
        input: opts.entry,
        external: opts.external,
        plugins: [
            replace({
                __WEEX__: !!opts.weex,
                __WEEX_VERSION__: weexVersion,
                __VERSION__: version,
            }),
            flow(),
            alias(Object.assign({}, aliases, opts.alias)),
        ].concat(opts.plugins || []),
        output: {
            file: opts.dest,
            format: opts.format,
            banner: opts.banner,
            name: opts.moduleName || "Vue",
        },
        onwarn: (msg, warn) => {
            if (!/Circular/.test(msg)) {
                warn(msg);
            }
        },
    };
    
    if (opts.env) {
        config.plugins.push(replace({
            'process.env.NODE_ENV': JSON.stringify(opts.env)
        }))
    }
    
    if (opts.transpile !== false) {
        config.plugins.push(buble())
    }
    
    Object.defineProperty(config, '_name', {
        enumerable: false,
        value: name
    })
    
    return config
}

if (process.env.TARGET) {
    module.exports = genConfig(process.env.TARGET)
} else {
    exports.getBuild = genConfig
    exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

在这段代码中,我们首先看最后一段代码,我们可以看到它调用了genConfig(process.env.TARGET),看向getConfig方法,不难得知这个方法是用于匹配build对象里面的参数并生成rollup配置,在package.json文件中我们看到 dev 中有 TARGET:web-full-dev ,所以我们在build对象中找到 web-full-dev ,根据resolve处的代码结合

const aliases = require('./alias')
const resolve = p => {
    const base = p.split('/')[0]
    if (aliases[base]) {
        return path.resolve(aliases[base], p.slice(base.length + 1))
    } else {
        return path.resolve(__dirname, '../', p)
    }
}

在这里找到./alias文件

const path = require('path')

const resolve = p => path.resolve(__dirname, '../', p)

module.exports = {
    vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
    compiler: resolve('src/compiler'),
    core: resolve('src/core'),
    shared: resolve('src/shared'),
    web: resolve('src/platforms/web'),
    weex: resolve('src/platforms/weex'),
    server: resolve('src/server'),
    entries: resolve('src/entries'),
    sfc: resolve('src/sfc')
}

最终可以确认入口文件为 src/platforms/web/entry-runtime-with-compiler
打开 src/platforms/web/entry-runtime-with-compiler 文件

import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'

import Vue from './runtime/index'   // 引入vue
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'

const idToTemplate = cached(id => {
    const el = query(id)
    return el && el.innerHTML
})

const mount = Vue.prototype.$mount  // 保存mount方法

// 把mount方法挂载到原型上
Vue.prototype.$mount = function (
    el?: string | Element,
    hydrating?: boolean
): Component {
    el = el && query(el)

    /* istanbul ignore if */
    // 判断el,el不能挂载到body上
    if (el === document.body || el === document.documentElement) {
        process.env.NODE_ENV !== 'production' && warn(
            `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
        )
        return this
    }
    
    const options = this.$options // 保存传递进来的options
    
    // resolve template/el and convert to render function
    if (!options.render) {
        ......  
        ......  
        ......
    }
    return mount.call(this, el, hydrating)
}

/**
 *  Vue为了解决获取DOM元素问题的方法,因为我们获取DOM节点时,很多时候不仅仅要获取该DOM节点里面包括的HTML代码,还**  需要获取他本身,但是当该DOM外层不存在元素包裹,例如文件节点,那么就会返回undefine,所以vue使用这一方法来解决 *  这个问题
 */
function getOuterHTML (el: Element): string {
    // 存在最外层元素则直接返回
    if (el.outerHTML) {
        return el.outerHTML
    } else {
        // 不存在则先创建一个DIV,然后往DIV中添加el,最终返回的就是DIv包裹着的el代码
        const container = document.createElement('div')
        container.appendChild(el.cloneNode(true))
        return container.innerHTML
    }
}

Vue.compile = compileToFunctions

export default Vue

由此可见该文件主要是在vueu原型上挂载了mount方法以及对el进行判断处理,所以我们根据 import Vue from ‘./runtime/index’ 继续往下寻找Vue实例,打开 ./runtime/index 文件

import Vue from 'core/index'    // 引入vue
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'

......  
......  
......  

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
Vue.prototype.$mount = function (
    el?: string | Element,
    hydrating?: boolean
): Component {
    el = el && inBrowser ? query(el) : undefined
    return mountComponent(this, el, hydrating)
}

在这个文件中并没有看到Vue实例方法,这里只是对 __patch__方法$mount方法 进行处理,我们继续往下找,打开 src/core/index 文件

import Vue from './instance/index' // 引入vue
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

initGlobalAPI(Vue)

Object.defineProperty(Vue.prototype, '$isServer', {
    get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
    get () {
        /* istanbul ignore next */
        return this.$vnode && this.$vnode.ssrContext
    }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
    value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue

由Object.defineProperty可以得知,这个文件主要是处理数据相应,因为Vue数据绑定的原理主要是通过Object.defineProperty进行数据劫持和使用观察者模式,完成发布订阅。既然这里没找到,那我们再看 ./instance/index 文件

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')
    }
    this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

Mixin的解说

在这个文件,我们找到了Vue实例对象以及在实例对象上挂载了不同的方法。需要了解上面方法的话打开相对应的文件就行。

  • initMixin()
    具体代码在 src/core/instance/init.js,主要是在Vue原型上面挂载_init方法,这个方法是Vue一个内部初始方法
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ... _init 方法的函数体,此处省略
  }
}

当我们调用 new Vue() 的时候会执行this._init(options)

  • stateMixin()
    在这里我们主要看 stateMixin 方法里面这段代码
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function (newData: Object) {
        warn(
            'Avoid replacing instance root $data. ' +
            'Use nested data properties instead.',
            this
        )
    }
    propsDef.set = function () {
        warn(`$props is readonly.`, this)
    }
}
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)

这里的代码很熟悉,后面两句定义了 d a t a < / f o n t > ∗ ∗ 和 ∗ ∗ < f o n t c o l o r = r e d > data</font>** 和 **<font color=red> data</font><fontcolor=red>props 两个属性,而这两个属性分别写在了 dataDefpropsDef 两个对象上,从上面代码能看到 dataDef.getpropsDef.get 分别代理了 _data_props 两个属性,然后就是一个生产环境的判断,当是生产环境时,就为他们设置set,实际上就是想说他们两个都是只读属性

  • renderMixin()
export function renderMixin (Vue: Class<Component>) {
    installRenderHelpers(Vue.prototype)
    
    Vue.prototype.$nextTick = function (fn: Function) {
        return nextTick(fn, this)
    }
    
    Vue.prototype._render = function (): VNode {
        ......  
        ......  
        ......
    }
}

上面代码很清楚写着,renderMixin 调用得时候首先执行 installRenderHelpers 方法,这个方法是与该文件同级的 ./render-helpers/index当中,这个方法主要在Vue原型上添加了一系列方法(详情可以去看文件),然后分别往原型上面挂载 $nextTick_render 方法

  • eventsMixin()
    eventsMixin,字面上意思就可以得知这是一个有关事件处理的方法,在这个方法中主要在原型上面挂载了4个方法
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
  • lifecycleMixin()
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}

这三个方法想必用Vue的都很熟悉,而lifecycleMixin这个方法主要是把这三个方法挂载到原型上面

从上面的解释可以得出,这里每个Mixin大概都是对Vue.Prototype(Vue原型)的一些包装,在其上面挂载一些属性和方法

Vue生命周期钩子

在讲解之前先放上一张图
生命周期

我们Vue的生命周期钩子函数严格来讲有10个:

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeDestroy
  • destroyed
  • activated
  • deactivate

大家都应该很了解这几个函数的作用,现在我们从源码角度上总结他们的调用时机:

  • beforeCreate
    在寻找Vue实例时我们就发现,在new Vue()创建实例开始,执行this._init()方法的时候,初始化生命周期,各种事件和渲染,
    接着调用beforeCreate,这时有关组件、数据的属性还没初始化,在这个阶段获取这些是无法成功的
// src/core/instance/init.js 第52行 ~ 55行
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
  • created
    调用beforeCreate之后,继续往属性注入、状态等等,然后调用created,这是数据可以被访问但是页面还没开始渲染,在这一步只适合做一些数据初始化的操作,完成这一步就开始进入页面渲染流程
// src/core/instance/init.js 第56行 ~ 59行
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
  • beforeMount
    页面渲染流程稍微复杂,从代码上看,执行完created函数后紧接着是执行$mount()
// src/core/instance/init.js 第68行 ~ 70行
if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}

但是 m o u n t 他 是 根 据 平 台 的 不 同 需 求 定 义 的 , 在 w e b 中 , 执 行 mount他是根据平台的不同需求定义的,在web中,执行 mountwebmount方法的时候开始装载组件,具体内容在 src/platforms/runtime/index.js

// 36 ~ 43 行
Vue.prototype.$mount = function (
    el?: string | Element,
    hydrating?: boolean
): Component {
    el = el && inBrowser ? query(el) : undefined
    return mountComponent(this, el, hydrating)
}

在这里执行 mountComponent 方法,该方法在最初渲染时就执行了beforeMount,然后调用updateComponent来渲染视图

// src/core/instance/lifecycle.js   第141 ~ 213行
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
    ......  
    ......  
    ......  
    callHook(vm, 'beforeMount')
    
    let updateComponent
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        updateComponent = () => {
            const name = vm._name
            const id = vm._uid
            const startTag = `vue-perf-start:${id}`
            const endTag = `vue-perf-end:${id}`

            mark(startTag)
            const vnode = vm._render()
            mark(endTag)
            measure(`vue ${name} render`, startTag, endTag)

            mark(startTag)
            vm._update(vnode, hydrating)
            mark(endTag)
            measure(`vue ${name} patch`, startTag, endTag)
        }
    } else {
    updateComponent = () => {
        vm._update(vm._render(), hydrating)
    }
  }
  ......  
  ......  
  ......  
}
  • mounted
    在视图渲染完成时,mounted会被调用,在这个时候观察器系统将监控所有的数据,执行数据更新并重新渲染视图
  • beforeUpdate
    在观察期系统的作用下,当数据更新时就会调用beforeUpdate函数
  • updated
    当数据更新并且视图重新渲染完成后就会调用updated
  • beforeDestroy 与 destroyed
    这两个钩子函数执行的是生命周期的最后阶段也就是销毁,在销毁之前执行beforeDestroy清楚所有的数据、引用、观察期、监听器等等,然后执行destroyed宣告生命周期终结
  • activated 与 deactivated
    这两个钩子函数比较特殊,他是在只有使用keep-alive的组件才有效,分别在组件激活和切换组件时触发,在keep-alive模式中,切换组件时其他钩子函数不会触发,如果需要做操作,这时就需要用到这两个钩子函数

Vue响应式原理

在开始之前先放上官方的一张图
vue响应式

官方文档(深入响应式原理)中有说到:

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因

响应式主要做了数据劫持、依赖收集、派发更新这三件事:

  • 数据劫持:new Vue()的时候遍历data对象,通过Object.defineProperty()给所有属性添加 gettersetter
  • 依赖收集:渲染的过程中,触发数据 getter ,在 getter 的时候把当前 watcher 对象收集起来
  • 派发更新:setter 的时候,遍历这个数据的watcher对象,进行数据更新

数据劫持

当我们new Vue()时,会触发initState方法,在这个方法里

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) { // 存在data对象调用initData方法,不存在对空对象做处理
        initData(vm)
    } else {
        observe(vm._data = {}, true /* asRootData */)
    }
    if (opts.computed) initComputed(vm, opts.computed)
    if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
    }
}
......  
......  
......  
function initData (vm: Component) {
    let data = vm.$options.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 */)
}

在上面这段代码大家可以看到,主要起作用的是 observe(data, true / asRootData /) 这句话,那么现在我们来看下这个方法,src/core/observe/index.js

export class Observer {
    ......  
    ......  
    ......  
    /**
     * Walk through all properties and convert them into
     * getter/setters. This method should only be called when
     * value type is Object.
     */
    // 遍历所有的属性并调用defineReactive方法对他们进行处理
    walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i])
        }
    }
    ......  
    ......  
    ......
}
......  
......  
......  
export function defineReactive (
    obj: Object,
    key: string,
    val: any,
    customSetter?: ?Function,
    shallow?: boolean
) {
    ......  
    ......  
    ......  
    Object.defineProperty(obj, key, {
        enumerable: true, // 属性可枚举
        configurable: true, // 属性可修改删除
        get: function reactiveGetter () {
            const value = getter ? getter.call(obj) : val
            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
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            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() // 派发更新
        }
  })
}

defineReactive 这个方法主要是使用 Object.defineProperty 给属性添加 gettersetter

收集依赖与派发更新

Vue使用了一个Dep(订阅者),它用来存放我们的观察者对象,当数据发生改变时,就通知观察器,观察器调用自己的update方法完成更新

// Dep在 "src/core/observer/dep.js"
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) // 在收集依赖得时候会往队列中添加watcher对象
        }
    }
    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)
        }
        // 更新时遍历watche进行更新
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}

// watcher在 "src/core/observer/watcher.js"
export default class Watcher {
    ......  
    ......  
    ......  
    /**
     * Subscriber interface.
     * Will be called when a dependency changes.
     */
    update () {
        /* istanbul ignore else */
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
            this.run()
        } else {
            queueWatcher(this)
        }
    }
    ......  
    ......  
    ......
}

虚拟DOM

什么是虚拟DOM?

虚拟DOM简称VDOM,可以把它看作是由javaScript模拟出来DOM结构的属性结构,这个树结构包含整个DOM的结构信息

为什么使用虚拟DOM

虚拟DOM就是为了解决浏览器性能问题而被设计出来的,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制

有关虚拟DOM的文件在 “src/sore/vdom” 里面# 【个人总结】Vue源码解读

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值