vue源码解析

2019-04-19  本文已影响0人  瘾_95f1

知识要点

vue工作机制
初始化

new Vue()之后。Vue会调用进行初始化,会初始化生命周期、事件、props、methods、data、computed与watch等。其中最重要的是通过Object.defineProperty设置settergetter,用来实现响应式以及依赖收集

初始化后调用$mount挂载组件。

new Vue() init初始化后,$mount挂载到你准备渲染的DOM节点中,然后compile() 会进行编译,生成渲染更新函数render function 更改的都是虚拟DOM节点,更新之前会做diff算法的比较,计算出最小的DOM更新。然后执行到patch()进行更新打补丁(用js的时间换DOM操作的时间)减少页面渲染的次数和数量,最后更新到真实的DOM。
Watcher 观察者,当数据变化时进行更新

编译

编译模式分为3个阶段

  1. parse

    • 使用正则解析template中的vue的指令(v-xxx)变量等等,形成语法书AST
  2. optimize

    • 标记一些静态节点,用做后面的性能优化,在diff的时候直接略过
  3. generate

    • 把第一部分生成的AST转化成渲染函数render function
响应式原理 defineProperty
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>defineProperty</title>
</head>
<body>
    <div id="app">
        <p id="name"></p>
    </div>
    <script>
        var obj = {};
        Object.defineProperty(obj, 'name', {
            get: function () {
                return document.querySelector('#name').innerHTML;
            },
            set: function (val) {
                document.querySelector('#name').innerHTML = val;
            }
        })
        obj.name = "htf";
    </script>
</body>
</html>
自己实现一个响应式
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>HVue</title>
</head>
<body>
    
</body>
<script src="./HVue.js"></script>
<script>
    const app = new HVue({
        data: {
            test: "I am test",
            foo: {
                bar: "bar"
            }
        }
    });
    app.$data.test = "hello, HTF";
    app.$data.foo.bar = "oh my bar";
</script>
</html>
class HVue {
    constructor(options) {
        this.$options = options;
        // 数据响应化
        this.$data = options.data;
        this.observe(this.$data);
    }
    observe(value) {
        if(!value || typeof value !== 'object') return;
        // 遍历该对象
        Object.keys(value).forEach(key => {
            this.defineReactive(value, key, value[key])
        })
    }
    // 数据响应化
    defineReactive(obj, key, val) {
        this.observe(val);  // 递归遍历,对象中的嵌套属性
        Object.defineProperty(obj, key, {
            get() {
                return val
            },
            set(newVal) {
                if(newVal === val) return;
                val = newVal
                console.log(`${key}属性更新了:${val}`);
            }
        })
    }
};

ps: vue数据响应的原理:
vue利用defineProperty进行数据劫持,简单描述下,vue利用的了Object.defineProperty()属性,把data里面的数据的每一个属性都定义了一个getter和setter的方法,让我们有机会去监听这些属性的变化,当这些属性发生变化的时候,可以通知那些需要更新的地方去更新

依赖收集与追踪

理解下面例子

new Vue({
  template:
     `<div>
          <span>{{ name1 }}</span>
          <span>{{ name2 }}</span>
          <span>{{ name1 }}</span>
    </div>`
  data: {
    name1: 'name1',
    name2: 'name2',
    name3: 'name3'
  },
  created() {
    this.name1 = "HTF",
    this.name3 = "HZH"
  }
})

name1被修改,视图更新,且需要更新两处
name2被修改,视图更新
name3没用到,不需要更新
如何实现呢,需要扫描视图收集依赖,知道视图中到底哪些地方对数据有依赖,这样当数据变化的时候就能知道需
要更新哪个,不需要更新哪个。

demo

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>HVue</title>
</head>
<body>
    
</body>
<script src="./HVue.js"></script>
<script>
    const app = new HVue({
        data: {
            test: "I am test",
            foo: {
                bar: "bar"
            }
        }
    });
    app.$data.test = "hello, HTF";
    app.$data.foo.bar = "oh my bar";
</script>
</html>

HVue.js

class HVue {
    constructor(options) {
        this.$options = options;
        // 数据响应化
        this.$data = options.data;
        this.observe(this.$data);
        // 模拟一下watcher创建
        new Watcher();
        this.$data.test;
        new Watcher();
        this.$data.foo.bar;
    }
    observe(value) {
        if(!value || typeof value !== 'object') return;
        // 遍历该对象
        Object.keys(value).forEach(key => {
            this.defineReactive(value, key, value[key])
        })
    }
    // 数据响应化
    defineReactive(obj, key, val) {
        this.observe(val);  // 递归遍历,对象中的嵌套属性
        const dep = new Dep();
        Object.defineProperty(obj, key, {
            get() {
                // 判断这个依赖项是否存在,存在就加到dep中
                Dep.target && dep.addDep(Dep.target)
                return val
            },
            set(newVal) {
                if(newVal === val) return;
                val = newVal
                console.log(`${key}属性更新了:${val}`);
                // 通知需要更新的依赖项
                dep.notify();
            }
        })
    }
};

// Dep: 用来管理Watcher
class Dep {
    constructor() {
        // 这里存放若干依赖(watcher)
        this.deps = [];
    }
    addDep(dep) {
        this.deps.push(dep)
    }
    notify() {
        this.deps.forEach(dep => dep.update())
    }
}
// Watcher
class Watcher {
    constructor() {
        // 将当前Watcher实例指定到Dep静态属性target
        Dep.target = this;
    }
    update() {
        console.log("属性更新了")
    }
}
编译compile

核心逻辑获取dom,遍历dom,获取{{}}格式的变量,以及每个dom的属性,截取k-和@开头的设置响应式

所有代码逻辑

index2.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>HVue</title>
</head>
<body>
    <div id="app">
        <p>{{name}}</p>
        <p k-text="name"></p>
        <p>{{age}}</p>
        <p>{{doubleAge}}</p>
        <input type="text" k-model="name">
        <button @click="changeName">呵呵</button>
        <div k-html="html"></div>
    </div>
</body>
<script src="./HVue.js"></script>
<script src="./compile.js"></script>
<script>
    const Htf = new HVue({
        el: "#app",
        data: {
            name: "I am test",
            age: "12",
            html: "<button>这是一个按钮</button>"
        },
        created () {
            console.log("开始了");
            setTimeout(() => {
                this.name = "我是测试"
            }, 1500);
        },
        methods: {
            changeName() {
                this.name = "哈喽, htf";
                this.age = 1
            }
        }
    })
</script>
</html>

HVue.js

class HVue {
    constructor(options) {
        this.$options = options;
        // 数据响应化
        this.$data = options.data;
        this.observe(this.$data);
        // 模拟一下watcher创建
        // new Watcher();
        // this.$data.test;
        // new Watcher();
        // this.$data.foo.bar;
        // 初始化编译器
        new Compile(options.el, this);
        // created的执行
        if (options.created) {
            options.created.call(this);
        }
    }
    observe(value) {
        if(!value || typeof value !== 'object') return;
        // 遍历该对象
        Object.keys(value).forEach(key => {
            this.defineReactive(value, key, value[key]);
            // 代理data中的属性到vue实例上
            this.proxyData(key);
        })
    }
    // 数据响应化
    defineReactive(obj, key, val) {
        this.observe(val);  // 递归遍历,对象中的嵌套属性
        const dep = new Dep();
        Object.defineProperty(obj, key, {
            get() {
                // 判断这个依赖项是否存在,存在就加到dep中
                Dep.target && dep.addDep(Dep.target)
                return val
            },
            set(newVal) {
                if(newVal === val) return;
                val = newVal
                console.log(`${key}属性更新了:${val}`);
                // 通知需要更新的依赖项
                dep.notify();
            }
        })
    }
    proxyData(key) {
        Object.defineProperty(this, key, {
            get() {
                return this.$data[key];
            },
            set(newVal) {
                this.$data[key] = newVal;
            }
        })
    }
};

// Dep: 用来管理Watcher
class Dep {
    constructor() {
        // 这里存放若干依赖(watcher)
        this.deps = [];
    }
    addDep(dep) {
        this.deps.push(dep)
    }
    notify() {
        this.deps.forEach(dep => dep.update())
    }
}
// Watcher
class Watcher {
    constructor(vm, key, cd) {
        this.vm = vm;
        this.key = key;
        this.cd = cd;
        // 将当前Watcher实例指定到Dep静态属性target
        Dep.target = this;
        this.vm[this.key]; // 触发getter,添加依赖
        Dep.target = null;
    }
    update() {
        console.log("属性更新了")
        this.cd.call(this.vm, this.vm[this.key])
    }
}

compile.js

// 用法 new Compile(el, vm)
class Compile{
    constructor(el, vm) {
        // 要遍历的宿主节点
        this.$el = document.querySelector(el);
        this.$vm = vm;
        // 编译
        if(this.$el) {
            // 转换内部内容为片段fragment
            this.$fragment = this.node2Fragment(this.$el);
            // console.log(this.$fragment)
            // 执行编译
            this.compile(this.$fragment);
            // 将编译完的结果追加到$el
            this.$el.appendChild(this.$fragment);
        }
    };
    // 将宿主元素中的代码片段拿出来遍历,这样子做比较高效
    node2Fragment(el) {
        const frag = document.createDocumentFragment();
        // 将el中所有子元素搬至frag中
        let child;
        while(child = el.firstChild ) {
            frag.appendChild(child);
        }
        return frag;
    };
    compile(el) {
        console.log(el.childNodes)
        const childNodes = el.childNodes;
        Array.from(childNodes).forEach(node => {
            // 类型判断
            if(this.isElement(node)) {
                // 元素
                // console.log(`编译元素${node.nodeName}`);
                // 查找k-, @, :
                const nodeAttrs = node.attributes;
                Array.from(nodeAttrs).forEach(attr => {
                    const attrName = attr.name; // 属性名字
                    const exp = attr.value; // 属性值
                    if(this.isDirective(attrName)) {
                        // k-text
                        const dir = attrName.substring(2);
                        // 执行指令
                        this[dir] && this[dir](node, this.$vm, exp);
                    }
                    if(this.isEvent(attrName)) {
                        const dir = attrName.substring(1);  // @click
                        this.eventHandler(node, this.$vm, exp, dir);
                    }
                })
            }else if(this.isInterpolation(node)) {
                // 文本
                // console.log(`编译文本${node.textContent}`);
                this.compileText(node);
            }
            // 递归子节点
            if(node.childNodes && node.childNodes.length > 0) {
                this.compile(node)
            }
        })
    };
    compileText(node) {
        // 其下的正则进行匹配分组所可以用.$1获取到插值表达式里面的值
        // console.log(RegExp.$1)
        this.updated(node, this.$vm, RegExp.$1, 'text' )
    }
    // 更新函数
    updated (node, vm, exp, dir) {
        const updaterFn = this[dir+'Updater'];
        // 初始化
        updaterFn && updaterFn(node, vm[exp]);
        // 依赖收集
        new Watcher(vm, exp, function(value) {
            updaterFn && updaterFn(node, value);
        })
    }
    text(node, vm, exp) {
        this.updated(node, vm, exp, 'text' )
    }
    // 双向绑定的处理
    model(node, vm, exp) {
        // 指定input的value属性
        this.updated(node, vm, exp, "model");
        // 视图对于模型的响应
        node.addEventListener("input", e => {
            vm[exp] = e.target.value;
        })
    }
    // k-html 的绑定
    html(node, vm, exp) {
        this.updated(node, vm, exp, "html");
    }
    // 事件处理器
    eventHandler(node, vm, exp, dir) {
        const fn = vm.$options.methods && vm.$options.methods[exp];
        if(dir && fn) {
            node.addEventListener(dir, fn.bind(vm));
        }
    }
    textUpdater(node, value) {
        node.textContent = value;
    }
    modelUpdater(node, value) {
        node.value = value;
    }
    htmlUpdater(node, value) {
        node.innerHTML = value;
    }
    isEvent(attr) {
        return attr.indexOf('@') == 0;
    }
    isDirective(attr) {
        return attr.indexOf('k-') == 0;
    }
    isElement(node) {
        return node.nodeType === 1;
    };
    // 差值文本
    isInterpolation(node) {
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
    }
}

vue编译过程是怎样的?

什么是编译,首先vue写的模板这些东西浏览器是根本就不识别的,通过编辑可以进行依赖收集,通过依赖收集就可以吧data中的这些数据模型和视图之间产生了依赖关系,如果以后模型发生变化的时候,我们就可以通知这些依赖的地方,让他们进行更新,这就是执行编译的目的,这样就可以做到模型驱动视图的变化。

双向绑定的原理是什么?

我们在做双向数据绑定的时候通常会使用v-model的指令,我们在编译的过程中可以解析出这个v-model,然后我在做操作的时候有两件事情,第一件事是在v-model所属的事件源加了一个事件监听,如果这个input发生变化的时候,我就可以把最新的值设置到vue的实例上,因为vue的实例上已经实现了数据的响应化,他的响应化的setter函数会触发界面中所有模型依赖的更新,会通知所有那些依赖做更新操作,所以界面中跟这些数据相关的所有部分就更新了。

如何监听数据的push

组件的实现

上一篇下一篇

猜你喜欢

热点阅读