手写vue二学习笔记4

一. 模板的依赖收集

1. 我们会给模板中的属性都加上一个属性收集器dep

        什么叫做模板中的属性呢?

        就是template属性里面写的模板或者vue挂载的标签里面,所使用的 vue的一些数据       

         如:        {{xxx}}

2. 我们会把渲染逻辑封装到watcher中

3. watcher和dep的关系

(1)每一个dep都对应着一个watcher,也可以多个dep对应一个watcher

(2)每个watcher又对应着多个dep,所有说他们是一个多对多的关系

有一些抽象,我们看下图进行理解

 具体实现

我们在虚拟Dom渲染为真实Dom的时候是通过watcher类里的get方法去调用_random的

如果不知道可以去看上一篇文章,好处就是说让我们知道了Dep所对应的watcher,具体是哪一个watcher渲染出来的真实dom

lifeCycle.js

export function mountComponent (vm,el) {
    vm.$el = el
    const updateComment = () => {
        vm._update(vm._random())
    }
    let watcher = new Watcher(vm,updateComment,true)
    console.log(watcher);
}

Dep收集器的实现,在observe文件夹下

let id = 0
class Dep {
    constructor() {
        this.id = id++
        this.subs = []
    }
    depend() {
        Dep.target.addDep(this)
    }
    addSub(watcher) {
        this.subs.push(watcher)
    }
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}
Dep.target = null;
export default Dep

watcher观察者的实现 在observe文件夹下

import Dep from "./Dep";

let id = 0;
class Watcher {
    constructor(vm,fn,options) {
        // 每一个组件都有一个watcher,要添加唯一标识
        this.id = id++;
        this.getter = fn;
        // 标识是否为初渲染
        this.renderWatcher = options
        // 用来记住dep方便后续的一些方法
        this.deps = []
        this.depsId = new Set()

        //调用渲染函数进行渲染
        this.get()
    }
    addDep(dep) {
        let id = dep.id
        if(!this.depsId.has(id)) {
            this.deps.push(dep)
            this.depsId.add(id)
            dep.addSub(this)
        }
    }
    get() {
        Dep.target = this
        this.getter()
        // 只对在模板当中使用的变量进行依赖收集
        Dep.target = null
    }
    update() {
        this.get()
    }
}
export default Watcher

具体调用,是在defineActive方法内,如果不知道这个方法,可以去看之前的关于对象和数组的响应式原理

export function defineActive(target,key,value) {
    // 如果属性是一个对象,那么就再次调用观测方法
    observe(value)
    // 给每个属性都加上dep
    let dep = new Dep();
    Object.defineProperty(target,key,{
        get() {
            if(Dep.target) {
                dep.depend()
            }
            return value
        },
        set(newValue) {
            if(newValue === value) return
            value = newValue
            dep.notify()
        }
    })
}

那么我们在object.defineProperty这里进行if判断的目的是:只对在模板当中使用的变量进行依赖收集

那么当数据发生变化时,会调用dep的notify方法对所有的依赖这个属性的watcher进行更新

二. 异步更新策略

执行上面的代码的时候我们会发现,渲染了两次。现在想在一次渲染视图就完成对于watcher内部的改变的dep更新

使用异步批处理的方式:

       就是把这些更新视图的通知放到一个队列里面,然后再拿一个函数进行异步处理

代码如下:

// 使用批处理的方式,进行更新
let queue = []
let has = {}
// 进行防抖,有多个watcher的情况下也只执行一次setTimeout
let pending = false
function flushSchedulerQueue() {
    let queueCopy = queue.slice(0)
    queue = []
    has = {}
    pending = false
    queueCopy.forEach(q => q.run())
}

function queueWatcher(watcher) {
    let id = watcher.id
    // 利用has对象对数组去重
    if(!has[id]) {
        has[id] = true
        // 添加到一个队列里面集中处理
        queue.push(watcher)
        if(!pending) {
            pending = true
            setTimeout(flushSchedulerQueue)
        }
    }
}

像这样处理就是有一个问题:

         当我们在html文件内,获取dom元素时发现获取的和页面上显示的不一样导致的原因就是因为进行了异步处理在同步代码之后执行,那么也可以拿setTimeout来进行一下延时也可以获取到,但是这样的话,两边就不一致了,所有vue就提供了一个api $nextTick

          我们就采用上述方式来写代码:

// 异步批处理
// 不直接使用setTimeout而是采用优雅降级的方式
let TimeFunc;
if(Promise) {
    TimeFunc = () => {
        Promise.resolve().then(flushCallbacks)
    }
}else if(MutationObserver) {
    let observer = new MutationObserver(flushCallbacks)
    let textNode = document.createTextNode(1)
    observer.observe(textNode,{
        characterData: true
    })
    TimeFunc = () => {
        textNode.textContent = 2
    }

}else if(setImmdiate) {
    TimeFunc = () => (
        setImmdiate(flushCallbacks)
    )
}else {
    TimeFunc = () => {
        setTimeout(flushCallbacks,0)
    }
}
let callbacks = []
let waiting = false
function flushCallbacks() {
    let cbs = callbacks.slice(0)
    callbacks = []
    waiting = false
    cbs.forEach(cb => cb())
}
export function nextTick(cb) {
    callbacks.push(cb)
    if(!waiting) {
        waiting = true
        TimeFunc()
    }
}
三. 实现mixin

关于mixin
什么是mixin:将组件的公共逻辑或者配置提取出来,哪个组件需要用到时,直接将提取的这部分混入到组件内部即可。
Mixin与vuex的区别:1. vuex是做一个公共的状态管理,如果一个组件中更改了所依赖的数据,其他组件依赖该数据的
                    也会发生改变
                2. mixin就是做一个混入,数据和方法在组件中都是独立的不会彼此影响。

想要了解详情:知乎大佬写的很清楚的解释
实现:
1. 在Vue上新增一个静态方法mixin静态属性options,利用mergeOptions进行合并有两个属性值相当于对象合并
2. 采用策略模式合并一些特殊的属性比如created等等
3.循环创建方法
4. 合并用户的选项,使用mergeOptions,在init里通过实例的构造器方法拿到Vue的options
5. 合并完成之后执行这些函数即可

const stratus = {}
let LIFECYCLE = [
    'beforeCreated',
    'created'
]
// 循环创建
LIFECYCLE.forEach(key => {
    stratus[key] = function (p,c) {
        if(c) {
            if(p) {
                return p.concat(c)
            }else {
                return [c]
            }
        }else {
            return p
        }
    }
})
export function mergeOptions(parent,child) {
    const options = {}
    for (let parentKey in parent) {
        merge(parentKey)
    }
    for (let childKey in child) {
        if(!parent.hasOwnProperty(childKey)) {
            merge(childKey)
        }
    }
    function merge(key) {
        // 采用策略模式合并一些特殊的属性
        if(stratus[key]) {
            options[key] = stratus[key](parent[key],child[key])
        }else {
            // 先用儿子再用父亲
            options[key] = child[key] || parent[key]
        }
    }
    return options
}
import Vue from "./index";
import {mergeOptions} from "./utils";

export function initGlobalAPI(Vue) {
    Vue.options = {}
    Vue.Mixin = function (minix) {
        this.options = mergeOptions(this.options,minix)
        return this
    }
}

init.js 文件内

vm.$options = mergeOptions(this.constructor.options,options)

        callHook(vm,'beforeCreated')
        // 初始化状态
        initState(vm)
        callHook(vm,'created')
// lifyCycle.js内
export function callHook(vm,hook) {
    let handlers = vm.$options[hook]
    if(handlers) handlers.forEach(handler => handler.call(vm))
}
四。数组的更新 

在数组的更新中,我们是采用数组的七个变异方法完成的响应式,那些方法呢,都是对数组产生添加,减少等一系列对数组元素产生影响的方法,那么我们就需要

1. 给对象和数组添加上一个dep

2. 收集watcher

3. 在添加响应式的后面调用notify进行通知

4. 就是对于数组里面还是一个数组的情况给与循环添加dep的操作

import {newArrayProto} from "./array.js";
import Dep from "./Dep";

class Observe {
    constructor(data) {
        this.dep = new Dep()
        // 添加一个ob属性给数组新添加的元素加响应式用,要调observeArray
        Object.defineProperty(data,'__ob__',{
            value: this,
            enumerable:false
        })
        if(Array.isArray(data)) {
            // 改变数组原型,增加变异方法
            data.__proto__ = newArrayProto;
            this.observeArray(data)
        }else {
            this.walk(data)
        }
    }
    walk(data) {
        // 重写属性
        Object.keys(data).forEach(key => defineActive(data,key,data[key]))
    }
    observeArray(data) {
        data.forEach(item => {
            observe(item)
        })
    }
}

function dependArray(value) {
    for(let i = 0;i < value.length;i++) {
        let current = value[i]
        current.__ob__ && current.__ob__.dep.depend()
        if(Array.isArray(current)) dependArray(current)
    }
}

export function defineActive(target,key,value) {
    // 如果属性是一个对象,那么就再次调用观测方法
    let childOb = observe(value)
    // 给每个属性都加上dep
    let dep = new Dep();
    Object.defineProperty(target,key,{
        get() {
            if(Dep.target) {
                dep.depend()
                if(childOb) {
                    childOb.dep.depend()
                    if(Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            return value
        },
        set(newValue) {
            if(newValue === value) return
            value = newValue
            dep.notify()
        }
    })
}

export function observe(data) {
    // 如果说是对象类型就对他进行观察
    if(typeof data != 'object' || data == null) return
    // 因为加了一条新的标识,可以对其进行验证是否观测过
    if(data.__ob__ instanceof Observe) return data.__ob__
    return new Observe(data);
}

如下图就代表着目前写成功了:

 

  • 19
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值