【转】Vue2,前端高分面试指南

19 阅读11分钟

下面是我在学习HTML和CSS的时候整理的一些笔记,有兴趣的可以看下:

HTML、CSS部分截图

进阶阶段

进阶阶段,开始攻 JS,对于刚接触 JS 的初学者,确实比学习 HTML 和 CSS 有难度,但是只要肯下功夫,这部分对于你来说,也不是什么大问题。

JS 内容涉及到的知识点较多,看到网上有很多人建议你从头到尾抱着那本《JavaScript高级程序设计》学,我是不建议的,毕竟刚接触 JS 谁能看得下去,当时我也不能,也没那样做。

我这部分的学习技巧是,增加次数,减少单次看的内容。就是说,第一遍学习 JS 走马观花的看,看个大概,去找视频以及网站学习,不建议直接看书。因为看书看不下去的时候很打击你学下去的信心。

然后通过一些网站的小例子,开始动手敲代码,一定要去实践、实践、实践,这一遍是为了更好的去熟悉 JS 的语法。别只顾着来回的看知识点,眼高手低可不是个好习惯,我在这吃过亏,你懂的。

1、JavaScript 和 ES6

在这个过程你会发现,有很多 JS 知识点你并不能更好的理解为什么这么设计,以及这样设计的好处是什么,这就逼着让你去学习这单个知识点的来龙去脉,去哪学?第一,书籍,我知道你不喜欢看,我最近通过刷大厂面试题整理了一份前端核心知识笔记,比较书籍更精简,一句废话都没有,这份笔记也让我通过跳槽从8k涨成20k。

JavaScript部分截图

2、前端框架

前端框架太多了,真的学不动了,别慌,其实对于前端的三大马车,Angular、React、Vue 只要把其中一种框架学明白,底层原理实现,其他两个学起来不会很吃力,这也取决于你以后就职的公司要求你会哪一个框架了,当然,会的越多越好,但是往往每个人的时间是有限的,对于自学的学生,或者即将面试找工作的人,当然要选择一门框架深挖原理。

以 Vue 为例,我整理了如下的面试题。

Vue部分截图

开源分享:docs.qq.com/doc/DSmRnRG… resolveConstructorOptions(vm.constructor),

options || {},

vm

)

}

/* istanbul ignore else */

if (process.env.NODE_ENV !== 'production') {

initProxy(vm)

} else {

vm._renderProxy = vm

}

// expose real self

vm._self = vm

initLifecycle(vm)

initEvents(vm)

callHook(vm, 'beforeCreate')

initState(vm)

callHook(vm, 'created')

initRender(vm)

}

_init() 方法在一开始的时候,在 this 对象上定义了两个属性:_uid 和 _isVue,然后判断有没有定义 options._isComponent,在使用 Vue 开发项目的时候,我们是不会使用 _isComponent 选项的,这个选项是 Vue 内部使用的,按照本节开头的例子,这里会走 else 分支,也就是这段代码:

vm.$options = mergeOptions(

resolveConstructorOptions(vm.constructor),

options || {},

vm

)

这样 Vue 第一步所做的事情就来了:使用策略对象合并参数选项

可以发现,Vue 使用 mergeOptions 来处理我们调用 Vue 时传入的参数选项(options),然后将返回值赋值给 this.$options (vm === this),传给 mergeOptions 方法三个参数,我们分别来看一看,首先是:resolveConstructorOptions(vm.constructor),我们查看一下这个方法:

export function resolveConstructorOptions (Ctor: Class) {

let options = Ctor.options

if (Ctor.super) {

const superOptions = Ctor.super.options

const cachedSuperOptions = Ctor.superOptions

const extendOptions = Ctor.extendOptions

if (superOptions !== cachedSuperOptions) {

// super option changed

Ctor.superOptions = superOptions

extendOptions.render = options.render

extendOptions.staticRenderFns = options.staticRenderFns

extendOptions._scopeId = options._scopeId

options = Ctor.options = mergeOptions(superOptions, extendOptions)

if (options.name) {

options.components[options.name] = Ctor

}

}

}

return options

}

这个方法接收一个参数 Ctor,通过传入的 vm.constructor 我们可以知道,其实就是 Vue 构造函数本身。所以下面这句代码:

let options = Ctor.options

相当于:

let options = Vue.options

大家还记得 Vue.options 吗?在寻找 Vue 构造函数一节里,我们整理了 Vue.options 应该长成下面这个样子:

Vue.options = {

components: {

KeepAlive,

Transition,

TransitionGroup

},

directives: {

model,

show

},

filters: {},

_base: Vue

}

之后判断是否定义了 Vue.super ,这个是用来处理继承的,我们后续再讲,在本例中,resolveConstructorOptions 方法直接返回了 Vue.options。也就是说,传递给 mergeOptions 方法的第一个参数就是 Vue.options。

传给 mergeOptions 方法的第二个参数是我们调用 Vue 构造函数时的参数选项,第三个参数是 vm 也就是 this 对象,按照本节开头的例子那样使用 Vue,最终运行的代码应该如下:

vm.$options = mergeOptions(

// Vue.options

{

components: {

KeepAlive,

Transition,

TransitionGroup

},

directives: {

model,

show

},

filters: {},

_base: Vue

},

// 调用Vue构造函数时传入的参数选项 options

{

el: '#app',

data: {

a: 1,

b: [1, 2, 3]

}

},

// this

vm

)

了解了这些,我们就可以看看 mergeOptions 到底做了些什么了,根据引用寻找到 mergeOptions 应该是在 src/core/util/options.js 文件中定义的。这个文件第一次看可能会头大,下面是我处理后的简略展示,大家看上去应该更容易理解了:

// 1、引用依赖

import Vue from '../instance/index'

其他引用...

// 2、合并父子选项值为最终值的策略对象,此时 strats 是一个空对象,因为 config.optionMergeStrategies = Object.create(null)

const strats = config.optionMergeStrategies

// 3、在 strats 对象上定义与参数选项名称相同的方法

strats.el =

strats.propsData = function (parent, child, vm, key){}

strats.data = function (parentVal, childVal, vm)

config._lifecycleHooks.forEach(hook => {

strats[hook] = mergeHook

})

config._assetTypes.forEach(function (type) {

strats[type + 's'] = mergeAssets

})

strats.watch = function (parentVal, childVal)

strats.props =

strats.methods =

strats.computed = function (parentVal: ?Object, childVal: ?Object)

// 默认的合并策略,如果有 childVal 则返回 childVal 没有则返回 parentVal

const defaultStrat = function (parentVal: any, childVal: any): any {

return childVal === undefined

? parentVal

: childVal

}

// 4、mergeOptions 中根据参数选项调用同名的策略方法进行合并处理

export function mergeOptions (

parent: Object,

child: Object,

vm?: Component

): Object {

// 其他代码

...

const options = {}

let key

for (key in parent) {

mergeField(key)

}

for (key in child) {

if (!hasOwn(parent, key)) {

mergeField(key)

}

}

function mergeField (key) {

const strat = strats[key] || defaultStrat

options[key] = strat(parent[key], child[key], vm, key)

}

return options

}

上面的代码中,我省略了一些工具函数,例如 mergeHook 和 mergeAssets 等等,唯一需要注意的是这段代码:

config._lifecycleHooks.forEach(hook => {

strats[hook] = mergeHook

})

config._assetTypes.forEach(function (type) {

strats[type + 's'] = mergeAssets

})

config 对象引用自 src/core/config.js 文件,最终的结果就是在 strats 下添加了相应的生命周期选项的合并策略函数为 mergeHook,添加指令(directives)、组件(components)、过滤器(filters)等选项的合并策略函数为 mergeAssets。

这样看来就清晰多了,拿我们贯穿本文的例子来说:

let v = new Vue({

el: '#app',

data: {

a: 1,

b: [1, 2, 3]

}

})

其中 el 选项会使用 defaultStrat 默认策略函数处理,data 选项则会使用 strats.data 策略函数处理,并且根据 strats.data 中的逻辑,strats.data 方法最终会返回一个函数:mergedInstanceDataFn。

这里就不详细的讲解每一个策略函数的内容了,后续都会讲到,这里我们还是抓住主线理清思路为主,只需要知道Vue在处理选项的时候,使用了一个策略对象对父子选项进行合并。并将最终的值赋值给实例下的 options属性即:this.options 属性即:this.options,那么我们继续查看 _init() 方法在合并完选项之后,又做了什么:

合并完选项之后,Vue 第二部做的事情就来了:初始化工作与Vue实例对象的设计

前面讲了 Vue 构造函数的设计,并且整理了 Vue 原型属性与方法 和 Vue 静态属性与方法,而 Vue 实例对象就是通过构造函数创造出来的,让我们来看一看 Vue 实例对象是如何设计的,下面的代码是 _init() 方法合并完选项之后的代码:

/* istanbul ignore else */

if (process.env.NODE_ENV !== 'production') {

initProxy(vm)

} else {

vm._renderProxy = vm

}

// expose real self

vm._self = vm

initLifecycle(vm)

initEvents(vm)

callHook(vm, 'beforeCreate')

initState(vm)

callHook(vm, 'created')

initRender(vm)

根据上面的代码,在生产环境下会为实例添加两个属性,并且属性值都为实例本身:

vm._renderProxy = vm

vm._self = vm

然后,调用了四个 init* 方法分别为:initLifecycle、initEvents、initState、initRender,且在 initState 前后分别回调了生命周期钩子 beforeCreate 和 created,而 initRender 是在 created 钩子执行之后执行的,看到这里,也就明白了为什么 created 的时候不能操作 DOM了。因为这个时候还没有渲染真正的 DOM 元素到文档中。created 仅仅代表数据状态的初始化完成。

根据四个 init* 方法的引用关系打开对应的文件查看对应的方法,我们发现,这些方法是在处理 Vue 实例对象,以及做一些初始化的工作,类似整理 Vue 构造函数一样,我同样针对 Vue 实例做了属性和方法的整理,如下:

// 在 Vue.prototype._init 中添加的属性 **********************************************************

this._uid = uid++

this._isVue = true

this.$options = {

components,

directives,

filters,

_base,

el,

data: mergedInstanceDataFn()

}

this._renderProxy = this

this._self = this

// 在 initLifecycle 中添加的属性 **********************************************************

this.$parent = parent

this.root=parent?parent.root = parent ? parent.root : this

this.$children = []

this.$refs = {}

this._watcher = null

this._inactive = false

this._isMounted = false

this._isDestroyed = false

this._isBeingDestroyed = false

// 在 initEvents 中添加的属性 **********************************************************

this._events = {}

this._updateListeners = function(){}

// 在 initState 中添加的属性 **********************************************************

this._watchers = []

// initData

this._data

// 在 initRender 中添加的属性 **********************************************************

this.$vnode = null // the placeholder node in parent tree

this._vnode = null // the root of the child tree

this._staticTrees = null

this.$slots

this.$scopedSlots

this._c

this.$createElement

以上就是一个 Vue 实例所包含的属性和方法,除此之外要注意的是,在 initEvents 中除了添加属性之外,如果有 vm.$options._parentListeners 还要调用 vm._updateListeners() 方法,在 initState 中又调用了一些其他 init 方法,如下:

export function initState (vm: Component) {

vm._watchers = []

initProps(vm)

initMethods(vm)

initData(vm)

initComputed(vm)

initWatch(vm)

}

最后在 initRender 中如果有 vm.options.el还要调用vm.options.el 还要调用 vm.mount(vm.$options.el),如下:

if (vm.$options.el) {

vm.mount(vm.mount(vm.options.el)

}

这就是为什么如果不传递 el 选项就需要手动 mount 的原因了。

那么我们依照我们本节开头的的例子,以及初始化的先后顺序来逐一看一看都发生了什么。我们将 initState 中的 init* 方法展开来看,执行顺序应该是这样的(从上到下的顺序执行):

initLifecycle(vm)

initEvents(vm)

callHook(vm, 'beforeCreate')

initProps(vm)

initMethods(vm)

initData(vm)

initComputed(vm)

initWatch(vm)

callHook(vm, 'created')

initRender(vm)

首先是 initLifecycle,这个函数的作用就是在实例上添加一些属性,然后是 initEvents,由于 vm.options._parentListeners的值为undefined所以也仅仅是在实例上添加属性,vm._updateListeners(listeners)并不会执行,由于我们只传递了eldata,所以initPropsinitMethodsinitComputedinitWatch这四个方法什么都不会做,只有initData会执行。最后是initRender,除了在实例上添加一些属性外,由于我们传递了el选项,所以会执行vm.options.\_parentListeners 的值为 undefined 所以也仅仅是在实例上添加属性, vm.\_updateListeners(listeners) 并不会执行,由于我们只传递了 el 和 data,所以 initProps、initMethods、initComputed、initWatch 这四个方法什么都不会做,只有 initData 会执行。最后是 initRender,除了在实例上添加一些属性外,由于我们传递了 el 选项,所以会执行 vm.mount(vm.$options.el)。

综上所述:按照我们的例子那样写,初始化工作只包含两个主要内容即:initData 和 initRender。

五、通过initData看Vue的数据响应系统

=======================

Vue 的数据响应系统包含三个部分:Observer、Dep、Watcher。关于数据响应系统的内容真的已经被文章讲烂了,所以我就简单的说一下,力求大家能理解就 ok,我们还是先看一下 initData 中的代码:

function initData (vm: Component) {

let data = vm.$options.data

data = vm._data = typeof data === 'function'

? data.call(vm)

: data || {}

if (!isPlainObject(data)) {

data = {}

process.env.NODE_ENV !== 'production' && warn(

'data functions should return an object:\n' +

'vuejs.org/v2/guide/co…',

vm

)

}

// proxy data on instance

const keys = Object.keys(data)

const props = vm.$options.props

let i = keys.length

while (i--) {

if (props && hasOwn(props, keys[i])) {

process.env.NODE_ENV !== 'production' && warn(

The data property "${keys[i]}" is already declared as a prop. +

Use prop default value instead.,

vm

)

} else {

proxy(vm, keys[i])

}

}

// observe data

observe(data)

data.ob && data.ob.vmCount++

}

首先,先拿到 data 数据:let data = vm.options.data,大家还记得此时vm.options.data,大家还记得此时 vm.options.data 的值应该是通过 mergeOptions 合并处理后的 mergedInstanceDataFn 函数吗?所以在得到 data 后,它又判断了 data 的数据类型是不是 ‘function’,最终的结果是:data 还是我们传入的数据选项的 data,即:

data: {

a: 1,

b: [1, 2, 3]

}

然后在实例对象上定义 _data 属性,该属性与 data 是相同的引用。

然后是一个 while 循环,循环的目的是在实例对象上对数据进行代理,这样我们就能通过 this.a 来访问 data.a 了,代码的处理是在 proxy 函数中,该函数非常简单,仅仅是在实例对象上设置与 data 属性同名的访问器属性,然后使用 _data 做数据劫持,如下:

function proxy (vm: Component, key: string) {

if (!isReserved(key)) {

Object.defineProperty(vm, key, {

configurable: true,

enumerable: true,

get: function proxyGetter () {

return vm._data[key]

},

set: function proxySetter (val) {

vm._data[key] = val

}

})

}

}

做完数据的代理,就正式进入响应系统。

observe(data)

我们说过,数据响应系统主要包含三部分:Observer、Dep、Watcher,代码分别存放在:observer/index.js、observer/dep.js 以及 observer/watcher.js 文件中,这回我们换一种方式,我们先不看其源码,大家先跟着我的思路来思考,最后回头再去看代码,你会有一种:”奥,不过如此“的感觉。

假如,我们有如下代码:

var data = {

a: 1,

b: {

c: 2

}

}

observer(data)

new Watch('a', () => {

alert(9)

})

new Watch('a', () => {

alert(90)

})

new Watch('b.c', () => {

alert(80)

})

这段代码目的是,首先定义一个数据对象 data,然后通过 observer 对其进行观测,之后定义了三个观察者,当数据有变化时,执行相应的方法,这个功能使用 Vue 的实现原来要如何去实现?其实就是在问 observer 怎么写?Watch 构造函数又怎么写?接下来我们逐一实现。

首先,observer 的作用是:将数据对象 data 的属性转换为访问器属性:

class Observer {

constructor (data) {

this.walk(data)

}

walk (data) {

// 遍历 data 对象属性,调用 defineReactive 方法

let keys = Object.keys(data)

for(let i = 0; i < keys.length; i++){

defineReactive(data, keys[i], data[keys[i]])

}

}

}

// defineReactive方法仅仅将data的属性转换为访问器属性

function defineReactive (data, key, val) {

// 递归观测子属性

observer(val)

Object.defineProperty(data, key, {

enumerable: true,

configurable: true,

get: function () {

return val

},

set: function (newVal) {

if(val === newVal){

return

}

// 对新值进行观测

observer(newVal)

}

})

}

// observer 方法首先判断data是不是纯JavaScript对象,如果是,调用 Observer 类进行观测

function observer (data) {

if(Object.prototype.toString.call(data) !== '[object Object]') {

return

}

new Observer(data)

}

上面的代码中,我们定义了 observer 方法,该方法检测了数据 data 是不是纯 JavaScript 对象,如果是就调用 Observer 类,并将 data 作为参数透传。在 Observer 类中,我们使用 walk 方法对数据data的属性循环调用 defineReactive 方法,defineReactive 方法很简单,仅仅是将数据 data 的属性转为访问器属性,并对数据进行递归观测,否则只能观测数据 data 的直属子属性。这样我们的第一步工作就完成了,当我们修改或者获取data属性值的时候,通过 get 和 set 即能获取到通知。

我们继续往下看,来看一下 Watch:

new Watch('a', () => {

alert(9)

})

现在的问题是,Watch 要怎么和 observer 关联???????我们看看 Watch 它知道些什么,通过上面调用 Watch 的方式,传递给 Watch 两个参数,一个是 ‘a’ 我们可以称其为表达式,另外一个是回调函数。所以我们目前只能写出这样的代码:

class Watch {

constructor (exp, fn) {

this.exp = exp

this.fn = fn

}

}

那么要怎么关联呢,大家看下面的代码会发生什么:

class Watch {

constructor (exp, fn) {

this.exp = exp

this.fn = fn

data[exp]

}

}

多了一句 data[exp],这句话是在干什么?是不是在获取 data 下某个属性的值,比如 exp 为 ‘a’ 的话,那么 data[exp] 就相当于在获取 data.a 的值,那这会放生什么?大家不要忘了,此时数据 data 下的属性已经是访问器属性了,所以这么做的结果会直接触发对应属性的 get 函数,这样我们就成功的和 observer 产生了关联,但这样还不够,我们还是没有达到目的,不过我们已经无限接近了,我们继续思考看一下可不可以这样:

既然在 Watch 中对表达式求值,能够触发 observer 的 get,那么可不可以在 get 中收集 Watch 中函数呢?

答案是可以的,不过这个时候我们就需要 Dep 出场了,它是一个依赖收集器。我们的思路是:data 下的每一个属性都有一个唯一的 Dep 对象,在 get 中收集仅针对该属性的依赖,然后在 set 方法中触发所有收集的依赖,这样就搞定了,看如下代码:

class Dep {

constructor () {

this.subs = []

}

addSub () {

this.subs.push(Dep.target)

}

notify () {

for(let i = 0; i < this.subs.length; i++){

this.subs[i].fn()

}

}

}

Dep.target = null

function pushTarget(watch){

Dep.target = watch

}

class Watch {

constructor (exp, fn) {

this.exp = exp

this.fn = fn

pushTarget(this)

data[exp]

}

}

上面的代码中,我们在 Watch 中增加了 pushTarget(this),可以发现,这句代码的作用是将 Dep.target 的值设置为该Watch对象。在 pushTarget 之后我们才对表达式进行求值,接着,我们修改 defineReactive 代码如下:

function defineReactive (data, key, val) {

observer(val)

let dep = new Dep() // 新增

Object.defineProperty(data, key, {

enumerable: true,

configurable: true,

get: function () {

dep.addSub() // 新增

return val

},

set: function (newVal) {

if(val === newVal){

return

}

observer(newVal)

dep.notify() // 新增

}

})

}

如标注,新增了三句代码,我们知道,Watch 中对表达式求值会触发 get 方法,我们在 get 方法中调用了 dep.addSub,也就执行了这句代码:this.subs.push(Dep.target),由于在这句代码执行之前,Dep.target 的值已经被设置为一个 Watch 对象了,所以最终结果就是收集了一个 Watch 对象,然后在 set 方法中我们调用了 dep.notify,所以当data属性值变化的时候,就会通过 dep.notify 循环调用所有收集的 Watch 对象中的回调函数:

notify () {

for(let i = 0; i < this.subs.length; i++){

this.subs[i].fn()

}

}

这样 observer、Dep、Watch 三者就联系成为一个有机的整体,实现了我们最初的目标,完整的代码可以戳这里:observer-dep-watch。这里还给大家挖了个坑,因为我们没有处理对数组的观测,由于比较复杂并且这又不是我们讨论的重点,如果大家想了解可以戳我的这篇文章:JavaScript 实现 MVVM 之我就是想监测一个普通对象的变化,另外,在 Watch 中对表达式求值的时候也只做了直接子属性的求值,所以如果 exp 的值为 ‘a.b’ 的时候,就不可以用了,Vue 的做法是使用 . 分割表达式字符串为数组,然后遍历一下对其进行求值,大家可以查看其源码。如下:

/**

  • Parse simple path.

*/

const bailRE = /[^\w.$]/

export function parsePath (path: string): any {

if (bailRE.test(path)) {

return

} else {

const segments = path.split('.')

return function (obj) {

for (let i = 0; i < segments.length; i++) {

if (!obj) return

obj = obj[segments[i]]

}

return obj

}

}

}

Vue 的求值代码是在 src/core/util/lang.js 文件中 parsePath 函数中实现的。总结一下 Vue 的依赖收集过程应该是这样的:

实际上,Vue 并没有直接在 get 中调用 addSub,而是调用的 dep.depend,目的是将当前的 dep 对象收集到 watch 对象中,如果要完整的流程,应该是这样的:(大家注意数据的每一个字段都拥有自己的 dep 对象和 get 方法。)

这样 Vue 就建立了一套数据响应系统,之前我们说过,按照我们的例子那样写,初始化工作只包含两个主要内容即:initData 和 initRender。现在 initData 我们分析完了,接下来看一看 initRender

六、通过initRender看Vue的 render(渲染) 与 re-render(重新渲染)

================================================

在 initRender 方法中,因为我们的例子中传递了 el 选项,所以下面的代码会执行:

if (vm.$options.el) {

vm.mount(vm.mount(vm.options.el)

}

这里,调用了 mount方法,在还原Vue构造函数的时候,我们整理过所有的方法,其中mount 方法,在还原 Vue 构造函数的时候,我们整理过所有的方法,其中 mount 方法在两个地方出现过:

1、在 web-runtime.js 文件中:

Vue.prototype.$mount = function (

el?: string | Element,

hydrating?: boolean

): Component {

el = el && inBrowser ? query(el) : undefined

return this._mount(el, hydrating)

}

它的作用是通过 el 获取相应的 DOM 元素,然后调用 lifecycle.js 文件中的 _mount 方法。

2、在 web-runtime-with-compiler.js 文件中:

// 缓存了来自 web-runtime.js 的 $mount 方法

const mount = Vue.prototype.$mount

// 重写 $mount 方法

Vue.prototype.$mount = function (

el?: string | Element,

hydrating?: boolean

): Component {

// 根据 el 获取相应的DOM元素

el = el && query(el)

// 不允许你将 el 挂载到 html 标签或者 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

// 如果我们没有写 render 选项,那么就尝试将 template 或者 el 转化为 render 函数

if (!options.render) {

let template = options.template

if (template) {

if (typeof template === 'string') {

if (template.charAt(0) === '#') {

template = idToTemplate(template)

/* istanbul ignore if */

if (process.env.NODE_ENV !== 'production' && !template) {

warn(

Template element not found or is empty: ${options.template},

this

)

}

}

} else if (template.nodeType) {

template = template.innerHTML

} else {

if (process.env.NODE_ENV !== 'production') {

warn('invalid template option:' + template, this)

}

return this

}

} else if (el) {

template = getOuterHTML(el)

}

if (template) {

const { render, staticRenderFns } = compileToFunctions(template, {

warn,

shouldDecodeNewlines,

delimiters: options.delimiters

}, this)

options.render = render

options.staticRenderFns = staticRenderFns

}

}

// 调用已经缓存下来的 web-runtime.js 文件中的 $mount 方法

return mount.call(this, el, hydrating)

}

分析一下可知 web-runtime-with-compiler.js 的逻辑如下:

1、缓存来自 web-runtime.js 文件的 $mount 方法

2、判断有没有传递 render 选项,如果有直接调用来自 web-runtime.js 文件的 $mount 方法

3、如果没有传递 render 选项,那么查看有没有 template 选项,如果有就使用 compileToFunctions 函数根据其内容编译成 render 函数

4、如果没有 template 选项,那么查看有没有 el 选项,如果有就使用 compileToFunctions 函数将其内容 (template = getOuterHTML(el)) 编译成 render 函数

5、将编译成的 render 函数挂载到 this.options属性下,并调用缓存下来的webruntime.js文件中的options 属性下,并调用缓存下来的 web-runtime.js 文件中的 mount 方法

简单的用一张图表示 mount 方法的调用关系,从上至下调用:

不过不管怎样,我们发现这些步骤的最终目的是生成 render 函数,然后再调用 lifecycle.js 文件中的 _mount 方法,我们看看这个方法做了什么事情,查看 _mount 方法的代码,这是简化过得:

Vue.prototype._mount = function (

el?: Element | void,

hydrating?: boolean

): Component {

const vm: Component = this

// 在Vue实例对象上添加 $el 属性,指向挂载点元素

vm.$el = el

// 触发 beforeMount 生命周期钩子

callHook(vm, 'beforeMount')

vm._watcher = new Watcher(vm, () => {

vm._update(vm._render(), hydrating)

}, noop)

// 如果是第一次mount则触发 mounted 生命周期钩子

if (vm.$vnode == null) {

vm._isMounted = true

callHook(vm, 'mounted')

}

return vm

}

上面的代码很简单,该注释的都注释了,唯一需要看的就是这段代码:

vm._watcher = new Watcher(vm, () => {

vm._update(vm._render(), hydrating)

}, noop)

看上去很眼熟有没有?我们平时使用 Vue 都是这样使用 watch 的:

this.$watch('a', (newVal, oldVal) => {

})

// 或者

this.$watch(function(){

return this.a + this.b

}, (newVal, oldVal) => {

})

第一个参数是 表达式或者函数,第二个参数是回调函数,第三个参数是可选的选项。原理是 Watch 内部对表达式求值或者对函数求值从而触发数据的 get 方法收集依赖。可是 _mount 方法中使用 Watcher 的时候第一个参数 vm 是什么鬼。我们不妨去看看源码中 watch函数是如何实现的,根据之前还原Vue构造函数中所整理的内容可知:watch 函数是如何实现的,根据之前还原 Vue 构造函数中所整理的内容可知:warch 方法是在 src/core/instance/state.js 文件中的 stateMixin 方法中定义的,源码如下:

Vue.prototype.$watch = function (

expOrFn: string | Function,

cb: Function,

options?: Object

): Function {

const vm: Component = this

options = options || {}

options.user = true

const watcher = new Watcher(vm, expOrFn, cb, options)

if (options.immediate) {

cb.call(vm, watcher.value)

}

return function unwatchFn () {

watcher.teardown()

}

}

我们可以发现,$warch 其实是对 Watcher 的一个封装,内部的 Watcher 的第一个参数实际上也是 vm 即:Vue实例对象,这一点我们可以在 Watcher 的源码中得到验证,代开 observer/watcher.js 文件查看:

export default class Watcher {

constructor (

vm: Component,

expOrFn: string | Function,

cb: Function,

options?: Object = {}

) {

}

}

可以发现真正的 Watcher 第一个参数实际上就是 vm。第二个参数是表达式或者函数,然后以此类推,所以现在再来看 _mount 中的这段代码:

vm._watcher = new Watcher(vm, () => {

vm._update(vm._render(), hydrating)

}, noop)

忽略第一个参数 vm,也就说,Watcher 内部应该对第二个参数求值,也就是运行这个函数:

() => {

vm._update(vm._render(), hydrating)

}

所以 vm._render() 函数被第一个执行,该函数在 src/core/instance/render.js 中,该方法中的代码很多,下面是简化过的:

Vue.prototype._render = function (): VNode {

const vm: Component = this

// 解构出 $options 中的 render 函数

const {

render,

staticRenderFns,

_parentVnode

} = vm.$options

...

let vnode

try {

// 运行 render 函数

vnode = render.call(vm._renderProxy, vm.$createElement)

} catch (e) {

...

}

// set parent

vnode.parent = _parentVnode

return vnode

}

_render 方法首先从 vm.options中解构出render函数,大家应该记得:vm.options 中解构出 render 函数,大家应该记得:vm.options.render 方法是在 web-runtime-with-compiler.js 文件中通过 compileToFunctions 方法将 template 或 el 编译而来的。解构出 render 函数后,接下来便执行了该方法:

vnode = render.call(vm._renderProxy, vm.$createElement)

其中使用 call 指定了 render 函数的作用域环境为 vm._renderProxy,这个属性在我们整理实例对象的时候知道,他是在 Vue.prototype._init 方法中被添加的,即:vm._renderProxy = vm,其实就是Vue实例对象本身,然后传递了一个参数:vm.$createElement。那么 render 函数到底是干什么的呢?让我们根据上面那句代码猜一猜,我们已经知道 render 函数是从 template 或 el 编译而来的,如果没错的话应该是返回一个虚拟 DOM 对象。我们不妨使用 console.log 打印一下 render 函数,当我们的模板这样编写时:

  • {{a}}

打印的 render 函数如下:

我们修改模板为:

  • {{a}}

打印出来的 render 函数如下:

其实了解 Vue2.x 版本的同学都知道,Vue 提供了 render 选项,作为 template 的代替方案,同时为 JavaScript 提供了完全编程的能力,下面两种编写模板的方式实际是等价的:

// 方案一:

new Vue({

el: '#app',

data: {

a: 1

},

template: '

  • {{a}}
  • {{a}}
'

})

// 方案二:

new Vue({

el: '#app',

render: function (createElement) {

createElement('ul', [

createElement('li', this.a),

createElement('li', this.a)

])

}

})

现在我们再来看我们打印的 render 函数:

function anonymous() {

with(this){

return _c('ul', {

attrs: {"id": "app"}

},[

_c('li', [_v(_s(a))])

])

}

}

是不是与我们自己写 render 函数很像?因为 render 函数的作用域被绑定到了 Vue 实例,即:render.call(vm._renderProxy, vm.$createElement),所以上面代码中 _c、_v、_s 以及变量 a 相当于 Vue 实例下的方法和变量。大家还记得诸如 _c、_v、_s 这样的方法在哪里定义的吗?我们在整理Vue构造函数的时候知道,他们在 src/core/instance/render.js 文件中的 renderMixin 方法中定义,除了这些之外还有诸如:_l、 _m、 _o 等等。其中 _l 就在我们使用 v-for 指令的时候出现了。所以现在大家知道为什么这些方法都被定义在 render.js 文件中了吧,因为他们就是为了构造出 render 函数而存在的。

现在我们已经知道了 render 函数的长相,也知道了 render 函数的作用域是Vue实例本身即:this(或vm)。那么当我们执行 render 函数时,其中的变量如:a,就相当于:this.a,我们知道这是在求值,所以 _mount 中的这段代码:

vm._watcher = new Watcher(vm, () => {

vm._update(vm._render(), hydrating)

}, noop)

当 vm._render 执行的时候,所依赖的变量就会被求值,并被收集为依赖。按照 Vue 中 watcher.js 的逻辑,当依赖的变量有变化时不仅仅回调函数被执行,实际上还要重新求值,即还要执行一遍:

() => {

vm._update(vm._render(), hydrating)

}

这实际上就做到了 re-render,因为 vm._update 就是文章开头所说的虚拟 DOM 中的最后一步:patch

vm_render 方法最终返回一个 vnode 对象,即虚拟 DOM,然后作为 vm_update 的第一个参数传递了过去,我们看一下 vm_update 的逻辑,在 src/core/instance/lifecycle.js 文件中有这么一段代码:

if (!prevVnode) {

// initial render

vm.$el = vm.patch(

vm.$el, vnode, hydrating, false /* removeOnly */,

vm.$options._parentElm,

vm.$options._refElm

)

} else {

// updates

vm.$el = vm.patch(prevVnode, vnode)

}

如果还没有 prevVnode 说明是首次渲染,直接创建真实 DOM。如果已经有了 prevVnode 说明不是首次渲染,那么就采用 patch 算法进行必要的 DOM 操作。这就是 Vue 更新 DOM 的逻辑。只不过我们没有将 virtual DOM 内部的实现。

现在我们来好好理理思路,当我们写如下代码时:

new Vue({

el: '#app',

data: {

a: 1,

b: [1, 2, 3]

}

})

Vue 所做的事:

1、构建数据响应系统,使用 Observer 将数据data转换为访问器属性;将 el 编译为 render 函数,render 函数返回值为虚拟 DOM

2、在 _mount 中对 _update 求值,而 _update 又会对 render 求值,render 内部又会对依赖的变量求值,收集为被求值的变量的依赖,当变量改变时,_update 又会重新执行一遍,从而做到 re-render。

最后

整理面试题,不是让大家去只刷面试题,而是熟悉目前实际面试中常见的考察方式和知识点,做到心中有数,也可以用来自查及完善知识体系。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

《前端基础面试题》,《前端校招面试题精编解析大全》,《前端面试题宝典》,《前端面试题:常用算法》

前端面试题宝典

前端校招面试题详解