【Vue2源码解析】02.依赖收集&&异步更新&&mixin实现原理

回顾:

  1. 将数据先处理成响应式 inintState(针对对象来说主要是增加defineProperty 针对数组就是重写方法)
  2. 模板编译:将模板通过正则匹配转换成AST语法树,然后将AST语法树生成render方法
  3. 调用render函数,将HTML模板转换成JS代码,并在实例中进行取值操作,产生对应的虚拟DOM render(){ _c('tag', attrs, children)} 出发get方法
  4. 将虚拟DOM渲染成真实DOM

这节主要内容

  • 观察者模式实现依赖收集
  • 异步更新策略
  • mixin的实现原理

依赖收集

  • 给模板中的属性增加一个收集器dep
  • 页面渲染的时候将渲染逻辑封装到watcher中,vm._update(vm._render())
  • 让页面记住这个watcher即可,稍后属性变化了可以找到对应的dep中存放的watcher进行重新渲染
    组件化的好处:复用、方便维护、局部更新

在属性劫持的方法中: 当属性取值时执行get方法,判断当前属性的Watcher是否存在,存在则调用dep.depend()为当前watcher添加属性dep(不重复),更新值则调用set方法执行dep.notify()对数据进行更新(对dep中的每个watcher都遍历更新调用render函数)

export function defineReactive(target, key, value){//闭包 属性劫持
    // debugger
    observe(value) //对多层对象递归进行属性劫持
    let dep = new Dep();//每一个属性都有一个Dep
    Object.defineProperty(target, key, {
        get(){//取值的时候会执行get
            if(Dep.target){
                dep.depend(); //让这个属性的收集器记住当前的watcher,只有需要渲染取值的属性才会去收集
            }
            return value
        },
        set(newV){
            if(newV === value) return
            observe(newV)
            value = newV //Q_lys:这里有一点不是很明白,defineReactive中的参数value应该只在defineReactive的函数内部有效,为什么这里直接更改value会反向更改data中的值
            // console.log('set data:', target)
            dep.notity()//通知更新
        }
    })
}

dep.js:每个属性都有一个Dep,属性获取值时会进行判断当前watcher(视图)中是否已存在该dep,没有则添加,并且也为该dep添加watcher

import watcher from "./watcher";
let id = 0;
class Dep{
    constructor(){
        this.id = id++;//属性的deo要收集watcher
        this.subs = [];//这里存放着当前属性对应的watcher有哪些
    }
    depend(){
        // this.subs.push(Dep.target);
        //不希望放置重复watcher
        Dep.target.addDep(this)
        //dep和watcher是一个多对多的关系 (一个属性可以在多个组件中使用dep->watcher)
        //一个组件可以有多个属性
    }
    addSub(watcher){
        this.subs.push(watcher)
    }
    notify(){
        this.subs.forEach(watcher=>watcher.update());//告诉watcher要更新了
    }
}
Dep.target = null
export default Dep

watcher.js:观察者模式:每个属性有个dep(属性就是被观察者),watcher就是观察者(属性变化了会通知观察者来更新)

  1. 当我们创建渲染watcher的时候我们会把当前的渲染watcher放到Dep.target上
  2. 调用_render() 会取值 走到 get上
import Dep from "./dep";

// 观察者模式:每个属性有个dep(属性就是被观察者),watcher就是观察者(属性变化了会通知观察者来更新)
// 1.当我们创建渲染watcher的时候我们会把当前的渲染watcher放到Dep.target上
// 2.调用_render() 会取值 走到 get上

let id = 0;
class watcher{
    //options为true则标识是渲染过程
    constructor(vm, fn, options){//不同组件有不同watcher 目前只有一个 渲染根实例
        this.id = id++;
        this.renderWatcher = options;//是一个渲染watcher
        this.getter = fn;//getter意味着调用这个函数可以发生取值操作
        this.deps = [];//让watch记住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);//watcher已经记住dep且去重了,此时让dep也记住了watcher
        }
    }
    get(){
        Dep.target = this;//静态属性只有一份
        this.getter();//会去vm上取值
        Dep.target = null;//渲染完毕就清空
    }
    update(){
        this.get();//重新渲染
    }
}
//需要给每个属性增加一个dep 目的就是收集watcher
// 一个视图(zujian )对应一个watcher n个属性对应一个视图 n个dep对应一个watcher
//1个属性对应多个视图(组件中可能出现同名属性) 1个dep对应多个watcher
//多对多关系
export default watcher

异步更新

上面的操作,如果每次更新num,都会调用render函数,现在考虑只有同步代码都执行完之后,才会开始刷新渲染调用render函数。

vm.name = 'jw',
vm.age = 30,
vm.name = 'lys',
vm.name = 'hzt',
vm.name = 'zyl'

一共调用五次update
异步更新也是一种优化策略,只有当同步代码vm.name = 'zyl'执行完才会开始渲染
watcher.js中改变: 当数据发生改变时在所劫持的set方法中调用dep.notify()通知所依赖的watcher调用update方法,现在在update中将会把当前的watcher暂存到队列中,队列中会进行去重操作,并且只有第一次放入队列时才会开启定时器,只有在主线程执行完毕后才会回调flushSchedulerQueue函数将队列中的watcher拿出来依次执行get()方法刷新页面

class watcher{
	....
	update(){
        // this.get();//重新渲染
        queueWatcher(this);//把当前的watcher暂存起来
    }
    run(){
        console.log("run")
        this.get()
    }
}
let queue = []
let has= {}
let pending = false;//防抖
function flushSchedulerQueue(){
    let flushQueue = queue.slice(0);
    queue = [];
    has = {};
    pending = false;
    flushQueue.forEach(q=>{ q.run() });//在刷新过程中可能还有新的watcher,重新放到queue中
}
function queueWatcher(watcher){
    const id = watcher.id;
    if(!has[id]){
        queue.push(watcher);
        has[id] = true;//不管uodate执行多少次,最终只执行一轮刷新操作
        if(!pending){
            setTimeout(flushSchedulerQueue, 0);//定时器只会在主执行栈执行完后才会调用回调函数
            pending = true;
        }
    }
}

nextTick在Vue源码内部没有直接使用某个api而是采用优雅降级的方式

  1. 内部先采用promise(ie不兼容)
  2. MutationObserver(h5的api)
  3. ie专享的setImmediate
  4. setTimeout
let callbacks = [];
let waiting = false;
function flushCallbacks(){
    let cbs = callbacks.slice(0);
    waiting = true;
    callbacks = [];
    cbs.forEach(cb=>cb());//按照顺序依次执行
}
export function nextTick(cb){
    callbacks.push(cb);//维护nextTick中的callback方法
    if(!waiting){//等主程序中代码执行完毕,最后一起刷新执行回调队列中的回调函数
        timerFunc();
        waiting = true;
    }
}

let timerFunc;
if(Promise){
    timerFunc = ()=>{
        Promise.resolve().then(flushCallbacks)
    }
}else if(MutationObserver){
    let observer = new MutationObserver(flushCallbacks);//这里传入的回调是异步执行的
    let textNode = document.createTextNode(1);
    observer.observe(textNode,{//监控文本变化,当timerFunc调用时将文本内容改变后,则执行flushCallbacks
        characterData: true
    });
    timerFunc = ()=>{
        textNode.textContent = 2
    }
}else if(setImmediate){
    timerFunc = ()=>{
        setImmediate(flushCallbacks);
    }
}else{
    timerFunc = () => {
        setTimeout(flushCallbacks)
    }
}

mixin实现原理

将用户选项和全局的options进行合并

Vue.options = {}
    Vue.mixin = function (mixin) {
        // debugger
        //将用户的选项和全局的options进行合并
        this.options = mergeOptions(this.options, mixin)
        return this;
    }
    const strats = {}

const LIFECYCLE = ['beforeCreate', 'created'];
LIFECYCLE.forEach(hook => {
    strats[hook] = 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 key in parent) {
        mergeField(key);
    }
    for (let key in child) {
        //hsaOwnProperty
        if (!parent.hasOwnProperty(key)) {
            mergeField(key)
        }
    }
    function mergeField(key) {
        if (strats[key]) {
            options[key] = strats[key](parent[key], child[key])
        } else {
            options[key] = child[key] || parent[key] //取值时如果不在策略中优先采用儿子,再采用父亲
        }
    }
    return options
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值