vue几个核心思想:
Vue.js 的源码在 src 目录下,其目录结构如下。
src
├── compiler # 编译相关
├── core # 核心代码
├── platforms # 不同平台的支持
├── server # 服务端渲染
├── sfc # .vue 文件解析
├── shared # 共享代码
基于 Rollup 构建,相关配置在 scripts 目录下。
构建时通过不同的命令执行不同的脚本,去读取不同用处的配置,然后生成适合各种场景的Vue源码。
vue2.0有以下几种场景:
在vue2.x版本中使用 Flow 作为js静态类型检查工具,3.x版本使用typescript实现,自带类型检查。
vue核心思想之一就是数据驱动
,指数据
驱动生成视图
,通过修改数据自动实现对视图的修改。这里主要分析模板和数据是如何渲染成最终的DOM的。
Vue 初始化主要就干了几件事情,
$mount方法
挂载组件:
mountComponent
核心就是先实例化一个渲染Watcher
,在它的回调函数中会调用 updateComponent
方法,在此方法中调用 vm._render
方法先生成虚拟 Node
,最终调用 vm._update
更新 DOM。
Watcher在这里起到两个作用:
294
个之多;判断第一个参数tag的类型,分为普通html标签、组件和其他类型,将子节点规范成 VNode 类型,递归整个树完成虚拟dom树的构建。
此方法是render函数的参数。
const app = new Vue({
el: '#app',
render: createElement => createElement(App)
})
调用的时机:一个是首次渲染,一个是数据更新的时候;
首次渲染会将虚拟dom树整个渲染为dom节点,数据更新的时候会经过diff
过程,只选取修改的虚拟dom节点进行局部更新。
update 的核心就是调用 vm.__patch__
方法,不同的平台实现不一样,web平台生成dom节点,ssr服务端渲染生成html字符串。
dom树节点的插入
顺序是先子后父
,
new Vue
➜ init
➜ $mount
➜ compile
➜ render
➜ vnode
➜ patch
➜ dom
在createElement里面调用,判断tag类型为组件时调用,用来将组件转换成虚拟dom。
核心步骤:
参考 前端进阶面试题详细解答
patch主要完成组件的渲染
工作。
createComponent过程把组件转换成了VNode,patch过程会调用createElm把 VNode 转换成真正的 DOM 节点。
递归实现深度遍历
整个VNode树,用先子后父
的方式插入dom树#app
的节点上,且挂载元素不能是html
或body
vue自身定义了一些默认配置,同时又可以在初始化阶段传入一些定义配置,然后去 merge 默认配置,来达到定制化不同需求的目的。
vue组件其实是一个js对象,我们写组件其实就是在写各种配置,这个配置在构建组件的时候会调用Vue.extent
方法构建成一个组件类(因此我们组件内部访问到的this
才是Vue的实例),那么在组件类实例化 new Vue()
的过程中,就会做合并配置
这件事。
合并配置分为两种方式:
new Vue
(例如挂载#app的时候)主要合并以下几方面的配置:
生命周期
是vue在运行期间的各个关键节点运行的钩子函数
,以便可以在特定场景做特定的事。
beforeCreate & created
this
访问当前实例,也无法访问data、props等;先父后子
beforeMount & mounted
先父后子
,mounted钩子函数执行顺序先子后父
ref
beforeUpdate & updated
beforeDestroy & destroyed
activated & deactivated
生命周期示意图
Vue.component(tagName, options)
, 挂载到Vue.options.components
上,所有组件均可访问;components:{componentName: component}
, 挂载到vm.$options.components
上,仅父组件可访问;Vue有 3
种异步组件,实现了 loading、resolve、reject、timeout 4 种状态。异步组件实现的本质是 2 次渲染,除了 0 delay 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 forceRender 强制重新渲染,这样就能正确渲染出我们异步加载的组件了。
响应式对象,核心就是利用 Object.defineProperty
给数据递归添加了 getter
和 setter
,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集
,setter 做的事情是派发更新
。本质上是发布订阅模式(观察者模式)
。
所以在 vm._render() 过程中,会触发所有数据的 getter,这样实际上已经完成了一个依赖收集的过程。
在定义相应式对象的的getter函数里,触发dep.depend
做依赖收集,将获取属性的地方全部加入订阅者列表中,当数据发生变化时,通过遍历订阅者列表实现变更发布。
再次render时会先做依赖清除,再次进行新的依赖收集,这样做是为了处理v-if条件渲染的数据不用再派发更新了。
实际上就是当数据发生变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher,都触发它们的 update 过程,这个过程又利用了队列做了进一步优化,在 nextTick 后执行所有 watcher 的 run,最后执行它们的回调函数。
通过setter来触发变量的更新,这里引入了一个队列的概念,这也是 Vue 在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发更新,而是先添加到一个队列里,然后在 nextTick 后执行更新,可以理解为等一段时间一起更新。
队列排序 queue.sort((a, b) => a.id - b.id) 对队列做了从小到大的排序,这么做主要有以下要确保以下几点:
此方法可以在数据修改触发dom更新完成之后调用。
在浏览器环境中,常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate;常见的 micro task 有 MutationObsever 和 Promise.then。
Object.defineProperty
监测到,需要通过Vue.set
方法手动告诉vue收集这个依赖并且派发更新。数组项的赋值
和直接修改长度
的,但是可以监测到splice
等方法的修改,原因在于defineProperty
,可通过Vue.set
实现对数组项的修改;Observer
类中单独对数组做了处理,对数组对能增加数组长度的 3 个方法重写push
、unshift
、sueplice
,现将方法原有逻辑执行完,再手动把新添加的值变成一个响应式对象,并且派发更新。Vue.del
方法,确保触发更新视图。计算属性的触发有以下两种情况:
lazy
和 active
两种模式,active模式依赖更新立即计算,lazy模式依赖变化仅设置this.dirty = true
,等访问计算属性时再重新计算,并加入缓存。延时计算
计算属性不会立刻求值(除非设置immediate: true
)computed: {
value () {
return function (a, b, c) {
/** do something */
return data
}
}
}
本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher。
计算属性 vs 监听属性 从应用场景看
计算属性
适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;侦听属性
适用于观测某个值的变化去完成一段复杂的业务逻辑(例如执行异步或开销较大的操作)。watcher的 4 种类型:deep
、user
、computed
、sync
deep: true
);immediate: true
)计算属性 vs 方法
computed: {now: ()=>Date.now()}
传参
;组件更新核心是响应式数据监控到数据的改变,重新生成了虚拟dom树,然后通过diff算法计算出前后虚拟dom树的差异点,更新dom时只更新变化的部分。
快问快答:
n^3
? 答: 老树的每一个节点都去遍历新树的节点,直到找到新树对应的节点。那么这个流程就是 O(n^2),再紧接着找到不同之后,再计算最短修改距离然后修改节点,这里是 O(n^3)。用作父组件给自组件传参,
编译的核心是把 template
模板编译成 render
函数。
vue有两种编译模式:
vue-loader
事先把模板编译成 render函数(Runtime only )运行时编译:入口compileToFunctions
// 解析模板字符串生成 AST
const ast = parse(template.trim(), options)
// 优化语法树
optimize(ast, options)
// 生成代码
const code = generate(ast, options)
AST
:种抽象语法树,是对源代码的抽象语法结构的树状表现形式。
主要采用标记化算法
的思路,解析器内部维护一个状态机
;
parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。
AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。【换成常量更好】
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`) // 开始标签打开
const startTagClose = /^\s*(\/?)>/ // 开始标签关闭
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) // 标签结束
const doctype = /^<!DOCTYPE [^>]+>/i // 文档类型节点
const comment = /^<!\--/ // 注释节点
const conditionalComment = /^<!\[/
优化AST树的原因:处理响应式、标记静态节点、处理指令等
静态节点的判断方法:
v-pre
指令,是静态;如果是普通元素非静态节点,则遍历它的所有 children,递归执行静态节点的标记,子节点有不是静态的情况,则它的父节点也为非静态。
标记静态根:缓存节点,优化diff过程,来减少操作dom
把AST语法树转换成可执行的render
函数,
主要处理AST的以下属性,将其变成render函数的写法:
主要介绍event、v-model、slot、keep-alive、transition等。
主要从下面三个角度分析:
编译解析
在编译过程中解析template模版,识别其中v-on
、@
等指令,记录下事件的名称
和回调函数
,其中回调函数可能使函数名称或者一个函数。
dom原生事件
绑定方法:在组件上使用原生事件需要加.native
修饰符(例如@click.native)
添加移除:DOM事件调用原生 addEventListener
和removeEventListener
;
组件自定义事件
通过事件中心实现,思想类似发布订阅模式:
注意
区别
:添加和删除事件的方式不一样;DOM事件调用原生 addEventListener
和removeEventListener
添加和删除;自定义事件调用vm.$off
方法删除回调函数即可;数据响应:data ➜ view
v-model双向数据绑定: data ↔ view
v-model 是一种语法糖
,即可以作用在普通表单元素
上,又可以作用在组件
上。
<input v-model="message" />
相当于: <input v-bind:value="message" v-on:input="message=$event.target.value" />
<child :value="message" @input="e => message = e"/>
组件可以配置子组件接收的 prop 名称和派发的事件名称{
props: ['msg'],
model: {
prop: 'msg',
event: 'change'
},
methods: {
updateValue(e) {
this.$emit('change', e.target.value)
}
}
}
插槽就像是子组件中的一个个空抽屉,父组件可以在调用子组件的时候自己决定放什么内容到不同的抽屉里。
编译
作用域插槽
作用域插槽作用:子组件给父组件传递数据。
读取 scoped-slot 属性并赋值给当前元素节点的 slotScope 属性,接下来在构造 AST树的时候,不会作为 children 添加到当前 AST 树中,而是存到父 AST 元素节点的 scopedSlots 属性上,它是一个对象,以插槽名称 name 为 key,以渲染函数为value。
然后在子组件渲染的时候,取到父组件的scopedSlots 里面的渲染函数,执行生成vnode。
普通插槽和作用域插槽的区别:
父组件实例
,子组件渲染的时候直接拿到这些渲染好的 vnodes子组件实例
。keep-alive
是一个内置抽象
组件,在组件实例建立父子关系的时候会被忽略;vnode.elm
插入dom树即可;keep-alive
也是一个内置抽象组件,是 web平台独有
的,同样也只处理一个子节点(多了会警告);在下列情形中添加过渡效果
Vue 的过渡实现分为以下几个步骤:
真正执行动画的是我们写的 CSS 或者是 JavaScript 钩子函数,而 Vue 的transition组件只是帮我们很好地管理了这些 CSS 的添加/删除,以及钩子函数的执行时机。
transition-group
路由的功能是
统筹分发
,告诉什么人应该干什么事情,对前端来说就是将不同的路径映射到不同的功能(视图)上去。
vue-router支持 hash
、history
、abstract
3 种路由方式,提供了 <router-link>
和 <router-view>
2 种组件
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
<router-link>
和 <router-view>
包含以下方法:
匹配过程主要做的事情:
路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据。
<router-link>
比起写死的 <a href="...">
会好一些,理由如下:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。