原生js实现一版迷你vue

1,232

前言

原生js实现一版迷你vue,了解vue中的MVVM和dom-diff、计算属性、watch、observe、dep、watcher、nextTick是啥,咋实现的。。。

本篇文章注重怎么去一步步实现一版本vue的核心代码及掌握vue原理,或者说vue得源码设计,不会去过多讲解vue得应用,且 vue源码的语法使用了Flow,为了使代码更简洁,本文代码中没有使用,对于vue得具体应用可查看官网文档: cn.vuejs.org/v2/guide/ ~

你从本篇文章中可以学习到:

  • vue目录结构设计
  • vue核心源码实现原理
  • Vue得两大核心点:MVVM和dom-diff
  • vue如何实现数据劫持、监听数据的读写?
  • vue中观察者模式
  • 计算属性和watch区别
  • vue中数据的批量更新
  • nextTick实现
  • proxy

本篇主要实现Vue得两大核心点:MVVM和dom-diff;

如果不知道MVVM是什么?可以查看: juejin.cn/post/684490…

如果不知道dom-diff是什么?可以查看我的文章: juejin.cn/post/684490… 都有具体的说明和实现过程。

完整项目代码可查看github.com/lixiaoyanle…

思考问题?

  • 你下载了vue,不知道怎么查看源码?vdom在哪个目录下?具体生命周期在哪?

  • vue的目录都是什么意思?

  • 只想了解核心代码怎么找?

  • new Vue的时候都干了什么?

  • 内部都是怎么执行的?

  • 怎么就能把数据直接渲染到页面了?

  • vue全局暴露什么Api?

  • 为什么说vue是数据驱动的呢?

Vue架构介绍

vue.js代码设计非常优雅,我们来下载一个vue.js,来看看vue的目录结构,都是干什么的,从而思考一下,我们在搭建项目中能不能吸取些什么经验:

Vue源码目录分工明确。Vue源码目录大致分为:

  • benchmarks:测试性能,对于一些复杂情况,处理大量数据时测试Demo;
  • dist:各环境所需的版本包、构建不同的vue版本,比如vue.runtime.js开发仅运行版本不编译、vue.esm.js:es模块完整版本、vue.min.js开发版本等
  • examples:用Vue实现的一些实用Demo;如:select2下拉框demo、svg分布图demo、todoMVC、
  • grid列表筛选、modal等
  • flow: 数据类型检测配置,强制器严格按照此什么的类型来执行
  • packages: 特定环境运行需要单独安装的插件,npm包,可以通过单独的npm install进行安装相应的版本,如vue-server-renderer服务器渲染、vue-template-compiler模板编译、weex-template-compiler weex多端模板编译等
  • src: 整个源码的核心。
  • script: npm脚本配置文件;npm脚本配置文件,可以结合webpack等工具进行编译、测试、构建;
  • test: 测试用例;
  • types: 新版typescript配置、主要是一些typescript类型申明文件;

src目录说明:

可以看到src为核心代码目录,我们如果想看vue的本文的代码时根据什么来实现的可以去这里查看:src中具体的文件夹目录说明如下:

  • Complier:解析模板相关
  • core:核心代码
  • platfroms:跨平台兼容
  • server:服务端ssr渲染
  • sfc:转换单文件组件 .vue文件解析
  • shared:全局方法常量、共享代码

核心代码原理详解:

Vue的入口路径是vue\src\platefroms\web\entry-runtime.js ,构建生成文件路径是vue\dist\vue.runtime.esm.js

主入口:src\core\index.js

(图0-1)

我们可以看到主入口主要做了三件事:

  • 1、引用./instance/index中暴露的Vue构造器
  • 2、调用initGlobalAPI方法,定义全局资源
  • 3、暴露Vue

initGlobalAPI:

对Vue进行各种方法和属性的定义,这些api我们在写源码或者自定义一些功能时,都会遇到

src\core\global-api\index.js

  • Vue.config:各种全家配置项
  • Vue.util:各种工具函数
  • Vue.set
  • Vue.nextTick
  • Vue.options 这个与new Vue(options)中的options不一样,是默认的提高资源比如可以组件、指令、过滤器;
  • Vue.use 通过initUse方法定义
  • Vue.mixin 通过initMixin方法定义
  • Vue.extend通过initExtend方法定义

Vue核心方法

在文件目录src\core\instance\index.js中

(图0-2)

在我们写vue代码时,往往会new Vue来操作,而new Vue(options)都干什么了呢?我们通过上边的代码也可以看到,创建一个Vue的构造函数,开始检测当前的环境是不是开发环境 ,如果Vue不是通过new实例化的将警告。然后初始化this._init(options)

还有几个方法分别是:

1、initMixin(Vue):初始化的入口,各种初始化的工作

2、stateMixin(Vue):数据绑定的核心方法,主要功能:

  • 初始化state,包括 props、methods、data、computed、watch;
  • 对于prop和data属性,将其设置为vm的响应式属性,即使用Object.defineProperty来绑定vm的prop和data属性并设置其getter和setter
  • 对于methods,则将每个method都挂载到vm上并将this指向vm
  • 对于computed,在将其设置为vm的响应式属性之外,还需要定义watcher,用于收集依赖
  • watch属性,也是将其设置为watcher实例,收集依赖

3、eventsMixin(Vue):事件的核心方法,on,off,$emit方法

4、lifecycleMixin(Vue):生命周期的核心方法

5、renderMixin():渲染的核心方法,用来生成人的人函数以及VNode

其他的函数有的方法如下图罗列:

(图0-3)

对于Vue这个以mixin的函数,是在原型上添加的方法,Vue.prototype. = function(){}; vue没有把所有的方法都写在函数内部,这样从代码上来说,每次实例化的时候不会生成重复的代码,也使得代码结构更清晰,利用mixin的概念,把每个模块都抽离开,这样代码在结构和扩展性都有很大提高。

ps:我们可以看到"_"为开头的方法,多半时Vue内部使用的,可以成为私有属性,属于不公开的api;以"$"为开头的方法,时文档中公开给用户使用的默认api。

准备工作

1、初始化项目

创建一个空文件lee-vue-simple, 在初始化项目:npm init -y ,如果你需要上传项目到git最好创建一个忽略文件来把忽略一些不必要的上传文件.gitignore

2、安装依赖包

在创建的空文件夹lee-vue-simple项目下,安装如果:

npm i webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev

3、配置

3.1 配置package.json中scripts部分

"scripts": {   
    "build": "webpack --mode=development",
    "dev": "webpack-dev-server --mode=development"
  },

3.2 配置webpack.config.js

这也不具体讲解webpage的用法,不懂可以查看官网文档: webpack.docschina.org/guides/gett…

在根目录新建一个文件webpack.config.js,配置如下:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry:'./src/index.js',
    output:{
        filename:'main.js',
        path: path.resolve(__dirname, 'dist')
    },
    devtool:'source-map', //便于追查源文件得错误位置
    resolve:{ //更改解析模块得查找方法
        modules:[path.resolve(__dirname,'source'),path.resolve('node_modules')]
    },
    plugins:[
        new HtmlWebpackPlugin({
            template:path.resolve(__dirname,'public/index.html')
        })
    ]
}

4、初步创建文件

按照webpage.config.js配置,创建入口文件src/index.js

创建public\index.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>实现vue源码</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

创建source\vue文件夹--》这个文件夹内得内容将是我们主要实现vue源码得文件

5、启动项目

我们现在启动一下项目看看能否正常跑通:

npm run dev

在工具中输入上述命令,没有报错,在打开提示启动得http://localhost:8080/

(图1)

测试一下:在src\index.js中输入

console.log('测试哈哈哈~');

(图2)

到此准备工作结束,下边开始正式来写代码吧~~~

正式

大致流程:new Vue(options) -> init -> $mount -> compile -> render -> vnode -> patch -> DOM

1、vue得基本应用

1.1、一个简单的案例

import Vue from "vue";
let vm = new Vue({
    el:'#app', //表示要渲染得元素app
    data(){
		return{
			msg:'hello'
		}
    },
    computed: {
        
    },
    watch: {
        
    }
});
console.log(vm);
<div  id="app" >
{{msg}}
</div>

简单解释:

第一行:import Vue from "vue";引入vue,准备得时候我们webpack.config.js中通过resolve配置更改了会先去source中引入在去node_modules中查找;

第二行:我们发现Vue是可以new得说明vue是一个类或者构造函数;实例vm

new Vue(options)中传入一个对象参数(options),即传入Vue中的配置选项,里边有一些钩子函数还有el表示要渲染得元素

2、实现MVVM

思想是: Vue采用数据劫持Observe配合发布者-订阅者模式,通过Object.defineProperty来()来劫持各个属性的get和set,在数据发生变化的时候,发布消息给依赖收集器,去通知观察者,做出对应的回调函数去更新视图。

Vue的数据响应式包含三个部分:ObserverDepWatcher

声明:此小节实现的更新视图,不包括虚拟dom,render方法,初步采用vue1.0的更新方法;虚拟dom会在后边的小节中在实现;在重写更新视图方法;

2.1、vue入口、初始化

第一步:创建导出文件

source\vue\index.js

第二步:导出Vue

source\vue\index.js

function Vue(options){
}

export default Vue

第三步:初始化vue

源码查看vue\src\core\i nstance\index.js

function Vue(options){
    this._init(options); //初始化vue 并且将用户选项传入
}
Vue.prototype._init = function (options) {
    // vue中初始化 this.$options 表示得是vue中得参数
    let vm = this;
    vm.$options = options;

    //MVVM原理 响应式数据 需要数据重新初始化 
    // 我们将不同得初始化放入到不同得文件夹中操作,方便维护
    initState(vm);
}
export default Vue

(图7)

ps: initState是本节的核心代码:主要实现的功能是:

  • 初始化state,包括 props、methods、data、computed、watch;
  • 对于prop和data属性,将其设置为vm的响应式属性,即使用Object.defineProperty来绑定vm的prop和data属性并设置其getter和setter
  • 对于methods,则将每个method都挂载到vm上并将this指向vm
  • 对于computed,在将其设置为vm的响应式属性之外,还需要定义watcher,用于收集依赖
  • watch属性,也是将其设置为watcher实例,收集依赖

2.2、Observe

Observe 类主要给响应式对象的属性和数组的一些方法添加 get/set 用于依赖收集与派发更新

2.2.1、对象劫持

第一步:data数据初始化

源码:vue\src\core\instance\state.js

1、创建文件source\vue\observe\index

export function initState(vm){
    let options = vm.$options;
    if (options.data) {
        initData(vm);//初始化数据
    }
}
// 观察
export function observe(data){
    if(typeof data !== 'object' || data == null){
        return;
    }

    return new Observe(data);
}
// 初始化数据  将数据重新定义 核心:Object.defineProperty
function initData(vm){
    let data = vm.$options.data;
    data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
    observe(vm._data);
}

observe()返回Observe类的实例,我们可以看到如果要观察得new Vue时传入得参数options中得data钩子返回得数据中如果不是个object或者是null、undefined我们就不需要在观察了,直接返回,如果是对象我们需要重新定义即需要劫持;

initData函数中,会判断data是不是函数, 是则取返回值不是则取自身。

2、实现响应式数据变化

source\vue\observe\observe.js

创建Observe类来劫持监听所有属性

// 定义响应式数据变化
export function defineReactive(data,key,val){
    Object.defineProperty(data,key,{
        get(){
            return val;
        },
        set(newVal){
            if (val === newVal) return;
            val = newVal;
        }
    });
}

class Observe{
    constructor(data) { // 此时得data就是定义得vm._data
        //将用户得数据使用Object.defineProperty重新定义
        this.walk(data);
        
    }
    walk(data){       
        for (let [key, val] of Object.entries(data)) {
            defineRective(data, key, val);
        }
    }
    
}

export default Observe;

3、data中得数据值可能还是个对象,需要递归处理

import {observe} from './index';
// 定义响应式数据变化
export function defineReactive(data,key,val){
    // 递归 判断val是不是对象{name:'lee',age:18}
    observe(val);
    Object.defineProperty(data,key,{
        get(){
            return val;
        },
        set(newVal){
            if (val === newVal) return;            // 如果设置的是一个对象的话,需要继续观察
            observe(newVal);
            val = newVal;
        }
    });
}

class Observe{
    constructor(data) { // 此时得data就是定义得vm._data
        //将用户得数据使用Object.defineProperty重新定义
        this.walk(data);
        
    }
    walk(data){       
        for (let [key, val] of Object.entries(data)) {
            defineRective(data, key, val);
        }
    }
    
}

export default Observe;

从上述代码中我们可以看到data中得数据实现响应式是使用了Object.defineProperty核心所以是不支持ie8及以下浏览器得。

第二步:将没有导入得文件导入:import

source\vue\index中import {initState} from './observe'

source\vue\oberve\index.js中import {Observe} from './observe'

第三步:简单测试

src\index.js

import Vue from "vue";

let vm = new Vue({
    el:'#app', //表示要渲染得元素是app
    data(){
        return {
            msg:'hello',
            obj:{name:'lee',age:18},
            arr:[1,2,3]
        }
    }
});

console.log(vm);

运行npm run dev打开浏览器http:localhost:8080/

(图3)

如图3我们可以看到重新定义得data在vm._data上边,有set、get方法,这样我们就可以获取和设置了。

比如重新设置msg得值:vm._data.msg = '更改值';

(图4)

2.2.2 实现代理(vm._data上得代码到vm)

从上变知道我们获取数据还是设置数据需要通过vm._data,这跟源码我们知道得vue不符合,也多了一层,我们希望直接可以用vm.msg = '更改值';

source\vue\index.js

import {Observe} from './observe'

export function initState(vm){
    let options = vm.$options;
    if (options.data) {
        initData(vm);//初始化数据
    }

    if (options.computed) {
        initComputed(); //初始化计算属性
    }

    if (options.watch) {
        initWatch(); //初始化watch
    }
}
// 观察
export function observe(data){
    if(typeof data !== 'object' || data == null){
        return;
    }

    return new Observe(data);
}
function proxy(vm,source,key){
    Object.defineProperty(vm,key,{
        get(){
            return vm[source][key];
        },
        set(newVal){
            vm[source][key] = newVal;
        }
    })
}
// 初始化数据  将数据重新定义 核心:Object.defineProperty
function initData(vm){
    let data = vm.$options.data;
    data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
    // 将在vm._data上得操作代理到vm上
    for(let key in data){
        proxy(vm,'_data',key);
    }
    observe(vm._data);
}

2.2.3 数组劫持

如果想找源码可以去vue\src\core\instance\observer\array.js

我们打印console.log(vm.arr.push(12)) 只有获取数据,并没有走设置数据,上述代码我们还需要增加数组的监控,且需要对数组的一些方法重写

1、什么样得数组会被监控observe

我们查看源码:vue\src\core\observer\array.js

为什么是这几个方法呢?数组的方法有很多呀?这些方法会改变原数组。这些使用了切片编程的思想,将原理的方法重写,还继续增加我们自己的一些逻辑。我们继续往下看源码:

我们需要对新增的数据继续观察,所以增加了上图的代码。因为增加的可能是对象等

2、实现

创建我们自己的array.js目录:source\vue\observe\array.js

import { observe } from "./index";

/**  拦截用户调用得push、shift、unshift、pop reverse sort splice
 * 因为这些方法会改变原来得数组 这些需要重写
 */

//  先获取老得数组得方法 目前只改写这7个方法
let oldArrProtoMethods = Array.prototype;

// 拷贝一个新得对象 可以查找到老得方法
export let arrayMethods = Object.create(oldArrProtoMethods);

let methods = [
    'push',
    'shift',
    'unshift',
    'pop',
    'reverse',
    'sort',
    'splice'
];

// 需要循环对新增的每一项数据进行观察
export function observeArray(inserted) {
    for(let i=0;i<inserted.length;i++){
        observe(inserted[i]);
    }
}
methods.forEach(m=>{
    // 函数劫持 切片编程
    arrayMethods[m] = function(...arg){
       const r = oldArrProtoMethods[m].apply(this,arg);
    //    我们需要对新增的数据进行监控
       let inserted;
       switch (m) {
           case 'push':
           case 'unshift':
               inserted = arg;
               break;
           case 'splice':
                inserted = args.slice(2)
               break;
       }
       if(inserted) observeArray(inserted);
       return r;
    }
})

增加数组监控

source\vue\observe\observe.js

import {observe} from './index';
import {observeArray, arrayMethods} from './array';
// 定义响应式数据变化
export function defineReactive(data,key,val){
    // 递归 判断val是不是对象{name:'lee',age:18}
    observe(val);
    Object.defineProperty(data,key,{
        get(){
            return val;
        },
        set(newVal){
            if (val === newVal) return;
            val = newVal;
        }
    });
}

class Observe{
    constructor(data) { // 此时得data就是定义得vm._data

        //将用户得数据使用Object.defineProperty重新定义
        if(Array.isArray(data)){
            // 我们需要通过原型链 调用我们重写的相关的数组的方法
            data.__proto__ = arrayMethods;
            // 监控数组中的每一项 因为有可能数组中里边也是对象
            observeArray(data);
        }else{
            this.walk(data);
        }
        
    }
    walk(data){       
        for (let [key, val] of Object.entries(data)) {
            defineReactive(data, key, val);
        }
    }
    
}
export default Observe

ps:需要注意:

  1. 无法监听数据的 length,导致 arr.length 这样的数据改变无法被监听
  2. 通过角标更改数据,即类似 arr[2] = 1 这样的赋值操作,也无法被监听

同时 Vue.js 提供了两个额外的“方法糖” $set$remove 来弥补这方面限制带来的不便。整体上看这是个取舍有度的设计。

2.3 初步渲染到DOM

流程:

new Vue -》initMixin(Vue) -》_init(options) -》$mount()->编译,转换成render -》mountComponent()-》 updateComponent()-》渲染Wather

执行完所有的初始化操作后,我们需要开始挂载vue实例到dom上了。

ps:注意:

vue 是分为运行时可编译和只运行的版本,所以如果需要编译,在Vue原型上添加了$mount方法。Vue不同构建版本可以看Vue对不同构建版本的解释

2.3.1、$mount挂载

可查看源码的目录:

vue\src\platforms\web\entry-runtime-with-compiler.js(运行时+编译器)

vue\src\platforms\web\runtime/index.js(运行时版本)

$mount方法就是整个渲染过程的起始点。具体定义是在vue\src\entries\web-runtime-with-compiler.js

(图8)

如图8,在渲染过程中,我们可以通过自定义render函数、template、el三种方式渲染页面。

案例:

el的写法:

var vm = new Vue({
    el: '#app',
	  data: {
        message: 'Hello!'
    }
})

template的写法:

var vm = new Vue({
	  data: {
        message: 'Hello!'
    },
    template:'<div>{{message}}</div>'
})

render函数的写法:

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

这三种方式都是要得到Render函数.

代码:

ps:先初步实现渲染el的方式,考虑从简到易,先不去判断render方法,因为之后的虚拟dom阶段会在重写;这边直接用渲染new watcher函数里边,第二个参数传入updateComponent更新函数(其中方法的vm._update()暂时用的vue1.0方法),会直接先执行。

source\vue\index.js

import {initState} from './observe'
import Watcher from './observe/watcher';
import { complier } from './compiler';
function Vue(options){
    this._init(options); //初始化vue 并且将用户选项传入
}
Vue.prototype._init = function (options) {
    // vue中初始化 $options 表示得是vue中得参数
    // this指向Vue的实例,所以这里是将Vue的实例缓存给vm变量
    let vm = this;
    vm.$options = options;

    //MVVM原理 响应式数据 需要数据重新初始化 
    // 我们将不同得初始化放入到不同得文件夹中操作,方便维护
    // 初始化state,包括props\methods\data\compunted\watch
    // props\data属性将其设置为vm的响应式属性即需要Object.defineProperty
    // 绑定vm的props和打他属性并设置其getter和setter
    initState(vm);

    // 将实例挂载到dom上
    if (vm.$options.el) {
        vm.$mount(vm.$options.el)
    }
}

//获取dom
function query(el){
    if(typeof el === 'string'){
        return document.querySelector(el);
    }
    return el;
}
const mount = Vue.prototype.$mount;
// 渲染页面将组件进行挂载
Vue.prototype.$mount = function(el){

    let vm = this;
    // 获取dom
    el = vm.$el = el && query(el);

    // 渲染时通过 watcher来渲染 
    /** 
     * 渲染watcher
     * vue2.0 组件级别更新 new Vue产生一个组件
     * 
     */
    // 更新组件、渲染逻辑 
     let updateComponent = ()=>{
        //  更新组件
        vm._update();
     }
    //  渲染watcher
     new Watcher(vm,updateComponent);

}
// 目前用vue1.0 没有使用组件
// 用户传入的数据 去更新视图
Vue.prototype._update = function(){
    let vm = this;
    let el = vm.$el;

    // -------------现在vue1.0之后会用vdom重写----
    // 要循环这个元素 将里边的内容 换成我们的数据
    let node = document.createDocumentFragment();
    let firstChild;
    while(firstChild = el.firstChild){
        node.appendChild(firstChild); //appendChild 具有移动的功能
        
    }
   //编译 一些指令{{}} v-html v-model
    complier(node,vm);
    el.appendChild(node);
    // 需要匹配{{}}的方式来进行替换
    // 我需要让每个数据 它更改了 重新渲染

}
export default Vue

2.3.2、初步渲染watcher

ps:2.4会在继续讲解watcher

import { isFun } from "../utils";
let uid = 0; // 每一个new watcher的唯一标识
class Watcher{
    /**
     *Creates an instance of Watcher.
     * @param {*} vm 当前组件的实例 new VUe
     * @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
     * @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
     * @param {*} ops 一些其他参数
     * @memberof Watcher
     */
    constructor(vm,exprOrFn,cb=()=>{},ops={}) {
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        if(isFun(exprOrFn)){
            //getter 就是new Watcher传入的第二个函数
            this.getter = exprOrFn;
        }
        this.cb = cb;
        this.ops = ops; 
        this.uid = uid++; 
        // 默认创建一个watcher 会调用自身的get方法
        this.get();
    }
    get(){        
        // 让这个当前传入的函数执行 默认就会执行
        this.getter();
    }
}
// 渲染使用 计算属性 也用vm.watch 
export default Watcher;

ps:

发现vue中会有大量的这种设置一个全局变量let uid = 0;在类的构造器中在this.uid = uid++;这种写法,表示每次new Watcher都会有一个每一标识;

watcher方法在渲染时、 计算属性、 vm.watch 都会被调用

2.3.3、complier

第一步:

source\vue\complier\complier.js

import {isElement, isText} from '../utils'
import { util} from './diretive'

export function complier(node,vm){
    let childNodes = node.childNodes;
    [...childNodes].forEach(c=>{
        if(isElement(c)){ // 元素节点
            complier(c,vm);
        }else if(isText(c)){ //文本节点
            return util.compilerText(c,vm);
        }
    })
}

第二步:指令 {{}}编译

source\vue\complier\diretive\diretives.js

// 编译指令 比如{{}}  v-mode v-text v-html

// 匹配 {{}}
const defaultReg = /\{\{((?:.|\r?\n)+?)\}\}/g;
export const util = {
    getValue(vm, expr) {
        let keys = expr.split('.');
        return keys.reduce((m, cur) => {
            m = m[cur];
            return m;
        }, vm)
    },
    // 编译文本 {{msg}} {{obj.name}}
    compilerText(node, vm) {
        if(!node.expr){
            // 给节点增加一个属性 为了后续更新操作
            node.expr = node.textContent;
        }
        node.textContent = node.expr.replace(defaultReg, function (...args) {
            return util.getValue(vm, args[1]);
        })
    }
}

第三步:导出

source\vue\complier\diretive\index.js 表示diretive文件夹的出口

export * from './diretives'

source\vue\complier\index.js 表示complier文件夹的出口

export * from './compiler';

2.3.4、测试:

在public\index.html中加入

<div id="app">
        {{msg}}
</div>

打开页面发现如下图,表示成功渲染

(图9)

ps:Vue.prototype._update方法:先从vue1.0版本实现,后边讲解虚拟dom时,重新实现渲染代码。

2.4、dep

实现数据收集(当前响应式对象的依赖关系),记录哪些watcher依赖自己的变化,使用了发布订阅设计模式;

源码所在目录:vue\src\core\instance\dep.js

1、对象收集

实现步骤:

  • 1、 全局定义一个uid,在Dep类中this.uid = uid++;,表示每次创建一个Dep实例时都有不同的uid,为了方便去重;
  • 2、在Dep类中创建数组subs=[],用来存放Watcher的实例;
  • 3、创建addSub方法,添加Watcher的实例;
  • 4、创建notify方法,通知所有的Wather执行更新。遍历subs数组,调用每个Wather的更新(update)方法
  • 5、创建depend方法,调用存在Dep.target上的addDep方法,而Dep.target时一个watcher实例,所以addDep方法是Watcher类的方法,作用是
  • 6、创建pushTarget方法,是为了将watcher观察者实例设置给Dep.target,用以依赖收集,同时将该实例存入stack栈中;
  • 7、创建popTarget方法,作用:将观察者实例从stack栈中取出并设置给Dep.target

代码

source\vue\observe\dep.js

import {remove} from '../utils'
let uid = 0;
// 收集数据 收集的时一个个watcher
class Dep{
    constructor() {
        // Dep实例的uid,为了方便去重
        this.uid = uid++;
        // 存储收集器中需要通知的Watcher
        this.subs = [];
        
    }
    // 添加订阅 搜集watcher Watcher 的实例,将来用来通知更新
    addSub(watcher){
        this.subs.push(watcher);
    }
    removeSub(sub) {
        remove(this.subs, sub)
    }
    // 通知watcher执行update方法更新
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
    depend(){
        // 防止直接调用depend方法 
        if(Dep.target){
            // Dep.target 是一个渲染的watcher
            // 希望与watcher互相记忆
            Dep.target.addDep(this);
        }
    }
}
let stack = [];//watcher 栈
// 用来保存当前的watcher
/**
 * 将watcher观察者实例设置给Dep.target, 用以依赖收集。
 *  同时将该实例存入stack栈中
 * * @param {*} watcher
 */
export function pushTarget(watcher){
    Dep.target = watcher;
    stack.push(watcher);
}
/** 
 将观察者实例从stack栈中取出并设置给Dep.target
 */
export function popTarget(){
    stack.pop();
    Dep.target = stack[stack.length-1];
}
export default Dep;

2、数组的依赖收集

步骤:

1、在Observe类的中创建一个Dep实例,这个实例dep专门用来收集数组的依赖关系;

2、我们对数据data每一项增加一个__ob__属性,返回值为当前Observe的实例

3、在defineReactive方法中,我们需要先观察每一个data的属性值,这个观察返回的就是一个Observe的实例,childOb;

4、我们在defineProperty的get中增加数组的收集方法,如果childOb存在,就增加收集方法,因为dep绑在了Observe的实例上;注意要对数组中的每一个属性收集,因为有可能是arr = [[1],2,3],这种二维数组

5、在调用数组方法的时候,通知watcher更新方法

6、在观察者中增加判断,如果data.__ob__存在就直接返回data.__ob__这里边已经保存了Observe实例,

代码:

source\vue\observe\observe.js

import {observe} from './index';
import {observeArray, arrayMethods,dependArray} from './array';
import Dep from './dep';
// 定义响应式数据变化
export function defineReactive(data,key,val){
    // 递归 判断val是不是对象{name:'lee',age:18} [1,2]
    let childOb = observe(val);
    // 收集依赖 收集的是watcher
    let dep = new Dep();
    Object.defineProperty(data,key,{
        get(){ // 只要对这个属性 精选取值操作,就会将当前的watcher存入
            // 如果有值 就是渲染的watcher
            if(Dep.target){
                /** 我们希望存入的watcher 不能重复,如果重复会造成更新多次
                 *dep.depend() 让中可以存watcher watcher中存dep
                 *  dep 实现多对多的关系
                 */
                dep.depend();
                // childOb 针对数组的收集
                if(childOb){
                    // 收集数组的依赖
                    childOb.dep.depend();
                    // [[1,2],3,4] 需要对数组中的每一项收集,比如二维数组
                    dependArray(val);
                }
            }
            return val;
        },
        set(newVal){
            if (val === newVal) return;
            // 如果设置的是一个对象的话,需要继续观察
            observe(newVal);
            val = newVal;
            // 通知订阅器找到对应的观察者,通知观察者更新视图
            dep.notify();
        }
    });
}

export class Observe{
    constructor(data) { // 此时得data就是定义得vm._data
        // this.dep 专门来收集数组的
        this.dep = new Dep();
        // 给data添加一个属性__ob__,每个数组和对象都有__obj__属性,返回的是当前Observe实例
        Object.defineProperty(data,'__ob__',{
            get:()=>this
        });
        //将用户得数据使用Object.defineProperty重新定义
        if(Array.isArray(data)){
            // 我们需要通过原型链 调用我们重写的相关的数组的方法
            data.__proto__ = arrayMethods;
            // 监控数组中的每一项 因为有可能数组中里边也是对象
            observeArray(data);
        }else{
            this.walk(data);
        }
        
    }
    walk(data){       
        for (let [key, val] of Object.entries(data)) {
            // 对每一个属性 重新用defineProperty定义
            defineReactive(data, key, val);
        }
    }
    
}

export default Observe;

source\vue\observe\array.js

import { observe } from "./index";
import { isArray } from "../utils";

//  先获取老得数组得方法 目前只改写这7个方法
let oldArrProtoMethods = Array.prototype;

// 拷贝一个新得对象 可以查找到老得方法
export let arrayMethods = Object.create(oldArrProtoMethods);

let methods = [
    'push',
    'shift',
    'unshift',
    'pop',
    'reverse',
    'sort',
    'splice'
];

// 需要循环对新增的每一项数据进行观察
export function observeArray(inserted) {
    for(let i=0;i<inserted.length;i++){
        observe(inserted[i]);
    }
}
// 递归收集数组中的每一项数据
export function dependArray(val){
    for(let i=0;i<val.length;i++){
        let cur = val[i];
        cur.__ob__ && cur.__ob__.dep.depend();
        if(isArray(cur)){
            dependArray(cur);
        }
    }
}
methods.forEach(m=>{
    // 函数劫持 切片编程
    arrayMethods[m] = function(...arg){
       const r = oldArrProtoMethods[m].apply(this,arg);
    //    我们需要对新增的数据进行监控
       let inserted;
       switch (m) {
           case 'push':
           case 'unshift':
               inserted = arg;
               break;
           case 'splice':
                inserted = args.slice(2)
               break;
       }
       if(inserted) observeArray(inserted);
       if(this.__ob__) this.__ob__.dep.notify();
       return r;
    }
})

source\vue\observe\index.js中observe方法

export function observe(data){
    if(typeof data !== 'object' || data == null){
        return;
    }
    // 已经被监控过了
    if(data.__ob__){
        return data.__ob__;
    }
    return new Observe(data);
}

思考问题:(优秀代码分析)

为什么会有pushTarget方法?

pushTarget:主要是为了watcher观察者实例设置给Dep.target, 用以依赖收集。同时将该实例存入stack栈中;而Dep.target在每次我们使用observe观察每个属性时,需要收集watcher,这时我们就用Dep.target来判断如果存在就表示这个属性所对的watcher已经被收集过了,不要在重复收集,也就避免了我们在更新的时候多次调用;

2.5、watcher

watcher:监听变化,是一个render函数的观察者,可用于更新视图;实例分为渲染watcher、计算属性 watcher、vm.$watch()调用 watcher ;

源码所在目录:vue\src\core\instance\watcher.js

什么时候会实例化Watcher呢?

  • 1、渲染watcher,Vue生成了render函数,更新视图时
  • 2、计算属性computed时
  • 3、使用vm.$watch()方法时
  • 4、watch选项

渲染watcher步骤:

  • 1、全局声明uid,初始为0,每次实例一个Watcher时,uid++;是唯一标识
  • 2、创建Watcher类,参数目前为:
  • 第一个参数:当前实例;第二个参数:传入的需要更新的表达式或者函数;第三个参数:用户传入的回调函数,比如vm.$watch('msg',cb)中的cb;第四个参数:一写其他的参数;
  • 3、判断第二个参数是函数时,getter函数执行;
  • 4、创建deps=[],收集dep,
  • 5、depsUid是一个set集合用于收集不能重复的dep的uid;
  • 6、创建update方法,调用get方法;调用时机是:在Observe观察者观察到数据更新了或者说是重新设置值了调用Dep的notify来通知Watcher的update方法;
  • 7、创建get方法;作用是执行调用Watcher的第二个参数是函数的方法,(即更新组件的_update方法);这个函数执行前我们需要将这个需要更新的Watcher搜集到Dep的栈stack中;函数执行;函数执行后,在将这个watcher从这个Dep的stack栈中去除;
  • 8、创建addDep,调用时机:在Observe观察数据读取数据时,我们将这个watcher添加收集到Dep中,为了不重复收集我们需要将watcher和dep相互绑定;

渲染watcher代码:

source\vue\observe\watcher.js

import { isFun } from "../utils";
import {pushTarget,popTarget} from './dep'
let uid = 0; // 每一个new watcher的唯一标识
class Watcher{
    /**
     *Creates an instance of Watcher.
     * @param {*} vm 当前组件的实例 new VUe
     * @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
     * @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
     * @param {*} ops 一些其他参数
     * @memberof Watcher
     */
    constructor(vm,exprOrFn,cb=()=>{},ops={}) {
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        if(isFun(exprOrFn)){
            //getter 就是new Watcher传入的第二个函数
            this.getter = exprOrFn;
        }
        this.cb = cb;
        this.ops = ops; 
        this.uid = uid++;   
        this.deps = [];
        this.depsUid = new Set();
        
        // 默认创建一个watcher 会调用自身的get方法
        this.get();
    }
    get(){
        // 
        /**收集watcher 作用是 {{msg}} 变化了 这个watcher重新执行
         *  Dep.target = watcher; 
         */
        pushTarget(this);
        // 让这个当前传入的函数执行 默认就会执行
        this.getter();
        popTarget();
    }
    update(){
        this.get();
    }
    addDep(dep){ //同一个watcher 不应该重复记录dep
        let uid = dep.uid; //msg的dep
        if(!this.depsUid.has(uid)){
            this.depsUid.add(uid);
            // 让watcher记住当前的dep
            this.deps.push(dep);
            dep.addSub(this);
        }
    }

}
// 渲染使用 计算属性 也用vm.watch 
export default Watcher;

2.6、为每个数据属性添加收集依赖

步骤:

  • 1、创建Dep实例;
  • 2、在每次读取数据时,收集依赖关系
  • 3、在每次设置数据时,调用dep.notify通知watcher的update执行

代码:

source\vue\observe\observe.js

import {observe} from './index';
import {observeArray, arrayMethods} from './array';
import Dep from './dep';
// 定义响应式数据变化
export function defineReactive(data,key,val){
    // 递归 判断val是不是对象{name:'lee',age:18}
    observe(val);
    // 收集依赖 收集的是watcher
    let dep = new Dep();
    Object.defineProperty(data,key,{
        get(){ // 只要对这个属性 精选取值操作,就会将当前的watcher存入
            // 如果有值 就是渲染的watcher
            if(Dep.target){
                /** 我们希望存入的watcher 不能重复,如果重复会造成更新多次
                 *dep.depend() 让中可以存watcher watcher中存dep
                 *  dep 实现多对多的关系
                 */
                dep.depend();
            }
            return val;
        },
        set(newVal){
            if (val === newVal) return;           
            // 如果设置的是一个对象的话,需要继续观察
            observe(newVal);
            val = newVal;
            // 通知订阅器找到对应的观察者,通知观察者更新视图
            dep.notify();
        }
    });
}
export class Observe{
    constructor(data) { // 此时得data就是定义得vm._data
        //将用户得数据使用Object.defineProperty重新定义
        if(Array.isArray(data)){
            // 我们需要通过原型链 调用我们重写的相关的数组的方法
            data.__proto__ = arrayMethods;
            // 监控数组中的每一项 因为有可能数组中里边也是对象
            observeArray(data);
        }else{
            this.walk(data);
        }        
    }
    walk(data){       
        for (let [key, val] of Object.entries(data)) {
            // 对每一个属性 重新用defineProperty定义
            defineReactive(data, key, val);
        }
    }    
}
export default Observe;

代码思考:

为什么我们在defineReactive调用Object.defineProperty中的get时,收集watcher用了dep.depend()没有使用dep.addSubs(Dep.target)?优化了什么问题?

我们在页面中会有多次调用同一个数据的情况比如

<div id="app">{{msg}}{{msg}}{{msg}}</div>

上述的html中我们调用了三次的数据msg,那么会调用Object.defineProperty中的get三次,如果使用dep.addSubs(Dep.target)将会增加重复的watcher,Dep.target就是一个watcher的实例;dep.depend()方法与watcher互相记忆,它会调用watcher的addDep方法,这个方法中有收集dep的唯一标识uid,如果uid已经存在就不会在添加,所以不会重复收集watcher,在notify通知watcher的update更新时就会重复多次执行相同的watcher方法;

3、异步批量更新

vue的特点就是批量更新 防止重复渲染

问题:

如果我们使用异步的方法,多次设置同一个值,根据目前已经写好的代码执行,我们的set方法会被多次执行那会多次通知notify订阅者watcher去调用update方法;update方法调用watcher中的get方法,会导致页面多次刷新;

测试:

import Vue from "vue";
let vm = new Vue({
    el:'#app', //表示要渲染得元素是app
    data(){
        return {
            msg:'hello',
            obj:{name:'lee',age:18},
            arr:[1,2,3]
        }
    },
    computed: {
        
    },
    watch: {
        
    }
});

setTimeout(() => {
    vm.msg = '第一次更新msg';
    vm.msg = '第二次更新msg';
    vm.msg = '第三次更新msg';
    vm.msg = '第四次更新msg';
    vm.msg = '第五次更新msg';
}, 2000);

解决问题的步骤:

1、创建has={}存放watcher每个实例的uid,queue存放每个watcher实例

2、创建queueWatcher方法,我们在通知更新watcher的update方法时,先判断当前的watcher的uid是不是在缓存uid中有、没有在缓存,在调用一个异步方法,异步方法的作用是这时的这个watcher是最后一次设置的值;

3、nextTick方法,实现异步方法,我们这里想尽快的执行异步方法,根据事件环原理我们异步方法也有先后顺序,先微任务、在宏任务,在考虑性能问题;

4、flushQueue方法:在执行当前watcher的get方法,我们这里新建了一个方法用run方法调用get方法,

代码:

source\vue\observe\watcher.js

import { isFun,isUndef,nextTick } from "../utils";
import {pushTarget,popTarget} from './dep'
let uid = 0; // 每一个new watcher的唯一标识
class Watcher{
 
    constructor(vm,exprOrFn,cb=()=>{},ops={}) {
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        if(isFun(exprOrFn)){
            //getter 就是new Watcher传入的第二个函数
            this.getter = exprOrFn;
        }
        this.cb = cb;
        this.ops = ops; 
        this.uid = uid++;   
        this.deps = [];
        this.depsUid = new Set();

        // 默认创建一个watcher 会调用自身的get方法
        this.get();
    }
    get(){
        // 
        /**收集watcher 作用是 {{msg}} 变化了 这个watcher重新执行
         *  Dep.target = watcher; 
         */
        pushTarget(this);
        // 让这个当前传入的函数执行 默认就会执行
        this.getter();
        popTarget();
    }
    update(){
        queueWatcher(this);
    }
    run(){
        this.get();
    }
    addDep(dep){ //同一个watcher 不应该重复记录dep
        let uid = dep.uid; //msg的dep
        if(!this.depsUid.has(uid)){
            this.depsUid.add(uid);
            // 让watcher记住当前的dep
            this.deps.push(dep);
            dep.addSub(this);
        }
    }

}

let has = {};
let queue = [];
// 
function flushQueue(){
    // 等待当前这一轮全部更新后 在去让watcher一次执行
    queue.forEach(watcher => watcher.run());
    has = {};
    queue = [];
}
// 对重复的watcher进行过滤操作
function queueWatcher(watcher){
   let uid = watcher.uid;
   if(isUndef(has[uid])){
       has[uid] = uid;
    //  相同的watcher只会存一个  
       queue.push(watcher);
    //  延迟清空队列
    nextTick(flushQueue);
   }
}
// 渲染使用 计算属性 也用vm.watch 
export default Watcher;

source\vue\utils\nextTick.js

let callbacks = [];
function flushCallbacks(){
    callbacks.forEach(cb=>cb());
}
/**
 *异步刷新这个callbacks,获取一个异步的方法
 异步是分支执行顺序的
 会先执行微任务(promise\mutationObserver\)
 在执行宏任务(setImmediate\setTimeout)
 * @export
 * @param {*} cb
 */
export function nextTick(cb){
    callbacks.push(cb);

    let timerFnCb = ()=>{
        flushCallbacks();
    }
    
    if(Promise) return Promise.resolve().then(timerFnCb);
    if(MutationObserver){
        let observe = new MutationObserver(timerFnCb);
        let textNode = document.createTextNode(1);
        observe.observe(textNode,{characterData:true});
        textNode.textContent(2);
        return;
    }
    if(setImmediate){
        return setImmediate(timerFnCb);
    }
    setTimeout(timerFnCb, 0);
}

我们再次时候用测试代码查看只执行了一次更新;

事件环:不了解的话可以查看文档: juejin.cn/post/684490…

MutationObserver: developer.mozilla.org/zh-CN/docs/…

4、watch实现

之前我们在小节2.5中实现watcher说过有三种方式会实例化Watcher,而这说的用户自己调用watch方法就是一种。

应用案例

src\index.js

import Vue from "vue";

let vm = new Vue({
    el:'#app', //表示要渲染得元素是app
    data(){
        return {
            msg:'hello',
            obj:{name:'lee',age:18},
            arr:[[1],2,3]
        }
    },
    watch: {
        msg(newVal,oldVal){
            console.log(newVal,oldVal);
        }
    }
});
vm.msg = "msg改变喽"

步骤

1、初始化watch

2、给watch中每一个属性的方法去调用Vue实例上的$watch()方法;

3、在Vue原型上实现$watch方法,原理:创建一个watcher,注意传入参数{user:true}标识是用户自己写的一个watcher,跟其他的实例化的watcher区分

4、在watcher方法中实现watch属性的方法handler,重点区分新老值,

代码:

source\vue\observe\index.js

function createWatcher(vm,key,handler){
    return vm.$watch(key,handler);
}

function initWatch(vm) {
    // 用户传入的watch属性
    let watch = vm.$options.watch;
    for (let [key, val] of Object.entries(watch)) {
        createWatcher(vm, key, val);
    }

}

source\vue\index.js增加下面方法$watch

Vue.prototype.$watch = function(expr,handler){
    let vm = this;
    // 原理 创建一个watcher {user:true}标识是用户自己写的一个watcher
    new Watcher(vm,expr,handler,{user:true});
}

source\vue\observe\watcher.js

import { isFun,isUndef,nextTick } from "../utils";
import {pushTarget,popTarget} from './dep'
import {util} from '../compiler/diretive'
let uid = 0; // 每一个new watcher的唯一标识
class Watcher{
    constructor(vm,exprOrFn,cb=()=>{},ops={}) {
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        if(isFun(exprOrFn)){
            //getter 就是new Watcher传入的第二个函数
            this.getter = exprOrFn;
        }else{
            this.getter = function(){
                return util.getValue(vm,exprOrFn);
            }
        }
        // 标识是用户自己写的watcher
        if (ops.user) {
          this.user = true;  
        }
        this.cb = cb;
        this.ops = ops; 
        this.uid = uid++;   
        this.deps = [];
        
        this.depsUid = new Set();

        // 默认创建一个watcher 会调用自身的get方法
        // 创建watcher时,先将表达式对应的值取出来
        this.value = this.get();
    }
    get(){
        // 
        /**收集watcher 作用是 {{msg}} 变化了 这个watcher重新执行
         *  Dep.target = watcher; 
         */
        pushTarget(this);
        // 让这个当前传入的函数执行 默认就会执行
        let value = this.getter();
        popTarget();
        return value;
    }
    update(){
        queueWatcher(this);
    }
    run(){
        let value = this.get();
        // this.value是老值,value是新值
        if(this.value !== value){
            this.cb(value,this.value);
        }
    }
    addDep(dep){ //同一个watcher 不应该重复记录dep
        let uid = dep.uid; //msg的dep
        if(!this.depsUid.has(uid)){
            this.depsUid.add(uid);
            // 让watcher记住当前的dep
            this.deps.push(dep);
            dep.addSub(this);
        }
    }

}

let has = {};
let queue = [];
// 
function flushQueue(){
    // 等待当前这一轮全部更新后 在去让watcher一次执行
    queue.forEach(watcher => watcher.run());
    has = {};
    queue = [];
}
// 对重复的watcher进行过滤操作
function queueWatcher(watcher){
   let uid = watcher.uid;
   if(isUndef(has[uid])){
       has[uid] = uid;
    //  相同的watcher只会存一个  
       queue.push(watcher);

    //  延迟清空队列
    nextTick(flushQueue);
   }
}
// 渲染使用 计算属性 也用vm.watch 
export default Watcher;

5、计算属性

案例:

import Vue from "vue";
let vm = new Vue({
    el:'#app', //表示要渲染得元素是app
    data(){
        return {
            msg:'hello',
            obj:{name:'lee',age:18},
            arr:[[1],2,3],
            firstName:'lee',
            lastName:'Hello'
        }
    }
    computed: {
        fullName(){
            return this.firstName+this.lastName;
        }
    }
});
vm.firstName = '周'
 <div id="app">
        {{fullName}}
    </div>

步骤、流程:

1、初始化计算属性initComputed

2、核心是为每个computed属性实例化一个watcher,需注意在实例watcher的过程中,需要传入参数{lazy:true}的配置项,表示此watcher实例是计算属性;并将这些实例化的watcher缓存到Vue的实例上vm;

3、在watcher类中,判断如果是lazy为true,表示是计算属性,暂时不执行函数(不调用get方法),即不收集依赖关系,定义dirty为当前传的lazy值,dirty为true时,我们就不收集依赖;

4、当用户在页面读取这个计算属性时,我们才调用这个计算属性的getter函数,所以我们之前初始化计算属性时,还需要给计算属性每个属性在读取时的创建一个函数:createComputedGetter,其返回值也是一个函数;

5、当前的这个页面读取的计算属性的watcher的dirty如果为true,表示我们需要调用watcher的get方法;

注意:此时watcher上定义的deps数组中有两个watcher,一个渲染watcher、一个计算属性的watcher,Dep.target指向计算属性watcher,执行getter方法即当前计算属性的方法,移除计算属性watcher,Dep.target指向渲染watcher,dirty为false,表示计算属性已经读取;

6、但是往往计算属性的值返回的数据也是需要读取的属性,例如:

fullName(){return this.firstName+this.lastName};

我们还要读取this.fristNamethis.lastName这两个;

调用这两个数据的获取属性,此时Dep.target存在,则会继续收集依赖,将这两个数据的watcher依赖放入到Watcher类的deps数组中,并添加到dep类的数组中subs,因为在data中设置了这两个数据的值,所以会调用这两个的数据的set方法通知这两个属性的收集notify方法执行就调用这两个属性的watcher的update方法执行,

7、计算属性改变了,重新设置了值,如下:

fullName(){return this.firstName+this.lastName}
vm.firstName="改变了值";

因为是设置了fristName的值,我们调用set方法,调用dep的notify方法,调用watcher的update方法,此时deps中有两个watcher,一个计算属性的watcher,value可以看到是上次的值leeHello,一个是修改的fristName的watcher,计算属性的watcher让dirty改为true,fristName的watcher调用update,Dep.target指向渲染watcher,

此时我们的计算属性的定义的get方法上createComputedGetter函数,还需要增加判断,如果Dep.target还存在说明watcher的deps数组中还有watcher我们需要执行(此时是firstName的渲染Watcher),所以我们需要在watcher中创建depend方法来判断deps数组中每一个dep都去调用即调用dep的depend方法,当然调用后就需要在这个deps数组去掉避免多次调用收集;

8、nextTick执行

代码:

source\vue\observe\index.js

import {Observe} from './observe'
import Watcher from './watcher';
import Dep from './dep'
export function initState(vm){
    let options = vm.$options;
    if (options.data)  initData(vm);//初始化数据
    if (options.watch) initWatch(vm); //初始化watch
    if (options.computed) initComputed(vm,options.computed); //初始化计算属性
}
//.....省略
function createComputedGetter(vm,key) {
    let watcher = vm._watchersComputed[key];
    return function(){ //用户取值会执行此方法
        if(watcher){
            // 如果页面取值,dirty是true,就会去调用watcher的get方法
        //    dirty是false 不需要重新执行计算属性的方法
            if(watcher.dirty){
                // 计算
                watcher.evaluate();
            }
            if(Dep.target){
                watcher.depend();
            }
            return watcher.value;
        }
    }
    
}
// 计算属性 默认不执行 等用户取值时在执行,会缓存取值结果
function initComputed(vm,computed){
    // 将计算属性的配置 放到vm上
    let watchers = vm._watchersComputed = Object.create(null); //创建储存watcher的对象
    // computed 是{fullName:()=>this.firstName+this.lastName}
    for (let key in computed) {
        const userDef = computed[key];
        //如果computed的属性值为函数,取这个函数为watcher的第二个参数 即getter
        // 如果不是function,则为这个属性定义一个
        const getter = typeof userDef === 'function' ? userDef : userDef.get;
        // lazy:true 表示刚调用的watcher先不执行此方法 实例化的过程中不去完成收集依赖
        watchers[key] = new Watcher(vm, getter, () => {}, {
            lazy: true
        });
        if(!(key in vm)){
            Object.defineProperty(vm, key, {
                get: createComputedGetter(vm, key)
            })
        }
        
    }
}

source\vue\observe\watcher.js

import { isFun,isUndef,nextTick } from "../utils";
import {pushTarget,popTarget} from './dep'
import {util} from '../compiler/diretive'
let uid = 0; // 每一个new watcher的唯一标识
class Watcher{
    constructor(vm,exprOrFn,cb=()=>{},ops={}) {
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        if(isFun(exprOrFn)){
            //getter 就是new Watcher传入的第二个函数
            this.getter = exprOrFn;
        }else{
            this.getter = function(){
                return util.getValue(vm,exprOrFn);
            }
        }
        // 标识是用户自己写的watcher
        if (ops.user) {
          this.user = true;  
        }
        this.lazy = ops.lazy;//如果lazy为true 说明是计算属性
        this.dirty = ops.lazy;
        this.cb = cb;
        this.ops = ops; 
        this.uid = uid++;   
        this.deps = [];
        this.depsUid = new Set();
        this.immediate = ops.immediate;
        // 默认创建一个watcher 会调用自身的get方法
        // 创建watcher时,先将表达式对应的值取出来
        // 如果当前我们是计算属性的话 不会立刻执行 在用户调用时才调用
        this.value = this.lazy ? undefined : this.get();
        if(this.immediate){
            this.cb(this.value);
        }
    }
    get(){  
        pushTarget(this);
        // 让这个当前传入的函数执行 默认就会执行
        let value = this.getter.call(this.vm);
        popTarget();
        return value;
    }
    evaluate(){
        this.value = this.get();
        this.dirty = false;
    }
    depend(){
        let i = this.deps.length;
        while (i--) {
            this.deps[i].depend();
        }
    }
    update(){
        // 计算属性
        if(this.lazy){
            this.dirty = true;
        }else{
            queueWatcher(this);
        }
        
    }
    run(){
        let value = this.get();
        // this.value是老值,value是新值
        if(this.value !== value){
            this.cb(value,this.value);
        }
    }
    addDep(dep){ //同一个watcher 不应该重复记录dep
        let uid = dep.uid; //msg的dep
        if(!this.depsUid.has(uid)){
            this.depsUid.add(uid);
            // 让watcher记住当前的dep
            this.deps.push(dep);
            dep.addSub(this);
        }
    }

}

let has = {};
let queue = [];
// 
function flushQueue(){
    // 等待当前这一轮全部更新后 在去让watcher一次执行
    queue.forEach(watcher => watcher.run());
    has = {};
    queue = [];
}
// 对重复的watcher进行过滤操作
function queueWatcher(watcher){
   let uid = watcher.uid;
   if(isUndef(has[uid])){
       has[uid] = uid;
    //  相同的watcher只会存一个  
       queue.push(watcher);

    //  延迟清空队列
    nextTick(flushQueue);
   }
}
// 渲染使用 计算属性 也用vm.watch 
export default Watcher;

6、虚拟dom和dom-diff

关于虚拟dom和dom-diff算法的概念和详细实现流程可查看文章: juejin.cn/post/684490… 有vue和react版本都有,如果只想看vue的原理实现可以查看小节2.1和4.4;

在本文章的小节2,我们讲过当时用的渲染是用的vue1.0操作的dom,虽然用了优化都放入到文档碎片,编译指令,最后在放入dom中,但是更改时,还是频繁的操作dom,造成重绘性能会变差;现在的vue中使用的是虚拟dom,就是一个描述dom树的一个js对象;在渲染到dom上,更新时,我们将两个虚拟dom,diff比对,一样的我们可以复用,patch在渲染;

6.1、一个简单案例

import Vue from "vue";
let vm = new Vue({
    el:'#app', //表示要渲染得元素是app
	render(h){
        return h('div',{id:"div",style:{'color':'red'}},'hello');
   }
});

6.2、初始化将虚拟dom渲染

步骤:

1、生成虚拟dom,即render函数中的h函数;

创建h函数的参数有('标签','属性',子节点);

返回虚拟dom类;

2、创建虚拟dom类,VNode,:

  • tag:表示当前的标签名
  • props:表示当前标签上的属性;
  • key:表示唯一的,用于dom-diff比对
  • children:表示子节点
  • elm:表示当前的元素
  • _type: VNODE_TYPE, 表示是虚拟dom节点
  • text :文本值,与children不可同时有值

3、创建一个render函数,初次时候只是渲染这个虚拟dom到页面上

代码:

1、创建虚拟dom的类

source\vue\vdom\vnode.js

// 通过 symbol 保证唯一性,用于检测是不是 vnode
const VNODE_TYPE = Symbol('virtual-node')

/**
   * @param {String} tag 'div' 标签名  可以是元素的选择器 可参考jq
   * @param {Object} props {'style': {background:'red'}} 对应的Vnode绑定的数据
   属性集 包括attribute、 eventlistener、 props, style、hook等等 
   * @param {Array} children [ h(), 'text'] 子元素集
   * @param {String} text当前的text 文本 itemA
   * @param {Element} elm 对应的真是的dom 元素的引用
   * @param { String}  key 唯一 而且需要一一对应 用于不同vnode之前的比对
   * 主要是用在需要循环渲染的dom元素在进行diff运算时的优化算法
   * @return {Object}  vnode  
   */

function vnode(tag, props = {}, children, key, text, elm) {
    return {
        _type: VNODE_TYPE,
        tag,
        props,
        children,
        key,
        text,
        elm
    }
}

export default vnode;

2、创建h函数

source\vue\vdom\h.js

/** 
 * h函数的主要工作就是把传入的参数封装为vnode
 */

import vnode from './vnode';
import {
  hasValidKey,
  isPrimitive, isArray
} from '../utils'


const hasOwnProperty = Object.prototype.hasOwnProperty;

/**
 * RESERVED_PROPS 要过滤的属性的字典对象
 * 在react源码中hasValidRef和hasValidKey方法用来校验config中是否存在ref和key属性,
 *  有的话就分别赋值给key和ref变量。 
 * 然后将config.__tagf和config.__source分别赋值给tagf和source变量, 如果不存在则为null。
 * 在本代码中先忽略掉ref、 __tagf、 __source这几个值
 */
const RESERVED_PROPS = {
  key: true,
  __tagf: true,
  __source: true
}
// 将原来的props通过for in循环重新添加到props对象中,
// 且过滤掉RESERVED_PROPS里边属性值为true的值
function getProps(props) {
  let props = {};
  const keys = Object.keys(props);
  if (keys.length == 0) {
    return props;
  }
  for (let propName in props) {
    if (hasOwnProperty.call(props, propName) && !RESERVED_PROPS[propName]) {
      props[propName] = props[propName]
    }
  }
  return props;
}

function h(tag, config, ...children) {
  let props = {},c,text,key; 
  c = children || [];
  // 获取key
  key = hasValidKey(props) ? props.key : undefined;
  props = getProps(config);
  // 因为children也可能是一个深层的套了好几层h函数所以需要处理扁平化
  return vnode(tag, props, c.map(child => {
    return isPrimitive(child) ? vnode(undefined, undefined, undefined, undefined, child) : child
  }), key, text, undefined);
}
export default h;

3、渲染虚拟dom到页面

source\vue\vdom\patch.js

// 不考虑hook 除了初次渲染还需要dom-diff操作

import htmlApi from '../utils/domUtils';
import {isDef
} from './utils';

import attr from '../utils/updataAttrUtils'

export function render(vnode,container){
  let el = createElement(vnode);
  htmlApi.appendChild(container,el);

}

/**
 * 从vdom生成真是dom *
 * @param {Object} vnode  虚拟dom
 * @returns 真是的dom
 */
function createElement(vnode) {
  let {tag,children,text,elm}  = vnode;
  // 如果没有选择器,则说ing这是一个文本节点
  if (isDef(tag)) {
    elm = vnode.elm = htmlApi.createElement(tag);
    attr.initCreateAttr(vnode);
    if (isArray(children)) {
      children.forEach(c => {
        htmlApi.appendChild(elm, createElement(c))
      });
    } else if (isPrimitive(text)) {
      // 子元素是文本节点直接插入当前到vnode节点
      htmlApi.appendChild(elm, htmlApi.createTextNode(text));
    }
        
  }else{
    elm = vnode.elm = htmlApi.createTextNode(text);
  }  
 return vnode.elm;
}

4、处理属性更新

source\vue\utils\updataAttrUtils.js

/** 处理属性
 */

import {
    isArray
} from './utils'

/**
 *更新style属性
 *
 * @param {Object} vnode 新的虚拟dom节点对象
 * @param {Object} oldStyle 
 * @returns
 */
function undateStyle(vnode, oldStyle = {}) {
    let doElement = vnode.elm;
    let newStyle = vnode.props.style || {};

    // 删除style
    for(let oldAttr in oldStyle){
        if (!newStyle.hasOwnPrpperty[oldAttr]) {
            doElement.style[oldAttr] = '';
        }
    }

    for(let newAttr in newStyle){
        doElement.style[newAttr] = newStyle[newAttr];
    }
}
function filterKeys(obj) {
    return Object.keys(obj).filter(k => {
        return k !== 'style' && k !== 'id' && k !== 'class'
    })
}
/**
 *更新props属性
 * 支持 vnode 使用 props 来操作其它属性。
 * @param {Object} vnode 新的虚拟dom节点对象
 * @param {Object} oldProps 
 * @returns
 */
function undateProps(vnode, oldProps = {}) {
    let doElement = vnode.elm;
    let props = vnode.props.props || {};

    filterKeys(oldProps).forEach(key => {
        if (!props.hasOwnPrpperty[key]) {
            delete doElement[key];
        }
     })

     filterKeys(props).forEach(key => {
         let old = oldProps[key];
         let cur = props[key];
         if (old !== cur && (key !== 'value' || doElement[key] !== cur)) {
            doElement[key] = cur;
         }
     })
}


/**
 *更新className属性 html 中的class
 * 支持 vnode 使用 props 来操作其它属性。
 * @param {Object} vnode 新的虚拟dom节点对象
 * @param {*} oldName 
 * @returns
 */
function updateClassName(vnode, oldName) {
    let doElement = vnode.elm;
    const newName = vnode.props.className || vnode.props.class;

    if (!oldName && !newName) return
    if (oldName === newName) return

    if (typeof newName === 'string' && newName) {
        doElement.className = newName.toString()
    } else if (isArray(newName)) {
        let oldList = [...doElement.classList];
        oldList.forEach(c => {
            if (!newName.indexOf(c)) {
                doElement.classList.remove(c);
            }
        })
        newName.forEach(v => {
            doElement.classList.add(v)
        })
    } else {
        // 所有不合法的值或者空值,都把 className 设为 ''
        doElement.className = ''
    }
}

function initCreateAttr(vnode) {
    updateClassName(vnode);
    undateProps(vnode);
    undateStyle(vnode);
}

function updateAttrs(oldVnode, vnode) {
    updateClassName(vnode, oldVnode.props.className || oldVnode.props.class);
    undateProps(vnode, oldVnode.props.props);
    undateStyle(vnode, oldVnode.props.style);
}

export const styleApis = {
    undateStyle,
    undateProps,
    updateClassName,
    initCreateAttr,
    updateAttrs
};
  export default styleApis;

5、导出

source\vue\vdom\index.js

import h from './h';
import {render} from './patch';
// 统一对外暴露的虚拟dom实现的出口
export {
    h,
    render
}

6、测试

import {h,render} from "../source/vue/vdom";

let container = document.getElementById('app');
let vnode =  h('div', null, 'hello',
    h('span',{style:{color:'red'}},'world'));
render(vnode, container);

6.3、diff、patch

步骤:

1、比较两个虚拟节点标签,不一样直接整个替换;

2、比较属性,更新属性

3、比较两个虚拟节点的文本节点

4、比较儿子节点,使用双指针比较:头头、尾尾、头尾、尾头,在就是比较key

代码:

1、在vnode.js中增加下面方法:

/**
 * 检查两个 vnode 是不是同一个: key 相同且 type 相同
 *
 * @param {Object} oldVnode
 * @param {Object} newVnode
 * @returns {Boolean}  是则 true,否则 false
 */
export function isSameVnode(oldVnode, newVnode) {
    return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
}
/** 
 * 校验是不是 vnode, 主要检查 __type。
 * @param  {Object}  vnode 要检查的对象
 * @return {Boolean}       是则 true,否则 false
 */
export function isVnode(vnode) {
    return vnode && vnode._type === VNODE_TYPE
}

2、在patch.js中增加方法:

// 不考虑hook 除了初次渲染还需要dom-diff操作

import htmlApi from '../utils/domUtils';
import {isDef,isUndef, isArray} from '../utils';

import attr from '../utils/updataAttrUtils'

import { isSameVnode} from './vnode'

export function render(vnode,container){
  let el = createElement(vnode);
  htmlApi.appendChild(container,el);
  return el;
}

/**
 * 从vdom生成真是dom *
 * @param {Object} vnode  虚拟dom
 * @returns 真是的dom
 */
function createElement(vnode) {
  let {tag,children,text,elm}  = vnode;
  // 如果没有选择器,则说ing这是一个文本节点
  if (isDef(tag)) {
    elm = vnode.elm = htmlApi.createElement(tag);
    attr.initCreateAttr(vnode);
    if (isArray(children)) {
      children.forEach(c => {
        htmlApi.appendChild(elm, createElement(c))
      });
    } else if (isPrimitive(text)) {
      // 子元素是文本节点直接插入当前到vnode节点
      htmlApi.appendChild(elm, htmlApi.createTextNode(text));
    }
        
  }else{
    elm = vnode.elm = htmlApi.createTextNode(text);
  }  
 return vnode.elm;
}

export function patch(oldVnode,vnode){
  let elm, parent;
   // 如果 oldVnode 和 vnode 是同一个 vnode(相同的 key 和相同的选择器),
   //  那么更新 oldVnode。
   if (isSameVnode(oldVnode, vnode)) {
     return patchVnode(oldVnode, vnode)
   } else {
     // 新旧vnode不同,那么直接替换掉 oldVnode 对应的 DOM
     elm = oldVnode.elm;
     parent = htmlApi.parentNode(elm);
     createElement(vnode);
     if (parent !== null) {
       // 如果老节点对应的dom父节点有并且有同级节点,
       // 那就在其同级节点之后插入 vnode 的对应 DOM。
       htmlApi.insertBefore(parent, vnode.elm, htmlApi.nextSibling(elm));
       // 在把 vnode 的对应 DOM 插入到 oldVnode 的父节点内后,移除 oldVnode 的对应 DOM,完成替换。
       removeVnodes(parent, [oldVnode], 0, 0);
     }
   }
   return elm;
}

/**
 * 
 * 1、更新 本身对应的dom
 * 2、根据children的变化去确定是否递归 patch children里的每个vnode
 *
 * @param {Object} oldVnode 老 vnode
 * @param {Object} vnode 新 vnode
 * @param {*} insertedVnodeQueue 记录插入的 vnode
 * 
 */
function patchVnode(oldVnode, vnode) {
  // let elm = vnode.elm = oldVnode.elm,因为vnode没有被渲染,这时的vnode.elm是undefined,
  // 新把老的给它
  let elm = vnode.elm = oldVnode.elm,
    oldCh = oldVnode.children,
    newCh = vnode.children;
  // 如果两个vnode完全相同,直接返回 不需要更新操作
  if (oldVnode === vnode) return;
  if (isDef(vnode.props)) {
    // 属性的比较更新
    attr.updateAttrs(oldVnode, vnode);
  }

  // 新节点不是文本节点
  if (isUndef(vnode.text)) {
    if (oldCh && newCh && oldCh.length > 0 && newCh.length > 0) {
      // 新旧节点均存在 children,且不一样时,对 children 进行 diff
      updateChildren(elm, oldCh, newCh);

    } else if (newCh && newCh.length > 0) {
      //如果vnode有子节点,oldvnode没子节点
      //oldvnode是text节点,则将elm的text清除,因为children和text不同同时有值
      if (!oldVnode.text) htmlApi.setTextContent(elm, '');
      //并添加vnode的children 
      addVnodes(elm, null, newCh, 0, newCh.length - 1);

    } else if (oldCh && oldCh.length > 0) {
      // 新节点不存在 children 旧节点存在 children 移除旧节点的 children
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    }
  } //如果oldvnode的text和vnode的text不同,则更新为vnode的text
  else if (oldVnode.text !== vnode.text) {
    htmlApi.setTextContent(elm, vnode.text);
  }
 return elm;
}

/**
 *从parent dom删除vnode 数组对应的dom
 *
 * @param {Element} parentElm 父元素
 * @param {Array} vnodes  vnode数组
 * @param {Number} startIdx 要删除的对应的vnodes的开始索引
 * @param {Number} endIdx  要删除的对应的vnodes的结束索引
 */
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    let ch = vnodes[startIdx];
    if (ch) {
      htmlApi.removeChild(parentElm, ch.elm);
    }
  }
}

/**
 * 比较新旧 children 并更新
 *
 * @param {Element} parentDOMElement 父 dom,children 对应的 dom 将要挂载的
 * @param {Array} oldChildren    旧 children,vnode 数组
 * @param {Array} newChildren    新 children,vnode 数组
/**
 *
 *
 * @param {*} parentDOMElement
 * @param {*} oldChildren
 * @param {*} newChildren
 */
function updateChildren(parentDOMElement, oldChildren, newChildren) {
  // 两组数据 首尾双指针比较
  let oldStartIdx = 0,
    oldStartVnode = oldChildren[0];
  let oldEndIdx = oldChildren.length - 1,
    oldEndVnode = oldChildren[oldEndIdx];

  let newStartIdx = 0,
    newStartVnode = newChildren[0];
  let newEndIdx = newChildren.length - 1,
    newEndVnode = newChildren[newEndIdx];

  // `oldKeyToIdx` 来映射新节点的`key`在oldChildren中的索引
  let oldKeyToIdx;
  // 记录当前节点key的索引通过在 oldKeyToIdx对象属性查找
  let oldIdxByKeyMap;

  let elmToMove;

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 先排除 vnode为空 4 个 vnode 非空,
    // 左侧的 vnode 为空就右移下标,右侧的 vnode 为空就左移 下标
    if (oldStartVnode == null) {
      oldStartVnode = oldChildren[++oldStartIdx];
    } else if (oldEndVnode == null) {
      oldEndVnode = oldChildren[--oldEndIdx];
    } else if (newStartVnode == null) {
      newStartVnode = newChildren[++newStartIdx];
    } else if (newEndVnode == null) {
      newEndVnode = newChildren[--newEndIdx];
    }
    /** oldStartVnode/oldEndVnode/newStartVnode/newEndVnode 两两比较,
     * 1、 oldStartVnode - > newStartVnode
     * 2、 oldEndVnode - > newEndVnode
     * 3、 newEndVnode - > oldStartVnode
     * 4、 newStartVnode - > oldEndVnode
     * 
     * 对上述四种情况执行对应的patch
     */
    // 1、新的开始节点跟老的开始节点相比较 是不是一样的vnode
    // oldStartVnode - > newStartVnode 比如在尾部新增、删除节点
    // 
    else if (isSameVnode(oldStartVnode, newStartVnode)) {
      patch(oldStartVnode, newStartVnode);
      oldStartVnode = oldChildren[++oldStartIdx];
      newStartVnode = newChildren[++newStartIdx];
    }
    // 2、oldEndVnode - > newEndVnode 比如在头部新增、删除节点
    else if (isSameVnode(oldEndVnode, newEndVnode)) {
      patch(oldEndVnode, newEndVnode);
      oldEndVnode = oldChildren[--oldEndIdx];
      newEndVnode = newChildren[--newEndIdx];
    }
    // 3、newEndVnode - > oldStartVnode 将头部节点移动到尾部
    else if (isSameVnode(oldStartVnode, newEndVnode)) {
      patch(oldStartVnode, newEndVnode);
      // 把旧的开始节点插入到末尾
      htmlApi.insertBefore(parentDOMElement, oldStartVnode.elm, htmlApi.nextSibling(oldEndVnode.elm));
      oldStartVnode = oldChildren[++oldStartIdx];
      newEndVnode = newChildren[--newEndIdx];

    }
    // 4、oldEndVnode  -> newStartVnode 将尾部移动到头部
    else if (isSameVnode(oldEndVnode, newStartVnode)) {
      patch(oldEndVnode, newStartVnode);
      // 将老的oldEndVnode移动到oldStartVnode的前边,
      htmlApi.insertBefore(parentDOMElement, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldChildren[--oldEndIdx];
      newStartVnode = newChildren[++newStartIdx];
    }
    /** 5、4种情况都不相等
     * // 1. 从 oldChildren 数组建立 key --> index 的 map。
     // 2. 只处理 newStartVnode (简化逻辑,有循环我们最终还是会处理到所有 vnode),
     //    以它的 key 从上面的 map 里拿到 index;
     // 3. 如果 index 存在,那么说明有对应的 old vnode,patch 就好了;
     // 4. 如果 index 不存在,那么说明 newStartVnode 是全新的 vnode,直接
     //    创建对应的 dom 并插入。
     */
    else {

      /** 如果 oldKeyToIdx 不存在,
       * 1、创建 old children 中 vnode 的 key 到 index 的
       * 映射, 方便我们之后通过 key 去拿下标
       *  */
      if (isUndef(oldKeyToIdx)) {
        oldKeyToIdx = createOldKeyToIdx(oldChildren, oldStartIdx, oldEndIdx);
      }
      // 2、尝试通过 newStartVnode 的 key 去拿下标
      oldIdxByKeyMap = oldKeyToIdx[newStartVnode.key];
      // 4、 下标索引不存在,说明newStartVnode 是全新的 vnode
      if (oldIdxByKeyMap == null) {
        // 那么为 newStartVnode 创建 dom 并插入到 oldStartVnode.elm 的前面。
        htmlApi.insertBefore(parentDOMElement, createElement(newStartVnode), oldStartVnode.elm);
        newStartVnode = newChildren[++newStartIdx];
      }
      //  3、下标存在 说明oldChildren中有相同key的vnode
      else {
        elmToMove = oldChildren[oldIdxByKeyMap];
        // key相同还要比较tag,tag不同,需要创建 新dom
        if (elmToMove.tag !== newStartVnode.tag) {
          htmlApi.insertBefore(parentDOMElement, createElement(newStartVnode), oldStartVnode.elm);
        }
        // tag相同,key也相同,说明是一样的vnode,需要打补丁patch
        else {
          patch(elmToMove, newStartVnode);
          oldChildren[oldIdxByKeyMap] = undefined;
          htmlApi.insertBefore(parentDOMElement, elmToMove.elm, oldStartVnode.elm);
        }
        newStartVnode = newChildren[++newStartIdx];
      }
    }

  }

  // 说明循环比较完后,新节点还有数据,这时候需要将这些虚拟节点的创建真是dom
  // 新增引用到老的虚拟dom的`elm`上,且新增位置是老节点的oldStartVnode即末尾;
  if (newStartIdx <= newEndIdx) {
    let before = newChildren[newEndIdx + 1] == null ? null : newChildren[newEndIdx + 1].elm;
    addVnodes(parentDOMElement, before, newChildren, newStartIdx, newEndIdx);
  }

  if (oldStartIdx <= oldEndIdx) {
    // newChildren 已经全部处理完成,而 oldChildren 还有旧的节点,需要将多余的节点移除
    removeVnodes(parentDOMElement, oldChildren, oldStartIdx, oldEndIdx);
  }
}

/**
 * 为 vnode 数组 begin~ end 下标范围内的 vnode 
 * 创建它的 key 和 下标 的映射。
 *
 * @param {Array} children
 * @param {Number} startIdx
 * @param {Number} endIdx
 * @returns {Object}  key在children中所映射的index索引对象
 * children = [{key:'A'},{key:'B'},{key:'C'},{key:'D'},{key:'E'}];
 * startIdx = 1; endIdx = 3;
 * 函数返回{'B':1,'C':2,'D':3}
 */
function createOldKeyToIdx(children, startIdx, endIdx) {
  const map = {};
  let key;
  for (let i = startIdx; i <= endIdx; ++i) {
    let ch = children[i];
    if (ch != null) {
      key = ch.key;
      if (!isUndef(key)) map[key] = i;
    }
  }
  return map;
}

/**
 *添加 vnodes 数组对应的 dom 到parentElm dom
 *
 * @param {Element} parentElm  父dom
 * @param {Element} before      添加到指定的 before dom 被参照的节点(即要插在该节点之前)
 * 当传入null时, 新插入的元素将会插入到父元素的子元素列表末尾。
 * @param {Array} vnodes        需要添加的vnode数组
 * @param {Number} startIdx     vnodes 开始索引
 * @param {Number} endIdx       vnodes  结束索引
 */
function addVnodes(parentElm, before, vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    const ch = vnodes[startIdx];
    if (ch != null) {
      htmlApi.insertBefore(parentElm, createElement(ch), before);
    }
  }
}

在source\vdom\index.js导出

import h from './h';
import {render,patch} from './patch';
// 统一对外暴露的虚拟dom实现的出口
export {
    h,
    render,
    patch
}

7、vue中融合虚拟dom

在本篇文章中小节2,中更新视图时使用了_update方法,我们将重写;

步骤:

1、应用使用了render

2、在$mount中updateComponent函数中调用Vue实例上的_update方法,参数vm._render方法;

代码:

source\vue\index.js

import {initState} from './observe'
import Watcher from './observe/watcher';
import { complier } from './compiler';
import {h,render,patch} from './vdom';

function Vue(options){
    this._init(options); //初始化vue 并且将用户选项传入
}
Vue.prototype._init = function (options) {
    // vue中初始化 this.$options 表示得是vue中得参数
    // this指向Vue的实例,所以这里是将Vue的实例缓存给vm变量
    let vm = this;
    vm.$options = options;

    //MVVM原理 响应式数据 需要数据重新初始化 
    // 我们将不同得初始化放入到不同得文件夹中操作,方便维护
    // 初始化state,包括props\methods\data\compunted\watch
    // props\data属性将其设置为vm的响应式属性即需要Object.defineProperty
    // 绑定vm的props和打他属性并设置其getter和setter
    initState(vm);

    // 将实例挂载到dom上
    if (vm.$options.el) {
        vm.$mount(vm.$options.el)
    }
}


function query(el){
    if(typeof el === 'string'){
        return document.querySelector(el);
    }
    return el;
}
const mount = Vue.prototype.$mount;
// 渲染页面将组件进行挂载
Vue.prototype.$mount = function(el){

    let vm = this;
    // 获取dom
    el = vm.$el = el && query(el);

    // 渲染时通过 watcher来渲染 
    //  
    /** 
     * 渲染watcher
     * vue2.0 组件级别更新 new Vue产生一个组件
     * 
     */
    // 更新组件、渲染逻辑 
     let updateComponent = ()=>{
        //  更新组件
        vm._update(vm._render());
     }
    //  渲染watcher
     new Watcher(vm,updateComponent);

}
// 
Vue.prototype._render = function(){
    let vm = this;
    let render = vm.$options.render; //获取用户编写的render方法

    // render函数中的this,是当前Vue的实例
    // render 返回的是虚拟节点
    let vnode = render.call(vm,h);
    return vnode;
}

// 用户传入的数据 去更新视图
Vue.prototype._update = function(vnode){
    console.log("更新操作");
    let vm = this;
    let el = vm.$el;
    let preVnode = vm.preVnode; 
    // 第一次渲染 preVnode不存在
    if(!preVnode){
        vm.preVnode = vnode;
        render(vnode,el);
    }else{
        vm.$el = patch(preVnode,vnode);
    }

}

Vue.prototype.$watch = function(expr,handler,opts){
    let vm = this;
    // 原理 创建一个watcher {user:true}标识是用户自己写的一个watcher
    new Watcher(vm,expr,handler,{user:true,...opts});
}
export default Vue

总结

回过头来看,这里的渲染逻辑并不是特别复杂,核心关键的几步流程还是非常清晰的:

  1. new Vue,执行初始化,对数据进行劫持、收集依赖关系,实现响应式数据;
  2. 挂载$mount方法,通过自定义Render方法、template、el等生成Render函数,本文中没有实现template,因为需要考虑到ast,后续再分析;
  3. 通过Watcher监听数据的变化;
  4. 当数据发生变化时,Render函数执行生成VNode对象
  5. 通过patch方法,对比新旧VNode对象,使用了diff算法,添加、修改、删除真正的DOM元素

vue源码中的知识点

1、 rollup: 模块打包工具,官方文档:www.rollupjs.com/

2、Flow :官方文档:flow.org/en/

VUE的源码采用rollupflow至于为什么不采用typescript,尤大在知乎说过主要考虑工程上成本和收益的考量。(3.0+版本确定改用typesript) 。

3、发布订阅模式

4、Object.defineProperty

5、es6

6、原型链、闭包

7、函数柯里化

8、事件环

9、切片编程

10、vnode

11、dom-diff算法

12、AST