博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从实现讲解vue的原理
阅读量:4084 次
发布时间:2019-05-25

本文共 6242 字,大约阅读时间需要 20 分钟。

首先我们上一张图

 

 

由图可知,MVVM是由两大块构成,Observer劫持监听响应式 以及 Compile指令解析。 下面我们就从这两方面来组合实现Vue。

响应式

Observer的实现

class Vue {    constructor(options) {        this.$options = options        this.$data = options.data        this.observe(this.$data)        // 运行created生命周期        this.$options.created && this.$options.created.call(this)            }    observe(data) {        if (!data || typeof data !== 'object') {            return        }        Object.keys(data).forEach((key) => {            this.defineProperty(data, key, data[key])            this.poxyData(key)        })    }    defineProperty(obj, key, val) {       const dep = new Dep()        // 递归遍历        this.observe(val)        Object.defineProperty(obj, key, {            get() {                // 初始化时添加wather进行观察                Dep.target && dep.addWather(Dep.target)                return val            },            set(newVal) {                val = newVal                // 被赋值时,通知更新                dep.notify()            }        })    }    poxyData(key) {        this.$data[key] && Object.defineProperty(this, key, {            get() {                return this.$data[key]            },            set(newVal) {                this.$data[key] = newVal            }        })    }}复制代码
const pan = new Vue({            el: '#app',            data: {                name: "I am test.",                age: 12,                fag: {                    bar: 'bar'                },                html: ''            }        })复制代码

在这里,我们首先运用Object.defineProperty()方法对我们传入Vue配置的data进行数据劫持,defineProperty(obj, key, val)方法我们再次调用this.observe(val),这里是因为数据可能不止一层,我们需要把data下面的所以数据都拦截到。 poxyData(key)这个方法就是把data里的数据代理到this上,我们可以pan.html访问到pan.$data.html里的数据。

Dep && Watcher的实现

class Dep {    constructor() {        this.wathers = []    }    addWather(wather) {        this.wathers.push(wather)    }    notify() {        this.wathers.forEach(wather => wather.update())    }}class Wather {    constructor(vm, key, cb) {        this.vm = vm        this.key = key        this.cb = cb        // 把新生成的wather附加到Dep.target         Dep.target = this        // 访问一次被代理的属性        this.vm[this.key]        // Dep.target 置为空,等待下一个wather的生成        Dep.target = null    }    update() {        this.cb && this.cb.call(this.vm, this.vm[this.key])    }}复制代码

Dep就相当简单了,只是两个方法一个addWathernotify两个方法, Wather就一个更新方法。这里就是我们常说的观察者模式。Dep保存多个wather,当Dep发现Wather有更新时,Dep会调用notify方法取通知所有的wather方法update进行更新。

模板编译

class Compile {    constructor(el, vm) {        this.$el = document.querySelector(el)        this.$vm = vm        // 模板移动到文档片段        this.$fragment = this.node2Fragment(this.$el)        // 编译        this.compile(this.$fragment)        // 把编译好的文档片段添加到el        this.$el.appendChild(this.$fragment)    }    node2Fragment(el) {        const fragment = document.createDocumentFragment()        let firstNode        while (firstNode = el.firstChild) {            fragment.appendChild(firstNode)        }        return fragment    }    compile(el) {        const nodes = el.childNodes        Array.from(nodes, (node) => {            // 标签            if (node.nodeType === 1) {                this.compileElement(node)            }            // 文本             else if (this.isInter(node)) {                this.compileText(node)            }            // 编译子节点            if (node.children && node.childNodes.length > 0) {                this.compile(node)            }        })    }    isInter(node) {        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)    }    compileElement(node) {        const nodeAttrs = node.attributes;        Array.from(nodeAttrs, (nodeAttr) => {            // 获取到标签内的属性名以及属性值            const attrName = nodeAttr.name            const attrValue = nodeAttr.value            // 匹配p-开头的属性名            if (attrName.includes('p-')) {                const dir = attrName.substring(2)                this[dir] && this[dir](node, attrValue)            }            // 匹配@开头的属性名            if (attrName.includes('@')) {                const dir = attrName.substring(1)                this[dir] && this[dir](node, attrValue)            }        })    }    compileText(node) {        // 拿取到文本标签里的{
{xxx}} // console.log(RegExp.$1); // {
{name}}花括号内匹配的值 this.update(node, RegExp.$1, 'text') } update(node, key, dir) { const updator = this[dir + 'Updator'].bind(this) updator && updator(node, this.$vm[key]) new Wather(this.$vm, key, (value) => { updator && updator(node, value) }) } eventListener(node, key, type) { const options = this.$vm.$options // 处理this指向问题 const eventFn = options.methods[key].bind(this.$vm) eventFn && this.addEventListener(node, type, eventFn) } textUpdator(node, value) { node.textContent = value } htmlUpdator(node, value) { node.innerHTML = value this.compile(node) } modelUpdator(node, value) { node.value = value } text(node, key) { this.update(node, key, 'text') } html(node, key) { this.update(node, key, 'html') } model(node, key) { this.update(node, key, 'model') // 通过input 事件双向绑定表单数据 node.addEventListener('input', () => { this.$vm[key] = node.value }) } click(node, key) { this.eventListener(node, key, 'click') } dblclick(node, key) { this.eventListener(node, key, 'dblclick') } addEventListener(node, key, fn) { node.addEventListener(key, fn) }}复制代码

这里面我们模板编译的思路主要是3步:

  • 把传入Vue的options配置的el移动到文档片段
  • 编译文档片段
  • 编译好的文档添加到html

这里有一个重点,为什么要添加到文档片段,文档片段用作一个临时的占位符放置项目,然后用appendChild()添加到dom中,这里做到最小化现场更新(一次更新),提升了连续dom操作的性能瓶颈

我们重点来讲讲如何编译文档片段,首先遍历出我们所有的dom,然后分为标签文本两块来进行解析

标签

  • 首先获取出标签中的属性,我们用node.attributes获取当前node的所有属性

  • 取出所有的属性名以及属性值

  • 分情况处理p-@(本文只针对这两种情况做处理)

    p-text 改变node.textContent的值,并添加wather监听后续变化

    p-html 改变node.innerHTML的值,并添加wather监听后续变化

    p-model 改变node.value的值,并添加wather监听后续变化,并且给node添加input事件,实现表单的双向绑定

    @click 通过node.addEventListener('click', fn)添加click事件

    @dblclick 通过node.addEventListener('dblclick', fn)dblclick事件

文本

  • 改变node.textContent的值

最后再把编译完成的文档片段添加到dom完成整个流程,详细代码请参考 。

作者:潘勇旭
链接:https://juejin.im/post/5e034b816fb9a0161f306657
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的文章
Redux入门学习系列教程
查看>>
webpack4源码分析
查看>>
深度剖析:如何实现一个 Virtual DOM 算法
查看>>
Vue2.5从0开发猫眼
查看>>
VUE缓存:动态keep-alive
查看>>
webpack4介绍与总结
查看>>
Vue项目Webpack优化实践,构建效率提高50%
查看>>
从输入URL到页面加载的过程?如何由一道题完善自己的前端知识体系!
查看>>
前端本地文件操作与上传
查看>>
数据动态绑定的简单实现——基于ES5对象的getter/setter机制
查看>>
vue源码系列文章good
查看>>
前端跨域解决方案
查看>>
Web前端面试——HTTP部分
查看>>
HTTP必知必会——常见面试题总结
查看>>
你不知道的Virtual DOM
查看>>
VUE面试题总结
查看>>
写好JavaScript条件语句的5条守则
查看>>
原生JS中DOM节点相关API合集
查看>>
新手快速上手webpack4打包工具的使用
查看>>
发布 react 组件到 npm
查看>>