[vue2]熬夜编写教你们去深入理解双向绑定,指令并手写一个

299 阅读4分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

更多文章

[vue2]熬夜编写为了让你们通俗易懂的去深入理解vue-router并手写一个

[vue2]熬夜编写为了让你们通俗易懂的去深入理解vuex并手写一个

[vue2]熬夜编写为了让你们通俗易懂的去深入理解nextTick原理

[vue2]熬夜编写为了让你们通俗易懂的去深入理解双向绑定以及解决监听Array数组变化问题

[vue2]熬夜编写为了让你们通俗易懂的去深入理解v-model原理

熬夜不易,点个赞再走吧

起步

我是在根目录新建了一个文件夹,里面分别有一个temp.html和一个reactive.js,这里先利用原本的vue.js调试下能不能正常运行

    <div id="app">
      <p>{{counter}}</p>
    </div>
    <script src="node_modules/vue/dist/vue.js"></script>
    <script>
      const app = new Vue({
        el: "#app",
        data: {
          counter: 0,
        }
      })
      setInterval(() => {
        app.counter++
      }, 1000)
    </script>

daddc37316e49ee7cc53bab7399542c.png

这样就是正常的,于是改一下 <script src="node_modules/vue/dist/vue.js"></script>
改为我们新建的reactive.js <script src="./reactive.js"></script>

defineReactive

众所周知,vue2的响应式是利用Object.defineProperty,那么这里先添加一个响应式函数

Object.defineProperty(obj, key, val)

  1. obj: 要定义的属性对象。这个在手写vue-router中我们定义的是 this,也就是vue实例
  2. key: 要定义或修改的属性名称。这个在手写vue-router中我们定义的是 current,也就是要变成响应式的目标
  3. val: 要定义或修改的属性描述符。这个在手写vue-router中我们定义的是默认给了一个 '/'
function defineReactive(obj, key, val) {}

现在对obj的key进行属性拦截

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {})
}

Object.defineProperty能添加或修改对象的属性,存在数据描述符和存取描述符两种形式
而这里会用到存取描述符的get和set两个函数,默认为undefined
set接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
            get() {
                console.log('get', val)
                return val
            },
            set(newVal) {
                if (newVal !== val) {
                    console.log('set', newVal)
                    observe(val)
                    val = newVal
                }
            }
    })
}

构建实例

src=http___img2018.cnblogs.com_blog_335097_201811_335097-20181129164544639-1479969337.png&refer=http___img2018.cnblogs.jfif

首先我们的起点是new一个vue实例,然后observer劫持所有属性和compile解析指令两个分支进入一个内循环

构建一个实例
并且老规矩,保存选项方便后面使用

class Vue {
    constructor(options) {
        this.$options = options
        this.$data = options.data
    }
}

添加observer劫持所有属性和compile解析指令

  1. observe()我们做了遍历对象的处理,这里传入data去处理下data
  2. 传入el树和实例对象,遍历options.el模板树,解析其中的动态部分,初始化并更新
class Vue {
    constructor(options) {
        this.$options = options
        this.$data = options.data
       
        observe(this.$data)
        new Compile(options.el, this)
    }
}

observe

observe observe只处理对象,不处理数组,因为 Vue 的实现中,从性能/体验的性价比考虑,放弃了这个特性,另外做处理,这个在另一篇observe讲过

是对象就遍历给对象中每个属性添加响应式

function observe(obj) {
    if (typeof obj !== 'obj' || !!obj) {
            return obj
    }
    Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key])
    })
}

proxy

在添加compiler前,还要做个事,就是代理,为什么要代理呢?
因为这个时候模板内是通过app.counter访问的,这个时候应该是个undefined,如果换成app.data.counter就能获取
所以现在需要换成app.counter,就需要添加一个代理

传入实例, 把方法放在observe()之后

proxy(this)

遍历实例中的data对象

function proxy(vm) {
    // 访问data中进行遍历,访问key
    Object.keys(vm.$data).forEach(key => {
            Object.defineProperty(vm, key, {
                    // 通过get传出去
                    get() {
                            return vm.$data[key]
                    },
                    // 再通过set附上新值
                    set(val) {
                            vm.$data[key] = val
                            console.log(vm.$data[key])
                    }
            })
    })
}

这个时候就能通过app.counter,相当于平时在组件里用this.counter访问

compile

new Compile(options.el, this)
  1. 保存选项
  2. 获取dom
  3. 编译dom
class Compile {
    constructor(el, vm) {
            // 保存选项
            this.$vm = vm
            // 获取dom
            const dom = document.querySelector(el)
            // 编译dom
            this.compile(dom)
    }
}

compile 传入的dom,去获取dom的子节点
用最简洁的话总结浏览器运行机制这篇讲了一些子节点的知识

const childNodes = el.childNodes

获得子节点后,判断是文本还是元素
this.isElement(node)判断是否是元素
this.isInter(node)判断是否是插值表达式{{}}

childNodes.forEach(node => {
    if (this.isElement(node)) {
        // 元素
    }else if (this.isInter(node)) {
        // 插值表达式 {{}}
        // 解析{{xxxx}}
        // console.log('插值表达式', node)
    }

这时候刷新下页面看看效果

5f42f7fb963e0db5b67fb1bede3d8fd.png

正常打印,继续

isInter
正则判断判断是否有 {{}}且nodeType等于3

isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}

首先先做渲染插值表达式{{}},简单点,新增一个方法compileText并传入node

if (this.isInter(node)) {
    this.compileText(node)
}
compileText(node) {
    node.textContent = this.$vm[RegExp.$1]
}

RegExp.$1是什么?看这个图

889a1ce88bc8e09c9204fd4328e4783.png

对就是解析{{xxxx}}中的xxxx

isElement
如果nodeType等于1,就代表是元素,给一个true进入if

isElement(node) {
    return node.nodeType === 1
}

node里有个属性叫attributes
创建一个变量attrs并赋予它
attrs拿到所有属性
解析动态指令,里面包含指令,属性绑定,事件等,同源码里也有attrs,源码里会有多种判断,这里简写

if (this.isElement(node)) {
    const attrs = node.attributes
}

转成数组后进行遍历

if (this.isElement(node)) {
    const attrs = node.attributes
    Array.from(attrs).forEach(attr=>{})
}

在循环里判断是否是一个动态属性
判断 v-xxx="counter" 之类的指令

v-xxx -> attr.name
counter -> attr.value

Array.from(attrs).forEach(attr=>{
    const attrName = attr.name
    const exp = attr.value
    if(this.isDir(attrName)){
        // 如果是v-的开头,就截断它
        const dir = attrName.substring(2)
        // 如果是合法的指令,就执行该指令对应的函数,传入节点和表达式两个参数
        this[dir] && this[dir](node, exp)
    }
})

但是不排除子节点下还有子节点,于是递归一下

if (node.childNodes.length > 0) {
    this.compile(node)
}

添加指令

this.isDir(attrName)isDir()中传入v-xxx进行截取,添加一个方法isDir()跟isElement()同级

isDir(attrName){
    // 判断前缀是否 v-
    return attrName.startsWith('v-')
}

这个时候能截取到指令并执行该指令对应的函数,我们添加几个指令,分别是v-text,v-html,v-if,并进行测试

text(node, exp){
    node.textContent = this.$vm[exp]
}

html(node, exp){
    node.innerHTML = this.$vm[exp]
}

if(node, exp){
    node.style.display = this.$vm[exp]?'block':'none'
}

回到html,标签内添加指令

<div id="app">
  <p>{{counter}}</p>
  <p v-html="VHtml"></p>
  <p v-text="VText"></p>
  <p v-if="VIf">show</p>
</div>
<script src="./reactive.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      counter: 0,
      VIf:false,
      VHtml:'<span style="color:blue">blue html</span>',
      VText: 0
    }
  })
  setInterval(() => {
    app.counter++
    app.VIf = !app.VIf
  }, 1000)
</script>

测试

测试v-text,v-html

1a72a604dc42da723b2cc4ae96e9177.png

通过计时器测试v-if

d4bb22a6e825c0fcf3defffffc033311.gif

测试正常

目前的reactive.js长这样,贴出来方便你们比对

function defineReactive(obj, key, val) {
    observe(val)
    Object.defineProperty(obj, key, {
        get() {
            return val
        },
        set(newVal) {
            if (newVal !== val) {
                observe(val)
                val = newVal
            }
        }
    })
}

function observe(obj) {
    if (typeof obj !== 'obj' || !!obj) {
        return obj
    }
    Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
    })
}

function proxy(vm) {
    Object.keys(vm.$data).forEach(key => {
        Object.defineProperty(vm, key, {
            get() {
                return vm.$data[key]
            },
            set(val) {
                vm.$data[key] = val
                new Compile(this.$options.el, this)
            }
        })
    })
}

class Vue {
    constructor(options) {
        this.$options = options
        this.$data = options.data

        observe(this.$data)
        proxy(this)
        new Compile(options.el, this)
    }
}

class Compile {
    constructor(el, vm) {
        this.$vm = vm
        const dom = document.querySelector(el)
        this.compile(dom)
    }
    compile(el) {
        const childNodes = el.childNodes
        childNodes.forEach(node => {
            if (this.isElement(node)) {
                const attrs = node.attributes
                Array.from(attrs).forEach(attr=>{
                    const attrName = attr.name
                    const exp = attr.value
                    if(this.isDir(attrName)){
                        const dir = attrName.substring(2)
                        this[dir] && this[dir](node, exp)
                    }
                })

                if (node.childNodes.length > 0) {
                    this.compile(node)
                }
            } else
            if (this.isInter(node)) {
                this.compileText(node)
            }
        })
    }

    compileText(node) {
        node.textContent = this.$vm[RegExp.$1]
    }

    isElement(node) {
        return node.nodeType === 1
    }

    isInter(node) {
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
    }

    isDir(attrName){
        return attrName.startsWith('v-')
    }

    text(node, exp){
        node.textContent = this.$vm[exp]
    }

    html(node, exp){
        node.innerHTML = this.$vm[exp]
    }

    if(node, exp){
        node.style.display = this.$vm[exp]?'block':'none'
    }
}

现在我们完成了new MVVM()defineReactiveobserveproxycompile,能正常实现响应式和模板编译