一、简答题
1、当我们点击按钮的时候动态给 data 增加的成员是否是响应式数据,如果不是的话,如何把新增成员设置成响应式数据,它的内部原理是什么。
let vm = new Vue({
el: '#el'
data: {
o: 'object',
dog: {}
},
method: {
clickHandler () {
// 该 name 属性是否是响应式的
this.dog.name = 'Trump'
}
}
})
this.dog.name = 'Trump' 不是响应式的
正确写法
Vue.$set(this.dog,'name','Trump')
原理:vue2.X 通过 Object.defindProperty 挟持 obj 并将 obj 中的属性转换为 get 和 set 实现响应式
通过 this.dog.name = 'Trump' 向 obj 中添加属性, 所添加的 name 属性并没有转换为 get 和 set,所以不是响应式的
2、请简述 Diff 算法的执行过程
由于操作 dom 比较耗费性能, 所以当 dom 改变时,不能将整个 dom 全部更新, 需要进行比较,找出 dom 中被修改的节点进行更新
这个过程就叫 diff 算法
diff 算法的主要执行过程:
老节点不存在,直接添加新节点到父元素
新节点不存在,从父元素删除老节点。
-
新老节点都存在
判断是否是相同节点(根据 key、tag、isComment、data 同时定义或不定义)相同直接返回,不是相同节点如果新老节点都是静态的,且 key 相同。
从老节点拿过来,跳过比对的过程。
如果新节点是文本节点,设置节点的 text,新节点不是文本节点。新老节点子节点都存在且不同,使用 updateChildren 函数来更新子节点
只有新节点字节点存在,如果老节点子节点是文本节点,删除老节点的文本,将新节点子节点插入
只有老节点存在子节点,删除老节点的子节点updateChildren
给新老节点定义开始、结束索引
循环比对新节点开始 VS 老节点开始、新节点结束 VS 老节点结束、新节点开始 VS 老节点结束、新节点结束 VS 老节点开始并移动对应的索引,向中间靠拢
根据新节点的 key 在老节点中查找,没有找到则创建新节点。
- 如果新开始节点和老开始节点相同,移动索引比较下一个开始节点
- 如果新开始节点和老结束节点相同, 将老结束节点放到前面去,移动索引,继续比较
- 如果新结束节点和老结束节点相同, 移动索引
- 如果新结束节点和老开始节点相同, 将老开始节点放到后面去,移动索引
- 如果四种都不满足, 就用新的开始节点的 key 去老节点中寻找相同的, 如果节点相同
就把老节点中的那个节点放到前面去, 如果 key 相同,节点不同,就创建新的节点放入对应的位置 - 循环结束后,如果老节点有多的,则删除。如果新节点有多的,则添加。
二、编程题
1、模拟 VueRouter 的 hash 模式的实现,实现思路和 History 模式类似,把 URL 中的 # 后面的内容作为路由的地址,可以通过 hashchange 事件监听路由地址的变化。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#nav a {
margin-right: 10px;
}
#nav a.act {
color: #ff0000;
}
</style>
</head>
<body>
<nav id="nav"></nav>
<main id="app"></main>
<script>
class Router {
constructor() {
this.navs = [
{
path: '#index',
title: '首页',
content: '首页-内容',
},
{
path: '#news',
title: '新闻',
content: '新闻-内容',
},
{
path: '#about',
title: '关于',
content: '关于-内容',
},
]
this.navNode = document.getElementById('nav')
this.el = document.getElementById('app')
}
init() {
this.createNav()
this.haddleHashChage()
//监听hash值变动
window.addEventListener('hashchange', this.haddleHashChage.bind(this))
}
createNav() {
//创建导航
let fragment = document.createDocumentFragment()
this.navs.forEach((nav) => {
let tagA = document.createElement('a')
tagA.href = nav.path
tagA.innerText = nav.title
fragment.appendChild(tagA)
})
this.navNode.appendChild(fragment)
}
haddleHashChage() {
//根据hash值,变动内容
const hashVal = window.location.hash || this.navs[0].path
this.navs.forEach((nav, index) => {
let curNodes = this.navNode.childNodes[index]
curNodes.className = ''
if (nav.path == hashVal) {
curNodes.className = 'act'
this.el.innerHTML = nav.content
}
})
}
}
const router = new Router()
router.init()
</script>
</body>
</html>
2、在模拟 Vue.js 响应式源码的基础上实现 v-html 指令,以及 v-on 指令。
class Compiler {
constructor(vm) {
this.el = vm.$el;
this.vm = vm;
this.compile(this.el);
}
// 编译模板,处理文本节点和元素节点
compile(el) {
let childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => {
// 处理文本节点
if (this.isTextNode(node)) {
this.compileText(node);
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node);
}
// 判断node节点,是否有子节点,如果有子节点,要递归调用compile
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
// 编译元素节点,处理指令
compileElement(node) {
// console.log(node.attributes)
// 遍历所有的属性节点
Array.from(node.attributes).forEach((attr) => {
// 判断是否是指令
let attrName = attr.name;
if (this.isDirective(attrName)) {
if(attrName.startsWith("v-on"))
attrName = attrName.substr(4);
else
// v-text --> text
attrName = attrName.substr(2);
let key = attr.value;
this.update(node, key, attrName);
}
});
}
update(node, key, attrName) {
let updateFn = this[attrName + "Updater"];
updateFn && updateFn.call(this, node, this.vm[key], key);
}
// 处理 v-on 指令
onUpdater (node, value,eventType) {
node.addEventListener(eventType, value)
new Watcher(this.vm, eventType, newValue => {
node.removeEventListener(eventType, value)
node.addEventListener(eventType, newValue)
})
}
// 处理 v-html 指令
htmlUpdater(node, value, key) {
node.innerHTML = value;
new Watcher(this.vm, key, (newValue) => {
node.innerHTML = newValue;
});
}
// 处理 v-text 指令
textUpdater(node, value, key) {
node.textContent = value;
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
// v-model
modelUpdater(node, value, key) {
node.value = value;
new Watcher(this.vm, key, (newValue) => {
node.value = newValue;
});
// 双向绑定
node.addEventListener("input", () => {
this.vm[key] = node.value;
});
}
// 编译文本节点,处理差值表达式
compileText(node) {
// console.dir(node)
// {{ msg }}
let reg = /\{\{(.+?)\}\}/;
let value = node.textContent;
if (reg.test(value)) {
let key = RegExp.$1.trim();
node.textContent = value.replace(reg, this.vm[key]);
// 创建watcher对象,当数据改变更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
}
// 判断元素属性是否是指令
isDirective(attrName) {
return attrName.startsWith("v-");
}
// 判断节点是否是文本节点
isTextNode(node) {
return node.nodeType === 3;
}
// 判断节点是否是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
}
3、参考 Snabbdom 提供的电影列表的示例,利用 Snabbdom 实现类似的效果,如图:
<img src="images/Ciqc1F7zUZ-AWP5NAAN0Z_t_hDY449.png" alt="Ciqc1F7zUZ-AWP5NAAN0Z_t_hDY449" style="zoom:50%;" />
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
import { styleModule } from 'snabbdom/build/modules/style'
import { eventListenersModule } from 'snabbdom/build/modules/eventlisteners'
import { originalData } from './originData'
let patch = init([styleModule, eventListenersModule])
let data = [...originalData]
const container = document.querySelector('#app')
let sortBy = 'rank'
let vnode = view(data)
// 初次渲染
let oldVnode = patch(container, vnode)
// 渲染
function render() {
oldVnode = patch(oldVnode, view(data))
}
// 生成新的VDOM
function view(data) {
return h('div#container', [
h('h1', 'Top 10 movies'),
h('div', [
h('a.btn.add', { on: { click: add }, style: { ['margin-right']: '10px' } }, '添加'),
'Sort by: ',
h('span.btn-group', [
h(
'a.btn.rank',
{
class: { active: sortBy === 'rank' },
on: {
click: () => {
changeSort('rank')
},
},
style: { margin: '10px' },
},
'Rank'
),
h(
'a.btn.title',
{
class: { active: sortBy === 'title' },
on: {
click: () => {
changeSort('title')
},
},
style: { margin: '10px' },
},
'Title'
),
h(
'a.btn.desc',
{
class: { active: sortBy === 'desc' },
on: {
click: () => {
changeSort('desc')
},
},
style: { margin: '10px' },
},
'Description'
),
]),
]),
h('div.list', data.map(movieView)),
])
}
// 添加一条数据 放在最上面
function add() {
const n = originalData[Math.floor(Math.random() * 10)]
data = [{ rank: data.length + 1, title: n.title, desc: n.desc, elmHeight: 0 }].concat(data)
render()
}
// 排序
function changeSort(prop) {
console.log(1111)
sortBy = prop
data.sort(function (a, b) {
if (a[prop] > b[prop]) {
return 1
}
if (a[prop] < b[prop]) {
return -1
}
return 0
})
render()
}
// 单条数据
function movieView(movie) {
return h(
'div.row',
{
key: movie.rank,
style: {
display: 'none',
delayed: { transform: 'translateY(' + movie.offset + 'px)', display: 'block' },
remove: { display: 'none', transform: 'translateY(' + movie.offset + 'px) translateX(200px)' },
},
hook: {
insert: function insert(vnode) {
movie.elmHeight = vnode.elm.offsetHeight
},
},
},
[
h('div', { style: { fontWeight: 'bold' } }, movie.rank),
h('div', movie.title),
h('div', movie.desc),
h(
'div.btn.rm-btn',
{
on: {
click: () => {
remove(movie)
},
},
},
'删除'
),
]
)
}
// 删除数据
function remove(movie) {
console.log(movie)
data = data.filter(function (m) {
return m.title !== movie.title
})
render()
}