vue数据处理
vue在初始化的时候会调用initState(vm),这个函数接受vue的实例作为参数,所有的数据操作都和由这个函数处理
vue2响应式数据实现原理
- 首先调用
observe(data)来观察数据,判断vue实例传递的数据是不是对象,不是对象返回,是对象在进行处理(函数也是对象)
export function observe(data) {
// 这里的data 已经是data()函数的返回值了,是对象
if (typeof data !== 'object' || data === null) return
if (data.__ob__) return
let ob = new Observe(data)
return ob
}
- 创建
Observe实例
Observe实例为数据新增增加了一个__ob__属性,这个属性可以标识,当前数据对象,是否已经设置过双向数据绑定,如果已经设置,在数据开始观察的地方(observe(data)),就可以直接返回。
在这个类中,分别有walk,observeArray来处理对象和数组。
class Observe {
constructor(value) {
this.dep = new Dep() // 为每对象,或者数组都绑定一个dep
Object.defineProperty(value, '__ob__', {
value: this,
enumerable: false,
configurable: false
})
if (Array.isArray(value)) {
// 重写数组方法
Object.setPrototypeOf(value, arrayMethods)
// 监测数组属性
this.observeArray(value)
} else {
this.walk(value)
}
}
walk(obj) {
const keys = Object.keys(obj)
// 处理数据对象的每一个属性
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray(value) {
for (let i = 0; i < value.length; i++) {
observe(value[i])
}
}
}
- 对象:
defineReactive就是对数据对象的每一个key做处理的 利用Object.defineProperty对对象的添加额外的操作,当用户修改对象的属性值的时候,触发set,获取的时候出发get,Object.defineProperty已经能及时的获取数据的最新值了,但是还不能实现数据变化,ui界面同时更新。所以又对get做了特别的处理。
export function defineReactive(obj, key) {
let val = obj[key]
let childOb = observe(val) // 对结果递归拦截
let dep = new Dep() // 为每个属性创建一个dep
Object.defineProperty(obj, key, {
get() {
// 取值的时候,把watcher 存入dep
if (Dep.target) {
dep.depend()
if (childOb) {
// 主要用来搜集数组的依赖,当然,对象也是可以搜集的,但是对象,没有这样的触发
childOb.dep.depend()
if (Array.isArray(val)) {
dependArray(val)
}
}
}
return val
},
set(newVal) {
if (val === newVal) return
observe(newVal) // 把属性设置新对象的时候,也能有set,get
val = newVal
dep.notify()
}
})
}
- 数组 数组的处理是循环数组的每一项
数组的对象元素,就会调用处理对象的方式,非对象元素,直接返回
observeArray(value) {
for (let i = 0; i < value.length; i++) {
observe(value[i])
}
}
- 数据变化通知ui视图 (dep/watcher)
- dep
dep的主要在数据更新触发
dep.notify()通知更新,数据获取的时候触发dep.depend()搜集数据,同时dep里会搜集所有的wather,每一个属性都有一个watcher
let id = 0
export default class Dep {
constructor() {
this.id = id++
this.subs = [] // 存放watcher
}
depend() {
// 实现watcher也记住dep
// Dep.target 是watcher
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify() {
// 更新
this.subs.forEach((watcher) => {
watcher.update()
})
}
addSub(watcher) {
// 实现dep 记住watcher
this.subs.push(watcher)
}
}
// 保存watcher
Dep.target = null
export function pushTarget(watcher) {
Dep.target = watcher
}
export function popTarget() {
Dep.target = null
}
- watcher
真正的更新操作在
queueWatcher里面,并且了,防止用户不挺的更改同一个属性,还设置了防抖操作
import Dep, { popTarget, pushTarget } from "./dep"
import { queueWatcher } from "./scheduler"
let id = 0
export default class Watcher {
constructor(vm, expOrFn, cb, options = {}, isRenderWatcher) {
this.vm = vm
this.cb = cb
this.options = options;
this.lazy = (options && options.lazy) || false;
this.dirty = this.lazy
this.user = (options && options.user) || false;
this.isRenderWatcher = isRenderWatcher
this.id = id++
this.deps = [] // 存放dep
this.depsId = new Set() // 防止存放多个相同的属性的dep
// 因为用户wacther 的传入, expOrFn 是属性字符,而不是函数
if (typeof expOrFn === "function") {
this.getter = expOrFn
} else {
this.getter = function () {
// 这里的方式是为了,用户深层取值a.b.c.d
let path = expOrFn.split('.')
let val = vm
for (let i = 0; i < path.length; i++) {
val = val[path[i]]
}
return val
}
}
this.value = this.lazy ? undefined : this.get()
}
get() {
// 还是执行更新函数,只是更加语义化
// 不是模版取值的操作,根本不会进入watcher
pushTarget(this)
let value = this.getter.call(this.vm)
popTarget()
return value
}
evaluate() {
this.value = this.get()
this.dirty = false
}
depend() {
// 为了调用compuetd的渲染组件,能在页面属性更新的时候,页面模版中的computed 也更新
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
addDep(dep) {
let id = dep.id
if (!this.depsId.has(id)) {
this.depsId.add(id)
this.deps.push(dep)
dep.addSub(this)
}
}
run() {
// oldValue, newValue 使用用户使用watch监听属性的时候会使用
let oldValue = this.value
let newValue = this.get()
this.value = newValue
if (this.user) {
this.cb.call(this.vm, newValue, oldValue)
}
}
update() {
// 这种更新太粗暴,用户不停的修改同一个属性,就会调用多次,所以这里需要一个防抖的操作
// this.get()
if (this.options && this.options.sync) {
this.run()
} else if (this.lazy) {
this.dirty = true
} else {
queueWatcher(this)
}
}
// 模版数据变化之后,程序主动调用watcher,就可以更新数据了,每个组件中的数据,都因该有自己的watcher,所以这里watcher 要保存起来,vue把watcher 保存在了dep 类中
}
总结: observe 遍历数据属性,为每个属性添加get,set操作,在数据set时痛过dep.notify()通知watcher更新视图,在数据get的时候,触发dep.depend()进行数据收集,让wather把当前的dep收集起来
vue3响应式原理
以reactive为例,处理对象目标的利用new Proxy包对象,免去了vue2循环处理对象对象属性的过程。
// 存放target的对象, 分只读的和可变的
export const readonlyMap = new WeakMap()
export const reactiveMap = new WeakMap()
function createReactiveObject(targrt, isReadonly, baseHandlers) {
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existProxy = proxyMap.get(targrt)
if (existProxy) {
return existProxy
}
const proxy = new Proxy(targrt, baseHandlers)
proxyMap.set(targrt, proxy)
return proxy
}
数据的依赖收集和触发更新
可以看到get时触发track收集数据,set时trigger通知更新
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
if (!isReadonly) {
//依赖搜集
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
const oldValue = target[key]
// 这里要区分新增属性和修改属性
// 新增肯定要更新,修改属性要判断新旧属性的值是否相等,不想等才修改
// 修改又涉及到array 是数组,并且下表小于数组长度,那就是修改
// 如果数组的下表大于数组长度,那就是新增
const res = Reflect.set(target, key, value, receiver)
const hasKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key)
if (!hasKey) {
// 新增
trigger(target, TriggerOpTypes.ADD, key, value)
} else {
// 修改
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return res
}
}
track
对象都有一个打的depsMap,当获取对象上的属性的时候,又把属性对应的dep存放到当前对象的depsMap里
let targetMap = new WeakMap()
// 搜集用户的更新,方便以后调用,在get时使用
export function track(target, type, key) {
if (activeEffect === undefined) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
}
}
trigger
tirgger里面收集了所有的需要被修改的属性,放在effects里面等待统一调用effect进行修改
// 通知依赖更新
export function trigger (target, type, key?, value?, oldValue?) {
let depsMap = targetMap.get(target)
// !depsMap 代表当前的属性根本没有被追踪,即,没有被使用过
if (!depsMap) return
// 要一块通知
const effects = new Set()
function add(effectsToAdd) {
if (effectsToAdd) {
effectsToAdd.forEach(effect => effects.add(effect))
}
}
// 修改数组的长度
if (key === 'length' && isArray(target)) {
depsMap.forEach((dep: any, key: any) => {
// 都代表为数组新增了元素
if (key === 'length' || (isIntegerKey(key) && Number(key) >= value )) {
add(dep)
}
})
} else {
// 对象
if (key !== undefined) {
add(depsMap.get(key))
}
switch(type) {
case TriggerOpTypes.ADD:
if (isArray(target) && isIntegerKey(key)) {
add(depsMap.get('length'))
}
}
}
// 搜集了,这次更新的所有的回调函数,然后统一更新
effects.forEach((effect: any) => {
return effect()
})
}
总结: vue3的数据响应主要用new Proxy取代Object.defineProperty,减少了循环操作,把数据修改的dep.depend和dep.notify变成了track,trigger但是主要的思想是不变的
vue的渲染流程
- vue是实例话的时候调用
_init方法,调用$mountoptions参数渲染优先级render>template>el,确定渲染对象之后调用compileToFunctions
Vue.prototype.$mount = function (el) {
el = el && document.querySelector(el)
const vm = this
const options = vm.$options
// 把真实的el 挂载到vm, 方便以后使用
vm.$el = el
if (!options.render) {
let template = options.template
if (!template && el) {
template = el.outerHTML
}
// 将模版编译成函数
const render = compileToFunctions(template)
options.render = render
}
mountComponent(vm)
}
- compileToFunctions 将模版转换成ast语法树,再由ast转换成可渲染的字符串
export function compileToFunctions(template) {
// 解析模版成ast
const ast = parseHTML(template)
// 转换ast 成 render
const code = generate(ast)
let render = `with(this){return ${code}}`
let fn = new Function(render) // 让code字符串变成函数
return fn
}
- mountComponent
然后调用
mountComponent,先使用vue原型上的_render产生虚拟节点,通过_update调用patch把虚拟节点转换成真实节点,再调用watcher执行渲染函数。
export function mountComponent(vm) {
// 这里可以表示更新是组件级别的更新,渲染模版,数据显示,都放在watcher _render() 返回虚拟dom, _update() 执行比对和渲染过程
let updateComponent = () => {
let res = vm._update(vm._render())
}
// 实际是执行updateComponent 函数
new Watcher(vm, updateComponent, () => { }, {}, true /* 标志是渲染wacher */)
}
总结:vue的渲染流程就是通过优先级,选择正确的模版,然后把模版转换成ast语法树,再把ast转换成可渲染的render,然后产生虚拟节点,通过_update中的patch转变成真实节点,渲染到页面上
vue的组件渲染原理
render可以创建元素虚拟节点和文本虚拟节点
export function renderMixin(Vue) {
Vue.prototype._c = function (...args) { // 元素虚拟节点
const vm = this;
return createElement(vm, ...args)
}
Vue.prototype._v = function (text) { // 文本虚拟节点
const vm = this;
return createTextVnode(vm, text)
}
Vue.prototype._s = function (val) { // 转化字符串
// 触发this.XXX 直接把变量的值取了出来
return val === null ? '' : (typeof val === 'object') ? JSON.stringify(val) : val;
}
Vue.prototype._render = function () {
const vm = this;
let render = vm.$options.render
let vnode = render.call(vm)
return vnode
}
}
render创建虚拟节点的时候会判断,当前是原始标签还是组件标签(vue内部有搜集所有的原始标签),如果不是原始标签就走createComponent创建组件
export function createElement(vm, tag, data = {}, children) {
// 这里要对标签做区分,区分组件标签和原始标签
debugger
if (isReservedTag(tag)) {
return vnode(vm, tag, data, data.key, children, undefined)
} else {
// 组件渲染 获取组件
const Ctor = vm.$options.components[tag]
return createComponent(vm, tag, data, data.key, children, Ctor)
}
}
createComponent
创建组件的时候,不管组件是不死函数最终都会利用Vue.extend把组件转变成函数,并且为组件增加hook,在hook上绑定init,在由虚拟节点变成真实节点的时候,手动的调用$mount挂载组件
function createComponent(vm, tag, data, key, children, Ctor) {
debugger
if (isObject(Ctor)) {
// 如果组件是对象,要转换成函数 Vue.extend
Ctor = vm.$options._base.extend(Ctor)
}
// 为组件增加生命周期
data.hook = {
init(vnode) {
// 这里是因为vue.extend 的sub 类中,有this._init 方法
const child = vnode.componentInstance = new vnode.componentOptions.Ctor({})
child.$mount()
} // 初始化钩子函数
}
return vnode(tag, `vue-component-${Ctor.cid}-${tag}`, data, key, undefined, undefined, {
Ctor
})
}
总结:在render创建虚拟dom的时候会注意当前是不是html的原始标签,不是原始标签的话,就在创建的虚拟节点,调用vue.extend生成组件的实例。组件虚拟节点上上绑定hook,存放_init方法,并且在之后的patch过程中,通过componentInstance来判断是不是组件虚拟节点,调用_init函数,并且手动调用$mount,组件的渲染顺序是先父后子
vue.component
initGlobalAPI
Vue.component = function (id, definition) {
definition.name = definition.name || id
definition = this.options._base.extend(definition) // 这里保证不管怎么调用extend,都指向vue
this.options.components[id] = definition
}
vue.component内部调用vue.extend
export function initExtend(Vue) {
Vue.extend = function (options) {
// extend 里面的this,指向的也是this 因为 Vue.options._base 已经这样设定过了
const Super = this
const Sub = function VueComponent(options) {
this._init(options)
}
Sub.cid = cid++
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.component = Super.component
Sub.options = mergeOptions(Super.options, options)
return Sub
}
}
通过mergeOptions把vue.component的属性合并到vue.components上
watch 原理
initWatch把watch的值都转变成单个的元素,传递给createWatcher
function initWatch(vm, watch) {
for (let key in watch) {
const handler = watch[key];
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
createWatcher 获取watch监听的元素是对象的时候,获取handler,调用具体响应的方法,
function createWatcher(vm, key, handler, options) {
if (isObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm.$options.methods[handler]
}
// watch 最终走到$watch
vm.$watch(key, handler, options)
}
$watch 最终还是要使用watcher
export function stateMixin(Vue) {
Vue.prototype.$watch = function (expOrFn, cb, options) {
// 这里调用Watcher类
const vm = this
options.user = true // 用来区分不是渲染watcher,而是用户watcher
new Watcher(vm, expOrFn, cb, options)
}
}
总结:vue收集用户传入的watch选项,根据数组和对象作不同处理,最终进入watcher更新数据,设定了一个user的标志,在值更新的时候,确定是用户watcher的时候,就直接调用watch的回调,让用户在watch里面得到数据的新旧值
computed 原理
分函数获取和对象获取,为了防止computed主动取值,特别设置了一个lazy传入watcher中
function initComputed(vm, computed) {
// 用__computedWatchers 存放每个属性对应的watcher
const watchers = vm._computedWatchers = {}
for (let key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
// lazy 是用来表示computed, 不要重复更新数据的
watchers[key] = new Watcher(vm, getter, () => {}, {
lazy: true
})
// 取值操作,把computed的属性值变成实例中可以取到的属性
defineComputed(vm, key, userDef)
}
}
defineComputed 把不同的computed的取值操作都挂载到,对象的get上Object.defineProperty实现属性值的获取与设置
function defineComputed(target, key, userDef) {
// 要设置缓存的,防止每次获取computed里的值,都从新执行computed函数,要数据变化,才触发函数的执行
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key)
} else {
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = userDef.set || (() => {})
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
在computed 获取值的时候,方式不停的调用相同的属性值,在取值的时候设置dirty缓存,同时也对comouted的值进行了依赖收集,当computed的依赖的值变化的时候,会在watcher的内部,改变dirty的值,让computed获取到最新的值
function createComputedGetter(key) {
return function () {
let watcher = this._computedWatchers[key] // 计算属性的watcher
if (watcher.dirty) {
// 这里这样写是应为,在watcher 中有 this.value = this.lazy ? undefined : this.get() 判断,当前value = undefined
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
总结:computed绑定了watcher,利用lazy实现在watcher内部不会主动的获取值,然后利用 Object.defineProperty来触发get和set,同时在get的时候,根据watcher的dirty设置缓存,防止不停的取同一个属性,然后在依赖的属性值update的时候,改变dirty,让computed的值在get时候,从新变成新的值
watch和computed的区别
- watch内部会主动的获取值,computed不会,除非被调用
- watch 没有缓存,computed有缓存设置
- watch不需要依赖,computed需要依赖
v-for/v-if/v-model
-
vue2
v-for的优先级大于v-if -
vue3
v-if的优先级大于v-for -
v-for 会解析成函数表达式
-
v-if 会被解析成三元运算符
-
v-show 指令,会先存一下原来的样式,显示的时候,就把原来的样式复原
-
v-model 是一个真实的指令
- 放在input上会给input绑定value和input事件
- 放在组件上会形成一个model属性,变成input和value的语法糖
if (el.model) { data += `model:{value:${ el.model.value },callback:${ el.model.callback },expression:${ el.model.expression }},` }
vue组件名字的好处
- 可以根据名字查找组件
vm.$options[name](递归组件) - 缓存(keep-alive)
- name识别组件尤其是跨级通信
vue组件传值方式
- props/$emit
- provide/ inject
- v-model
- listeners
- $refs
- children
- eventBus
- vuex
$on/$emit
- 组件上的
data.on,最终在虚拟节点上产生一个listeners - init的时候
opts._parentListeners = vnodeComponentOptions.listeners - 然后在初始化事件时
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
add就是$on 收集组件上的所有方法,是一个数组
Vue.prototype.$on = function (event, fn){
const vm = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
$emit触发找到对应的事件触发里面的函数
Vue.prototype.$emit = function (event) {
const vm = this
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
$parent/$children
组件在调用init的时候会保留当前的虚拟节点,绑定在当前的组件上_parent,然后在leftCirlce的时候,绑定到parnet增加$children保存当前的组件
export function initLifecycle(vm: Component) {
debugger
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
// 判断父组件是不是抽象组件
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
// 父级挂在
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
provide/ inject
在父组件的实例上添加provide之后,会存放在实例的_provided中,子组件遇到inject通过$parent一直向上查找到具有provide的父组件,获取值
export function initProvide (vm) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
export function initInjections (vm) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
defineReactive(vm, key, result[key])
})
toggleObserving(true)
}
}
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (key === '__ob__') continue
const provideKey = inject[key].from
let source = vm
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${key}" not found`, vm)
}
}
}
return result
}
}
vue.set 原理
- 传入的属性是数组的话,直接调用数组的splice方法,因为数组的splice方法已经被改写了
- 如果这个属性以前就被监听过,直接返回
- 如果属性值不需要数据响应,就直接返回
- 如果是个新值,就重新执行一遍数据的依赖收集,且触发一下更新操作
export function set(target, key val) {
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 (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
vue.use 原理
vue.use 接收插件,插件有2种形式,函数和带有install属性的对象,当vue之前已经加载过插件,就会直接返回(单例模式)。如果plugin是函数,就直接调用,如果有install 就调用install
export function initUse (Vue) {
Vue.use = function (plugin) {
// 单例模式
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// 去除插件的第一个参数 vue
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
slot
- 普通插槽 父组件中渲染,渲染完之后,遇到slot就替换成之前渲染的内容
- 作用域插槽
子组件中渲染 slot-scope
keep-alive
缓存,使用lru算法(最久未使用法)keep-alive是一个抽象组件
- 获取keep-alive下第一个子组件的实例,通过这个实例获取组件的name
- 功过include和exclude判断当前组件是否需要被缓存,不需要缓存的话,直接返回当前组件实例的vnode
- 需要缓存的话,就把当前组件的key移除,并且把这个key放到keys的最后面
- 如果不存在话,添加缓存key,判断当前的keys是否超过设置的max,超过的话,就把最长没有使用的数组key从keys移除
- 然后把数组的keepAlive设置为true
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot) // 取第一个数组元素
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
vue中的设计模式
- 工厂模式:根据不同的参数,返回不同的实例
- 单例模式:vue插件
- 发布订阅:emit
- 代理模式: vue3 proxy
- 中介者模式:vuex
vue diff算法
diff算法真正发生在新旧节点的比较过程中
- 比较节点标签是否一致,不一致直接新节点替换旧节点
if (oldVnode.tag !== vnode.tag) {
return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el)
}
- 新旧节点都是文本节点,但是内容不一致,替换文本
if (!oldVnode.tag) {
if (oldVnode.text !== vnode.text) {
return oldVnode.el.textContent = vnode.text
}
}
- 标签元素相等,但是属性不相等替换属性
let el = vnode.el = oldVnode.el
updateProperties(vnode, oldVnode.data)
- 节点的儿子比较
- 旧节点有孩子,新节点没有孩子,移除孩子
- 旧节点没孩子,新节点有孩子,插入孩子
- 新旧节点都有孩子(diff的核心,采用双指针算法)
采用4个指针分别指向,新旧节点的首位元素,同时记录当前的indexif (oldChildren.length > 0 && newChildren.length > 0) { // 新旧节点都有儿子,才涉及真正的diff updateChildren(el, oldChildren, newChildren) } else if (oldChildren.length > 0) { // 旧节点有孩子,新节点没孩子,移除 el.innerHTML = '' } else { // 旧节点,没孩子,新节点有孩子,插入 newChildren.forEach(child => el.appendChild(createElm(child)) ) }- 头头比较
- 尾尾比较
- 头尾比较
- 尾头比较
- 2端的比较都不符合,就要孩子节点之间--比较了
- 循环结束之后,看新节点多,还是旧节点多,然后进行响应的插入和删除
function updateChildren(parent, oldChildren, newChildren) { // 比较孩子节点,这里采用双指针的算法 let oldStartIndex = 0 let oldEndIndex = oldChildren.length - 1 let oldStartVnode = oldChildren[0] let oldEndVnode = oldChildren[oldEndIndex] let newStartIndex = 0 let newEndIndex = newChildren.length - 1 let newStartVnode = newChildren[0] let newEndVnode = newChildren[newEndIndex] // 创建oldVnode 的key 和index 的映射表 function makeIndexByKey(oldChildren) { let map = {} oldChildren.forEach((item, index) => { map[item.key] = index }) return map } const map = makeIndexByKey(oldChildren); // 指针比较的时候,新旧孩子,都要从头开始比较 while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { // push 向后插入的情况 // 要比较指针指向的孩子,是不是相同的节点 // !oldStartVnode,!oldEndVnode 是因为在乱序比较的时候,会有把节点设置为空的情况,这个时候移动指针,遇到空,直接跳过 if (!oldStartVnode) { oldStartVnode = oldChildren[++oldStartIndex] } else if (!oldEndVnode) { oldEndVnode = oldChildren[--oldEndIndex] } else if (isSameVnode(oldStartVnode, newStartVnode)) { // 头头比较 // 相同的节点,可以比较属性了 patch(oldStartVnode, newStartVnode) oldStartVnode = oldChildren[++oldStartIndex] newStartVnode = newChildren[++newStartIndex] } else if (isSameVnode(oldEndVnode, newEndVnode)) { // 尾尾比较 // 向前插入的情况, 要从后面开始比对 patch(oldEndVnode, newEndVnode) oldEndVnode = oldChildren[--oldEndIndex] newEndVnode = newChildren[--newEndIndex] } else if (isSameVnode(oldStartVnode, newEndVnode)) { // 头尾比较 // 头头相比不行,尾尾相比也不行,开始头尾相比 patch(oldStartVnode, newEndVnode) // 把旧节点的头指针元素,移动到最后一个指针的后面 parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling) oldStartVnode = oldChildren[++oldStartIndex] newEndVnode = newChildren[--newEndIndex] } else if (isSameVnode(oldEndVnode, newStartVnode)) { // 尾头比较 patch(oldEndVnode, newStartVnode) // 插入头指针的前面 parent.insertBefore(oldEndVnode.el, oldStartVnode.el) oldEndVnode = oldChildren[--oldEndIndex] newStartVnode = newChildren[++newStartIndex] } else { let moveVnodeIndex = map[newStartVnode.key] // 在map 表中并不存在,就在旧节点的开始指针的前面插入这个新节点 if (moveVnodeIndex === undefined) { parent.insertBefore(createElm(newStartVnode), oldStartVnode.el) } else { // map 表中有,就要获取index,比对2个节点 let moveVnode = oldChildren[moveVnodeIndex] // 把移动了的节点,变成undefied oldChildren[moveVnodeIndex] = undefined patch(moveVnode, newStartVnode) // 比对之后移动节点(真实的) parent.insertBefore(moveVnode.el, oldStartVnode.el) } newStartVnode = newChildren[++newStartIndex] } } // 新节点后面多出来的孩子 newStartIndex - newEndIndex 新节点比老节点多 if (newStartIndex <= newEndIndex) { for (let i = newStartIndex; i <= newEndIndex; i++) { const nextElm = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].el : null // 为了保证新节点,是在前面增加的 parent.insertBefore(createElm(newChildren[i]), nextElm) // 实际中测试appendChild也可以插入前面 // parent.appendChild(createElm(newChildren[i])) } } // 旧节点比新节点多,要删除 if (oldStartIndex <= oldEndIndex) { for (let i = oldStartIndex; i <= oldEndIndex; i++) { const child = oldChildren[i] if (child !== undefined) { parent.removeChild(child.el) } } } }
vue3-- ref 原理
ref 内部也是使用类绑定value,在get,set的时候分别使用track tigger收集数据,更新数据,并且在数据是对象的时候,调用reactive
export function ref(value) {
return createRef(value)
}
export function createRef(value, shallow = false) {
// 类会在babel 转化为Object.defineProperty()的方式
return new RefImpl(value, shallow)
}
function convert(value) {
return isObject(value) ? reactive(value): value
}
class RefImpl {
public _value
public readonly __v_isRef = true
constructor(public _rawValue, public readonly _shallow = false) {
// 这里的_value 要特别判断,如果是浅层的就直接返出,不是浅层的要针对对象做特别处理
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
track(this, TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
// 先判断数据有没有变化
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = newVal
trigger(this, TriggerOpTypes.SET, 'value', newVal)
}
}
}
vue3-- toRef原理
内部使用类,绑定value,对象的值是proxy包装过的,不用担心数据更新问题
export function toRef(target, key) {
return new ObjectRefImpl(target, key)
}
class ObjectRefImpl {
public readonly __v_isRef = true
constructor(public _object, public _key) {
}
// 取值和赋值的操作,都操作的object,而且object是proxy的对象,自然可以实现更新
get value() {
return this._object[this._key]
}
set value(newVal) {
this._object[this._key] = newVal
}
}
vue3-- toRefs
toRefs的实现主要是循环调用toRef
export function toRefs(object) {
const ret = isArray(object) ? new Array(object.length) : {}
for(let key in object) {
ret[key] = toRef(object, key)
}
return ret
}
vue3--computed
computed也是通过内部的类来实现获取value,并在get,set的时候出发依赖收集,但是computed也设置了缓存和数据变化的时候才更新
export function computed(getterOrOptions) {
let getter
let setter
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = () => {
console.log('=====函数不能设置set')
}
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return new ComputedRefImpl(getter, setter, isFunction(getterOrOptions) || !getterOrOptions.get)
}
class ComputedRefImpl {
private effect
private _value
public __v_isReadonly
public __v_isRef = true
public _dirty = true
constructor(getter, public _setter, public isReadonly) {
this.__v_isReadonly = isReadonly
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value')
}
}
})
}
get value() {
if (this._dirty) {
this._value = this.effect()
this._dirty = false
}
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
this._setter(newVal)
}
}
vue-router模式
- hash: hashChange 没有兼容问题,但是丑,hash的部分不会在页面变化的时候发送到服务端,所以路由都是在浏览器端的历史记录里。
- history: popState页面会404,需要后端配置。主要是pushState和replaceState可以无刷新页面改变历史记录,但是不会主动触发popstate所以需要手动触发
vuex的理解
- 单向数据流的概念
- 状态集中管理,实现多组件状态共享
- vuex也是通过new vue产生实例,达到响应式数据的目的