1、vue-router原理实现
1、动态路由
const routes = [{path:'/detail:id',name:'Detial',props:true,component:()=>('../views/Detail.vue')}]//接收时推荐使用props:['id']来接收参数,路由中需要开启props。
2、嵌套路由
const routes = [{path:'/',component:Layout,children:[{name:'index',path:'',component:Index},{name:'detail',path:'detail/:id',props:true,component:()=>('../views/Detail.vue')}]}]
3、编程式导航
this.$router.replace('/login')this.$router.push('/')this.$router.push(name:'Detail',params:{id:1})this.$router.go(-2)this.$router.back()
4、Hash模式和History模式的区别
区别:
- Hash模式是基于锚点,以及onhashchange事件
- History模式是基于HTML5中的HistoryAPI
- history.pushState() IE10以后才支持
- history.repalceState()
5、History模式
- History需要服务器的支持
- 单页应用中,服务端不存在http://www.testurl.com/login这样的地址会返回找不到该页面
- 在服务端应该除了静态资源外都返回单页应用的index.html
6、VueRouter实现原理
Hash模式:
- URL中#后面的内容作为路径地址
- 监听hashchange事件
- 根据当前路由地址找到对应的组件重新渲染
History模式
- 通过history.pushState()方法改变地址栏
- 监听popstate事件
- 根据当前路由地址找到对应组件重新渲染
7、VueRouter-install
let _Vue = nullexport default class VueRouter{static install (Vue){//1.判断当前插件是否已经被安装if(VueRouter.inatall.installed){return}VueRouter.install.installed = true//2.把vue构造函数记录到全局变量_Vue = Vue//3.把创建Vue实例时候传入的router对象注入到Vue实例上//混入_Vue.mixin({beforeCreate(){if(this.$options.router){_Vue.prototype.$router = this.$options.router}}})}}
8、VueRouter-构造函数
let _Vue = nullexport default class VueRouter{static install (Vue){//1.判断当前插件是否已经被安装if(VueRouter.inatall.installed){return}VueRouter.install.installed = true//2.把vue构造函数记录到全局变量_Vue = Vue//3.把创建Vue实例时候传入的router对象注入到Vue实例上//混入_Vue.mixin({beforeCreate(){if(this.$options.router){_Vue.prototype.$router = this.$options.router}}})}constructor(options){this.options = optionsthis.routeMap = {}this.data = _Vue.observable({current:'/'})}}
9、VueRouter-createRounteMap
let _Vue = nullexport default class VueRouter{static install (Vue){//1.判断当前插件是否已经被安装if(VueRouter.inatall.installed){return}VueRouter.install.installed = true//2.把vue构造函数记录到全局变量_Vue = Vue//3.把创建Vue实例时候传入的router对象注入到Vue实例上//混入_Vue.mixin({beforeCreate(){if(this.$options.router){_Vue.prototype.$router = this.$options.router}}})}constructor(options){this.options = optionsthis.routeMap = {}this.data = _Vue.observable({current:'/'})}createRouteMap(){//遍历所有的路由规则,把路由规则解析成键值对的形式 存储到routeMap中this.options.routes.forEach(route=>{this.routeMap[route.path] = route.component})}}
10、VueRouter-router-link
vue的构建版本
- 运行时版本:不支持template模板,需要打包的时候提前编译
- 完整版:包含运行时和编译器,体积比运行时版大10k左右,程序运行的时候把模板转换成render函数
let _Vue = nullexport default class VueRouter{static install (Vue){//1.判断当前插件是否已经被安装if(VueRouter.inatall.installed){return}VueRouter.install.installed = true//2.把vue构造函数记录到全局变量_Vue = Vue//3.把创建Vue实例时候传入的router对象注入到Vue实例上//混入_Vue.mixin({beforeCreate(){if(this.$options.router){_Vue.prototype.$router = this.$options.routerthis.$options.router.init()}}})}constructor(options){this.options = optionsthis.routeMap = {}this.data = _Vue.observable({current:'/'})}init(){this.createRouterMap()this.initComponents(_Vue)}createRouteMap(){//遍历所有的路由规则,把路由规则解析成键值对的形式 存储到routeMap中this.options.routes.forEach(route=>{this.routeMap[route.path] = route.component})}initComponents(Vue){Vue.component('router-link',{props:{ro:String},template:'<a :href="to"><slot></slot></a>'})}}
11、VueRouter完整版的vue
开启完整版本的vue
module.exports={runtimeCompiler:true}
12、VueRouter-render
let _Vue = nullexport default class VueRouter{static install (Vue){//1.判断当前插件是否已经被安装if(VueRouter.inatall.installed){return}VueRouter.install.installed = true//2.把vue构造函数记录到全局变量_Vue = Vue//3.把创建Vue实例时候传入的router对象注入到Vue实例上//混入_Vue.mixin({beforeCreate(){if(this.$options.router){_Vue.prototype.$router = this.$options.routerthis.$options.router.init()}}})}constructor(options){this.options = optionsthis.routeMap = {}this.data = _Vue.observable({current:'/'})}init(){this.createRouterMap()this.initComponents(_Vue)}createRouteMap(){//遍历所有的路由规则,把路由规则解析成键值对的形式 存储到routeMap中this.options.routes.forEach(route=>{this.routeMap[route.path] = route.component})}initComponents(Vue){Vue.component('router-link',{props:{ro:String},render(h){return h('a',{arrts:{href:this.to}},[this.$slots.default])}})}}
13、VueRouter-router-view
let _Vue = nullexport default class VueRouter{static install (Vue){//1.判断当前插件是否已经被安装if(VueRouter.inatall.installed){return}VueRouter.install.installed = true//2.把vue构造函数记录到全局变量_Vue = Vue//3.把创建Vue实例时候传入的router对象注入到Vue实例上//混入_Vue.mixin({beforeCreate(){if(this.$options.router){_Vue.prototype.$router = this.$options.routerthis.$options.router.init()}}})}constructor(options){this.options = optionsthis.routeMap = {}this.data = _Vue.observable({current:'/'})}init(){this.createRouterMap()this.initComponents(_Vue)}createRouteMap(){//遍历所有的路由规则,把路由规则解析成键值对的形式 存储到routeMap中this.options.routes.forEach(route=>{this.routeMap[route.path] = route.component})}initComponents(Vue){Vue.component('router-link',{props:{ro:String},render(h){return h('a',{arrts:{href:this.to},on:{click:this.chickHandler}},[this.$slots.default])}})methods:{chickHandler(e){history.pushState({},'',this.to)this.$router.data.current = this.toe.preventDefault()}}}const self = thisVue.component('router-view',{render(h){const component = self.routeMap[self.data.current]return h(component)}}}
14、VueRouter-initEvent
let _Vue = nullexport default class VueRouter{static install (Vue){//1.判断当前插件是否已经被安装if(VueRouter.inatall.installed){return}VueRouter.install.installed = true//2.把vue构造函数记录到全局变量_Vue = Vue//3.把创建Vue实例时候传入的router对象注入到Vue实例上//混入_Vue.mixin({beforeCreate(){if(this.$options.router){_Vue.prototype.$router = this.$options.routerthis.$options.router.init()}}})}constructor(options){this.options = optionsthis.routeMap = {}this.data = _Vue.observable({current:'/'})}init(){this.createRouterMap()this.initComponents(_Vue)this.initEvent()}createRouteMap(){//遍历所有的路由规则,把路由规则解析成键值对的形式 存储到routeMap中this.options.routes.forEach(route=>{this.routeMap[route.path] = route.component})}initEvent(){window.addEventListener('popstate',()=>{this.data.current = window.location.pathname})}initComponents(Vue){Vue.component('router-link',{props:{ro:String},render(h){return h('a',{arrts:{href:this.to},on:{click:this.chickHandler}},[this.$slots.default])}})methods:{chickHandler(e){history.pushState({},'',this.to)this.$router.data.current = this.toe.preventDefault()}}}const self = thisVue.component('router-view',{render(h){const component = self.routeMap[self.data.current]return h(component)}}}
2、模拟vue.js响应式原理
1、数据驱动
数据响应式:
- 数据模型仅仅是普通的javascript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率
双向绑定
- 数据改变,视图改变,视图改变,数据也随之改变
- 我们可以使用v-model在表单元素上创建双向数据绑定
数据驱动是Vue最独特的特性之一
- 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图的
2、数据响应式核心原理Vue2
function porxyData(data){Object.key(data).forEach(key =>{Object.defineProperty(vm,key,{get(){return data[key]},set(newValue){if(newValue === data[key]){return}data[key] = newValuedocument.querySelector('#app').textContent = data[key]}})})}
3、数据响应式核心原理Vue3
let vm = new Proxy(data,{get(target,key){return target[key]}set(target,key,newValue){if(taeget[key]===newValue){return}target[key] = newValuedocument.querySelector('#app').textContent = target[key]}})
4、发布订阅模式
发布/订阅模式
- 订阅者
- 发布者
- 信号中心
//事件中心let eventHub = new Vue()//ComponentA.vue//发布者addTodo:function(){eventHub.$emit('add-todo',{text:this.newTodoText})this.newTodoText = ''}//ComponentB.vuecreate:function(){//订阅者(实现)eventHub.$on('add-todo',this.addTodos)}
class EventEmitter {constructor(){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()})}}}
5、观察者模式
- 观察者(订阅者)—watcher
- update():当事件发生时,具体要做的事情
- 目标(发布者)—Dep
- subs数组:存储所有的观察者
- addSub():添加观察者
- notify():当事件发生,调用所有观察者的update()方法
- 没有事件中心
//发布者-目标class Dep{constructor(){//记录所有的订阅者this.subs = []}addSub(sub){if(sub && sub.upadte){this.subs.push(sub)}}notify(){this.subs.forEach(sub =>{sub.update()})}}//订阅者-观察者class Wather {update(){console.log('update')}}//测试let dep = new Dep()let watcher = new Watcher()dep.addSub(watcher)dep.notify()
6、Vue
功能:
- 负责接收初始化的参数(选项)
- 负责把data中的属性注入到Vue实例,转换成getter/setter
- 负责调用observe监听data中所有属性的变化
- 负责调用compiler解析指令/差值表达式
class Vue{constructor(options){//1.通过属性保存选项的数据this.$options = options || {}this.$data = options.data || {}this.$el = typeof options.el === 'string'?documnet.querySelector(options.el):options.el//2.把data中的成员转换成getter和setter注入到Vue实例中this._proxyData(this.$data)//3.调用observe监听data中所有属性的变化//4.调用compiler解析指令/差值表达式}_proxyData(data){//遍历Data中的所有属性Object.keys(data).forEach(key =>{//把data的属性注入到vue实例中Object.defineProperty(this,key,{enumerable:true,configurable:true,get(){return data[key]}set(newValue){if(newValue == data[key]){return}data[key] = newValue}})})}}
7、Observer
功能:
- 负责把data选项中的属性转换成响应式数据
- data中的某个属性也是对象,把该属性转换成响应式数据
- 数据变化发送通知
class Observer{constructor(data){this.walk(data)}walk(data){//1.判断data是否是对象if(!data || typeof !== 'object'){return}//2.遍历data对象所有属性Object.key(data).forEach(key=>{this.defineReactive(data,key,data[key])})}defineReactive(obj,key,val){let that = thisthis.walk(val)Object.defineProperty(obj,key,{enumerable:true,configurable:true,get(){return val}set(newValue){if(newValue = val){return}val = newValuethat.walk(newValue)}})}}
class Vue{constructor(options){//1.通过属性保存选项的数据this.$options = options || {}this.$data = options.data || {}this.$el = typeof options.el === 'string'?documnet.querySelector(options.el):options.el//2.把data中的成员转换成getter和setter注入到Vue实例中this._proxyData(this.$data)//3.调用observe监听data中所有属性的变化new Observer(this.$data)//4.调用compiler解析指令/差值表达式}_proxyData(data){//遍历Data中的所有属性Object.keys(data).forEach(key =>{//把data的属性注入到vue实例中Object.defineProperty(this,key,{enumerable:true,configurable:true,get(){return data[key]}set(newValue){if(newValue == data[key]){return}data[key] = newValue}})})}}
8、Compiler
功能:
- 负责编译模板,解析指令/差值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
class Compiler{constructor(vm){this.el = vm.$elthis.vm = vmthis.compile(this.el)}//编译模板,处理文本节点和元素节点compile(el){let el.childNodes = el.childNodesArray.from(childNodes).forEach(node =>{if(this.isTextNode(node){this.compileText(node)})else if(this.isElementNode(node)){this.compileElement(node)}//判断node节点是否有子节点,如果有子节点,要递归调用compileif(node.childNodes && node.childNodes.length){this.compile(node)}})}//编译元素节点,处理指令compileElement(node){Array.from(node.attributes).forEach(attr =>{//判断是否是指令let arrtName = attr.nameif(this.isDirective(attrName)){attrName = attrName.substr(2)let key = attr.valuethis.update(node,key,attrName)}})}update(node,key,attrName){let updateFn = this[attrName + 'Updater']updateFn && updateFn(node,this.vm[key])}//处理v-text指令textUpdater(node,value){node.textContent = value}//处理v-modelmodelUpdater(node,value){node.value = value}//编译文本节点,处理差值表达式compileText(node){//{{ msg }}let reg = /\{\{(.+?)\}\}/let value = node.textContentif(reg.test(value)){let key = RegExp.$1.trim()node.textContent = value.replace(reg,this.vm[key])}}//判断元素属性是否是指令isDirective(attrName){return attrName.startWith('v-')}//判断节点是否文本节点isTextNode(node){return node.nodeType === 3}//判断节点是否是元素节点isElementNode(node){return node.nodeType === 1}}
class Vue{constructor(options){//1.通过属性保存选项的数据this.$options = options || {}this.$data = options.data || {}this.$el = typeof options.el === 'string'?documnet.querySelector(options.el):options.el//2.把data中的成员转换成getter和setter注入到Vue实例中this._proxyData(this.$data)//3.调用observe监听data中所有属性的变化new Observer(this.$data)//4.调用compiler解析指令/差值表达式new Compiler(this)}_proxyData(data){//遍历Data中的所有属性Object.keys(data).forEach(key =>{//把data的属性注入到vue实例中Object.defineProperty(this,key,{enumerable:true,configurable:true,get(){return data[key]}set(newValue){if(newValue == data[key]){return}data[key] = newValue}})})}}
9、Dep
功能:
- 收集依赖,添加观察者
- 通知所有观察者
class Dep{constructor(){//存储所有的观察者this.subs = []}//添加观察者addSub(sub){if(sub && sub.update){this.subs.push(sub)}}//发送通知notify(){this.subs.forEach(sub =>{sub.update()})}}
class Observer{constructor(data){this.walk(data)}walk(data){//1.判断data是否是对象if(!data || typeof !== 'object'){return}//2.遍历data对象所有属性Object.key(data).forEach(key=>{this.defineReactive(data,key,data[key])})}defineReactive(obj,key,val){let that = this//负责收集依赖,并发送通知let dep = new Dep()this.walk(val)Object.defineProperty(obj,key,{enumerable:true,configurable:true,get(){//收集依赖Dep.target && dep.addSub(Dep.target)return val}set(newValue){if(newValue = val){return}val = newValuethat.walk(newValue)dep.notify()}})}}
10、watcher
功能:
- 当数据变化触发依赖,dep通知所有的watcher实例更新视图
- 自身实例化的时候往dep对象添加自己
class Watcher{constructor(vm,key,cb){this.vm = vm//data中的属性名称this.key = key//回调函数负责更新视图this.cb = cb//把watcher对象记录到Dep类的静态属性target中Dep.targrt = this//触发get方法,在get方法中调用addSubthis.oldValue = vm[key]Dep.target = null}//当数据发生变化的时候更新视图update(){let newValue = this.vm[this.key]if(this.oldValue === newValue){return}this.cb(newValue)}}
class Compiler{constructor(vm){this.el = vm.$elthis.vm = vmthis.compile(this.el)}//编译模板,处理文本节点和元素节点compile(el){let el.childNodes = el.childNodesArray.from(childNodes).forEach(node =>{if(this.isTextNode(node){this.compileText(node)})else if(this.isElementNode(node)){this.compileElement(node)}//判断node节点是否有子节点,如果有子节点,要递归调用compileif(node.childNodes && node.childNodes.length){this.compile(node)}})}//编译元素节点,处理指令compileElement(node){Array.from(node.attributes).forEach(attr =>{//判断是否是指令let arrtName = attr.nameif(this.isDirective(attrName)){attrName = attrName.substr(2)let key = attr.valuethis.update(node,key,attrName)}})}update(node,key,attrName){let updateFn = this[attrName + 'Updater']updateFn && updateFn.call(this,node,this.vm[key],key)}//处理v-text指令textUpdater(node,value,key){node.textContent = valuenew Watcher(this.vm,key,(newValue)=>{node.textContent = newValue})}//处理v-modelmodelUpdater(node,value,key){node.value = valuenew Watcher(this.vm,key,(newValue)=>{node.value = newValue})}//编译文本节点,处理差值表达式compileText(node){//{{ msg }}let reg = /\{\{(.+?)\}\}/let value = node.textContentif(reg.test(value)){let key = RegExp.$1.trim()node.textContent = value.replace(reg,this.vm[key])//创建watcher,当数据改变的时候更新视图new Watcher(this.vm,key,(newValue)=>{node.textContent = newValue})}}//判断元素属性是否是指令isDirective(attrName){return attrName.startWith('v-')}//判断节点是否文本节点isTextNode(node){return node.nodeType === 3}//判断节点是否是元素节点isElementNode(node){return node.nodeType === 1}}
11、双向绑定
class Compiler{constructor(vm){this.el = vm.$elthis.vm = vmthis.compile(this.el)}//编译模板,处理文本节点和元素节点compile(el){let el.childNodes = el.childNodesArray.from(childNodes).forEach(node =>{if(this.isTextNode(node){this.compileText(node)})else if(this.isElementNode(node)){this.compileElement(node)}//判断node节点是否有子节点,如果有子节点,要递归调用compileif(node.childNodes && node.childNodes.length){this.compile(node)}})}//编译元素节点,处理指令compileElement(node){Array.from(node.attributes).forEach(attr =>{//判断是否是指令let arrtName = attr.nameif(this.isDirective(attrName)){attrName = attrName.substr(2)let key = attr.valuethis.update(node,key,attrName)}})}update(node,key,attrName){let updateFn = this[attrName + 'Updater']updateFn && updateFn.call(this,node,this.vm[key],key)}//处理v-text指令textUpdater(node,value,key){node.textContent = valuenew Watcher(this.vm,key,(newValue)=>{node.textContent = newValue})}//处理v-modelmodelUpdater(node,value,key){node.value = valuenew Watcher(this.vm,key,(newValue)=>{node.value = newValue})//双向绑定node.addEventListener('input',()=>{this.vm[key] = node.value})}//编译文本节点,处理差值表达式compileText(node){//{{ msg }}let reg = /\{\{(.+?)\}\}/let value = node.textContentif(reg.test(value)){let key = RegExp.$1.trim()node.textContent = value.replace(reg,this.vm[key])//创建watcher,当数据改变的时候更新视图new Watcher(this.vm,key,(newValue)=>{node.textContent = newValue})}}//判断元素属性是否是指令isDirective(attrName){return attrName.startWith('v-')}//判断节点是否文本节点isTextNode(node){return node.nodeType === 3}//判断节点是否是元素节点isElementNode(node){return node.nodeType === 1}}
3、Virtual DOM
1、什么是虚拟DOM
虚拟Dom就是一个普通的javascript对象
2、为什么使用虚拟DOM
- 手动操作DOM比较麻烦,还需要考虑浏览器兼容性问题,虽然有jQuery等库简化DOM操作,但随着项目的复杂DOM操作复杂提升
- 为了简化DOM的复杂操作于是出现了各种MVVM框架,MVVM框架解决了视图和状态的同步问题
- 为了简化视图的操作我么使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是Virtual DOM出现了
- Virtual DOM的好处是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述DOM,Virtual DOM内部将农村清除如何有效(diff)更新DOM
3、虚拟DOM的作用和虚拟DOM库
只有在DOM操作比较复杂的时候使用虚拟DOM才能提高性能,若DOM操作比较简单,则使用虚拟DOM并不能提高性能
4、创建项目
//md snabbdom-demo//cd snabbdom-dom//yarn init -y//yarn add parcel-bundler//package.json"script":{"dev":"parcel index.html --open""build":"parcel build index.html"}
5、导入Snabbdom
//yarn add snabbdomimport {h,thunk,init} from 'snabbdom'
6、snabbdom的基本用法
import {h,init} from 'snabbdom'//1.hello world//参数:数组,模块//返回值:patch函数,作用对比两个vnode差异更新到真是DOMlet patch = init([])//第一个参数:标签+选择器//第二个参数:如果是字符串的话就是标签中的内容let vnode = h('div#container.cls','Hello World')let app = document.querySelector('#app')//第一个参数:可以使DOM元素,内部会把DOM元素转换成VNode//第二个参数:VNode//返回值:VNodelet oldVnode = patch(app,vnode)//假设的时刻vnode = h('div','Hello Snabbdom')patch(oldVnode,vnode)
//2.div中放置子元素h1,pimport {h,init} from 'snabbdom'let patch = init([])let vnode = h('div#container',[h('h1','Hello Snabbdom'),h('p','这是一个P标签')])let app = documnet.querySelector('#app')let oldVnode = patch(app,vnode)setTimeout(()=>{vnode = h('div#container',[h('h1','Hello World'),h('p','Hello p')patch(oldVnode,vnode)//清空页面元素patch(oldVnode,h('!'))])},2000)
7、模块
Snabbdom的核心库并不能处理元素属性/样式/事件,如果需要处理的话可以使用模块
常用的模块(官方提供了6个模块)
- attributes
- props
- class
- dataset
- eventlisteners
- style
模块的使用
- 导入需要的模块
- init()中注册模块
- 使用h()函数创建VNode的时候,可以把第二个参数设置为对象,其他参数往后移
import {init,h} from 'snabbdom'import style from 'snabbdom/modules/style'import evenlisters from 'snabbdom/modules/evenlisters'let patch = init([style,evenlisters])let vnode = h('div',{style:{backgroundColor:'red'}on:{click: eventHandler}},[h('h1','Hello Snabbdom'),h('p','这是个P标签')])function eventHandler(){console.log('点击我了')}let app = documnet.querySelector('#app')patch(app,vnode)
8、snabbdom源码解析
如何学习源码:
- 先宏观了解
- 带着目标源码
- 看源码的工程要不求甚解
- 调试
- 参考资料
snabbdom的核心
- 使用h()函数创建javascript对象(VNode)描述真实DOM
- init()设置模块,创建patch()
- patch()比较新旧两个vnode
- 把变化的内容更新到真实DOM树上
9、h函数
- h()函数最早见于Hyperscript,使用javaScript创建超文本
- Snabbdom中的h()函数不是用来创建超文本,而是创建vnode
函数重载:
概念:
1. 参数个数或类型不同的函数2. javaScript中没有重载的概念3. TypeScript中有重载,不过重载的实现还是通过代码调整参数
核心:调用vnode函数返回一个虚拟节点
10、必备快捷键
- 光标移动到is,按f2,跳到定义处
- alt+方向键左,返回
- ctrl+鼠标左键,跳到定义处
11、vnode
vnode函数就是接受一些参数并返回一个Vnode
12、patch的整体过程
- patch(oldVnode,newVnode)
- 打补丁,把心节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理得旧节点
- 对比新旧vNode是否相同节点(节点的key和sel相同)
- 如果不是相同的节点,删除之前的内容,重新渲染
- 如果是相同的节点,再判断新的VNode是否有text,如果有并且和oldValue的text不同,直接更新文本内容
- 如果新的VNode有children,判断子节点是否有变化,判断子节点的过程使用的就是diff算法
- diff过程值进行同层级比较
13、init
接收参数modules和Domapi,并把所有的模块的钩子函数统一存储到cbs对象中,并返回一个patch函数,init为一个高阶函数
14、patch
对比新老节点直接的差异,并把差异渲染到页面上,同时触发钩子函数
15、createElm
用于创建DOM元素,不会把DOM元素挂载到页面上
16、updateChildren
updatechildren是diff算法的核心,对比新旧节点的children,更新DOM
