vue2.0|实现篇|依赖收集

1,712 阅读5分钟

前言

Vue.mixin实现及生命周期实现

在Vue类上挂载options属性用于存储“混入”的数据或函数
  • 多次使用mixin时会先将多个mixin对象合并成一个对象,Vue.options指向此对象
  • 在Vue初始化数据前将Vue.options与用户传递过来的配置对象进行合并,从而完成mixin操作

首先,Vue.mixin是挂载在类上的函数,即全局函数(如extend/component等),那就同样新建一个模块global-api进行挂载

global-api/index.js
import { mergeOptions } from "../util";

export function initGlobalApi(Vue) {
    Vue.options = {};
    Vue.mixin = function (mixin) {
        this.options = mergeOptions(this.options,mixin);
        console.log(this.opions);
    }
}
策略模式,定义一个策略类(对象),根据不同key对应不同的处理函数
util.js
strats.data = function (parentVal,childValue){
    return childValue
}
strats.computed = function (){}
strats.watch = function (){}
export const LIFECYCLE_HOOKS   = [
    "beforeCreate",
    "created",
    "beforeMount",
    "mounted",
    "beforeUpdate",
    "updated",
    "beforeDestory",
    "destoryed"
];
LIFECYCLE_HOOKS.forEach(lifeCycle=>{
    strats[lifeCycle] = mergeHook;
})
此时为实现合并操作,需定义一个合并函数,功能--根据传递的parent,children进行特定策略的合并
  • 处理父对象所有属性
  • 处理父对象无但子对象有的属性
  • 根据key的不同通过策略对象的配置进行不同的处理
export function mergeOptions(parent,child) {
    const options = {};
    // 遍历处理父亲所有key
    for (const key in parent) {
            mergeField(key);
    }
    // 遍历所有父无子有属性
    for (const key in child) {
        if (!parent.hasOwnProperty(key)) {
            mergeField(key);
        }
    }
    // 子有父无
    /**
     * 合并字段
     * @param {*} key
     */
    function mergeField(key) {
        if(strats[key]){
            options[key] = strats[key](parent[key],child[key]);
        }else {
            options[key] = child[key];
        }
    }
    return options;
}
以生命周期为例
  • 记住,父元素初次是空对象,子元素只可能是两个来源
    • 初次初始化时用户传递的options
    • 多次mixin时Vue类上的option
  • 执行逻辑
    • 子元素不存在,直接返回父元素
    • 子元素存在且父元素不存在,返回一个含有子元素数组
    • 子元素存在且父元素存在,父元素拼接子元素
function mergeHook(parentVal,childValue){ // 生命周期的合并
    if(childValue){
        // 父有子有
        if (parentVal) {
            return parentVal.concat([childValue]);
        }else {
            // 父无子有
            return [childValue];
        }
    }
    else {
        // 父有子无
        return parentVal;
    }
}
对于生命周期的实现

如果理解了之前【vue2.0|思路篇|数据劫持】、【Vue|思路篇|编译ast】,不难理解,其实就是在特定的地方执行用户传递的回调;以beforeCreate为例:

  • 在Vue执行_init方法时,需要执行initState函数,在此函数调用前执行即可
  • 需注意,由于存在mixin的可能,回调必是采用数组的形式

在lifecycle.js中定义执行hook的函数

  • hook是一个数组,遍历执行
lifecycle.js
export function callHook(vm,hook) {
    const handlers = vm.$options[hook];
    if(handlers){
        for (let index = 0; index < handlers.length; index++) {
            const handler = handlers[index];
            handler.call(vm);
        }
    }
}

watcher+dep实现依赖收集(对象)

定义Watcher概念,取代直接使用render
Watcher.js

需要四个参数:1. vue实例 2. 用户传递表达式或函数 (相当于 vm._update(vm._render());)3. 回调函数 4. 配置信息对象

  • 定义id,标识是一个独特的id
  • 做兼容处理,因为可能用户传递的exprOrFn不是函数,所以定义一个get方法,后期将实现将expr改写为Fn的情况
let id = 0;
class Watcher {

    constructor(vm,exprOrFn,cb,options){
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        this.cb = cb;
        this.options = options;
        this.id = id++;
        if(typeof exprOrFn == 'function'){
            this.getter = exprOrFn;
        }
        this.get();
    }
    get(){
        this.getter();
    }
    update(){
        this.get();
    }
    。。。
}
export default Watcher;
Watcher改写mountComponent逻辑
lifecycle.js
 let updateComponent=()=>{
        vm._update(vm._render());
    }
 new Watcher(vm,updateComponent,()=>{
     callHook(vm,'beforeUpdate')
 },true);
订阅发布,在对象属性取值时进行订阅,属性对应的dep(可理解为存储多个watcher的数组)收集依赖
  • 为实现属性对应的dep能访问到当前执行的Watcher实例:利用js单线程的特性,在一个Watcher执行时先将自身存在Dep.target(全局可访问即可)

    # Dep.js
    export function pushTarget(watcher) {
        Dep.target = watcher;
    }
    export function popTarget() {
        Dep.target = null;
    }
    #Watcher.js
       get(){
           pushTarget(this);
            this.getter();
            popTarget();
        }
    
  • 为实现属性/Dep一对一关系,在defineReactive函数时new Dep形成闭包

    
    function defineReactuve(data,key,value){
        // 实现递归
        observe(value)
    
        let dep = new Dep();
      。。。
    }
    
    
  • 依赖收集

    • 判断如果当前处于watcher渲染,则进行订阅,属性对应dep存入此依赖,以便后面属性改值时触发
    • 在数据改值时发布,执行watcher的update方法,update方法内部默认执行get函数,从而实现了【数据改变-》页面更新/执行逻辑】
    # Dep
    let id = 0;
    class Dep {
        constructor(){
            this.subs = [];
            this.id = id++;
        }
        depend(){
           this.subs.push(Dep.target)
        }
        notify(){ 
            this.subs.push.forEatch(sub=>sub.update());
        }
        ...
    }
    # watcher.js
    update(){
            this.get();
        }
    # init.js
    function defineReactuve(data,key,value){
        // 实现递归
        observe(value)
    
        let dep = new Dep();
        Object.defineProperty(data,key,{
            get(){
                if(Dep.target){
                    dep.depend();
                }
                console.log('用户取值');
                return value
            },
            set(newValue){
                console.log('用户赋值');
                if(value !== newValue){
                    // 对新赋值的对象进行观测
                    observe(newValue);
                    value = newValue
                }
                // 发布
                dep.notify();
            }
        }
        )
    }
    
    
解决收集了相同的watcher的问题

如果页面多次取值

{{age}}{{age}}{{age}}{{age}}
问题:

按照目前逻辑就会多次触发get,在渲染页面(渲染watcher)执行的过程中,age对应的dep会多次存入渲染watcher,导致后面一旦改变一次age的值,会触发四次更新;

解决:

让dep和watcer彼此记住,然后相同则不存入;

  • Dep收集时不直接push,而是调用将存入的watcher的记录Dep的方法,此方法除使得watcher记录dep外还会默认将dep存储watcher
# watcher.js
depend(){
       if(Dep.target){
        Dep.target.addDep(this);
       }
    }
# watcher.js
 addDep(dep){
        let id = dep.id;
        if(!this.depsId.has(id)){
            this.deps.push(dep);
            this.depsId.add(id);
            dep.addSub(this); 
        }
    }


至此对象的依赖收集完成

watcher+dep实现依赖收集(数组)

通过observe返回值获取到观察者对象,在观测者对象上挂载一个dep属性,这样就可以在这个dep上进行依赖收集
observe.js
class Observer {
    constructor(data){
        this.dep = new Dep();
observe/index.js
function defineReactuve(data,key,value){
    // 实现递归
    // 获取到数组对应的dep
    let childDep = observe(value);


    let dep = new Dep();
    Object.defineProperty(data,key,{
        get(){
            if(Dep.target){
                dep.depend();
                if(typeof childDep == 'object'){
                    childDep.dep.depend();
                }
            }
            console.log('用户取值');
            return value
        },
在数组重写方法的逻辑中去通过__ob__.dep获取到收集到的依赖,并发布,从而实现数组的依赖收集
observe/array
 arrayMethods[method] = function (...args){
        let r = oldArrayProtoMethods[method].apply(this,args)

        // todo
        let inserted;
        let ob = this.__ob__;
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args;break;
            case "splice":
                inserted = args.slice(2)
            default:
                break;
        }
        // console.log(inserted);

        if(inserted) ob.observerArray(inserted)
        ob.dep.notify();
        return r;

    }

至此数组的依赖收集完成

最终实现