Vue框架实现原理

304 阅读2分钟

作为当下最为流行的前端框架之一的Vue,在学习过程中,我们需要对其实现原理进行掌握,需要知道vue框架做了什么样的操作才能实现数据驱动页面的渲染,在此次学习过程中做出总结如下:

image.png

根据上图所示,在我们进行new Vue()创建构造函数时,我们需要进行初始化操作,对我们的数据进行响应式处理,也就是通过Observer进行劫持监听所有的属性,同时我们需要对模板进行编译,也就是通过Compile进行解析指令,它会进行初始化视图,这个时候如果数据发生变化时,我们的页面还不会发生改变,我们引入了一个概念(Watcher),我们在页面每进行一个数据的绑定时,我们会创建一个Watcher,它用来观察绑定的Dom的监听操作,每当数据变更时,我们的Watcher会进行更新Dom的操作,如果页面绑定了多个数据时,那么我们就引入另外一个概念(Dep),它用来集中管理多个Watcher,这样我们就形成了一个闭环。代码如下:

class Kvue {
    constructor(options){
        //保存选项
        this.$options = options;
        this.$data = options.data;
        //响应化处理
        observe(this.$data);
        //代理,让我们可以直接访问$data上的属性
        proxy(this,'$data');
        
    }
}
//劫持监听所有的属性
class Observer {
    constructor(value){
        //保存value
        this.value = value;
        //将参数变成响应化
        if(typeof value === 'object'){
            this.walk(value);
        }else if(Array.isArray(value)){
            //...数组的响应化处理
        }
    }
    //对象数据响应化处理
    walk(value){
        Object.keys(value).forEach(key=>{
            defineReactive(obj, key, obj[ky]);
        })
    }
}
//代理函数,方便用户直接访问$data中的数据
function proxy(vm, sourceKey) {
    //这里的ym指vue实例,sourceKey指$data
    将data中的变量全部遍历并使其能够直接使用而不需要依赖与data上
    Object.keys(vm[sourceKey]).forEach(key=>{
        //key指data上的属性
        Object.defineProperty(vm, key, {
            get(){
                return vm[sourceKey][key]
            },
            set(newVal){
                vm[sourceKey][key] = newVal
            }
        })
    })
}

function observe(obj) {
    if(typeof obj !== 'object' || obj == null){
        return;
    }
    //创建Observer
    new Observer(obj);
}

function defineReactive(obj, key, val){
    //递归,考虑到object的属性也有可能是object
    observe(val)
    Object.defineProperty(obj, key, {
        get(){
            return val
        },
        set(newVal){
            return val = newVal
        }
    })
}

完成以上代码,我们就成功实现了Observer的部分,接着我们需要完成Compile的实现,来完成解析指令并初始化视图渲染Dom,代码如下:

//编译器
//递归遍历Dom
//判断节点类型,如果是文本,则判断是否是插值绑定
//如果是元素,则遍历其属性判断的是否是指令或事件,然后递归子元素
class Compiler {
    constructor(el, vm){
        //保存Vue实例
        this.$vm = vm;
        this.$el = document.querySelector(el);
        if(this.$el) {
            //执行编译
            this.compile(this.$el)
        }
    }
    compile(el) {
        //遍历el树
        const childNodes = el.childNodes;
        Array.form(childrenNodes).forEach(node => {
            //判断是否是元素
            if(this.isElement(node)){
                this.complieElement(node)
            }else if(this.isInter(node)) {
                //编译插值绑定
                this.compileText(node)
            }
            
            if(node.childNodes && node.childNodes.length > 0) {
                this.compile(node)
            }
        })
    }
    isElement(node) {
        return node.nodeType === 1
    }
    isInter(node) {
        //首先判断是否是文本标签,然后解析{{XXX}}中的内容
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
    }
    compileText(node) {
        this.update(node, RegExp.$1, 'text')
    }
    
    //元素编译
    compileElement(node) {
        const nodeAttrs = node.attributes
        Array.from(nodeAttrs).forEach(attr => {
            const attrName = attr.name  // k-xx
            const exp = attr.value  //{{xx}}
            if(this.isDirective(attrName)) {
                const dir = attrName.substring(2)  //获取k-后面的指令内容
                this[dir] && this[dir](node, exp)   //执行指令
            }
        })
    }
    isDirective(attr) {
        return attr.indexOf(k-) === 0
    }
    
    text(node, exp) {
        this.update(node, exp, 'text')
    }
    textUpdater(node, value) {
        node.textContent = value
    }
    
    html(node, exp) {
        this.update(node, exp, 'html')
    }
    htmlUpdater(node, value) {
        node.innerHTML = value
    }
    //更新函数作用:
    // 1.初始化Dom
    // 2.根据相对应的指令做出更新(...Updater())
    update(node, exp, dir) {
        const fn  = this[dir + 'Updater']
        fn && fn(node, this.$vm[exp])
    }
  
}

完成这一步后,我们的compile进行初始化Dom就完成了,不过我们还缺少关键的一部操作,就是创建Watcher这一操作,当我们的Observer(发布者)来监听数据变化后,需要通知我们的观察者(Watcher),这个时候需要去收集依赖的工作,,这个时候我们引入Dep这一概念,我们通过Dep来通知观察者(Watcher),Wacther订阅Dep,数据变化时,由Dep通知Wacher变化,当Watcher接收到消息后,最终通知更新我们的视图,从而形成一个闭环,这就是我们整个从监听到渲染的过程,Wacther和Dep功能实现的代码如下:

const watchers = []; //临时用于保存watcher

class Watcher {
    constructor(vm, ky, updateFn) {
        this.vm = vm;
        this.key = key;
        this.updateFDn = updateFn;
        
        //读一下当前的key,触发依赖收集
        Dep.target = this
        vm[key]
        Dep.target = null
    }
    //未来会被Dep调用
    update() {
        this.updateFn.call(this.vm, this.vm[this.key])
    }
}

声明Dep

//Dep:保存所有watcher,当某个key发生变化时,通知更新
class Dep {
    constructor() {
        this.deps = []
    }
    addDep(watcher) {
        this.deps.push(watcher)
    }
    notify() {
        this.deps.forEach(dep => dep.update())
    }
}

创建Watcher时触发getter

class Wacther {
    constructor(vm, ky, updateFn) {
        //读一下当前的key,触发依赖收集
        Dep.target = this
        vm[key]
        Dep.target = null
    }
}

收集依赖,创建Dep实例

//在初始进行数据响应式的时候
function defineReactive(obj, key, val) {
    //递归处理
    observe()
    //创建一个Dep实例
    const dep = new Dep()
    
    Object.defineProperty(obj, key, {
        get() {
            //收集依赖:把watcher和dep关联
            //希望watcher实例化时,访问一下对应的key,同时把这个实例设置在Dep.target上
            Dep.tart && dep.addDep(Dep.target)
        }
        set() {
            if(newVal !== val){
                observe(newVal)
                val = newVal
                
                //通知更新
                dep.notify()
            }
        }
    })
}

在进行compile解析模板时,我们在监听到值发生变化时,同时需要执行watcher的更新视图

class Compiler {
    constructor(el, vm) {
        this.$vm = vm;
        this.$el = document.querySelector(el)
        
        //执行编译
        this.compile(this.$el)
    }
    //...
    update(node, exp, dir) {
        const fn = this[dir + 'Updater']
        fn && fn(node, val)
        
        //更新,创建一个Watcher实例
        new Watcher(this.$vm, exp, val => {
            fn && fn(node, val)
        })
    }
}