vue v-mode双向绑定原理

前言

深度解析vue2源码是如何实现数据双向绑定

我们先实现一个简单的vue实例 demo.html


<html lang="en">
  <head>
    <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
  </head>
  <body>
    <div id="app" :title="user.name">
      <input type="text" v-model="user.name" />
        {
   {
   user.age}}
        <div>{
   {
   user.name}}{
   {
   user.age}}</div>
        <div>aabb</div>
    </div>
  </body>
  <script>
    let vm = new Vue({
   
      el: "#app",
      data() {
   
        return {
   
          user: {
   
            name: "whs",
            age: 10,
          },
        };
      },
    });
  </script>
</html>

其中v-model 和{ {}}表达式都可以正常渲染,我们创建自己的myvue.js 文件, 在demo.html中使用myvue.js替换官方的vue文件<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>

1.创建vue对象,获取目标节点

<html lang="en">
<head>
    <script src="myvue.js"></script>
</head>
<body>
    <div id="app">
        <input type="text" v-model="user.name">
        <div>{
   {
   user.name}}</div>
        <div>{
   {
   user.age}}</div>
    </div>
</body>
<script>
    let vm = new Vue({
   
        el:"#app",
        data(){
   
            return {
   
                user:{
   
                    name:"whs",
                    age:10
                }
            }
        }

    })
</script>
</html>

创建myvue.js文件
定义vue类,获取构造参数

//基础类
class Vue{
   
    constructor (options){
   
        // this.$el   this.$data   
        this.$el = options.el
        this.$data = options.data()
        if(this.$el){
   
            new Compiler(this.$el,this)
        }
    }
    
}

为了把vue中data的数据渲染的vue作用域中的{ {}}和v-model
上,我们创建一个编译器用来渲染页面,其次我们需要获取vue的作用域,也就是目标节点

class Compiler {
   
    constructor(el,vm){
   
        this.el = this.isElementNode(el)? el:document.querySelector(el)
        console.log(this.el);
    }
    //判断是不是元素节点
    isElementNode(node){
   
        return node.nodeType===1
    }
}

我们的模板上有多处使用data数据的地方,比如user.name和user.age

如果识别一个节点数据就渲染一个节点,多次渲染会导致页面多次重排重绘,vue使用虚拟dom节点将整个模板都准备好后统一渲染一次页面

我们现在需要做的是

  • 删除页面上的模板元素dom节点

  • 根据模板数据和data数据重新创建dom节点

  • 将新的dom节点放回到页面上

2.替换页面上的模板元素dom节点

因为节点上还有我们需要的v-model等表达式,我们需要记录节点上的数据,window提供了一种文档碎片 fragment ,是一种内存中的节点。其中提供的appendChild(dom)在给fragment添加child的同时会把参数上dom从页面上移除掉。
反之构建好的fragment后,我们的this.el的dom节点可以使用appendChild(fragment)将编译好的节点渲染出来

class Compiler {
   
    constructor(el,vm){
   
        this.el = this.isElementNode(el)? el:document.querySelector(el)
        this.vm = vm
        //将dom节点转换为内存中的数据
        let fragment = this.node2fragment(this.el)
        //替换碎片中的数据

        //编译模板

        //将准备好的数据放回页面
        this.el.appendChild(fragment)
    }
    node2fragment(node){
   
        let fragment = document.createDocumentFragment()
        let firstChild;
        while(firstChild=node.firstChild){
   
            fragment.appendChild(firstChild)
        }
        return fragment
    }

    //判断是不是元素节点
    isElementNode(node){
   
        return node.nodeType===1
    }
}

3.编译模板

3.1 获取需要编译的节点

根据模板数据和data数据重新创建dom节点,我们创建一个compile方法来编译模板,将刚才创建的fragment的各个子节点获取,在下文中的log中我们知道,页面上除了我们定义的<div>节点以外还有#text节点,不同的节点我们使用不同的方法来渲染compileElementcompileText

compile(fragment){
   
        let childNode =fragment.childNodes;
        console.log(childNode);
        [...childNode].forEach(node=>{
   
            if(this.isElementNode(node)){
   
                this.compileElement(node)
            }else{
   
                this.compileText(node)
            }
        })
    }

compileElement方法中我们接受的是dom节点,但是我们只需要渲染绑定v-model的节点,我们使用attributes获取节点属性,isDirective判断一下属性是不是v-开头,(除了v-model外还有很多vue的指令,本文只实现v-model)这里在name上我们获得了v-model,在value上获得了绑定的user.name

    isDirective(attrName){
   
        return attrName.startsWith("v-")
    }
    //编译元素
    compileElement(node){
   
        let attributes = node.attributes;
        [...attributes].forEach(attr => {
   
            let {
   name,value} = attr
            if(this.isDirective(name)){
   
                console.log(name,value,node);
            }
        });
    }

然后我们实现对应的compileText来获取同样的数据,在#text节点上我们使用{ {user.age}}{ {user.name}}这种表达式,我们使用textContent获取文本内容

//编译文本
    compileText(node){
   
        let text = node.textContent
        console.log(text);
    }

我们会发现打印的只有user.age并没有user.name,是因为user.namediv中的子文本节点,我们需要在刚才的compile方法中实现递归遍历所有子节点,如下图添加this.compile(node)


//编译内存中的dom
    compile(fragment){
   
        let childNode =fragment.childNodes;
        [...childNode].forEach(node=>{
   
            if(this.isElementNode(node)){
   
                this.compileElement(node)
                //递归遍历子节点
                this.compile(node)//追加代码
            }else{
   
                this.compileText(node)
            }
        })
    }

再使用正则表达式过滤到需要渲染的text节点

  //编译文本
    compileText(node){
   
        let textContent = node.textContent
        if(/\{\{(.+?)\}\}/.test(textContent)){
   
            console.log('textContent-filter',textContent);
        }
    }

接下来我们来分别渲染v-model{ {}}

3.2 渲染v-model

我们创建一个CompilerUtils工具包来保存各种渲染方法,首先是v-model对应的渲染方法

1.我们在compileElement 中已经获取到 node节点,v-model绑定的表达式user.name的expr,还有当前的this.vm 三个参数

2.页面上是input元素,我们直接使用dom元素的value属性修改,我们需要两个参数modelUpdater(node, value)

CompilerUtils = {
   
    updater: {
   
        modelUpdater(node, value) {
   
            node
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值