let vm = new Vue({
el: '#el'
data: {
o: 'object',
dog: {}
},
method: {
clickHandler () {
// 该 name 属性是否是响应式的
this.dog.name = 'Trump'
}
}
})
答:不是响应式数据。响应式对象和响应式数组是指在vue初始化时期,利用Object.defineProperty()方法对其进行监听,这样在修改数据时会及时体现在页面上。
设置为响应式数据有两种方法:
1、给 dog 的属性 name 设置一个初始值,可以为空字符串或者 undefined 之类的,代码和原因如下:
let vm = new Vue({
el: '#app',
data: {
msg: 'object',
dog: {
name: ''
}
},
method: {
clickHandler() {
// 该 name 属性是否是响应式的
this.dog.name = 'Trump'
}
}
})
原因:vm[key] setter 操作的时候会触发 data[key] 的 setter 操作,data[key] 的 setter 操作会 walk 这个新的值(walk方法是给data里的对象类型的值设置响应式),而题目中的 data 的 dog 是个空对象,没有任何属性,所以初始化 Vue 实例的时候,在给 dog 设置 proxy 的时候没有任何属性有 getter 和 setter 方法,所以在点击按钮动态的给 dog 添加 name 属性,并设置值的时候是不会触发 dog 对象下的属性 name 的 setter 方法,故不是响应式数据。而给 dog 对象添加了 name 的初始值后,dog 对象的 name 属性就有了 getter 和 setter 方法,故可以实现响应式。代码如下:
vue.js(监听 vm[key]的 getter 和 setter 操作 )
class Vue {
constructor(options) {
// 1、通过属性保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2、把 data 中的成员转换成 getter 和 setter,并注入到 vue 实例中
this._proxyData(this.$data)
// 3、调用 observer 对象,监听数据的变化
new Observer(this.$data)
// 4、调用 compiler 对象,解析指令和插值表达式
new Compiler(this)
}
_proxyData(data) {
// 遍历 data 中的所有属性
Object.keys(data).forEach(key => {
// 把 data 的属性注入到 vue 实例中
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
if (newValue !== data[key]) {
data[key] = newValue
}
}
})
})
}
}
observe.js (data[key] 的 setter 操作会 walk 这个新的值,walk方法是给data里的对象类型的值设置响应式)
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
// 1、判断 data 是否是对象
if (!data || typeof data !== 'object') {
return
}
// 2、遍历 data 对象的所有属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(obj, key, val) {
let that = this
// 负责收集依赖,并发送通知
let dep = new Dep()
// 如果 val 是对象,把 val 内部的属性转换成响应式数据
that.walk(val)
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 收集依赖 Dep.target && dep.addSub(Dep.target) return val }, set(newValue) { console.log(newValue, '>>>>>', val) if (newValue === val) { return } val = newValue that.walk(newValue) // 发送通知 dep.notify() } }) } }
2、使用 Vue.set(target, key, value) 时,target 为需要添加属性的对象,key 是要添加的属性名,value 为属性 key 对应的值, vue 中 set 的源码如下:
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
上面源码的执行逻辑如下(参考链接:https://www.cnblogs.com/heavenYJJ/p/9559439.html):
defineReactive(ob.value, key, val) ,将新增属性设置为响应式; ob.dep.notify() 手动触发通知该属性值的更新, 所以我们可以修改代码如下:
let vm = new Vue({
el: '#app',
data: {
msg: 'object',
dog: {
name: undefined
}
},
method: {
clickHandler() {
// 该 name 属性是否是响应式的
this.$set(this.data.dog, name, 'Trump')
}
}
})
diff算法是一种通过同层的树节点进行比较的高效算法,避免了对树进行逐层搜索遍历,所以时间复杂度只有 O(n)。
diff算法有两个比较显著的特点:
1、比较只会在同层级进行, 不会跨层级比较。
2、在diff比较的过程中,循环从两边向中间收拢。
diff流程:
1 、首先定义 oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 分别是新老两个 VNode 的两边的索引。
2、接下来是一个 while 循环,在这过程中,oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 会逐渐向中间靠拢。while 循环的退出条件是直到老节点或者新节点的开始位置大于结束位置。
while 循环中会遇到四种情况:
情形一:当新老 VNode 节点的 start 是同一节点时,直接 patchVnode 即可,同时新老 VNode 节点的开始索引都加 1。
情形二:当新老 VNode 节点的 end 是同一节点时,直接 patchVnode 即可,同时新老 VNode 节点的结束索引都减 1。
情形三:当老 VNode 节点的 start 和新 VNode 节点的 end 是同一节点时,这说明这次数据更新后 oldStartVnode 已经跑到了 oldEndVnode 后面去了。这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldEndVnode 的后面,同时老 VNode 节点开始索引加 1,新 VNode 节点的结束索引减 1。
情形四:当老 VNode 节点的 end 和新 VNode 节点的 start 是同一节点时,这说明这次数据更新后 oldEndVnode 跑到了 oldStartVnode 的前面去了。这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldStartVnode 的前面,同时老 VNode 节点结束索引减 1,新 VNode 节点的开始索引加 1。
3、while 循环的退出条件是直到老节点或者新节点的开始位置大于结束位置。
情形一:如果在循环中,oldStartIdx大于oldEndIdx了,那就表示oldChildren比newChildren先循环完毕,那么newChildren里面剩余的节点都是需要新增的节点,把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
情形二:如果在循环中,newStartIdx大于newEndIdx了,那就表示newChildren比oldChildren先循环完毕,那么oldChildren里面剩余的节点都是需要删除的节点,把[oldStartIdx, oldEndIdx]之间的所有节点都删除
import Vue from 'vue'
console.dir(Vue)
let _Vue = null
export default class VueRouter {
// 实现 vue 的插件机制
static install(Vue) {
//1 判断当前插件是否被安装
if (VueRouter.install.installed) {
return;
}
VueRouter.install.installed = true
//2 把Vue的构造函数记录在全局
_Vue = Vue
//3 把创建Vue的实例传入的router对象注入到Vue实例
// _Vue.prototype.$router = this.$options.router
// 混入
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
}
}
})
}
// 初始化属性
constructor(options) {
this.options = options // options 记录构造函数传入的对象
this.routeMap = {} // routeMap 路由地址和组件的对应关系
// observable data 是一个响应式对象
this.data = _Vue.observable({
current: "/" // 当前路由地址
})
this.init()
}
// 调用 createRouteMap, initComponent, initEvent 三个方法
init() {
this.createRouteMap()
this.initComponent(_Vue)
this.initEvent()
}
// 用来初始化 routeMap 属性,路由规则转换为键值对
createRouteMap() {
//遍历所有的路由规则 把路由规则解析成键值对的形式存储到routeMap中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
});
}
// 用来创建 router-link 和 router-view 组件
initComponent(Vue) {
// router-link 组件
Vue.component('router-link', {
props: {
to: String
},
// render --- 可在 vue 运行时版直接使用
render(h) {
// h(选择器(标签的名字), 属性,生成的某个标签的内容)
return h('a', {
attrs: {
href: '#' + this.to,
},
// 注册事件
// on: {
// click: this.clickHandler // 点击事件
// },
}, [this.$slots.default]) // this.$slot.default 默认插槽
},
});
// router-view 组件
const self = this; //这里的 this 指向 vueRouter 实例
Vue.component('router-view', {
render(h) {
// 根据 routerMap 中的对应关系,拿到当前路由地址所对应的组件
const component = self.routeMap[self.data.current]
return h(component)
}
})
}
// 用来注册 hashchange 事件
initEvent () {
window.addEventListener('hashchange', () => {
this.data.current = this.getHash();
});
window.addEventListener('load', () => {
if (!window.location.hash) {
window.location.hash = '#/';
}
});
}
getHash() {
return window.location.hash.slice(1) || '/';
}
}
// 不同指令所调用的函数
update (node, key, attrName) {
let updateFn = this[attrName + 'Updater'];
// 处理 v-on 指令
if (attrName.startsWith('on:')) {
updateFn = this['on' + 'Updater']
const eventType = attrName.split(':')[1]
updateFn.call(this, node, this.vm[key], key, eventType)
return
}
updateFn && updateFn.call(this, node, this.vm[key], key) // 此处的 this 就是 compiler 对象
}
// 处理 v-html 指令
htmlUpdater (node, value, key) {
node.innerHTML = value
new Watcher(this.vm, key, (newValue) => {
node.innerHTML = newValue
})
}
// 处理 v-on 指令
onUpdater (node, value, key, eventType) {
// console.log(value, key, eventType, this.vm.$options.methods[key])
node.addEventListener(eventType, this.vm.$options.methods[key])
}