Vue原理深度剖析

一、Vue基础语法

Vue.js基础结构

使用el方式:
Vue原理深度剖析_第1张图片
使用render函数方式:
Vue原理深度剖析_第2张图片

Vue生命周期Vue原理深度剖析_第3张图片

Vue.js语法和概念

插值表达式
插值表达式相当于一个占位符,只会替换掉其中的占位置的内容。
v-text只能显示Vue对象传递过来的数据,会替换掉节点里已有的内容。
插值表达式和v-text不能够解析html标签,v-html能够解析html标签。

结论:

  • 如果只是单独展示Vue对象里的数据,建议使用“v-text”指令。
  • 如果要同时展示用户前台数据,那么就需要用插值表达式,但是不要忘记和“v-cloak”属性一起使用(同时需要设置样式[v-cloak]{display:none;})。
  • 如果Vue对象传递过来的数据含有HTML标签,则使用v-html

指令
VUE提供的14个指令:

  • v-model :双向数据绑定,表单元素常用 input select radio checkbox textarea 等,v-model有三个修饰符,例如input元素 v-model.trim去掉输入值的前后空格和v-model.number,将输入的字符串转换为number,v-model.lazy 输入的数据不再实时更新,而是数据失去焦点的时候再更新输入的数据
  • v-show: 元素的显示和隐藏,频繁操作元素的显示和隐藏,就用v-show ,原理是操作的dom 的css样式 display的值是true还是false
  • v-if:元素的显示和隐藏,原理是,是否创建元素的dom,例如表格中某条数据是否显示编辑,删除按钮,由后台传的数据解决的,这种不频繁操作的情况可用v-if,v-if 可以加入template标签中判断 v-show 不可以
  • v-else : 和v-if 搭配使用
  • v-else-if :条件满足v-if ?不满足判断v-else-if 如果还不满足直接走v-else 这个的使用方式和我们的js 中的 if ,else if() ,else 是类似的使用方式
  • v-bind: 绑定 v-bind:class v-bind:style v-bind:attribute v-bind可以省略成: 最后写成 :class, :style, :attribute
  • v-on :绑定常用事件 下面的常用事件去掉on 改为@click:点击某个对象时触发@clickondblclick:双击某个对象时触发@dblclickonmouseover:鼠标移入某个元素时触发@mouseoveronmouseout:鼠标移出某个元素时触发@mouseoutonmouseenter:鼠标进入某个元素时触发@onmouseenter
  • v-for:项目中常用循环数组的指令。
  • v-html :将字符串html 转换为结构显示,项目中基本不这种方式去处理,涉及到安全性问题
  • v-text:防止为了{{}} 闪烁问题 项目不常用
  • v-once: 指令指的是元素仅仅绑定一次,只是渲染一次
  • v-cloak:指的是cloak 等元素编译结束以后才会显示dom
  • v-pre :跳过当前元素及子元素的编译过程,先进行编译,项目中基本没有用过
  • v-slot:插槽

自定义指令

计算属性和侦听器

  • 当模板中有太多逻辑需要处理时,推荐使用计算属性。计算属性的结果会被缓存,下次再访问该计算属性时,会从缓存中获取相应的结果,提高性能。
  • 如果我们需要监听数据的变化,做一些比较复杂的操作,例如异步操作或者开销比较大的操作,此时我们可以使用侦听器。

Class和Style绑定
当绑定样式时,我们可以使用Class和Style。它们分别可以绑定数组或者对象,实际开发中,我们推荐使用Class绑定,可以实现样式复用。

条件渲染/列表渲染

  • v-if :控制元素显示/隐藏。条件为false时,是不会输出相应的元素。
  • v-show:控制元素显示/隐藏。元素会渲染到页面,通过样式控制其隐藏。
  • v-for:列表渲染。Vue推荐我们给循环项都设置一个key,用来跟踪每个节点的身份,让每一项都能最大程度地被重用,从而提高性能。

表单输入绑定
当我们使用v-model绑定表单元素时,它负责去监听用户的输入事件,以及更新数据,即双向绑定。

组件
组件是可复用的Vue实例,一个组件封装了html,css,js,它可以实现页面上的一个功能区域,可以无限次的被重用。

插槽
插槽一般用于在自定义组件中挖坑,使用这个组件时去填坑。这样可以使组件更灵活。

插件
如vuex、vue-router都是插件。也可以自己开发插件。

混入mixin
如果多个组件都有相同的选项,就可以使用mixin方式,把相同的选项进行合并,让代码重用。是让组件重用的一种方式。

深入响应式原理
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。

二、Vue Route原理分析与实现

Vue Router使用步骤Vue原理深度剖析_第4张图片Vue原理深度剖析_第5张图片

Vue原理深度剖析_第6张图片

动态路由Vue原理深度剖析_第7张图片Vue原理深度剖析_第8张图片

嵌套路由

当多个路由组件都有相同的内容,我们可以把这个相同的内容提取到一个公共的组件当中。
Vue原理深度剖析_第9张图片
Vue原理深度剖析_第10张图片

编程式导航

常用方法:
$router.push()

  • 参数为跳转路由地址
this.$router.push('/')
  • 参数为对象
this.$router.push({ name: 'Detail', params: { id: 1 } })

push方法会记录本次历史。

$router.replace()

this.$router.replace('/login')

注意:和push方法有些类似,都可以跳转到指定的路径,参数形式也是一样的。但是,replace方法不会记录本次历史,它会把我们当前的历史改变为我们指定的路径。

$router.go()
go是跳转到历史中的某一次,负数为后退。

this.$router.go(-2)

Hash和History模式区别

这两种模式都是客户端路由的实现方式,即当路径发生变化时,不会像服务器发送请求,是由JS监视路径的变化,然后根据不同的地址渲染不同的内容。如果需要服务端内容,会发送Ajax请求去获取。
表现形式的区别

  • Hash模式在这里插入图片描述
    URL中#后面的内容作为路径地址;监听hashchange事件;根据当前路由地址找到对应组件重新渲染。

  • History模式
    在这里插入图片描述
    History模式是一个正常的url。要用好History模式,还需要服务端配置支持。
    通过调用history.pushState()改变地址栏;监听popstate事件;根据当前路由地址找到对应组件重新渲染。

原理的区别

  • Hash模式
    Hash模式是基于瞄点,以及onhashchange事件。
    Vue Router 默认使用的是 hash 模式,使用 hash 来模拟一个完整的 URL,通过
    onhashchange 监听路径的变化
  • History模式
    基于HTML5中的History API
history.pushState() //IE10以后才支持
history.replaceState()
history.go()

History模式的使用

  • 开启 History 模式:
const router = new VueRouter({
// mode: 'hash',
mode: 'history',
routes
})
  • History需要服务器的支持
  • 单页应用中,服务端不存在 http://www.testurl.com/login 这样的地址会返回找不到该页面
  • 在服务端应该除了静态资源外都返回单页应用的 index.html
    Vue原理深度剖析_第11张图片
    History模式-Node.js服务器配置:
    const path = require('path')
    // 导入处理 history 模式的模块
    const history = require('connect-history-api-fallback')
    // 导入 express
    const express = require('express')
    
    const app = express()
    // 注册处理 history 模式的中间件
    app.use(history())
    // 处理静态资源的中间件,网站根目录 ../web
    app.use(express.static(path.join(__dirname, '../web')))
    
    // 开启服务器,端口是 3000
    app.listen(3000, () => {
      console.log('服务器开启,端口:3000')
    })
    

History模式-nginx服务器配置:

  • 从官网下载 nginx 的压缩包
  • 把压缩包解压到 c 盘根目录,c:\nginx-1.18.0 文件夹
  • 修改 conf\nginx.conf 文件
    location / {
    root html;
    index index.html index.htm;
    #新添加内容
    #尝试读取$uri(当前请求的路径),如果读取不到读取$uri/这个文件夹下的首页
    #如果都获取不到返回根目录中的 index.html
    try_files $uri $uri/ /index.html;
    }
    
  • 打开命令行,切换到目录c:\nginx-1.18.0
  • nginx 启动、重启和停止
    #
    启动
    start nginx
    # 重启
    nginx -s reload
    # 停止
    nginx -s stop
    

Vue Router模拟实现

Vue Router 的核心代码

//router/index.js
// 注册插件
// Vue.use() 内部调用传入对象的 install 方法
Vue.use(VueRouter)
// 创建路由对象
const router = new VueRouter({
routes: [
{ name: 'home', path: '/', component: homeComponent }
]
})
// 创建 Vue 实例,注册 router 对象
new Vue({
router,
render: h => h(App)
}).$mount('#app')

类图
Vue原理深度剖析_第12张图片
实现思路

  • 创建 LVueRouter 插件,静态方法 install
    1. 判断插件是否已经被加载/安装
    2. 把Vue构造函数记录到全局变量
    3. 当 Vue 加载的时候把创建Vue实例时传入的 router 对象注入/挂载到 Vue 实例上(注意:只执行一次)
  • 创建 LVueRouter 类
    1. 初始化,options、routeMap、app(简化操作,创建 Vue 实例作为响应式数据记录当前路
      径)
    2. initRouteMap() 遍历所有路由信息,把组件和路由的映射记录到 routeMap 对象中
    3. 注册 popstate 事件,当路由地址发生变化,重新记录当前的路径
    4. 创建 router-link 和 router-view 组件
    5. 当路径改变的时候通过当前路径在 routerMap 对象中找到对应的组件,渲染 router-view

实现代码(History模式)

let _Vue = null

//创建 VueRouter 插件,即实现 VueRouter 类
export default class VueRouter {
    static install(Vue) {
        // 1. 判断插件是否已经被加载/安装
        // 如果插件已经安装直接返回
        if (VueRouter.install.installed) {
            return
        }
        VueRouter.install.installed = true
        // 2. 把Vue构造函数记录到全局变量
        _Vue = Vue
        // 3. 把创建Vue实例时传入的 router 对象注入/挂载到 Vue 实例上(注意:只执行一次)
        //混入
        _Vue.mixin({
            beforeCreate() {
                // 插件的 install() 方法中调用 init() 初始化
                if (this.$options.router) {
                    _Vue.prototype.$router = this.$options.router
                    // 初始化插件的时候,调用 init
                    this.$options.router.init()
                }
            }
        })
    }

    //构造函数
    constructor(options) {
        this.options = options
        // 记录路径和对应的组件
        this.routeMap = {}
        this.data = _Vue.observable({
            // 当前的默认路径
            current: '/'
        })
    }

    //初始化
    init() {
        this.initRouteMap()
        this.initComponents()
        this.initEvent()
    }

    //解析路由规则成键值对形式
    initRouteMap() {
        // routes => [{ name: '', path: '', component: }]
        //遍历所有的路由规则,把路由规则解析成键值对的形式,存储到routeMap中
        this.options.routes.forEach(route => {
            // 记录路径和组件的映射关系
            this.routeMap[route.path] = route.component
        })
    }

    //创建 router-link 和 router-view 组件
    initComponents() {
        _Vue.component('router-link', {
            props: {
                to: String
            },
            // template: ''              
            //运行时版本不支持template,需要使用render
            render(h) {
                return h('a', {
                    attrs: {
                        href: this.to
                    },
                    on: {
                        click: this.clickHandler
                    }
                }, [this.$slots.default])
            },
            methods: {
                clickHandler(e) {
                    history.pushState({}, '', this.to)
                    this.$router.data.current = this.to
                    e.preventDefault()
                }
            }
        })

        const self = this
        _Vue.component('router-view', {
            render(h) {
                // 根据当前路径找到对应的组件,注意 this 的问题
                const component = self.routeMap[self.data.current]
                return h(component)
            }
        })
    }

    //注册事件
    initEvent() {
        window.addEventListener('popstate', () => {
            this.data.current = window.location.pathname
        })
    }
}

Vue的构建版本

  • 运行时版:不支持template模板,需要打包的时候提前编译
  • 完整版:包含运行时和编译器,体积比运行时版大10K左右,程序运行的时候把模板转换成render函数

注意:
vue-cli 创建的项目默认使用的是运行时版本的 Vue.js

  • 如果想切换成带编译器版本的 Vue.js,需要修改 vue-cli 配置
  • 项目根目录创建 vue.config.js 文件,添加 runtimeCompiler
module.exports = {
	runtimeCompiler: true
}

三、模拟 Vue.js 响应式原理

数据驱动

数据响应式、双向绑定、数据驱动。

  • 数据响应式
    数据模型仅仅是普通的 JavaScript 对象,而当我们修改数据时,视图会进行更新,避免了繁
    琐的 DOM 操作,提高开发效率。
  • 双向绑定
    数据改变,视图改变;视图改变,数据也随之改变。
    我们可以使用 v-model 在表单元素上创建双向数据绑定。
  • 数据驱动
    数据驱动是 Vue 最独特的特性之一。
    开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图。

响应式的核心原理

Vue 2.x(基于Object.defineProperty)

  • Vue 2.x深入响应式原理
  • MDN - Object.defineProperty
  • 浏览器兼容 IE8 以上(不兼容 IE8)

一个对象中一个属性需要转换 getter/setter ,处理方式:

 // 模拟 Vue 中的 data 选项
    let data = {
      msg: 'hello'
    }

    // 模拟 Vue 的实例
    let vm = {}

    // 数据劫持:当访问或者设置 vm 中的成员的时候,做一些干预操作
    Object.defineProperty(vm, 'msg', {
      // 可枚举(可遍历)
      enumerable: true,
      // 可配置(可以使用 delete 删除,可以通过 defineProperty 重新定义)
      configurable: true,
      // 当获取值的时候执行
      get () {
        console.log('get: ', data.msg)
        return data.msg
      },
      // 当设置值的时候执行
      set (newValue) {
        console.log('set: ', newValue)
        if (newValue === data.msg) {
          return
        }
        data.msg = newValue
        // 数据更改,更新 DOM 的值
        document.querySelector('#app').textContent = data.msg
      }
    })

    // 测试
    vm.msg = 'Hello World'
    console.log(vm.msg)

一个对象中多个属性需要转换 getter/setter ,处理方式:

// 模拟 Vue 中的 data 选项
    let data = {
      msg: 'hello',
      count: 10
    }

    // 模拟 Vue 的实例
    let vm = {}

    proxyData(data)

    function proxyData(data) {
      // 遍历 data 对象的所有属性
      Object.keys(data).forEach(key => {
        // 把 data 中的属性,转换成 vm 的 setter/setter
        Object.defineProperty(vm, key, {
          enumerable: true,
          configurable: true,
          get () {
            console.log('get: ', key, data[key])
            return data[key]
          },
          set (newValue) {
            console.log('set: ', key, newValue)
            if (newValue === data[key]) {
              return
            }
            data[key] = newValue
            // 数据更改,更新 DOM 的值
            document.querySelector('#app').textContent = data[key]
          }
        })
      })
    }

    // 测试
    vm.msg = 'Hello World'
    console.log(vm.msg)

Vue 3.x(基于Proxy)

  • MDN - Proxy
  • 直接监听对象,而非属性。
  • ES 6中新增,IE 不支持,性能由浏览器优化。
// 模拟 Vue 中的 data 选项
    let data = {
      msg: 'hello',
      count: 0
    }

    // 模拟 Vue 实例
    let vm = new Proxy(data, {
      // 执行代理行为的函数
      // 当访问 vm 的成员会执行
      get (target, key) {
        console.log('get, key: ', key, target[key])
        return target[key]
      },
      // 当设置 vm 的成员会执行
      set (target, key, newValue) {
        console.log('set, key: ', key, newValue)
        if (target[key] === newValue) {
          return
        }
        target[key] = newValue
        document.querySelector('#app').textContent = target[key]
      }
    })

    // 测试
    vm.msg = 'Hello World'
    console.log(vm.msg)

发布订阅模式和观察者模式

发布订阅模式

  • 发布/订阅模式
    • 订阅者
    • 发布者
    • 信号中心

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern)。

  • Vue 的自定义事件:
    https://cn.vuejs.org/v2/guide/migration.html#dispatch-%E5%92%8C-broadcast-
    %E6%9B%BF%E6%8D%A2
// Vue 自定义事件
    let vm = new Vue()
    // { 'click': [fn1, fn2], 'change': [fn] }

    // 注册事件(订阅消息)
    vm.$on('dataChange', () => {
      console.log('dataChange')
    })

    vm.$on('dataChange', () => {
      console.log('dataChange1')
    })
    // 触发事件(发布消息)
    vm.$emit('dataChange')
  • 兄弟组件通信过程:
// eventBus.js
// 事件中心
let eventHub = new Vue()
// ComponentA.vue
// 发布者
addTodo: function () {
// 发布消息(事件)
eventHub.$emit('add-todo', { text: this.newTodoText })
this.newTodoText = ''
} 
// ComponentB.vue
// 订阅者
created: function () {
// 订阅消息(事件)
eventHub.$on('add-todo', this.addTodo)
}
  • 模拟 Vue 自定义事件的实现
//事件触发器
class EventEmitter {
    constructor() {
        //{ 'click': [fn1,fn2], "change": [fn] }
        this.subs = Object.create(null)
    }
    //注册事件
    $on(eventType, handler) {
        this.subs[eventType] = this.subs[eventType] || []
        this.subs[eventType].push(handler)
    }
    //触发事件
    $emit(eventType) {
        if (this.subs[eventType]) {
            this.subs[eventType].forEach(handler => {
                handler()
            })
        }
    }
}

//测试
let em = new EventEmitter()
em.$on('click', () => {
    console.log('click1');
})
em.$on('click', () => {
    console.log('click2');
})

em.$emit('click')
//click1
//click2

观察者模式

  • 观察者(订阅者) – Watcher
    • update():当事件发生时,具体要做的事情
  • 目标(发布者) – Dep
    • subs 数组:存储所有的观察者
    • addSub():添加观察者
    • notify():当事件发生,调用所有观察者的 update() 方法
  • 没有事件中心
//发布者-目标
class Dep {
    constructor() {
        //存储所有的观察者
        this.subs = []
    }
    //添加观察者
    addSub(sub) {
        if (sub && sub.update) {
            this.subs.push(sub)
        }
    }
    //发布通知
    notify() {
        this.subs.forEach(sub => {
            sub.update()
        })
    }
}
//订阅者-观察者
class Watcher {
    update() {
        console.log('update');
    }
}

//测试
const dep = new Dep()
const watcher = new Watcher()
dep.addSub(watcher)
dep.notify()
//update

发布/订阅模式与观察者模式区别

  • 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。
    Vue原理深度剖析_第13张图片

模拟Vue响应式原理

整体结构:Vue原理深度剖析_第14张图片

  • Vue
    把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
  • Observer
    能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep
  • Compiler
    解析每个元素中的指令/插值表达式,并替换成相应的数据
  • Dep
    添加观察者(watcher),当数据变化通知所有观察者
  • Watcher
    数据变化更新视图

Vue

  • 功能
    • 负责接收初始化的参数(选项)
    • 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
    • 负责调用 observer 监听 data 中所有属性的变化
    • 负责调用 compiler 解析指令/插值表达式
  • 结构类图Vue原理深度剖析_第15张图片
    Observer
  • 功能
    • 负责把 data 选项中的属性转换成响应式数据
    • data 中的某个属性也是对象,把该属性转换成响应式数据
    • 数据变化发送通知
  • 结构类图Vue原理深度剖析_第16张图片
    Compiler
  • 功能
    • 负责编译模板,解析指令/插值表达式
    • 负责页面的首次渲染
    • 当数据变化后重新渲染视图
  • 结构类图Vue原理深度剖析_第17张图片
    Dep(Dependency)Vue原理深度剖析_第18张图片
  • 功能
    • 收集依赖,添加观察者(watcher)
    • 通知所有观察者
  • 结构类图Vue原理深度剖析_第19张图片
    Watcher
    Vue原理深度剖析_第20张图片
  • 功能
    • 当数据变化触发依赖, dep 通知所有的 Watcher 实例更新视图
    • 自身实例化的时候往 dep 对象中添加自己
  • 结构类图
    Vue原理深度剖析_第21张图片
    总结-整体流程
    Vue原理深度剖析_第22张图片
  • Vue
    • 记录传入的选项,设置 d a t a / data/ data/el
    • 把 data 的成员注入到 Vue 实例
    • 负责调用 Observer 实现数据响应式处理(数据劫持)
    • 负责调用 Compiler 编译指令/插值表达式等
  • Observer
    • 数据劫持
      • 负责把 data 中的成员转换成 getter/setter
      • 负责把多层属性转换成 getter/setter
      • 如果给属性赋值为新对象,把新对象的成员设置为 getter/setter
    • 添加 Dep 和 Watcher 的依赖关系
    • 数据变化发送通知
  • Compiler
    • 负责编译模板,解析指令/插值表达式
    • 负责页面的首次渲染过程
    • 当数据变化后重新渲染
  • Dep
    • 收集依赖,添加订阅者(watcher)
    • 通知所有订阅者
  • Watcher
    • 自身实例化的时候往dep对象中添加自己
    • 当数据变化dep通知所有的 Watcher 实例更新视图

四、Virtual DOM 的实现原理

Virtual DOM

什么是 Virtual DOM
Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,
所以叫 Virtual DOM。
创建虚拟DOM的开销要比创建真实DOM开销小很多。
如真实DOM成员:

let element = document.querySelector('#app')
let s = ''
for (var key in element) {
s += key + ','
} c
onsole.log(s)
// 打印结果
align,title,lang,translate,dir,hidden,accessKey,draggable,spellcheck,aut
ocapitalize,contentEditable,isContentEditable,inputMode,offsetParent,off
setTop,offsetLeft,offsetWidth,offsetHeight,style,innerText,outerText,onc
opy,oncut,onpaste,onabort,onblur,oncancel,oncanplay,oncanplaythrough,onc
hange,onclick,onclose,oncontextmenu,oncuechange,ondblclick,ondrag,ondrag
end,ondragenter,ondragleave,ondragover,ondragstart,ondrop,ondurationchan
ge,onemptied,onended,onerror,onfocus,oninput,oninvalid,onkeydown,onkeypr
ess,onkeyup,onload,onloadeddata,onloadedmetadata,onloadstart,onmousedown
,onmouseenter,onmouseleave,onmousemove,onmouseout,onmouseover,onmouseup,
onmousewheel,onpause,onplay,onplaying,onprogress,onratechange,onreset,on
resize,onscroll,onseeked,onseeking,onselect,onstalled,onsubmit,onsuspend
,ontimeupdate,ontoggle,onvolumechange,onwaiting,onwheel,onauxclick,ongot
pointercapture,onlostpointercapture,onpointerdown,onpointermove,onpointe
rup,onpointercancel,onpointerover,onpointerout,onpointerenter,onpointerl
eave,onselectstart,onselectionchange,onanimationend,onanimationiteration
,onanimationstart,ontransitionend,dataset,nonce,autofocus,tabIndex,click
,focus,blur,enterKeyHint,onformdata,onpointerrawupdate,attachInternals,n
amespaceURI,prefix,localName,tagName,id,className,classList,slot,part,at
tributes,shadowRoot,assignedSlot,innerHTML,outerHTML,scrollTop,scrollLef
t,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight
,attributeStyleMap,onbeforecopy,onbeforecut,onbeforepaste,onsearch,eleme
ntTiming,previousElementSibling,nextElementSibling,children,firstElement
Child,lastElementChild,childElementCount,onfullscreenchange,onfullscreen
error,onwebkitfullscreenchange,onwebkitfullscreenerror,setPointerCapture
,releasePointerCapture,hasPointerCapture,hasAttributes,getAttributeNames
,getAttribute,getAttributeNS,setAttribute,setAttributeNS,removeAttribute
,removeAttributeNS,hasAttribute,hasAttributeNS,toggleAttribute,getAttrib
uteNode,getAttributeNodeNS,setAttributeNode,setAttributeNodeNS,removeAtt
ributeNode,closest,matches,webkitMatchesSelector,attachShadow,getElement
sByTagName,getElementsByTagNameNS,getElementsByClassName,insertAdjacentE
lement,insertAdjacentText,insertAdjacentHTML,requestPointerLock,getClien
tRects,getBoundingClientRect,scrollIntoView,scroll,scrollTo,scrollBy,scr
ollIntoViewIfNeeded,animate,computedStyleMap,before,after,replaceWith,re
move,prepend,append,querySelector,querySelectorAll,requestFullscreen,web
kitRequestFullScreen,webkitRequestFullscreen,createShadowRoot,getDestina
tionInsertionPoints,ELEMENT_NODE,ATTRIBUTE_NODE,TEXT_NODE,CDATA_SECTION_
NODE,ENTITY_REFERENCE_NODE,ENTITY_NODE,PROCESSING_INSTRUCTION_NODE,COMME
NT_NODE,DOCUMENT_NODE,DOCUMENT_TYPE_NODE,DOCUMENT_FRAGMENT_NODE,NOTATION
_NODE,DOCUMENT_POSITION_DISCONNECTED,DOCUMENT_POSITION_PRECEDING,DOCUMEN
T_POSITION_FOLLOWING,DOCUMENT_POSITION_CONTAINS,DOCUMENT_POSITION_CONTAI
NED_BY,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,nodeType,nodeName,baseU
RI,isConnected,ownerDocument,parentNode,parentElement,childNodes,firstCh
ild,lastChild,previousSibling,nextSibling,nodeValue,textContent,hasChild
Nodes,getRootNode,normalize,cloneNode,isEqualNode,isSameNode,compareDocu
mentPosition,contains,lookupPrefix,lookupNamespaceURI,isDefaultNamespace
,insertBefore,appendChild,replaceChild,removeChild,addEventListener,remo
veEventListener,dispatchEvent

如 Virtual DOM成员:

{
	sel: "div",
	data: {},
	children: undefined,
	text: "Hello Virtual DOM",
	elm: undefined,
	key: undefined
}

为什么使用 Virtual DOM

  • 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery 等库简化 DOM 操作,但是随着项目的复杂 DOM 操作复杂提升。
  • 为了简化 DOM 的复杂操作于是出现了各种 MVVM 框架,MVVM 框架解决了视图和状态的同步问题。
  • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是Virtual DOM 出现了。
  • Virtual DOM 的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述
    DOM, Virtual DOM 内部将弄清楚如何有效(diff)的更新 DOM。
  • 参考 github 上 virtual-dom 的描述
    • 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
    • 通过比较前后两次状态的差异更新真实 DOM

虚拟 DOM 的作用

  • 维护视图和状态的关系
  • 复杂视图情况下提升渲染性能
  • 除了渲染 DOM 以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等Vue原理深度剖析_第23张图片
    Virtual DOM 库
  • Snabbdom
    • Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom
    • 大约 200 SLOC(single line of code)
    • 通过模块可扩展
    • 源码使用 TypeScript 开发
    • 最快的 Virtual DOM 之一
  • virtual-dom

Snabbdom

Snabbdom 基本使用

Snabbdom 文档
看文档的意义

  • 学习任何一个库都要先看文档
  • 通过文档了解库的作用
  • 看文档中提供的示例,自己快速实现一个 demo
  • 通过文档查看 API 的使用
  • 文档地址
    • https://github.com/snabbdom/snabbdom
    • 中文翻译

安装 Snabbdom

yarn add snabbdom

导入 Snabbdom

  • 官方使用commonjs 模块化导入方式:
var snabbdom = require('snabbdom');
  • 使用ES6模块化导入方式:
import { init, h, thunk } from 'snabbdom'

注意:导入时候不能使用 import snabbdom from ‘snabbdom’
原因:node_modules/src/snabbdom.ts 末尾导出使用的语法是 export 导出 API,没有使用export default 导出默认输出。Vue原理深度剖析_第24张图片
关于模块化的语法请参考:

  • 阮一峰老师的 Module 的语法
  • ES6 模块与 CommonJS 模块的差异

模块
Snabbdom 的核心库并不能处理元素的属性/样式/事件等,如果需要处理的话,可以使用模块。
常用模块
官方提供了 6 个模块

  • attributes
    • 设置 DOM 元素的属性,使用 setAttribute ()
    • 处理布尔类型的属性
  • props
    • 和 attributes 模块相似,设置 DOM 元素的属性 element[attr] = value
    • 不处理布尔类型的属性
  • class
    • 切换类样式
    • 注意:给元素设置类样式是通过 sel 选择器
  • dataset
    • 设置 data-* 的自定义属性
  • eventlisteners
    • 注册和移除事件
  • style
    • 设置行内样式,支持动画
    • delayed/remove/destroy

模块使用
模块使用步骤:

  • 导入需要的模块
  • init() 中注册模块
  • 使用 h() 函数创建 VNode 的时候,可以把第二个参数设置为对象,其他参数往后移

Snabbdom 源码解析

1、概述

如何学习源码

  • 先宏观了解
  • 带着目标看源码
  • 看源码的过程要不求甚解
  • 调试
  • 参考资料

看源码必备快捷键

  • 快速定位到定义处:
    • 光标移动到变量上,按F12;
    • 光标移动到变量上,Ctrl + 单击鼠标左键
  • 返回刚刚跳转过来的位置: ALT + 方向左箭头

Snabbdom 的核心

  • 使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
  • init() 设置模块,创建 patch()
  • patch() 比较新旧两个 VNode
  • 把变化的内容更新到真实 DOM 树上

Snabbdom 源码

  • 源码地址
  • src 目录结构:
    Vue原理深度剖析_第25张图片

2、h 函数

  • h() 函数介绍:
    在使用 Vue 的时候见过 h() 函数

    new Vue({
    router,
    store,
    render: h => h(App)
    }).$mount('#app')
    

    h() 函数最早见于hyperscript,使用 JavaScript 创建超文本。
    Snabbdom 中的 h() 函数不是用来创建超文本,而是创建 VNode。

  • 函数重载

    • 参数个数或类型不同的函数
    • JavaScript 中没有重载的概念
    • TypeScript 中有重载,不过重载的实现还是通过代码调整参数
    function add (a, b) {
    console.log(a + b)
    } f
    unction add (a, b, c) {
    console.log(a + b + c)
    } a
    dd(1, 2)
    add(1, 2, 3)
    
  • 源码:

// h 函数的重载
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData | null, children:
    VNodeChildren): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
    var data: VNodeData = {}, children: any, text: any, i: number;
    // 处理参数,实现重载的机制
    if (c !== undefined) {
        // 处理三个参数的情况
        // sel、data、children/text
        if (b !== null) { data = b; }
        if (is.array(c)) { children = c; }
        // 如果 c 是字符串或者数字
        else if (is.primitive(c)) { text = c; }
        // 如果 c 是 VNode
        else if (c && c.sel) { children = [c]; }
    } else if (b !== undefined && b !== null) {
        // 处理两个参数的情况
        // 如果 b 是数组
        if (is.array(b)) { children = b; }
        // 如果 b 是字符串或者数字
        else if (is.primitive(b)) { text = b; }
        // 如果 b 是 VNode
        else if (b && b.sel) { children = [b]; }
        else { data = b; }
    } if (children !== undefined) {
        // 处理 children 中的原始值(string/number)
        for (i = 0; i < children.length; ++i) {
            // 如果 child 是 string/number,创建文本节点
            if (is.primitive(children[i])) children[i] = vnode(undefined,
                undefined, undefined, children[i], undefined);
        }
    } if (
        sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
        (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
    ) {
        // 如果是 svg,添加命名空间
        addNS(data, children, sel);
    }
    // 返回 VNode
    return vnode(sel, data, children, text, undefined);
};

3、VNode

一个 VNode 就是一个虚拟节点用来描述一个 DOM 元素,如果这个 VNode 有 children 就是
Virtual DOM。
源码:

export interface VNode {
  //选择器
  sel: string | undefined
  //节点数据:属性/样式/事件等
  data: VNodeData | undefined
  //子节点:和text只能互斥
  children: Array<VNode | string> | undefined
  //记录vnode对应的真实DOM
  elm: Node | undefined
  //节点中的内容,和children只能互斥
  text: string | undefined
  //优化用
  key: Key | undefined
}

export function vnode(sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined): VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}

4、snabbdom

  • patch(oldVnode, newVnode)
  • 打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
  • 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
  • 如果不是相同节点,删除之前的内容,重新渲染
  • 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
  • 如果新的 VNode 有 children,判断子节点是否有变化,判断子节点的过程使用的就是 diff 算法
  • diff 过程只进行同层级比较
    Vue原理深度剖析_第26张图片
    init函数
  • 功能:init(modules, domApi),返回 patch() 函数(高阶函数)
  • 为什么要使用高阶函数?
    • 因为 patch() 函数再外部会调用多次,每次调用依赖一些参数,比如:
      modules/domApi/cbs
    • 通过高阶函数让 init() 内部形成闭包,返回的 patch() 可以访问到 modules/domApi/cbs,而 不需要重新创建
  • init() 在返回 patch() 之前,首先收集了所有模块中的钩子函数存储到 cbs 对象中
  • 源码
const hooks: (keyof Module)[] = ['create', 'update', 'remove',
    'destroy', 'pre', 'post'];
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
    let i: number, j: number, cbs = ({} as ModuleHooks);
    // 初始化 api
    const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
// 把传入的所有模块的钩子方法,统一存储到 cbs 对象中
// 最终构建的 cbs 对象的形式 cbs = [ create: [fn1, fn2], update: [], ...
]
    for (i = 0; i < hooks.length; ++i) {
        // cbs['create'] = []
        cbs[hooks[i]] = [];
        for (j = 0; j < modules.length; ++j) {
            // const hook = modules[0]['create']
            const hook = modules[j][hooks[i]];
            if (hook !== undefined) {
                (cbs[hooks[i]] as Array<any>).push(hook);
            }
        }
    } …
    …
    ……
    ……
    return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { }
}

patch函数

  • 功能:
    • 传入新旧 VNode,对比差异,把差异渲染到 DOM
    • 返回新的 VNode,作为下一次 patch() 的 oldVnode
  • 执行过程:
    • 首先执行模块中的钩子函数 pre
    • 如果 oldVnode 和 vnode 相同(key 和 sel 相同)
      • 调用 patchVnode(),找节点的差异并更新 DOM
    • 如果 oldVnode 是 DOM 元素
      • 把 DOM 元素转换成 oldVnode
      • 调用 createElm() 把 vnode 转换为真实 DOM,记录到 vnode.elm
      • 把刚创建的 DOM 元素插入到 parent 中
      • 移除老节点
      • 触发用户设置的 create 钩子函数
  • 源码
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    // 保存新插入节点的队列,为了触发钩子函数
    const insertedVnodeQueue: VNodeQueue = [];
    // 执行模块的 pre 钩子函数
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
    // 如果 oldVnode 不是 VNode,创建 VNode 并设置 elm
    if (!isVnode(oldVnode)) {
        // 把 DOM 元素转换成空的 VNode
        oldVnode = emptyNodeAt(oldVnode);
    }
    // 如果新旧节点是相同节点(key 和 sel 相同)
    if (sameVnode(oldVnode, vnode)) {
        // 找节点的差异并更新 DOM
        patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
        // 如果新旧节点不同,vnode 创建对应的 DOM
        // 获取当前的 DOM 元素
        elm = oldVnode.elm!;
        parent = api.parentNode(elm);
        // 触发 init/create 钩子函数,创建 DOM
        createElm(vnode, insertedVnodeQueue);
        if (parent !== null) {
            // 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中
            api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
            // 移除老节点
            removeVnodes(parent, [oldVnode], 0, 0);
        }
    }
    // 执行用户设置的 insert 钩子函数
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
        insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
    }
    // 执行模块的 post 钩子函数
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    // 返回 vnode
    return vnode;
};

createElm函数

  • 功能:
    • createElm(vnode, insertedVnodeQueue),返回创建的 DOM 元素
    • 创建 vnode 对应的 DOM 元素
  • 执行过程:
    • 首先触发用户设置的 init 钩子函数
    • 如果选择器是!,创建评论节点
    • 如果选择器为空,创建文本节点
    • 如果选择器不为空
      • 解析选择器,设置标签的 id 和 class 属性
      • 执行模块的 create 钩子函数
      • 如果 vnode 有 children,创建子 vnode 对应的 DOM,追加到 DOM 树
      • 如果 vnode 的 text 值是 string/number,创建文本节点并追击到 DOM 树
      • 执行用户设置的 create 钩子函数
      • 如果有用户设置的 insert 钩子函数,把 vnode 添加到队列中
  • 思维导图
    Vue原理深度剖析_第27张图片
  • 源码
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any, data = vnode.data;
    if (data !== undefined) {
        // 执行用户设置的 init 钩子函数
        const init = data.hook?.init;
        if (isDef(init)) {
            init(vnode);
            data = vnode.data;
        }
    }
    let children = vnode.children, sel = vnode.sel;
    if (sel === '!') {
        // 如果选择器是!,创建评论节点
        if (isUndef(vnode.text)) {
            vnode.text = '';
        }
        vnode.elm = api.createComment(vnode.text!);
    } else if (sel !== undefined) {
        // 如果选择器不为空
        // 解析选择器
        // Parse selector
        const hashIdx = sel.indexOf('#');
        const dotIdx = sel.indexOf('.', hashIdx);
        const hash = hashIdx > 0 ? hashIdx : sel.length;
        const dot = dotIdx > 0 ? dotIdx : sel.length;
        const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0,
            Math.min(hash, dot)) : sel;
        const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
            ? api.createElementNS(i, tag)
            : api.createElement(tag);
        if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
        if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot +
            1).replace(/\./g, ' '));
        // 执行模块的 create 钩子函数
        for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode,
            vnode);
        // 如果 vnode 中有子节点,创建子 vnode 对应的 DOM 元素并追加到 DOM 树上
        if (is.array(children)) {
            for (i = 0; i < children.length; ++i) {
                const ch = children[i];
                if (ch != null) {
                    api.appendChild(elm, createElm(ch as VNode,
                        insertedVnodeQueue));
                }
            }
        } else if (is.primitive(vnode.text)) {
            // 如果 vnode 的 text 值是 string/number,创建文本节点并追加到 DOM 树
            api.appendChild(elm, api.createTextNode(vnode.text));
        } c
        onst hook = vnode.data!.hook;
        if (isDef(hook)) {
            // 执行用户传入的钩子 create
            hook.create?.(emptyNode, vnode);
            if (hook.insert) {
                // 把 vnode 添加到队列中,为后续执行 insert 钩子做准备
                insertedVnodeQueue.push(vnode);
            }
        }
    } else {
        // 如果选择器为空,创建文本节点
        vnode.elm = api.createTextNode(vnode.text!);
    }
    // 返回新创建的 DOM
    return vnode.elm;
}

patchVnode函数

  • 功能:
    • patchVnode(oldVnode, vnode, insertedVnodeQueue)
    • 对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM
  • 执行过程:
    • 首先执行用户设置的 prepatch 钩子函数
    • 执行 create 钩子函数
      • 首先执行模块的 create 钩子函数
      • 然后执行用户设置的 create 钩子函数
    • 如果 vnode.text 未定义
      • 如果 oldVnode.children 和 vnode.children 都有值
        • 调用 updateChildren()
        • 使用 diff 算法对比子节点,更新子节点
      • 如果 vnode.children 有值, oldVnode.children 无值
        • 清空 DOM 元素
        • 调用 addVnodes() ,批量添加子节点
      • 如果 oldVnode.children 有值, vnode.children 无值
        • 调用 removeVnodes() ,批量移除子节点
      • 如果 oldVnode.text 有值
        • 清空 DOM 元素的内容
    • 如果设置了 vnode.text 并且和和 oldVnode.text 不等
      • 如果老节点有子节点,全部移除
      • 设置 DOM 元素的 textContent 为 vnode.text
    • 最后执行用户设置的 postpatch 钩子函数
  • 源码
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue:
    VNodeQueue) {
    const hook = vnode.data?.hook;
    // 首先执行用户设置的 prepatch 钩子函数
    hook?.prepatch?.(oldVnode, vnode);
    const elm = vnode.elm = oldVnode.elm!;
    let oldCh = oldVnode.children as VNode[];
    let ch = vnode.children as VNode[];
    // 如果新老 vnode 相同返回
    if (oldVnode === vnode) return;
    if (vnode.data !== undefined) {
        // 执行模块的 update 钩子函数
        for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode,
            vnode);
        // 执行用户设置的 update 钩子函数
        vnode.data.hook?.update?.(oldVnode, vnode);
    } /
        / 如果 vnode.text 未定义
    if (isUndef(vnode.text)) {
        // 如果新老节点都有 children
        if (isDef(oldCh) && isDef(ch)) {
            // 使用 diff 算法对比子节点,更新子节点
            if (oldCh !== ch) updateChildren(elm, oldCh, ch,
                insertedVnodeQueue);
        } else if (isDef(ch)) {
            // 如果新节点有 children,老节点没有 children
            // 如果老节点有text,清空dom 元素的内容
            if (isDef(oldVnode.text)) api.setTextContent(elm, '');
            // 批量添加子节点
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
        } else if (isDef(oldCh)) {
            // 如果老节点有children,新节点没有children
            // 批量移除子节点
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
        } else if (isDef(oldVnode.text)) {
            // 如果老节点有 text,清空 DOM 元素
            api.setTextContent(elm, '');
        }
    } else if (oldVnode.text !== vnode.text) {
        // 如果没有设置 vnode.text
        if (isDef(oldCh)) {
            // 如果老节点有 children,移除
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
        } /
            / 设置 DOM 元素的 textContent 为 vnode.text
        api.setTextContent(elm, vnode.text!);
    }
    // 最后执行用户设置的 postpatch 钩子函数
    hook?.postpatch?.(oldVnode, vnode);
}

updateChildren函数

  • 功能:
    diff 算法的核心,对比新旧节点的 children,更新 DOM
  • 执行过程:
    • 要对比两棵树的差异,我们可以取第一棵树的每一个节点依次和第二课树的每一个节点比
      较,但是这样的时间复杂度为 O(n^3)
    • 在DOM 操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点
    • 因此只需要找同级别的子节点依次比较,然后再找下一级别的节点比较,这样算法的时间复 杂度为 O(n)Vue原理深度剖析_第28张图片
    • 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引
    • 在对开始和结束节点比较的时候,总共有四种情况
      • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
      • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
      • oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点)
      • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)
        Vue原理深度剖析_第29张图片
    • 开始节点和结束节点比较,这两种情况类似
      • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
      • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
    • 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同)
      • 调用 patchVnode() 对比和更新节点
      • 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++ Vue原理深度剖析_第30张图片
    • oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同
      • 调用 patchVnode() 对比和更新节点
      • 把 oldStartVnode 对应的 DOM 元素,移动到右边
      • 更新索引
        Vue原理深度剖析_第31张图片
    • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同
      • 调用 patchVnode() 对比和更新节点
      • 把 oldEndVnode 对应的 DOM 元素,移动到左边
      • 更新索引
        Vue原理深度剖析_第32张图片
    • 如果不是以上四种情况
      • 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点
      • 如果没有找到,说明 newStartNode 是新节点
        • 创建新节点对应的 DOM 元素,插入到 DOM 树中
      • 如果找到了
        • 判断新节点和找到的老节点的 sel 选择器是否相同
        • 如果不相同,说明节点被修改了
          • 重新创建对应的 DOM 元素,插入到 DOM 树中
        • 如果相同,把 elmToMove 对应的 DOM 元素,移动到左边
          Vue原理深度剖析_第33张图片
    • 循环结束
      • 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束
      • 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束
    • 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边
      Vue原理深度剖析_第34张图片
    • 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批量删除
      Vue原理深度剖析_第35张图片
  • 源码
function updateChildren(parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0, newStartIdx = 0;
    let oldEndIdx = oldCh.length - 1;
    let oldStartVnode = oldCh[0];
    let oldEndVnode = oldCh[oldEndIdx];
    let newEndIdx = newCh.length - 1;
    let newStartVnode = newCh[0];
    let newEndVnode = newCh[newEndIdx];
    let oldKeyToIdx: KeyToIndexMap | undefined;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        // 索引变化后,可能会把节点设置为空
        if (oldStartVnode == null) {
            // 节点为空移动索引
            oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been
            moved left
        } else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx];
        } else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx];
        } else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx];
            // 比较开始和结束节点的四种情况
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            // 1. 比较老开始节点和新的开始节点
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            // 2. 比较老结束节点和新的结束节点
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
            // 3. 比较老开始节点和新的结束节点
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
            api.insertBefore(parentElm, oldStartVnode.elm!,
                api.nextSibling(oldEndVnode.elm!));
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode
            moved left
            // 4. 比较老结束节点和新的开始节点
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
            api.insertBefore(parentElm, oldEndVnode.elm!,
                oldStartVnode.elm!);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        } else {
            // 开始节点和结束节点都不相同
            // 使用 newStartNode 的 key 再老节点数组中找相同节点
            // 先设置记录 key 和 index 的对象
            if (oldKeyToIdx === undefined) {
                oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx,
                    oldEndIdx);
            } /
                / 遍历 newStartVnode, 从老的节点中找相同 key 的 oldVnode 的索引
            idxInOld = oldKeyToIdx[newStartVnode.key as string];
            // 如果是新的vnode
            if (isUndef(idxInOld)) { // New element
                // 如果没找到,newStartNode 是新节点
                // 创建元素插入 DOM 树
                api.insertBefore(parentElm, createElm(newStartVnode,
                    insertedVnodeQueue), oldStartVnode.elm!);
                // 重新给 newStartVnode 赋值,指向下一个新节点
                newStartVnode = newCh[++newStartIdx];
            } else {
                // 如果找到相同 key 相同的老节点,记录到 elmToMove 遍历
                elmToMove = oldCh[idxInOld];
                if (elmToMove.sel !== newStartVnode.sel) {
                    // 如果新旧节点的选择器不同
                    // 创建新开始节点对应的 DOM 元素,插入到 DOM 树中
                    api.insertBefore(parentElm, createElm(newStartVnode,
                        insertedVnodeQueue), oldStartVnode.elm!);
                } else {
                    // 如果相同,patchVnode()
                    // 把 elmToMove 对应的 DOM 元素,移动到左边
                    patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
                    oldCh[idxInOld] = undefined as any;
                    api.insertBefore(parentElm, elmToMove.elm!,
                        oldStartVnode.elm!);
                } /
                    / 重新给 newStartVnode 赋值,指向下一个新节点
                newStartVnode = newCh[++newStartIdx];
            }
        }
    } /
        / 循环结束,老节点数组先遍历完成或者新节点数组先遍历完成
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
        if (oldStartIdx > oldEndIdx) {
            // 如果老节点数组先遍历完成,说明有新的节点剩余
            // 把剩余的新节点都插入到右边
            before = newCh[newEndIdx + 1] == null ? null :
                newCh[newEndIdx + 1].elm;
            addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx,
                insertedVnodeQueue);
        } else {
            // 如果新节点数组先遍历完成,说明老节点有剩余
            // 批量删除老节点
            removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
        }
    }
}

5、Modules

  • patch() -> patchVnode() -> updateChildren()
  • Snabbdom 为了保证核心库的精简,把处理元素的属性/事件/样式等工作,放置到模块中
  • 模块可以按照需要引入
  • 模块的使用可以查看官方文档
  • 模块实现的核心是基于 Hooks

Hooks

  • 预定义的钩子函数的名称
  • 源码
export interface Hooks {
    // patch 函数开始执行的时候触发
    pre?: PreHook;
    // createElm 函数开始之前的时候触发
    // 在把 VNode 转换成真实 DOM 之前触发
    init?: InitHook;
    // createElm 函数末尾调用
    // 创建完真实 DOM 后触发
    create?: CreateHook;
    // patch 函数末尾执行
    // 真实 DOM 添加到 DOM 树中触发
    insert?: InsertHook;
    // patchVnode 函数开头调用
    // 开始对比两个 VNode 的差异之前触发
    prepatch?: PrePatchHook;
    // patchVnode 函数开头调用
    // 两个 VNode 对比过程中触发,比 prepatch 稍晚
    update?: UpdateHook;
    // patchVnode 的最末尾调用
    // 两个 VNode 对比结束执行
    postpatch?: PostPatchHook;
    // removeVnodes -> invokeDestroyHook 中调用
    // 在删除元素之前触发,子节点的 destroy 也被触发
    destroy?: DestroyHook;
    // removeVnodes 中调用
    // 元素被删除的时候触发
    remove?: RemoveHook;
    // patch 函数的最后调用
    // patch 全部执行完毕触发
    post?: PostHook;
}

Modules
模块文件的定义
Snabbdom 提供的所有模块在:src/modules 文件夹下,主要模块有:

  • attributes.ts
    使用 setAttribute/removeAttribute 操作属性
    能够处理 boolean 类型的属性
  • class.ts
    切换类样式
  • dataset.ts
    操作元素的 data-* 属性
  • eventlisteners.ts
    注册和移除事件
  • module.ts
    定义模块遵守的钩子函数
  • props.ts
    和 attributes.ts 类似,但是是使用 elm[attrName] = value 的方式操作属性
  • style.ts
    操作行内样式
    可以使动画更平滑
  • hero.ts
    自定义的模块,examples/hero 示例中使用

你可能感兴趣的:(笔记,vue,js,es6,前端,typescript)