视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
Vue源码之文件结构与运行机制
2020-11-27 19:34:03 责编:小采
文档


可以看到这里 rollup 去运行 scripts/config.js 文件,并且给了个参数 TARGET:web-full-dev,那来看看 scripts/config.js 里面是啥

// scripts/config.js

const builds = {
 '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 // 每个包前面的注释-版本/作者/日期.etc }, }

format 编译方式说明:
es:ES Modules,使用ES6的模板语法输出
cjs:CommonJs Module,遵循CommonJs Module规范的文件输出
amd:AMD Module,遵循AMD Module规范的文件输出
umd:支持外链规范的文件输出,此文件可以直接使用script标签

这里的 web-full-dev 就是对应刚刚我们在命令行里传入的命令,那么 rollup 就会按下面的 entry 入口文件开始去打包,还有其他很多命令和其他各种输出方式和格式可以自行查看一下源码。

因此本文主要的关注点在包含 compiler 编译器的 src/platforms/web/entry-runtime-with-compiler.js 文件,在生产和开发环境中我们使用 vue-loader 来进行 template 的编译从而不需要带 compiler 的包,但是为了更好的理解原理和流程还是推介从带 compiler 的入口文件看起。

先看看这个文件,这里导入了个 Vue ,看看它从哪来的

// src/platforms/web/entry-runtime-with-compiler.js

import Vue from './runtime/index'

继续看

// src/platforms/web/runtime/index.js

import Vue from 'core/index'

keep moving

// src/core/index.js

import Vue from './instance/index'

keep moving*2

// src/core/instance/index.js

/* 这里就是vue的构造函数了,不用ES6的Class语法是因为mixin模块划分的方便 */
function Vue(options) {
 this._init(options) // 初始化方法,位于 initMixin 中
}

// 下面的mixin往Vue.prototype上各种挂载
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

当我们 new Vue( ) 的时候,实际上调用的就是这个构造函数,可以从这里开始看了。

3. 运行机制

这里我用xmind粗略的画了一张运行机制图,基本上后面的分析都在这张图上面的某些部分了

本文 Vue 实例都是用 vm 来表示

上面这个图可以分为多个部分细加阅读,具体的实现我们在后面的文章中详细讨论,这里先贴一部分源码尝尝鲜

3.1 初始化 _init( )

当我们在 main.js 里 new Vue( ) 后,Vue 会调用构造函数的 _init( ) 方法,这个方法是位于 core/instance/index.js 的 initMixin( ) 方法中定义的

// src/core/instance/index.js

/* 这里就是Vue的构造函数 */
function Vue(options) {
 this._init(options) // 初始化方法,位于 initMixin 中
}

// 下面的mixin往Vue.prototype上各种挂载,这是在加载的时候已经挂载好的
initMixin(Vue) // 给Vue.prototype添加:_init函数,...
stateMixin(Vue) // 给Vue.prototype添加:$data属性, $props属性, $set函数, $delete函数, $watch函数,...
eventsMixin(Vue) // 给Vue.prototype添加:$on函数, $once函数, $off函数, $emit函数, $watch方法,...
lifecycleMixin(Vue) // 给Vue.prototype添加: _update方法, $forceUpdate函数, $destroy函数,...
renderMixin(Vue) // 给Vue.prototype添加: $nextTick函数, _render函数,...

export default Vue

我们可以看看 init( ) 这个方法到底进行了哪些初始化:

// src/core/instance/index.js

Vue.prototype._init = function(options?: Object) {
 const vm: Component = this

 initLifecycle(vm) // 初始化生命周期 src/core/instance/lifecycle.js
 initEvents(vm) // 初始化事件 src/core/instance/events.js
 initRender(vm) // 初始化render src/core/instance/render.js
 callHook(vm, 'beforeCreate') // 调用beforeCreate钩子
 initInjections(vm) // 初始化注入值 before data/props src/core/instance/inject.js
 initState(vm) // 挂载 data/props/methods/watcher/computed
 initProvide(vm) // 初始化Provide after data/props
 callHook(vm, 'created') // 调用created钩子

 if (vm.$options.el) { // $options可以认为是我们传给 `new Vue(options)` 的options
 vm.$mount(vm.$options.el) // $mount方法
 }
}

这里 _init() 方法中会对当前 vm 实例进行一系列初始化设置,比较重要的是初始化 State 的方法 initState(vm) 的时候进行 data/props 的响应式化,这就是传说中的通过 Object.defineProperty() 方法对需要响应式化的对象设置 getter/setter,以此为基础进行依赖搜集(Dependency Collection),达到数据变化驱动视图变化的目的。

最后检测 vm.$options 上面有没有 el 属性,如果有的话使用 vm.$mount 方法挂载 vm,形成数据层和视图层的联系。这也是如果没有提供 el 选项就需要自己手动 vm.$mount('#app') 的原因。

我们看到 created 钩子是在挂载 $mount 之前调用的,所以我们在 created 钩子触发之前是无法操作 DOM 的,这是因为还没有渲染到 DOM 上。

3.2 挂载 $mount( )

挂载方法 vm.$mount( ) 在多个地方有定义,是根据不同打包方式和平台有关的,src/platform/web/entry-runtime-with-compiler.jssrc/platform/web/runtime/index.jssrc/platform/weex/runtime/index.js,我们的关注点在第一个文件,但在 entry-runtime-with-compiler.js 文件中会首先把 runtime/index.js 中的 $mount 方法保存下来,并在最后用 call 运行:

// src/platform/web/entry-runtime-with-compiler.js

const mount = Vue.prototype.$mount // 把原来的$mount保存下来,位于 src/platform/web/runtime/index.js
Vue.prototype.$mount = function(
 el?: string | Element, // 挂载的元素
 hydrating?: boolean // 服务端渲染相关参数
): Component {
 el = el && query(el)
 
 const options = this.$options
 if (!options.render) { // 如果没有定义render方法
 let template = options.template
 
 // 把获取到的template通过编译的手段转化为render函数
 if (template) {
 const { render, staticRenderFns } = compileToFunctions(template, {...}, this)
 options.render = render
 }
 }
 return mount.call(this, el, hydrating) // 执行原来的$mount
}

在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法。这里的 compileToFunctions 就是把 template 编译为 render 的方法,后面会介绍。

// src/platform/weex/runtime/index.js

Vue.prototype.$mount = function (
 el?: string | Element, // 挂载的元素
 hydrating?: boolean // 服务端渲染相关参数
): Component {
 el = el && inBrowser ? query(el) : undefined // query就是document.querySelector方法
 return mountComponent(this, el, hydrating) // 位于core/instance/lifecycle.js
}

这里的 el 一开始如果不是DOM元素的话会被 query 方法换成DOM元素再被传给 mountComponent 方法,我们继续看 mountComponent 的定义:

// src/core/instance/lifecycle.js

export function mountComponent (
 vm: Component,
 el: ?Element,
 hydrating?: boolean
): Component {
 vm.$el = el
 if (!vm.$options.render) {
 vm.$options.render = createEmptyVNode
 }
 callHook(vm, 'beforeMount') // 调用beforeMount钩子

 // 渲染watcher,当数据更改,updateComponent作为Watcher对象的getter函数,用来依赖收集,并渲染视图
 let updateComponent
 updateComponent = () => {
 vm._update(vm._render(), hydrating)
 }

 // 渲染watcher, Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数
 // ,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数
 new Watcher(vm, updateComponent, noop, {
 before () {
 if (vm._isMounted) {
 callHook(vm, 'beforeUpdate') // 调用beforeUpdate钩子
 }
 }
 }, true /* isRenderWatcher */)

 // 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例
 if (vm.$vnode == null) {
 vm._isMounted = true // 表示这个实例已经挂载
 callHook(vm, 'mounted') // 调用mounted钩子
 }
 return vm
}

mountComponent 方法里实例化了一个渲染 Watcher,并且传入了一个 updateComponent ,这个方法:() => { vm._update(vm._render(), hydrating) } 首先使用 _render 方法生成 VNode,再调用 _update 方法更新DOM。可以看看视图更新部分的介绍

这里调用了几个钩子,他们的时机可以关注一下。

3.3 编译 compile( )

如果在需要转换 render 的场景下,比如我们写的 template ,将会被 compiler 转换为 render 函数,这其中会有几个步骤组成:

入口位于刚刚 src/platform/web/entry-runtime-with-compiler.js 的 compileToFunctions 方法:

// src/platforms/web/compiler/index.js

const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }

继续看这里的 createCompiler 方法:

// src/compiler/index.js

export const createCompiler = createCompilerCreator(function baseCompile (
 template: string,
 options: CompilerOptions
): CompiledResult {
 const ast = parse(template.trim(), options)
 if (options.optimize !== false) {
 optimize(ast, options)
 }
 const code = generate(ast, options)
 return {
 ast,
 render: code.render,
 staticRenderFns: code.staticRenderFns
 }
})

这里可以看到有三个重要的过程 parseoptimizegenerate,之后生成了 render 方法代码。

  • parse:会用正则等方式解析 template 模板中的指令、class、style等数据,形成抽象语法树 AST

  • optimize:优化AST,生成模板AST树,检测不需要进行DOM改变的静态子树,减少 patch 的压力

  • generate:把 AST 生成 render 方法的代码

  • 3.4 响应式化 observe( )

    Vue作为一个MVVM框架,我们知道它的 Model 层和 View 层之间的桥梁 ViewModel 是做到数据驱动的关键,Vue的响应式是通过 Object.defineProperty 来实现,给被响应式化的对象设置 getter/setter ,当 render 函数被渲染的时候会触发读取响应式化对象的 getter 进行依赖收集,而在修改响应式化对象的时候会触发设置 settersetter 方法会 notify 它之前收集到的每一个 watcher 来告诉他们自己的值更新了,从而触发 watcherupdatepatch 更新视图。

    响应式化的入口位于 src/core/instance/init.js 的 initState 中:

    // src/core/instance/state.js
    
    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 /* asRootData */)
     }
     if (opts.computed) initComputed(vm, opts.computed)
     if (opts.watch && opts.watch !== nativeWatch) {
     initWatch(vm, opts.watch)
     }
    }

    它非常规律的定义了几个方法来初始化 propsmethodsdatacomputedwathcer,这里只看 initData 方法,来窥一豹

    // src/core/instance/state.js
    
    function initData(vm: Component) {
     let data = vm.$options.data
     data = vm._data = typeof data === 'function'
     ? getData(data, vm)
     : data || {}
     
     observe(data, true /* asRootData */) // 给data做响应式处理
    }

    首先判断了下 data 是不是函数,是则取返回值不是则取自身,之后有一个 observe 方法对 data 进行处理,看看这个方法

    // src/core/observer/index.js
    
    export function observe (value: any, asRootData: ?boolean): Observer | void {
     let ob: Observer | void
     ob = new Observer(value)
     return ob
    }

    这个方法主要用 data 去实例化一个 Observer 对象实例,Observer 是一个 Class,Observer 的构造函数使用 defineReactive 方法给对象的键响应式化,它给对象的属性递归添加 getter/setter,用于依赖收集和 notify 更新,这个方法大概是这样的

    // src/core/observer/index.js
    
    function defineReactive (obj, key, val) {
     Object.defineProperty(obj, key, {
     enumerable: true,
     configurable: true,
     get: function reactiveGetter () {
     /* 进行依赖收集 */
     return val;
     },
     set: function reactiveSetter (newVal) {
     if (newVal === val) return;
     notify(); // 触发更新
     }
     });
    }

    3.5 视图更新 patch( )

    当使用 defineReactive 方法将对象响应式化后,当 render 函数被渲染的时候,会读取响应化对象的 getter 从而触发 getter 进行 watcher 依赖的收集,而在修改响应化对象的值的时候,会触发 setter 通知 notify 之前收集的依赖,通知自己已被修改,请按需重新渲染视图。被通知的 watcher 调用 update 方法去更新视图,位于上面介绍过的传递给 new Watcher( )updateComponent 方法中,这个方法会调用 update 方法去 patch 更新视图。

    // src/core/instance/lifecycle.js
    
    let updateComponent
    updateComponent = () => {
     vm._update(vm._render(), hydrating)
    }
    
    // 渲染watcher, Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数
    // ,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数
    new Watcher(vm, updateComponent, noop, {...}, true /* isRenderWatcher */)

    这个 _render 方法生成虚拟 Node, _update 方法中的会将新的 VNode 与旧的 VNode 一起传入 patch

    // src/core/instance/lifecycle.js
    
    Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) { // 调用此方法去更新视图
     const vm: Component = this
     const prevVnode = vm._vnode
     vm._vnode = vnode
    
     if (!prevVnode) {
     // 初始化
     vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
     } else {
     //更新
     vm.$el = vm.__patch__(prevVnode, vnode)
     }
    }

    _update 调用 __patch__ 方法,它主要是对新老 VNode 进行比较 patchVnode,经过 diff 算法得出它们的差异,最后这些差异的对应 DOM 进行更新。

    下载本文
    显示全文
    专题