vue2.6 从入口分析源码的理解
目的:源码的学习及临摹简单实现,提升自身设计能力
数据视图的双向绑定
观察着模式 目标 obj ---> observe
被观察目标实体 add:ob, remove:ob, dep.notify()
贯彻着 watcher.update()
观察着模式的实现,如何实现响应式,比如哪些 react\vue
vue 的6大模块
1、compiler: 模板解析语法树,template、render 函数的写法最终被转换为 render
- directives
-codegen
- parser 转换 template --> render 所以用template 会更好,会带有优化功能
- html-parser
- entity-decoder
- filter-parser
2、core 核心:vDom / observe/components/instance
3、platforms: web/ weex 跨平台
4、server: 服务端渲染
5、sfc: 单文件.vue的解析
6、shared: 工具、变量
从 core 的文件夹 index.js 开始
// core/index.js
import Vue from './instance/index'
// initGlobalAPI(Vue) 依赖注入Vue 完成一些属性 prototype 的挂载安装
initGlobalAPI(Vue)
进入 instance/index 文件下查看Vue
//instance/index.js
function Vue (options) {
this._init(options)
}
// 单独抽离下面的流程,不仅仅是初始化,还生成了vue必要的参数属性 是必要的
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
initMixin(Vue)
、stateMixin(Vue)
.. 等状态文件单独抽离出来,不仅仅是初始化,还生成了vue必要的参数属性。
1、initMixin 初始化
- 配置合并(props\inject\directives)
-init
- initLifecycle: 初始化一些对象的状态 vm._watcher = null
- initEvents : 初始化状态 vm._events 、vm._hasHookEvent
- initRender :渲染初始化 vm._vnode = null/ vm.$slots / vm.$scopedSlots/ /vm.$createElement /defineReactive(vm, '$listeners') /defineReactive(vm, '$attrs')
callHook(vm, 'beforeCreate')
-init
- initInjections :注入的数据递归响应式挂载
- initState: initProps /initData / initMethods/ initComputed/ initWatch
initProps /initData 递归属性,校验属性格式及递归定义 props 及 data 为响应式对象
initMethods检验 methods[key] 和props 里面不能有重复的,递归执行methods[key]对应的函数。
initComputed 是每个属性 为一个new Watcher 实例 并且有缓存的watcher
initwatch 执行每一个属性都是new watcher 实例
- initProvide
callHook(vm, 'created')
挂载$mount
Vue.prototype._init=function(options){
// 合并属性
mergeOptions(options)
// 初始化生命周期
initLifecycle(vm)
// 初始化事件
initEvents(vm)
// 初始化化渲染
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// 挂载
vm.$mount(vm.$options.el)
}
initMixin 总结:
- 1、initState 执行 initData 函数把 data 处理为 observe(vm.$options.data, true /* asRootData */) 作为初始化的观察对象 遍历属性为 defineReactive -- get\set --dep.notify()
- 2、在initRender渲染时 defineReactive方法 定义了 `$attr` 和 `$listeners` 为加入依赖收集
- 3、initData 和initProps 里面 处理了proxy 代理 使得 vm._data.xxx = vm.xxx vm._props.xxx = vm.xxx 写在 data 和 props 中的属性都暴露在vm 上。
- 4、 在initState 中 处理了
data 的observe,
props 的 defineReactive(props, key, value)
computed 每个属性都是一个new Watcher()--待缓存的watcher 实例
initWatch 也是每个属性都是一个watcher vm.$watch(expOrFn, handler, options)
相关问题:
1.1、为何初始化的的是vue 没有用class 类对象,用的function 方法
1、除了可读性和易读性外,后续再vue 的proototype 上进行拓展
用函数对象更方便可读,维护及拓展
2、用类实现Vue 会用辅助类,来丰富基础类,辅助类和其他类的隔离存放,查看不太方便
1.2、beforeCreate 之前做了什么?
合并对象 、初始化事件、初始化vm.watcher=null
beforecreate 后才有initInject initState initProvide
1.3、props 和 data是如何把属性挂在vm 上的
在initState 阶段的 initDta 和initProps 函数中 通过代理,把私有属性_data 和_props 挂载到了vm 上
获取vm.key 就能拿到 vm._data.key || vm._props.key 值
proxy(vm, `_data`, key)
proxy(vm, `_props`, key)
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
// defineProperty 代理设置 get set 方法
Object.defineProperty(target, key, sharedPropertyDefinition)
}
1.4 为什么根vue 实例上 data 是对象,compontent 里面的data是函数?
data 如果是函数且没有返回值会报错handleError。
当 vue 组件是根组件的时候,是同 data 可以是对象,因为单页应用中只有一个根vue 实例,引用的最外层data 是同一个实例,组件中components data 是一个函数并有返回对象,如果是对象,两个组件会互相引用干扰错乱。
2、stateMinxin 做的就是挂载 一些数据到 Vue.prototype 上
Vue.prototype.$data
Vue.prototype.$props
Vue.prototype.$set
Vue.prototype.$delete
Vue.prototype.$watch 有 watcher实例生成, 完成后会返回一个 watch的卸载函数 unwatchFn
2.1 Vue.$set 的本质
就是手动设置了 针对数组用 splice 添加新对象,针对obj 新对象添加属性 然后设置了 definReactive 响应式依赖收集
数组的方法也被重写 splice、pop 、push、unshift 、shift、sorts会改变
export function set (target: Array<any> | Object, key: any, val: any): 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)) {
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
3、eventMinxin Vue 挂载一些事件
- Vue.prototype.$on 挂载一些生命周期对应的函数 hook:beforeCreate hook:created @click 等
const hookRE = /^hook:/
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
}
- Vue.prototype.$once 挂载后执行一次后删除
Vue.prototype.$once = function(event,fn){
const vm = this
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
-
Vue.prototype.$off 卸载数据
考虑 没有参数、没有fn 的卸载等边界情况没有写进去
Vue.prototy.$off = function(event, fn){
const vm = this
// array of events
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
- Vue.prototype.$emit 递归触发函数
Vue.prototy.$off = 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++) {
// 这里封装了promise 一遍执行
// invokeWithErrorHandling(cbs[i], vm, args, vm, info)
// 简单写就这样
cbs[i].apply(vm, args)
}
}
return vm
}
}
4、lifecycleMixin
Vue.prototype._update 对比vnode 去更新 Vnode 的更新
Vue.prototype.$forceUpdate vm._watcher.update()
Vue.prototype.$destroy 里面有 callHook(vm, 'beforeDestroy')、 移除自身、移除watch\派发更新树、移除所有监听、删除vm.$vode.parent 虚拟父节点为null
4.1 Vue.prototype.$destroy 里面 beforeDestory 到destroyed 过程做了什么?
如果有父级在父组件中移除自身dom
watchers 删除
更新 vnode,派发
destroyed 后移除 所有vm的监听事件 hook:created @click 等
5、renderMixin
// 安装运行方便函数
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick
Vue.prototype._render
注意一点,installRenderHelpers 是一个帮助函数,这个不是一开始加上去的,而是后面需要的时候在挂载到 对象中,节约性能
// 安装运行方便函数
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
5.1 nextTick
promise 存在的时候,多个 nextTick 回调函数都是在 primise.then 回调里面循环同步执行 ,可能有风险会死循环,当nextTick 里面的回调函数里面用 $set 设置内容改变,监听data 改变后又触发 nextTick 回调就会死循环。
timerFunc = () => {
p.then(flushCallbacks)
}
new MutationObserver: MutationObserver is 在 IE11 不可靠
counter =1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
// 监听数据
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
// 改变数据
counter = (counter + 1) % 2
textNode.data = String(counter)
}
setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
initGlobalAPI(Vue) 依赖注入Vue 完成一些属性 prototype 的挂载安装
引用挂载-
//global-api/index.js
Vue.config
// 这里是不被暴露出来的,有风险
Vue.util
Vue.set
Vue.delete
Vue.nextTick
Vue.options
Vue.observable
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
后续问题:
1、扩展 面试题: 选runtime only 还是 runtime+ complier 有啥区别?