深入浅出Vue.js(四) 整体流程

整体流程

vm.$set(target,key,val)

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.__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
}

vm.$delete(target,key)

function del(target,key){
    if(Array.isArray(target) && isValidArrayIndex(key)){
        target.splice(key,1)
        return
    }
    const ob = target.__ob__
    if(target.isVue || (ob && ob.vmCount)){
        return
    }
    // 如果key不是target自身的属性,则终止程序继续执行
    if(!hasOwn(target,key)){
        return
    }
    delete target[key]
    // 如果ob不存在(判断target是不是一个响应式数据),则直接终止程序
    if(!ob){
        return
    }
    ob.dep.notify()
}

vm.$on(event,fn)

Vue.prototype.$on = function(event,fn){
    const vm = this
    if(Array.isArray(event)){
        for(let i = 0,len = event.length;i < len;i++){
            this.$on(event[i],fn)
        }
    }else{
        (vm._events[event] || (vm._events[event] == [])).push(fn)
    }
    return vm
}

vm._events是一个对象,在执行new Vue()时,Vue会执行this._init()方法进行一系列的初始化操作,其中就会在Vue.js的实例上创建一个_events属性,用来存储事件。vm._events = Object.create(null)。

vm.$off(event,fn)

移除自定义事件监听器

  • 如果没有提供参数,则移除所有的事件监听器。
  • 如果只提供了事件,则移除该事件所有的监听器。
  • 如果同时提供了事件与回调,则只移除这个回调的监听器。
Vue.prototype.off = function(event,fn){
    const vm = this
    if(!arguments.length){
        vm._events = Object.create(null)
        return vm
    }
    if(Array.isArray(event)){
        for(let i = 0,len = event.length;i < len;i++){
            this.$off(event[i],fn)
        }
        return vm
    }
    const cbs = vm._events[event]
    if(!cbs){
        return vm
    }
    if(arguments.length == 1){
        vm._events[event] = null
        return vm
    }
    if(fn){
        const cbs = vm._events[event]
        let cb;
        let i = cbs.length
        while(i--){
            cb = cbs[i]
            if(cb == fn || cb.fn == fn){
                cbs.splice(i,1)
                break
            }
        }

    }
    return vm
}

vm.$once(event,fn)

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
}

vm.$emit(event[,...args])

Vue.prototype.$emit = function(event,...args){
    const vm = this
    let cbs = tis._events[event]
    if(cbs){
        for(let i = 0;i < cbs.length;i++){
            try{
                cbs[i].apply(vm,args)
            }catch(err){
                console.log(err)
            }
        }
    }
}

vm..$forceUpdate()

Vue.prototype.$forceUpdate = function(){
    const vm = this
    if(vm._watcher){
        wm._watcher.update()
    }
}

vm.$destory()

function remove(arr,item){
    if(arr.length){
        const index = arr.indexOf(item)
        if(index > -1){
            arr.splice(index,1)
        }
    }
}

Vue.prototype.$destroy = function(){
    const vm = this
    if(vm._isBeingDestroyed){
        return
    }
    callHook(vm,'beforeDestroy')
    vm._isBeingDestroyed = true
    const parent = vm.$parent
    if(parent && !parent._isBeingDestroyed && !vm.$options.abstract){
        remove(parent.$children,vm)
    }
    if(vm._watcher){
        vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while(i--){
        vm._watchers[i].teardown()
    }
    vm._isDestroyed = true
    vm.__patch__(vm.vnode,null)
    callHook(vm,'destroyed')
    vm.$off()
}   

vm.$nextTick([fn])

const callbacks = []
let pending = false
function flushCallbacks(){
    pending = false
    const copies = callbacks.splice(0)
    callbacks.length = 0
    for(let i = 0;i < copies.length;i++){
        copies[i]()
    }
}

let microTimerFunc
let macroTimerFunc
let useMacroTask = false

if(typeof setImmediate !=='undefined' && isNative(setImmediate)){
    macroTimerFunc = () => {
        setImmediate(flushCallbacks)
    }
}else if(typeof MessageChannel !=='undefined' && (isNative(MessageChannel) || MessageChannel.toString() === '[object MessageChannelConstructor]')){
    const channel = new MessageChannel()
    const port = channel.port2
    channel.port1.onmessage = flushCallbacks
    macroTimerFunc = () => {
        port.postMessage(1)
    }
}else{
    macroTimerFunc = () => {
        setTimeout(flushCallbacks, 0);
    }
}
if(typeof Promise !== 'undefined' && isNative(Promise)){
    const p = Promise.resolve()
    microTimerFunc = () => {
        p.then(flushCallbacks)
    }
}else{
    microTimerFunc = macroTimerFunc
}

function withMacroTask(fn){
    return fn._withTask || (fn._withTask = function(){
        useMacroTask = true
        const res = fn.apply(null,arguments)
        useMacroTask = false
        return res
    })
}

function nextTick(cb,ctx){//参数:回调函数,上下文
    let _resolve
    callbacks.push(()=>{
        if(cb){
            cb.call(ctx)
        }else if(_resolve){
            _resolve(ctx)
        }
    })
    if(!pending){
        pending = true
        if(useMacroTask){
            macroTimerFunc()
        }else{
            microTimerFunc()
        }
    }
    if(!cb && typeof Promise !=='undefined'){
        return new Promise(resolve => {
            _resolve = resolve
        })
    }
}

Vue.prototype.$nextTick = function(fn){
    return nextTick(fn,this)
}

vm.$mount() 

如果vue.js实例在实例化时没有收到el选项,则它处于未挂载状态,没有关联的DOM元素。我们可以使用vm.$mount手动挂载一个未挂载的实例。如果没有提供elementOrSelector参数,模板将被渲染为文档之外的元素,并且必须使用原生的DOM的API把它插入文档中。这个方法返回实例自身,因而可以链式调用其他实例方法。

完整版和只包含运行时版本之间的差异在于是否有编译器,而是否有编译器的差异主要在于vm.$mount方法的表现形式。在只包含运行时的构建版本中,vm.$mount的作用如前面介绍的那样。而在完整的构建版本中,vm.$mount的作用会稍有不同,它会首先检查template或el选项所提供的模板是否已经转换成渲染函数(render函数)。如果没有,则立即进入编译过程,将模板编译为渲染函数,完成之后再进入挂载与渲染的流程中。

只包含运行时的vm.$mount没有编译步骤,它会默认实例上已经存在的渲染函数,如果不存在,则会设置一个。并且,这个渲染函数在执行时会返回一个空节点VNode,以保证执行时不会因为函数不存在而报错。同时如果在开发环境下运行,vue.js会触发警告,提示当前使用的是只包含运行时版本,会让我们提供渲染函数,或者去使用完整的构建版本。

完整版vm.$mount的实现原理

function query(el){
    if(typeof el === 'string'){
        const selected = document.querySelector(el)
        if(!selected){
            return document.createElement('div')
        }
        return selected
    }else{
        return el
    }
}

function getOuterHTML(el){
    if(el.outerHTML){
        return el.outerHTML
    }else{
        const container = document.createElement('div')
        container.appendChild(el.cloneNode(true))
        return container.innerHTML
    }
}

function idToTemplate(id){
    const el = query(id)
    return el && el.innerHTML
}

function createFunction(render){
    return new Function(render)
}

function compileToFunctions(tempalte,options,vm){
    options = extend({},options)
    const key = options.delimiters ? String(options.delimiters) + tempalte : tempalte
    if(cache[key]){
        return cache[key]
    }
    const compiled = compile(tempalte,options)
    const res = {}
    res.render = createFunction(compiled.render)
    return (cache[key] = res)
}

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function(el){ //函数劫持
    el = el && query(el)
    const options = this.options
    if(!options.render){
        let template = options.tempalte
        if(template){
            if(typeof template == 'string'){
                if(template.charAt(0) == '#'){
                    template = idToTemplate(template)
                }
            }else if(template.nodeType){
                template = template.innerHTML
            }else{
                return this
            }
        }else if(el){
            template = getOuterHTML(el)
        }
        if(template){
            const {render} = compileToFunctions(template,{...},this)
            options.render = render
        }
    }
    return mount.call(this,el)
}

只包含运行时版本的vm.$mount的实现原理

function mountComponent(vm,el){
    if(!vm.$options.render){
        vm.$options.render = createEmptyVNode()
    }
    callHook(vm,'beforeMount')
    vm._watcher = new Watcher(vm,() => {
        vm._update(vm._render())
    },noop)
    callHook(vm,'mounted')
    return vm
}

Vue.prototype.$mount = function(el){
    el = el && inBrowser ? query(el) : undefined
    return mountComponent(this,el)
} 

Vue.directive方法接受两个参数id和definition,它可以注册或获取指令,这取决于definition参数是否存在。如果definition参数不存在,则使用id从this.options['directives']中读出指令并将其返回;如果definition参数存在,则说明是注册操作,那么近而判断definition参数的类型是否是函数。

如果是函数,则默认监听bind和update两个事件,所以代码中将definition函数分别赋值给对象中的bind和update这两个方法,并使用这个对象覆盖definition;如果definition不是函数则说明它是用户自定义的指令对象,此时不需要做任何操作。直接将用户提供的指令对象保存在this.options['directives']上即可。

Vue.options = Object.create(null)
Vue.options['directives'] = Object.create(null)

Vue.directive = function(id,definition){
    if(!definition){
        return this.options['directives'][id]
    }else{
        if(typeof definition == 'function'){
            definition = {
                bind:definition,
                update:definition
            }
        }
        this.options['directives'][id] = definition
        return definition
    }
}

生命周期

  • new Vue()到created之间的阶段叫做初始化阶段。这个阶段的主要目的是在vue.js实例上初始化一些属性、事件以及响应式数据。如:props、methods、data、computed、watch、provide和inject等
  • 在created钩子函数与beforeMount钩子函数之间的阶段是模板编译阶段。这个阶段的主要目的是将模板编译为渲染函数。
  • beforeMount钩子函数到mounted钩子函数之间是挂载阶段。在这个阶段,vue.js会将其实例挂载到DOM元素上,在挂载的过程中,vue.js会开启watcher来持续追踪依赖的变化。
  • 应用调用vm.$destroy方法后,vue.js的生命周期会进入卸载阶段。在这个阶段,vue.js会将自身从父组件中删除,取消实例上所有依赖的追踪并且移除所有的事件监听器。

未完待续...

原文地址:https://www.cnblogs.com/zhenjianyu/p/13175996.html