共计 7879 个字符,预计需要花费 20 分钟才能阅读完成。
怎么封装一个组件?
// 父组件
<template>
<div>
<h1>{{title}}</h1>
<child :name="name" :age="age" :hobby="hobby" @titleChanged="titleChanged"></child>
</div>
</template>
import child from "./components/child"
export default {
name: 'App',
data(){
return{
title: '父级内容',
name: 'hello',
age: 19,
hobby: ['swim','run','walk']
}
},
components:{"child":child},
titleChanged(val){this.title = val;}
}
// 子组件
<template>
<div class="hello">
<h3>{{name}}</h3>
<p>{{age}}</p>
<ul>
<li v-for="h in hobby">{{h}}</li> // 遍历传递过去的值,而后出现到页面
</ul>
<button @click="changeTitle"> 向父级传值 </button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props:{
users:{ // 这个就是父组件中子标签自定义名字
type:String,
required:true
},
age: {
type: Number,
default: 0,
},
hobby: {
type: Array,
defautl: ()=>[]
}
},
methods: {changeTitle(){this.$emit("titleChanged","子向父组件传值");
}
}
}
</script>
请说一下响应式数据的了解?
Vue 通过设定对象属性的 setter/getter 办法来监听数据的变动,通过 getter 进行依赖收集,而每个 setter 办法就是一个观察者,在数据变更的时候告诉订阅者更新视图。
function observe (obj) { // 咱们来用它使对象变成可察看的
// 判断类型
if (!obj || typeof obj !== 'object') {return}
Object.keys(obj).forEach(key => {defineReactive(obj, key, obj[key])
})
function defineReactive (obj, key, value) {
// 递归子属性
observe(value)
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举(能够遍历)configurable: true, // 可配置(比方能够删除)get: function reactiveGetter () {console.log('get', value) // 监听
return value
},
set: function reactiveSetter (newVal) {observe(newVal) // 如果赋值是一个对象,也要递归子属性
if (newVal !== value) {console.log('set', newVal) // 监听
render()
value = newVal
}
}
})
}
}
observe
这个函数传入一个 obj
(须要被追踪变动的对象),通过遍历所有属性的形式对该对象的每一个属性都通过 defineReactive
解决, 以此来达到实现侦测对象变动。值得注意的是,observe
会进行递归调用。
因为 Vue 通过 Object.defineProperty
来将对象的 key 转换成 getter/setter
的模式来追踪变动,但 getter/setter
只能追踪一个数据是否被批改,无奈追踪新增属性和删除属性。如果是删除属性,咱们能够用 vm.$delete
实现,那如果是新增属性,该怎么办呢?
1)能够应用 Vue.set(location, a, 1)
办法向嵌套对象增加响应式属性;
2)也能够给这个对象从新赋值,比方data.location = {...data.location,a:1}
Object.defineProperty
不能监听数组的变动,须要进行数组办法的重写,具体代码如下:
let methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push'];
// 先获取到原来的原型上的办法
let arrayProto = Array.prototype;
// 创立一个本人的原型 并且重写 methods 这些办法
let proto = Object.create(arrayProto)
methods.forEach(method => {proto[method] = function() {
// AOP
arrayProto[method].call(this, ...arguments)
render()}
})
function observer(obj) {
// 把所有的属性定义成 set/get 的形式
if (Array.isArray(obj)) {
obj.__proto__ = proto
return
}
if (typeof obj == 'object') {for (let key in obj) {defineReactive(obj, key, obj[key])
}
}
}
function defineReactive(data, key, value) {observer(value)
Object.defineProperty(data, key, {get() {return value},
set(newValue) {observer(newValue)
if (newValue !== value) {render()
value = newValue
}
}
})
}
observer(obj)
function $set(data, key, value) {defineReactive(data, key, value)
}
这种办法将数组的罕用办法进行重写,进而笼罩掉原生的数组办法,重写之后的数组办法须要可能被拦挡。
vue 中模板编译原理?
对于 Vue 编译原理这块的整体逻辑次要分三个局部,也能够说是分三步,这三个局部是有前后关系的:
- 第一步是将
模板字符串
转换成element ASTs
(解析器)
<div>
<p>{{name}}</p>
</div>
下面这样一个简略的 模板
转换成 element AST
后是这样的:
{
tag: "div"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: undefined,
attrsList: [],
attrsMap: {},
children: [
{
tag: "p"
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: {tag: "div", ...},
attrsList: [],
attrsMap: {},
children: [{
type: 2,
text: "{{name}}",
static: false,
expression: "_s(name)"
}]
}
]
}
这段模板字符串会扔到 while
中去循环,而后 一段一段 的截取,把截取到的 每一小段字符串 进行解析,直到最初截没了,也就解析完了
- 第二步是对
AST
进行动态节点标记,次要用来做虚构 DOM 的渲染优化(优化器)
优化器的指标是找出那些动态节点并打上标记,而动态节点指的是 DOM
不须要发生变化的节点。
每次从新渲染的时候不须要为动态节点创立新节点;在 Virtual DOM 中 patching 的过程能够被跳过。
- 第三步是 应用
element ASTs
生成render
函数代码字符串(代码生成器)
{render: `with(this){return _c('div',[_c('p',[_v(_s(name))])])}`
}
通过递归去拼一个函数执行代码的字符串,递归的过程依据不同的节点类型调用不同的生成办法,如果发现是一颗元素节点就拼一个 _c(tagName, data, children)
的函数调用字符串,而后 data
和 children
也是应用 AST
中的属性去拼字符串。
如果 children
中还有 children
则递归去拼;最初拼出一个残缺的 render
函数代码。
vue 中 diff 的原理?
渲染实在 DOM 的开销是很大的,比方有时候咱们批改了某个数据,如果间接渲染到实在 dom 上会引起整个 dom 树的重绘和重排,有没有可能咱们只更新咱们批改的那一小块 dom 而不要更新整个 dom 呢?diff 算法可能帮忙咱们。
咱们先依据实在 DOM 生成一颗 virtual DOM
,当virtual DOM
某个节点的数据扭转后会生成一个新的 Vnode
,而后Vnode
和oldVnode
作比照,发现有不一样的中央就间接批改在实在的 DOM 上,而后使 oldVnode
的值为Vnode
。
diff 的过程就是调用名为 patch
的函数,比拟新旧节点,一边比拟一边给 实在的 DOM打补丁。
应用双指针模式,对虚构节点进行比对,对雷同的节点进行复用,对发生变化的节点进行 patch。以下是 vue 源码中对虚构节点的比对形式:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 旧头索引
let newStartIdx = 0 // 新头索引
let oldEndIdx = oldCh.length - 1 // 旧尾索引
let newEndIdx = newCh.length - 1 // 新尾索引
let oldStartVnode = oldCh[0] // oldVnode 的第一个 child
let oldEndVnode = oldCh[oldEndIdx] // oldVnode 的最初一个 child
let newStartVnode = newCh[0] // newVnode 的第一个 child
let newEndVnode = newCh[newEndIdx] // newVnode 的最初一个 child
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
// 如果 oldStartVnode 和 oldEndVnode 重合,并且新的也都重合了,证实 diff 完了,循环完结
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果 oldVnode 的第一个 child 不存在
if (isUndef(oldStartVnode)) {
// oldStart 索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// 如果 oldVnode 的最初一个 child 不存在
} else if (isUndef(oldEndVnode)) {
// oldEnd 索引左移
oldEndVnode = oldCh[--oldEndIdx]
// oldStartVnode 和 newStartVnode 是同一个节点
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// patch oldStartVnode 和 newStartVnode,索引左移,持续循环
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode 和 newEndVnode 是同一个节点
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// patch oldEndVnode 和 newEndVnode,索引右移,持续循环
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode 和 newEndVnode 是同一个节点
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// patch oldStartVnode 和 newEndVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 如果 removeOnly 是 false,则将 oldStartVnode.eml 挪动到 oldEndVnode.elm 之后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// oldStart 索引右移,newEnd 索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果 oldEndVnode 和 newStartVnode 是同一个节点
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// patch oldEndVnode 和 newStartVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 如果 removeOnly 是 false,则将 oldEndVnode.elm 挪动到 oldStartVnode.elm 之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// oldEnd 索引左移,newStart 索引右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 如果都不匹配
} else {if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 尝试在 oldChildren 中寻找和 newStartVnode 的具备雷同的 key 的 Vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果未找到,阐明 newStartVnode 是一个新的节点
if (isUndef(idxInOld)) { // New element
// 创立一个新 Vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
// 如果找到了和 newStartVnodej 具备雷同的 key 的 Vnode,叫 vnodeToMove
} else {vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error.' +
'Make sure each v-for item has a unique key.'
)
}
// 比拟两个具备雷同的 key 的新节点是否是同一个节点
// 不设 key,newCh 和 oldCh 只会进行头尾两端的互相比拟,设 key 后,除了头尾两端的比拟外,还会从用 key 生成的对象 oldKeyToIdx 中查找匹配的节点,所以为节点设置 key 能够更高效的利用 dom。if (sameVnode(vnodeToMove, newStartVnode)) {
// patch vnodeToMove 和 newStartVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 革除
oldCh[idxInOld] = undefined
// 如果 removeOnly 是 false,则将找到的和 newStartVnodej 具备雷同的 key 的 Vnode,叫 vnodeToMove.elm
// 挪动到 oldStartVnode.elm 之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 如果 key 雷同,然而节点不雷同,则创立一个新的节点
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
// 右移
newStartVnode = newCh[++newStartIdx]
}
}
vue 的渲染流程?
从模板到实在 dom 节点还须要通过一些步骤
把模板编译为 render 函数;
实例进行挂载, 依据根节点 render 函数的调用,递归的生成虚构 dom;
比照虚构 dom,渲染到实在 dom;
组件外部 data 发生变化,组件和子组件援用 data 作为 props 从新调用 render 函数,生成虚构 dom, 返回到步骤 3。
为何 vue 采纳异步渲染?
如果不采取异步更新,那么每次更新数据都会对以后组件进行从新渲染,为了性能思考,Vue 会在本轮数据更新后,再去异步更新数据。
原理:
- dep.notify() 告诉 watcher 进行更新操作
- subs[i].update() 顺次调用 watcher 的 update
- queueWatcher 将 watcher 从新放到队列中
- nextTick(flushSchedulerQueue) 异步清空 watcher 队列
谈一谈你对 Vue 性能优化的了解 ?
次要包含:上线代码包打包、源码编写优化、用户体验优化。
1. 代码包优化
- 屏蔽 sourceMap
- 对我的项目代码中的 JS/CSS/SVG(*.ico)文件进行 gzip 压缩
- 对路由组件进行懒加载
2. 源码优化
- v-if 和 v-show 抉择调用
- 为 item 设置惟一 key 值
- 细分 vuejs 组件
- 缩小 watch 的数据
- 内容类零碎的图片资源按需加载
- SSR(服务端渲染)
3. 用户体验优化
- 菊花 loading
- 骨架屏加载